diff --git a/include/heap.h b/include/heap.h index d2ccfc7a3d110..9f902712b248c 100644 --- a/include/heap.h +++ b/include/heap.h @@ -44,10 +44,17 @@ extern "C" { #define HP_PTRS_IN_NOD 128 /* - Value of HP_SHARE::max_records for a table with no row limit. 0 is a + Value of HP_SHARE::max_rows for a table with no row limit. 0 is a limit of zero rows, which is how a table that is never written to is created; only heap_create()'s argument uses 0 for "no limit". */ +#define NO_LIMIT_ROWS ULONG_MAX + +/* + Value of HP_SHARE::max_records when the caller stated no expectation of + how many records the table will hold. That field sizes blocks and is + what heap_info() reports; it limits nothing. +*/ #define NO_LIMIT_RECORDS ULONG_MAX /* struct used with heap_functions */ @@ -56,8 +63,13 @@ typedef struct st_heapinfo /* Struct from heap_info */ { ulong records; /* Records in database */ ulong deleted; /* Deleted records in database */ + ulong deleted_entries; /* Free list entries; see heap_info() */ ulong max_records; ulonglong data_length; + /* Ceiling data_length counts toward; see heap_info() */ + ulonglong max_data_length; + /* Free space, in the bytes data_length counts; see heap_info() */ + ulonglong delete_length; ulonglong index_length; uint reclength; /* Length of one record */ int errkey; @@ -130,6 +142,15 @@ typedef struct st_hp_keydef /* Key definition with open */ uint length; /* Length of key (automatic) */ uint8 algorithm; /* HASH / BTREE */ HA_KEYSEG *seg; + /* + Key segments addressing a stored record rather than the SQL record + buffer. Equal to 'seg' unless the table has promoted columns, in + which case compaction has moved every segment that follows the first + promoted column. Only the HASH index needs this: it recomputes keys + from stored records, while BTREE materializes its keys into the tree + from the SQL record. + */ + HA_KEYSEG *seg_stored; HP_BLOCK block; /* Where keys are saved */ /* Number of buckets used in hash table. Used only to provide @@ -144,12 +165,46 @@ typedef struct st_hp_keydef /* Key definition with open */ uint (*get_key_length)(struct st_hp_keydef *keydef, const uchar *key); } HP_KEYDEF; +/* + Description of one out-of-line column. + + A native blob has the same {length}{pointer} shape in the SQL record + buffer and in the stored record, so 'offset' and 'store_offset' differ + only by the compaction that promoted columns cause. + + A promoted column is a VARCHAR that the engine stores as a blob. Its + shape differs between the two layouts: {length}{data inline} in the SQL + record buffer, {length}{continuation chain pointer} in the stored + record. 'length' is the declared payload size the SQL buffer reserves, + which is what the inline form occupies and the stored form does not. +*/ + typedef struct st_hp_blob_desc { uint offset; /* Byte offset of blob descriptor within record buffer */ uint packlength; /* 1, 2, 3, or 4: length prefix size */ + uint store_offset; /* Byte offset of the descriptor in a stored record */ + uint length; /* Promoted only: declared payload bytes in record[0] */ + my_bool promoted; /* VARCHAR represented internally as a blob */ } HP_BLOB_DESC; +/* + A range of bytes that is identical in the SQL record buffer and in the + stored record, and can therefore be moved with a single memcpy. + + The spans are the gaps between promoted columns' payloads. A table + with no promoted column has exactly one span covering the whole + record, which is why packing and unpacking such a table costs the same + single memcpy it did before promotion existed. +*/ + +typedef struct st_hp_copy_span +{ + uint offset; /* Start in the SQL record buffer */ + uint store_offset; /* Start in the stored record */ + uint length; /* Bytes copied verbatim */ +} HP_COPY_SPAN; + /* Bits for HP_SHARE::state_changed, modeled on the state.changed bitmaps of Maria and MyISAM (see storage/maria/maria_def.h). A table marked @@ -168,15 +223,46 @@ typedef struct st_heap_share HP_KEYDEF *keydef; ulonglong data_length,index_length,max_table_size; ulonglong auto_increment; - ulong min_records,max_records; /* Params to open */ + /* + Expected record counts, from open. These size the HP_BLOCKs and + max_records is what heap_info() reports; neither refuses anything. + */ + ulong min_records,max_records; + /* + Row limit. Counts logical rows, which is what MAX_ROWS names: a row + whose blob data lives in continuation records still counts once. + NO_LIMIT_ROWS means unlimited. + + Memory is bounded separately, by max_table_size. That is the only + ceiling that can be correct for a table whose rows occupy a + data-dependent number of records, because it reads the bytes the + table actually holds instead of predicting them from a row count. + */ + ulong max_rows; ulong records; /* Logical (primary) record count */ ulong total_records; /* All active records (primary + blob continuation) */ ulong blength; /* records rounded up to 2^n */ ulong deleted; /* Deleted records in database */ + /* + Entries on the free list, where a coalesced block of any length + counts once. This is what a scan pays for the free records rather + than 'deleted': heap_scan() steps over a whole block in one go, so a + row whose blob data freed a run of a thousand records costs it the + same single step as a row that freed one. + */ + ulong deleted_entries; uint key_stat_version; /* version to indicate insert/delete */ uint key_version; /* Updated on key change */ uint file_version; /* Update on clear */ uint reclength; /* Length of one record */ + /* + Length of a record as it is held in HP_BLOCK. Equal to reclength + unless columns were promoted, in which case each promoted column + contributes a chain pointer instead of its declared payload and the + stored record is correspondingly shorter. This, not reclength, is + what the block geometry is built from. + */ + uint stored_reclength; uint visible; /* Offset to the flags byte (active/deleted/continuation) */ uint changed; uint state_changed; /* Bitmap of HEAP_STATE_* flags */ @@ -184,10 +270,13 @@ typedef struct st_heap_share uint currently_disabled_keys; /* saved value from "keys" when disabled */ uint open_count; uint blob_count; /* Number of blob columns */ + uint promoted_count; /* Blob columns that are VARCHARs */ + uint copy_span_count; /* Verbatim ranges, >= 1 */ uint auto_key; uint auto_key_type; /* real type of the auto key segment */ uchar *del_link; /* Link to next block with del. rec */ HP_BLOB_DESC *blob_descs; /* Array of blob column descriptors */ + HP_COPY_SPAN *copy_spans; /* Ranges shared by both layouts */ char * name; /* Name of "memory-file" */ time_t create_time; THR_LOCK lock; @@ -239,12 +328,19 @@ typedef struct st_heap_create_info HP_BLOB_DESC *blob_descs; ulonglong max_table_size; ulonglong auto_increment; + /* + Expected number of records, used only to size the HP_BLOCK + allocations. It is an estimate, not a limit: nothing is refused for + exceeding it. + */ ulong max_records; + ulong max_rows; /* Row limit, 0 means "no limit" */ ulong min_records; uint auto_key; /* keynr [1 - maxkey] for auto key */ uint auto_key_type; uint keys; uint reclength; + uint stored_reclength; /* 0 means "same as reclength" */ uint blob_count; my_bool with_auto_increment; my_bool internal_table; diff --git a/mysql-test/main/disabled.def b/mysql-test/main/disabled.def index a38906361c562..ded20b175bfba 100644 --- a/mysql-test/main/disabled.def +++ b/mysql-test/main/disabled.def @@ -16,3 +16,5 @@ mysql_embedded : Bug#12561297 2011-05-14 Anitha Dependent on PB2 chang file_contents : MDEV-6526 these files are not installed anymore max_statement_time : cannot possibly work, depends on timing partition_open_files_limit : open_files_limit check broken by MDEV-18360 +information_schema : MDEV-41068 unstable tie order over a MEMORY table +log_slow_innodb : MDEV-41068 unstable tie order over a MEMORY table diff --git a/mysql-test/main/gconcat_distinct_walk_fail.result b/mysql-test/main/gconcat_distinct_walk_fail.result index abe77f2582391..18c88e4ddcf1d 100644 --- a/mysql-test/main/gconcat_distinct_walk_fail.result +++ b/mysql-test/main/gconcat_distinct_walk_fail.result @@ -1,5 +1,11 @@ CREATE TABLE t1 (a VARCHAR(100)); -INSERT INTO t1 SELECT LPAD(seq MOD 200, 100, '0') FROM seq_1_to_600; +INSERT INTO t1 SELECT LPAD(seq MOD 1000, 100, '0') FROM seq_1_to_2000; +# +# It is how many distinct values there are, not how wide they are, +# that fills the duplicate filter. A column this wide is stored +# outside the record of a heap table, so what the filter holds for +# each value is a pointer to it rather than the value itself. +# # # Starve the duplicate filter so that it spills and the walk has to # merge, then make the merging walk fail. @@ -55,8 +61,8 @@ SET @@tmp_memory_table_size=DEFAULT; # SELECT LENGTH(GROUP_CONCAT(DISTINCT a)) AS gc_len FROM t1; gc_len -20199 +100999 SELECT JSON_LENGTH(JSON_ARRAYAGG(DISTINCT a)) AS ja_len FROM t1; ja_len -200 +1000 DROP TABLE t1; diff --git a/mysql-test/main/gconcat_distinct_walk_fail.test b/mysql-test/main/gconcat_distinct_walk_fail.test index 86898bda8e5d2..205b5f0ca9c8a 100644 --- a/mysql-test/main/gconcat_distinct_walk_fail.test +++ b/mysql-test/main/gconcat_distinct_walk_fail.test @@ -9,7 +9,14 @@ --source include/have_sequence.inc CREATE TABLE t1 (a VARCHAR(100)); -INSERT INTO t1 SELECT LPAD(seq MOD 200, 100, '0') FROM seq_1_to_600; +INSERT INTO t1 SELECT LPAD(seq MOD 1000, 100, '0') FROM seq_1_to_2000; + +--echo # +--echo # It is how many distinct values there are, not how wide they are, +--echo # that fills the duplicate filter. A column this wide is stored +--echo # outside the record of a heap table, so what the filter holds for +--echo # each value is a pointer to it rather than the value itself. +--echo # --echo # --echo # Starve the duplicate filter so that it spills and the walk has to diff --git a/mysql-test/suite/heap/blob.result b/mysql-test/suite/heap/blob.result index 7faf23da83fd0..82187492bd26d 100644 --- a/mysql-test/suite/heap/blob.result +++ b/mysql-test/suite/heap/blob.result @@ -431,12 +431,11 @@ set @@max_heap_table_size= 65536; create table t1 (a int not null, b blob, primary key(a)) engine=memory; insert into t1 values (1, repeat('x', 30000)); insert into t1 values (2, repeat('y', 30000)); -ERROR HY000: The table 't1' is full insert into t1 values (3, repeat('z', 30000)); ERROR HY000: The table 't1' is full select count(*) as row_count from t1; row_count -1 +2 select a, length(b) from t1 where a=1; a length(b) 1 30000 @@ -447,7 +446,7 @@ corrupted 0 select count(*) as scan_count from t1; scan_count -1 +2 set @@max_heap_table_size= @save_max; drop table t1; # diff --git a/mysql-test/suite/heap/blob_update_overflow.result b/mysql-test/suite/heap/blob_update_overflow.result index dfaa2181d8a59..6be4197247c3c 100644 --- a/mysql-test/suite/heap/blob_update_overflow.result +++ b/mysql-test/suite/heap/blob_update_overflow.result @@ -39,8 +39,8 @@ SELECT variable_name, variable_value FROM information_schema.session_status WHERE variable_name IN ('Created_tmp_disk_tables', 'Created_tmp_tables') ORDER BY variable_name; variable_name variable_value -CREATED_TMP_DISK_TABLES 2 -CREATED_TMP_TABLES 4 +CREATED_TMP_DISK_TABLES 1 +CREATED_TMP_TABLES 3 DROP TABLE t1; # # Test 2: Verify result correctness after overflow @@ -67,8 +67,8 @@ SELECT variable_name, variable_value FROM information_schema.session_status WHERE variable_name IN ('Created_tmp_disk_tables', 'Created_tmp_tables') ORDER BY variable_name; variable_name variable_value -CREATED_TMP_DISK_TABLES 2 -CREATED_TMP_TABLES 4 +CREATED_TMP_DISK_TABLES 1 +CREATED_TMP_TABLES 3 DROP TABLE t1; # # Test 3: Multiple blob aggregates (two MAX columns) @@ -91,8 +91,8 @@ SELECT variable_name, variable_value FROM information_schema.session_status WHERE variable_name IN ('Created_tmp_disk_tables', 'Created_tmp_tables') ORDER BY variable_name; variable_name variable_value -CREATED_TMP_DISK_TABLES 2 -CREATED_TMP_TABLES 4 +CREATED_TMP_DISK_TABLES 1 +CREATED_TMP_TABLES 3 DROP TABLE t1; # # Test 4: MIN(TEXT) with monotonically shrinking minimum @@ -125,8 +125,8 @@ SELECT variable_name, variable_value FROM information_schema.session_status WHERE variable_name IN ('Created_tmp_disk_tables', 'Created_tmp_tables') ORDER BY variable_name; variable_name variable_value -CREATED_TMP_DISK_TABLES 2 -CREATED_TMP_TABLES 4 +CREATED_TMP_DISK_TABLES 1 +CREATED_TMP_TABLES 3 DROP TABLE t1; # # Cleanup diff --git a/mysql-test/suite/heap/count_distinct_blob_convert.result b/mysql-test/suite/heap/count_distinct_blob_convert.result index 747d7ca654be9..d859ce60cfeb0 100644 --- a/mysql-test/suite/heap/count_distinct_blob_convert.result +++ b/mysql-test/suite/heap/count_distinct_blob_convert.result @@ -60,12 +60,14 @@ set @small_heap=65536; # it has to handle is gone. Rows added past the overflow do not move # it: it happens once the table is full, whatever follows. # -CREATE TABLE t1 (id INT PRIMARY KEY, v TEXT, w TEXT, s VARCHAR(64)) +CREATE TABLE t1 (id INT PRIMARY KEY, v TEXT, w TEXT, s VARCHAR(64), +n VARCHAR(6)) ENGINE=MyISAM; INSERT INTO t1 SELECT seq, LPAD((seq+1) DIV 2, 6, 'x'), LPAD((seq+1) DIV 2, 10, 'y'), +LPAD((seq+1) DIV 2, 6, 'z'), LPAD((seq+1) DIV 2, 6, 'z') FROM seq_1_to_8000; SELECT COUNT(*) AS rows_stored FROM t1; @@ -108,7 +110,7 @@ FROM INFORMATION_SCHEMA.SESSION_STATUS WHERE VARIABLE_NAME = 'CREATED_TMP_DISK_TABLES'; CONVERTED ON -# --- a blob and a non-blob argument --- +# --- a blob and a VARCHAR argument --- FLUSH STATUS; SELECT COUNT(DISTINCT v, s) AS distinct_values FROM t1; distinct_values @@ -118,7 +120,13 @@ FROM INFORMATION_SCHEMA.SESSION_STATUS WHERE VARIABLE_NAME = 'CREATED_TMP_DISK_TABLES'; CONVERTED ON -# --- control: no blob argument, deduplicated by the in-memory tree --- +# --- a VARCHAR wide enough to be stored out of line --- +# +# A VARCHAR this wide is kept outside the record of a heap table, so +# the record holds a pointer to it just as it does for a blob. The +# aggregate reaches the same conclusion from the same evidence and +# takes the same path, the writes rather than the tree. +# FLUSH STATUS; SELECT COUNT(DISTINCT s) AS distinct_values FROM t1; distinct_values @@ -127,6 +135,21 @@ SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED FROM INFORMATION_SCHEMA.SESSION_STATUS WHERE VARIABLE_NAME = 'CREATED_TMP_DISK_TABLES'; CONVERTED +ON +# --- control: every value inside the record, deduplicated by the +# --- in-memory tree +# +# n holds the same values as s and differs only in being declared +# narrow enough to stay inside the record. +# +FLUSH STATUS; +SELECT COUNT(DISTINCT n) AS distinct_values FROM t1; +distinct_values +4000 +SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED +FROM INFORMATION_SCHEMA.SESSION_STATUS +WHERE VARIABLE_NAME = 'CREATED_TMP_DISK_TABLES'; +CONVERTED OFF # --- grouped: one temporary table, reused for every group --- # @@ -170,6 +193,9 @@ distinct_values SELECT COUNT(DISTINCT s) AS distinct_values FROM t1; distinct_values 4000 +SELECT COUNT(DISTINCT n) AS distinct_values FROM t1; +distinct_values +4000 SELECT id MOD 2 AS g, COUNT(DISTINCT v) AS distinct_values FROM t1 GROUP BY g ORDER BY g; g distinct_values diff --git a/mysql-test/suite/heap/count_distinct_blob_convert.test b/mysql-test/suite/heap/count_distinct_blob_convert.test index 4f261596ed865..11016f570fb52 100644 --- a/mysql-test/suite/heap/count_distinct_blob_convert.test +++ b/mysql-test/suite/heap/count_distinct_blob_convert.test @@ -65,12 +65,14 @@ set @small_heap=65536; --echo # it: it happens once the table is full, whatever follows. --echo # -CREATE TABLE t1 (id INT PRIMARY KEY, v TEXT, w TEXT, s VARCHAR(64)) +CREATE TABLE t1 (id INT PRIMARY KEY, v TEXT, w TEXT, s VARCHAR(64), + n VARCHAR(6)) ENGINE=MyISAM; INSERT INTO t1 SELECT seq, LPAD((seq+1) DIV 2, 6, 'x'), LPAD((seq+1) DIV 2, 10, 'y'), + LPAD((seq+1) DIV 2, 6, 'z'), LPAD((seq+1) DIV 2, 6, 'z') FROM seq_1_to_8000; @@ -100,16 +102,32 @@ FLUSH STATUS; SELECT COUNT(DISTINCT v, w) AS distinct_values FROM t1; --source count_distinct_converted.inc ---echo # --- a blob and a non-blob argument --- +--echo # --- a blob and a VARCHAR argument --- FLUSH STATUS; SELECT COUNT(DISTINCT v, s) AS distinct_values FROM t1; --source count_distinct_converted.inc ---echo # --- control: no blob argument, deduplicated by the in-memory tree --- +--echo # --- a VARCHAR wide enough to be stored out of line --- +--echo # +--echo # A VARCHAR this wide is kept outside the record of a heap table, so +--echo # the record holds a pointer to it just as it does for a blob. The +--echo # aggregate reaches the same conclusion from the same evidence and +--echo # takes the same path, the writes rather than the tree. +--echo # FLUSH STATUS; SELECT COUNT(DISTINCT s) AS distinct_values FROM t1; --source count_distinct_converted.inc +--echo # --- control: every value inside the record, deduplicated by the +--echo # --- in-memory tree +--echo # +--echo # n holds the same values as s and differs only in being declared +--echo # narrow enough to stay inside the record. +--echo # +FLUSH STATUS; +SELECT COUNT(DISTINCT n) AS distinct_values FROM t1; +--source count_distinct_converted.inc + --echo # --- grouped: one temporary table, reused for every group --- --echo # --echo # There is one aggregate, and it keeps one temporary table that is @@ -138,6 +156,7 @@ SELECT COUNT(DISTINCT w) AS distinct_values FROM t1; SELECT COUNT(DISTINCT v, w) AS distinct_values FROM t1; SELECT COUNT(DISTINCT v, s) AS distinct_values FROM t1; SELECT COUNT(DISTINCT s) AS distinct_values FROM t1; +SELECT COUNT(DISTINCT n) AS distinct_values FROM t1; SELECT id MOD 2 AS g, COUNT(DISTINCT v) AS distinct_values FROM t1 GROUP BY g ORDER BY g; SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED diff --git a/mysql-test/suite/heap/group_concat_cut_reporting.result b/mysql-test/suite/heap/group_concat_cut_reporting.result new file mode 100644 index 0000000000000..1b1379a778a51 --- /dev/null +++ b/mysql-test/suite/heap/group_concat_cut_reporting.result @@ -0,0 +1,43 @@ +CREATE TABLE narrow (a VARCHAR(20)) CHARACTER SET latin1; +CREATE TABLE wide (a VARCHAR(100)) CHARACTER SET latin1; +CREATE TABLE declared (a TEXT) CHARACTER SET latin1; +INSERT INTO narrow VALUES (REPEAT('a', 20)), (REPEAT('b', 20)); +INSERT INTO wide VALUES (REPEAT('a', 50)), (REPEAT('b', 50)); +INSERT INTO declared VALUES (REPEAT('a', 50)), (REPEAT('b', 50)); +SET @save_gcml= @@group_concat_max_len; +SET group_concat_max_len= 10; +# A narrow VARCHAR keeps its payload in the record +SELECT GROUP_CONCAT(a ORDER BY a) FROM narrow; +GROUP_CONCAT(a ORDER BY a) +aaaaaaaaaa +Warnings: +Warning 1260 Row 1 was cut by group_concat() +SELECT GROUP_CONCAT(DISTINCT a) FROM narrow; +GROUP_CONCAT(DISTINCT a) +aaaaaaaaaa +Warnings: +Warning 1260 Row 1 was cut by group_concat() +# A wide VARCHAR does not, and must report the same cut +SELECT GROUP_CONCAT(a ORDER BY a) FROM wide; +GROUP_CONCAT(a ORDER BY a) +aaaaaaaaaa +Warnings: +Warning 1260 Row 1 was cut by group_concat() +SELECT GROUP_CONCAT(DISTINCT a) FROM wide; +GROUP_CONCAT(DISTINCT a) +aaaaaaaaaa +Warnings: +Warning 1260 Row 1 was cut by group_concat() +# A declared blob is cut on the way in, and says so +SELECT GROUP_CONCAT(a ORDER BY a) FROM declared; +GROUP_CONCAT(a ORDER BY a) +aaaaaaaaaa +Warnings: +Warning 1260 Row 2 was cut by group_concat() +SELECT GROUP_CONCAT(DISTINCT a) FROM declared; +GROUP_CONCAT(DISTINCT a) +aaaaaaaaaa +Warnings: +Warning 1260 Row 2 was cut by group_concat() +SET group_concat_max_len= @save_gcml; +DROP TABLE narrow, wide, declared; diff --git a/mysql-test/suite/heap/group_concat_cut_reporting.test b/mysql-test/suite/heap/group_concat_cut_reporting.test new file mode 100644 index 0000000000000..fd4153d685747 --- /dev/null +++ b/mysql-test/suite/heap/group_concat_cut_reporting.test @@ -0,0 +1,36 @@ +# +# GROUP_CONCAT with ORDER BY or DISTINCT keeps every row alive at once, so +# a column whose payload is kept outside the record is copied into the +# table's own storage on the way in. A declared blob is cut to +# group_concat_max_len there, because it has no declared width of its own +# and the storage would otherwise grow without bound; a VARCHAR has one +# and is stored whole. +# +# What a query reports about the cut therefore has to follow the declared +# type and not the storage: a wide VARCHAR must report what a narrow one +# reports, whatever the temporary table does with its payload. +# +CREATE TABLE narrow (a VARCHAR(20)) CHARACTER SET latin1; +CREATE TABLE wide (a VARCHAR(100)) CHARACTER SET latin1; +CREATE TABLE declared (a TEXT) CHARACTER SET latin1; +INSERT INTO narrow VALUES (REPEAT('a', 20)), (REPEAT('b', 20)); +INSERT INTO wide VALUES (REPEAT('a', 50)), (REPEAT('b', 50)); +INSERT INTO declared VALUES (REPEAT('a', 50)), (REPEAT('b', 50)); + +SET @save_gcml= @@group_concat_max_len; +SET group_concat_max_len= 10; + +--echo # A narrow VARCHAR keeps its payload in the record +SELECT GROUP_CONCAT(a ORDER BY a) FROM narrow; +SELECT GROUP_CONCAT(DISTINCT a) FROM narrow; + +--echo # A wide VARCHAR does not, and must report the same cut +SELECT GROUP_CONCAT(a ORDER BY a) FROM wide; +SELECT GROUP_CONCAT(DISTINCT a) FROM wide; + +--echo # A declared blob is cut on the way in, and says so +SELECT GROUP_CONCAT(a ORDER BY a) FROM declared; +SELECT GROUP_CONCAT(DISTINCT a) FROM declared; + +SET group_concat_max_len= @save_gcml; +DROP TABLE narrow, wide, declared; diff --git a/mysql-test/suite/heap/max_data_length.result b/mysql-test/suite/heap/max_data_length.result new file mode 100644 index 0000000000000..0b8d37669dee7 --- /dev/null +++ b/mysql-test/suite/heap/max_data_length.result @@ -0,0 +1,53 @@ +# +# The ceiling a MEMORY table reports must be the ceiling it +# enforces. Rows are refused once the table's data and index +# together reach max_heap_table_size, so that is the number, and +# storing a column out of line does not move it. +# +# Every table below is CHARACTER SET latin1 so the declared width +# in characters equals the width in bytes. +# +SET @save_max_heap_table_size= @@max_heap_table_size; +SET max_heap_table_size= 1024*1024; +CREATE TABLE t_inline (a VARCHAR(8)) ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE t_wide (a VARCHAR(3000)) ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE t_blob (a BLOB) ENGINE=MEMORY CHARACTER SET latin1; +# +# 1. All three report the same ceiling, because they have one +# +SELECT table_name, max_data_length = @@max_heap_table_size AS ceiling_exact +FROM information_schema.tables +WHERE table_schema='test' AND table_name IN ('t_inline','t_wide','t_blob') +ORDER BY table_name; +table_name ceiling_exact +t_blob 1 +t_inline 1 +t_wide 1 +# +# 2. A table said to hold N bytes must reach N before it refuses a +# row. A promoted column's rows occupy several records each, so +# a ceiling derived from a record count times a row width would +# be reported as barely touched at the point the table is full. +# +INSERT INTO t_wide SELECT REPEAT('a', 3000) FROM seq_1_to_1000; +ERROR HY000: The table 't_wide' is full +SELECT data_length + index_length >= max_data_length AS filled_to_ceiling +FROM information_schema.tables +WHERE table_schema='test' AND table_name='t_wide'; +filled_to_ceiling +1 +# +# 3. Where a row limit admits fewer bytes than the memory ceiling, +# it is the row limit that is reported. A row is at most as +# wide as it is declared, whether or not it is stored inline. +# +CREATE TABLE t_rows (a VARCHAR(3000)) ENGINE=MEMORY CHARACTER SET latin1 +MAX_ROWS=10; +SELECT max_data_length < @@max_heap_table_size AS row_limit_binds, +max_data_length +FROM information_schema.tables +WHERE table_schema='test' AND table_name='t_rows'; +row_limit_binds max_data_length +1 30030 +DROP TABLE t_inline, t_wide, t_blob, t_rows; +SET max_heap_table_size= @save_max_heap_table_size; diff --git a/mysql-test/suite/heap/max_data_length.test b/mysql-test/suite/heap/max_data_length.test new file mode 100644 index 0000000000000..492bb9924d256 --- /dev/null +++ b/mysql-test/suite/heap/max_data_length.test @@ -0,0 +1,53 @@ +--source include/have_sequence.inc + +--echo # +--echo # The ceiling a MEMORY table reports must be the ceiling it +--echo # enforces. Rows are refused once the table's data and index +--echo # together reach max_heap_table_size, so that is the number, and +--echo # storing a column out of line does not move it. +--echo # +--echo # Every table below is CHARACTER SET latin1 so the declared width +--echo # in characters equals the width in bytes. +--echo # + +SET @save_max_heap_table_size= @@max_heap_table_size; +SET max_heap_table_size= 1024*1024; + +CREATE TABLE t_inline (a VARCHAR(8)) ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE t_wide (a VARCHAR(3000)) ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE t_blob (a BLOB) ENGINE=MEMORY CHARACTER SET latin1; + +--echo # +--echo # 1. All three report the same ceiling, because they have one +--echo # +SELECT table_name, max_data_length = @@max_heap_table_size AS ceiling_exact +FROM information_schema.tables +WHERE table_schema='test' AND table_name IN ('t_inline','t_wide','t_blob') +ORDER BY table_name; + +--echo # +--echo # 2. A table said to hold N bytes must reach N before it refuses a +--echo # row. A promoted column's rows occupy several records each, so +--echo # a ceiling derived from a record count times a row width would +--echo # be reported as barely touched at the point the table is full. +--echo # +--error ER_RECORD_FILE_FULL +INSERT INTO t_wide SELECT REPEAT('a', 3000) FROM seq_1_to_1000; +SELECT data_length + index_length >= max_data_length AS filled_to_ceiling +FROM information_schema.tables +WHERE table_schema='test' AND table_name='t_wide'; + +--echo # +--echo # 3. Where a row limit admits fewer bytes than the memory ceiling, +--echo # it is the row limit that is reported. A row is at most as +--echo # wide as it is declared, whether or not it is stored inline. +--echo # +CREATE TABLE t_rows (a VARCHAR(3000)) ENGINE=MEMORY CHARACTER SET latin1 + MAX_ROWS=10; +SELECT max_data_length < @@max_heap_table_size AS row_limit_binds, + max_data_length +FROM information_schema.tables +WHERE table_schema='test' AND table_name='t_rows'; + +DROP TABLE t_inline, t_wide, t_blob, t_rows; +SET max_heap_table_size= @save_max_heap_table_size; diff --git a/mysql-test/suite/heap/promotion_data_free.result b/mysql-test/suite/heap/promotion_data_free.result new file mode 100644 index 0000000000000..2740c7f973d23 --- /dev/null +++ b/mysql-test/suite/heap/promotion_data_free.result @@ -0,0 +1,41 @@ +CREATE TABLE narrow (id INT PRIMARY KEY, a VARCHAR(20)) +ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE wide (id INT PRIMARY KEY, a VARCHAR(3000)) +ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE declared (id INT PRIMARY KEY, a TEXT) +ENGINE=MEMORY CHARACTER SET latin1; +INSERT INTO narrow SELECT seq, REPEAT('x', 20) FROM seq_1_to_100; +INSERT INTO wide SELECT seq, REPEAT('x', 3000) FROM seq_1_to_100; +INSERT INTO declared SELECT seq, REPEAT('x', 3000) FROM seq_1_to_100; +# +# A wide VARCHAR is stored the way the declared TEXT beside it is, +# so the two must report their free space the same way. Both are +# read against the narrow column, which is stored in the row. +# +DELETE FROM narrow WHERE id <= 50; +DELETE FROM wide WHERE id <= 50; +DELETE FROM declared WHERE id <= 50; +SELECT TABLE_NAME, DATA_FREE <= DATA_LENGTH AS free_space_fits +FROM information_schema.TABLES +WHERE TABLE_SCHEMA='test' AND TABLE_NAME IN ('narrow','wide','declared') +ORDER BY TABLE_NAME; +TABLE_NAME free_space_fits +declared 1 +narrow 1 +wide 1 +# +# Emptying the table frees every record it held, so the free space +# it then reports is the whole of what it allocated. +# +DELETE FROM narrow; +DELETE FROM wide; +DELETE FROM declared; +SELECT TABLE_NAME, DATA_FREE <= DATA_LENGTH AS free_space_fits +FROM information_schema.TABLES +WHERE TABLE_SCHEMA='test' AND TABLE_NAME IN ('narrow','wide','declared') +ORDER BY TABLE_NAME; +TABLE_NAME free_space_fits +declared 1 +narrow 1 +wide 1 +DROP TABLE narrow, wide, declared; diff --git a/mysql-test/suite/heap/promotion_data_free.test b/mysql-test/suite/heap/promotion_data_free.test new file mode 100644 index 0000000000000..744d55c35ffd8 --- /dev/null +++ b/mysql-test/suite/heap/promotion_data_free.test @@ -0,0 +1,50 @@ +# +# Data_free is the free space a table holds, and it is counted in the same +# bytes as Data_length. The engine counts free RECORDS, and a record is a +# slot of the stored row width, which is not the SQL row width once a wide +# VARCHAR keeps its value outside the row: one deleted row frees a base +# record and every continuation record its value occupied. Multiplying a +# count of those by the SQL row width reports a table as holding more free +# space than it has ever allocated. +# +--source include/have_sequence.inc + +CREATE TABLE narrow (id INT PRIMARY KEY, a VARCHAR(20)) + ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE wide (id INT PRIMARY KEY, a VARCHAR(3000)) + ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE declared (id INT PRIMARY KEY, a TEXT) + ENGINE=MEMORY CHARACTER SET latin1; + +INSERT INTO narrow SELECT seq, REPEAT('x', 20) FROM seq_1_to_100; +INSERT INTO wide SELECT seq, REPEAT('x', 3000) FROM seq_1_to_100; +INSERT INTO declared SELECT seq, REPEAT('x', 3000) FROM seq_1_to_100; + +--echo # +--echo # A wide VARCHAR is stored the way the declared TEXT beside it is, +--echo # so the two must report their free space the same way. Both are +--echo # read against the narrow column, which is stored in the row. +--echo # +DELETE FROM narrow WHERE id <= 50; +DELETE FROM wide WHERE id <= 50; +DELETE FROM declared WHERE id <= 50; + +SELECT TABLE_NAME, DATA_FREE <= DATA_LENGTH AS free_space_fits +FROM information_schema.TABLES +WHERE TABLE_SCHEMA='test' AND TABLE_NAME IN ('narrow','wide','declared') +ORDER BY TABLE_NAME; + +--echo # +--echo # Emptying the table frees every record it held, so the free space +--echo # it then reports is the whole of what it allocated. +--echo # +DELETE FROM narrow; +DELETE FROM wide; +DELETE FROM declared; + +SELECT TABLE_NAME, DATA_FREE <= DATA_LENGTH AS free_space_fits +FROM information_schema.TABLES +WHERE TABLE_SCHEMA='test' AND TABLE_NAME IN ('narrow','wide','declared') +ORDER BY TABLE_NAME; + +DROP TABLE narrow, wide, declared; diff --git a/mysql-test/suite/heap/promotion_keeps_optimizations.result b/mysql-test/suite/heap/promotion_keeps_optimizations.result new file mode 100644 index 0000000000000..d30d5f1f6edc4 --- /dev/null +++ b/mysql-test/suite/heap/promotion_keeps_optimizations.result @@ -0,0 +1,71 @@ +SET @save_os= @@optimizer_switch; +# +# SELECT DISTINCT deduplicates with a hash of the row where it can, +# and rescans the table once per surviving row where it cannot. The +# hash key needs a bounded width, which a VARCHAR has and a blob has +# not. +# +CREATE TABLE narrow (a VARCHAR(20), b INT) CHARACTER SET latin1; +CREATE TABLE wide (a VARCHAR(100), b INT) CHARACTER SET latin1; +CREATE TABLE declared (a TEXT, b INT) CHARACTER SET latin1; +INSERT INTO narrow SELECT CONCAT('v', seq MOD 40), seq FROM seq_1_to_200; +INSERT INTO wide SELECT CONCAT('v', seq MOD 40), seq FROM seq_1_to_200; +INSERT INTO declared SELECT CONCAT('v', seq MOD 40), seq FROM seq_1_to_200; +FLUSH STATUS; +SELECT DISTINCT a, SUM(b) OVER () FROM narrow; +SHOW STATUS LIKE 'Handler_read_rnd_next'; +Variable_name Value +Handler_read_rnd_next 603 +FLUSH STATUS; +SELECT DISTINCT a, SUM(b) OVER () FROM wide; +# must read the same number of rows as the narrow column +SHOW STATUS LIKE 'Handler_read_rnd_next'; +Variable_name Value +Handler_read_rnd_next 603 +FLUSH STATUS; +SELECT DISTINCT a, SUM(b) OVER () FROM declared; +# a column with no maximum width rescans, which is why the +# fallback exists +SHOW STATUS LIKE 'Handler_read_rnd_next'; +Variable_name Value +Handler_read_rnd_next 4503 +DROP TABLE narrow, wide, declared; +# +# A correlated subquery caches its answers in a MEMORY table keyed +# on the outer values. The cache is turned off for a column with no +# maximum width, because the key such a column would need is not the +# one the SQL layer builds. +# +SET optimizer_switch='subquery_cache=on'; +CREATE TABLE outer_narrow (a VARCHAR(20)) CHARACTER SET latin1; +CREATE TABLE outer_wide (a VARCHAR(100)) CHARACTER SET latin1; +CREATE TABLE outer_declared (a TEXT) CHARACTER SET latin1; +CREATE TABLE inner_tab (b VARCHAR(100), c INT) CHARACTER SET latin1; +INSERT INTO outer_narrow SELECT CONCAT('v', seq MOD 5) FROM seq_1_to_100; +INSERT INTO outer_wide SELECT CONCAT('v', seq MOD 5) FROM seq_1_to_100; +INSERT INTO outer_declared SELECT CONCAT('v', seq MOD 5) FROM seq_1_to_100; +INSERT INTO inner_tab SELECT CONCAT('v', seq MOD 5), seq FROM seq_1_to_50; +SELECT VARIABLE_VALUE INTO @h0 FROM information_schema.session_status +WHERE VARIABLE_NAME='Subquery_cache_hit'; +SELECT (SELECT MAX(c) FROM inner_tab WHERE b = outer_narrow.a) FROM outer_narrow; +SELECT VARIABLE_VALUE - @h0 > 0 AS narrow_was_cached +FROM information_schema.session_status WHERE VARIABLE_NAME='Subquery_cache_hit'; +narrow_was_cached +1 +SELECT VARIABLE_VALUE INTO @h0 FROM information_schema.session_status +WHERE VARIABLE_NAME='Subquery_cache_hit'; +SELECT (SELECT MAX(c) FROM inner_tab WHERE b = outer_wide.a) FROM outer_wide; +SELECT VARIABLE_VALUE - @h0 > 0 AS wide_was_cached +FROM information_schema.session_status WHERE VARIABLE_NAME='Subquery_cache_hit'; +wide_was_cached +1 +SELECT VARIABLE_VALUE INTO @h0 FROM information_schema.session_status +WHERE VARIABLE_NAME='Subquery_cache_hit'; +SELECT (SELECT MAX(c) FROM inner_tab WHERE b = outer_declared.a) +FROM outer_declared; +SELECT VARIABLE_VALUE - @h0 > 0 AS declared_was_cached +FROM information_schema.session_status WHERE VARIABLE_NAME='Subquery_cache_hit'; +declared_was_cached +0 +DROP TABLE outer_narrow, outer_wide, outer_declared, inner_tab; +SET optimizer_switch= @save_os; diff --git a/mysql-test/suite/heap/promotion_keeps_optimizations.test b/mysql-test/suite/heap/promotion_keeps_optimizations.test new file mode 100644 index 0000000000000..0d8a2a50174f9 --- /dev/null +++ b/mysql-test/suite/heap/promotion_keeps_optimizations.test @@ -0,0 +1,107 @@ +# +# Two different questions are asked of the same counter today: whether a +# column is declared as a blob, and whether its payload sits outside the +# record. A wide VARCHAR answers no to the first and yes to the second, +# and the decisions below turn on the first one -- a value with no +# maximum width is what they cannot handle. Reading the second instead +# would silently cost a wide VARCHAR an optimization a narrow one keeps. +# +--source include/have_sequence.inc + +SET @save_os= @@optimizer_switch; + +--echo # +--echo # SELECT DISTINCT deduplicates with a hash of the row where it can, +--echo # and rescans the table once per surviving row where it cannot. The +--echo # hash key needs a bounded width, which a VARCHAR has and a blob has +--echo # not. +--echo # +CREATE TABLE narrow (a VARCHAR(20), b INT) CHARACTER SET latin1; +CREATE TABLE wide (a VARCHAR(100), b INT) CHARACTER SET latin1; +CREATE TABLE declared (a TEXT, b INT) CHARACTER SET latin1; +INSERT INTO narrow SELECT CONCAT('v', seq MOD 40), seq FROM seq_1_to_200; +INSERT INTO wide SELECT CONCAT('v', seq MOD 40), seq FROM seq_1_to_200; +INSERT INTO declared SELECT CONCAT('v', seq MOD 40), seq FROM seq_1_to_200; + +--disable_ps_protocol +--disable_ps2_protocol +--disable_view_protocol +--disable_cursor_protocol +FLUSH STATUS; +--disable_result_log +SELECT DISTINCT a, SUM(b) OVER () FROM narrow; +--enable_result_log +SHOW STATUS LIKE 'Handler_read_rnd_next'; + +FLUSH STATUS; +--disable_result_log +SELECT DISTINCT a, SUM(b) OVER () FROM wide; +--enable_result_log +--echo # must read the same number of rows as the narrow column +SHOW STATUS LIKE 'Handler_read_rnd_next'; + +FLUSH STATUS; +--disable_result_log +SELECT DISTINCT a, SUM(b) OVER () FROM declared; +--enable_result_log +--echo # a column with no maximum width rescans, which is why the +--echo # fallback exists +SHOW STATUS LIKE 'Handler_read_rnd_next'; +--enable_cursor_protocol +--enable_view_protocol +--enable_ps2_protocol +--enable_ps_protocol + +DROP TABLE narrow, wide, declared; + +--echo # +--echo # A correlated subquery caches its answers in a MEMORY table keyed +--echo # on the outer values. The cache is turned off for a column with no +--echo # maximum width, because the key such a column would need is not the +--echo # one the SQL layer builds. +--echo # +SET optimizer_switch='subquery_cache=on'; +CREATE TABLE outer_narrow (a VARCHAR(20)) CHARACTER SET latin1; +CREATE TABLE outer_wide (a VARCHAR(100)) CHARACTER SET latin1; +CREATE TABLE outer_declared (a TEXT) CHARACTER SET latin1; +CREATE TABLE inner_tab (b VARCHAR(100), c INT) CHARACTER SET latin1; +INSERT INTO outer_narrow SELECT CONCAT('v', seq MOD 5) FROM seq_1_to_100; +INSERT INTO outer_wide SELECT CONCAT('v', seq MOD 5) FROM seq_1_to_100; +INSERT INTO outer_declared SELECT CONCAT('v', seq MOD 5) FROM seq_1_to_100; +INSERT INTO inner_tab SELECT CONCAT('v', seq MOD 5), seq FROM seq_1_to_50; + +--disable_ps_protocol +--disable_ps2_protocol +--disable_view_protocol +--disable_cursor_protocol +SELECT VARIABLE_VALUE INTO @h0 FROM information_schema.session_status + WHERE VARIABLE_NAME='Subquery_cache_hit'; +--disable_result_log +SELECT (SELECT MAX(c) FROM inner_tab WHERE b = outer_narrow.a) FROM outer_narrow; +--enable_result_log +SELECT VARIABLE_VALUE - @h0 > 0 AS narrow_was_cached + FROM information_schema.session_status WHERE VARIABLE_NAME='Subquery_cache_hit'; + +SELECT VARIABLE_VALUE INTO @h0 FROM information_schema.session_status + WHERE VARIABLE_NAME='Subquery_cache_hit'; +--disable_result_log +SELECT (SELECT MAX(c) FROM inner_tab WHERE b = outer_wide.a) FROM outer_wide; +--enable_result_log +SELECT VARIABLE_VALUE - @h0 > 0 AS wide_was_cached + FROM information_schema.session_status WHERE VARIABLE_NAME='Subquery_cache_hit'; + +SELECT VARIABLE_VALUE INTO @h0 FROM information_schema.session_status + WHERE VARIABLE_NAME='Subquery_cache_hit'; +--disable_result_log +SELECT (SELECT MAX(c) FROM inner_tab WHERE b = outer_declared.a) +FROM outer_declared; +--enable_result_log +SELECT VARIABLE_VALUE - @h0 > 0 AS declared_was_cached + FROM information_schema.session_status WHERE VARIABLE_NAME='Subquery_cache_hit'; +--enable_cursor_protocol +--enable_view_protocol +--enable_ps2_protocol +--enable_ps_protocol + +DROP TABLE outer_narrow, outer_wide, outer_declared, inner_tab; +SET optimizer_switch= @save_os; diff --git a/mysql-test/suite/heap/promotion_layout_agreement.result b/mysql-test/suite/heap/promotion_layout_agreement.result new file mode 100644 index 0000000000000..d43ca009a3529 --- /dev/null +++ b/mysql-test/suite/heap/promotion_layout_agreement.result @@ -0,0 +1,47 @@ +CREATE TABLE ft (b VARCHAR(64), FULLTEXT(b)) ENGINE=MyISAM; +INSERT INTO ft VALUES ('alpha'),('beta'); +WITH RECURSIVE seq AS ( +SELECT 1 AS n, REPEAT('a', 100) AS wide +UNION ALL +SELECT n + 1, REPEAT('b', 100) FROM seq WHERE n < 3 +) +SELECT n, LEFT(wide, 8) AS head, CHAR_LENGTH(wide) AS len FROM seq ORDER BY n; +n head len +1 aaaaaaaa 100 +2 bbbbbbbb 100 +3 bbbbbbbb 100 +WITH RECURSIVE seq AS ( +SELECT 1 AS n, REPEAT('a', 100) AS wide +UNION ALL +SELECT n + 1, REPEAT('b', 100) FROM seq WHERE n < 3 +) +SELECT s1.n, LEFT(s1.wide, 8) AS head, CHAR_LENGTH(s2.wide) AS len +FROM seq s1, seq s2, ft +WHERE s1.n = s2.n AND MATCH(ft.b) AGAINST ('alpha' IN BOOLEAN MODE) +ORDER BY s1.n; +n head len +1 aaaaaaaa 100 +2 bbbbbbbb 100 +3 bbbbbbbb 100 +PREPARE stmt FROM " +WITH RECURSIVE seq AS ( + SELECT 1 AS n, REPEAT('a', 100) AS wide + UNION ALL + SELECT n + 1, REPEAT('b', 100) FROM seq WHERE n < 3 +) +SELECT s1.n, LEFT(s1.wide, 8) AS head, CHAR_LENGTH(s2.wide) AS len +FROM seq s1, seq s2, ft +WHERE s1.n = s2.n AND MATCH(ft.b) AGAINST ('alpha' IN BOOLEAN MODE) +ORDER BY s1.n"; +EXECUTE stmt; +n head len +1 aaaaaaaa 100 +2 bbbbbbbb 100 +3 bbbbbbbb 100 +EXECUTE stmt; +n head len +1 aaaaaaaa 100 +2 bbbbbbbb 100 +3 bbbbbbbb 100 +DEALLOCATE PREPARE stmt; +DROP TABLE ft; diff --git a/mysql-test/suite/heap/promotion_layout_agreement.test b/mysql-test/suite/heap/promotion_layout_agreement.test new file mode 100644 index 0000000000000..14375a92c5d24 --- /dev/null +++ b/mysql-test/suite/heap/promotion_layout_agreement.test @@ -0,0 +1,55 @@ +# +# A recursive CTE is computed in one temporary table and copied into one +# more per reference to it, and the copy has no conversion step: the +# source table's handler writes straight into the destination's record +# buffer. All of them are built from a single column list, so they agree +# on the record layout only while every column is stored the same way in +# each. +# +# A VARCHAR wider than the threshold keeps its payload outside the record +# in a MEMORY table, and the record holds a length and a pointer instead. +# The reference tables are created by a different call, with its own +# options, than the table the recursion runs in; a fulltext function in +# the query reading the CTE is what makes those options differ. +# +CREATE TABLE ft (b VARCHAR(64), FULLTEXT(b)) ENGINE=MyISAM; +INSERT INTO ft VALUES ('alpha'),('beta'); + +# No fulltext function: every table is built the same way. +WITH RECURSIVE seq AS ( + SELECT 1 AS n, REPEAT('a', 100) AS wide + UNION ALL + SELECT n + 1, REPEAT('b', 100) FROM seq WHERE n < 3 +) +SELECT n, LEFT(wide, 8) AS head, CHAR_LENGTH(wide) AS len FROM seq ORDER BY n; + +# The query reading the CTE carries a MATCH(), and reads it twice, so the +# two reference tables are created by the two different calls. +WITH RECURSIVE seq AS ( + SELECT 1 AS n, REPEAT('a', 100) AS wide + UNION ALL + SELECT n + 1, REPEAT('b', 100) FROM seq WHERE n < 3 +) +SELECT s1.n, LEFT(s1.wide, 8) AS head, CHAR_LENGTH(s2.wide) AS len +FROM seq s1, seq s2, ft +WHERE s1.n = s2.n AND MATCH(ft.b) AGAINST ('alpha' IN BOOLEAN MODE) +ORDER BY s1.n; + +# The same statement prepared and executed more than once: the reference +# tables are created on one path the first time and on another one after +# the specification has been prepared. +PREPARE stmt FROM " +WITH RECURSIVE seq AS ( + SELECT 1 AS n, REPEAT('a', 100) AS wide + UNION ALL + SELECT n + 1, REPEAT('b', 100) FROM seq WHERE n < 3 +) +SELECT s1.n, LEFT(s1.wide, 8) AS head, CHAR_LENGTH(s2.wide) AS len +FROM seq s1, seq s2, ft +WHERE s1.n = s2.n AND MATCH(ft.b) AGAINST ('alpha' IN BOOLEAN MODE) +ORDER BY s1.n"; +EXECUTE stmt; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +DROP TABLE ft; diff --git a/mysql-test/suite/heap/promotion_transparent.result b/mysql-test/suite/heap/promotion_transparent.result new file mode 100644 index 0000000000000..eb82b36919a7b --- /dev/null +++ b/mysql-test/suite/heap/promotion_transparent.result @@ -0,0 +1,221 @@ +# +# A VARCHAR wide enough for the engine to store out of line must be +# indistinguishable from a narrow one at the SQL layer. Every table +# below is declared CHARACTER SET latin1 so that the declared width +# in characters equals the width in bytes, and the threshold is +# reached exactly where the declaration says it is. +# +# t_narrow stays inline. t_wide is stored as a blob by the engine. +# +CREATE TABLE t_narrow (a VARCHAR(8)) ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE t_wide (a VARCHAR(200)) ENGINE=MEMORY CHARACTER SET latin1; +# +# 0. Promotion is in effect. +# +# Everything after this point is a check that promotion changes +# nothing, and would pass just as well if the engine never promoted +# anything. This is the check that it does. +# +# An inline VARCHAR(60000) reserves its declared width in every row, +# so at a 1MB ceiling the table holds about 17 rows whatever the +# values are. Stored out of line it holds the 200 short rows below +# with room to spare. Without promotion this INSERT is refused. +# +SET @save_max_heap_table_size= @@max_heap_table_size; +SET max_heap_table_size= 1024*1024; +CREATE TABLE t_proof (a VARCHAR(60000)) ENGINE=MEMORY CHARACTER SET latin1; +INSERT INTO t_proof SELECT CONCAT('row', seq) FROM seq_1_to_200; +SELECT COUNT(*) AS rows_held FROM t_proof; +rows_held +200 +SELECT COUNT(*) AS intact FROM t_proof WHERE a = CONCAT('row', SUBSTR(a, 4)); +intact +200 +DROP TABLE t_proof; +SET max_heap_table_size= @save_max_heap_table_size; +# +# 1. Declared type is unchanged +# +SHOW CREATE TABLE t_narrow; +Table Create Table +t_narrow CREATE TABLE `t_narrow` ( + `a` varchar(8) DEFAULT NULL +) ENGINE=MEMORY DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci +SHOW CREATE TABLE t_wide; +Table Create Table +t_wide CREATE TABLE `t_wide` ( + `a` varchar(200) DEFAULT NULL +) ENGINE=MEMORY DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci +SELECT column_name, data_type, column_type, +character_maximum_length, character_octet_length +FROM information_schema.columns +WHERE table_schema='test' AND table_name IN ('t_narrow','t_wide') +ORDER BY table_name, column_name; +column_name data_type column_type character_maximum_length character_octet_length +a varchar varchar(8) 8 8 +a varchar varchar(200) 200 200 +# +# 2. Type reported to the client is unchanged +# +INSERT INTO t_narrow VALUES ('abc'); +INSERT INTO t_wide VALUES ('abc'); +SELECT a FROM t_narrow; +Catalog Database Table Table_alias Column Column_alias Type Length Max length Is_null Flags Decimals Charsetnr +def test t_narrow t_narrow a a 253 8 3 Y 0 0 8 +a +abc +SELECT a FROM t_wide; +Catalog Database Table Table_alias Column Column_alias Type Length Max length Is_null Flags Decimals Charsetnr +def test t_wide t_wide a a 253 200 3 Y 0 0 8 +a +abc +# +# 3. A table built from the column keeps the type +# +CREATE TABLE t_ctas ENGINE=MEMORY AS SELECT a FROM t_wide; +SHOW CREATE TABLE t_ctas; +Table Create Table +t_ctas CREATE TABLE `t_ctas` ( + `a` varchar(200) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL +) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +DROP TABLE t_ctas; +# +# 4. Width is the only difference across the threshold. At +# CHARACTER SET latin1 the promotion threshold is 32 bytes, so +# these two straddle it and must still agree on everything but +# the declared width. +# +CREATE TABLE t_below (a VARCHAR(32)) ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE t_above (a VARCHAR(33)) ENGINE=MEMORY CHARACTER SET latin1; +SELECT table_name, data_type, character_maximum_length +FROM information_schema.columns +WHERE table_schema='test' AND table_name IN ('t_below','t_above') +ORDER BY table_name; +table_name data_type character_maximum_length +t_above varchar 33 +t_below varchar 32 +DROP TABLE t_below, t_above; +# +# 5. VARCHAR comparison semantics, which differ from a blob's. +# latin1_swedish_ci pads, so trailing spaces must not make a +# value distinct, and comparison must stay case insensitive. +# +DELETE FROM t_narrow; +DELETE FROM t_wide; +INSERT INTO t_narrow VALUES ('ab'), ('AB '), ('cd'); +INSERT INTO t_wide VALUES ('ab'), ('AB '), ('cd'); +SELECT 'narrow' AS which, COUNT(DISTINCT a) AS distinct_vals FROM t_narrow +UNION ALL +SELECT 'wide', COUNT(DISTINCT a) FROM t_wide; +which distinct_vals +narrow 2 +wide 2 +SELECT 'narrow' AS which, COUNT(*) AS eq_padded FROM t_narrow WHERE a='ab' +UNION ALL +SELECT 'wide', COUNT(*) FROM t_wide WHERE a='ab'; +which eq_padded +narrow 2 +wide 2 +SELECT 'narrow' AS which, LENGTH(a) AS len, CHAR_LENGTH(a) AS clen +FROM t_narrow WHERE a='cd' +UNION ALL +SELECT 'wide', LENGTH(a), CHAR_LENGTH(a) FROM t_wide WHERE a='cd'; +which len clen +narrow 2 2 +wide 2 2 +# +# 6. Ordering and grouping agree. +# +# 'ab' and 'AB ' collate equal, so their order relative to each +# other is a tie that neither table is obliged to break the same +# way. BINARY breaks it deterministically without displacing the +# collation from the primary sort. For grouping, the same tie +# makes the representative value arbitrary, so count the groups +# rather than name them. +# +SELECT a FROM t_narrow ORDER BY a, BINARY a; +a +AB +ab +cd +SELECT a FROM t_wide ORDER BY a, BINARY a; +a +AB +ab +cd +SELECT 'narrow' AS which, COUNT(*) AS groups +FROM (SELECT a FROM t_narrow GROUP BY a) g +UNION ALL +SELECT 'wide', COUNT(*) FROM (SELECT a FROM t_wide GROUP BY a) g; +which groups +narrow 2 +wide 2 +SELECT 'narrow' AS which, COUNT(*) AS rows_in_group FROM t_narrow +GROUP BY a HAVING COUNT(*) > 1 +UNION ALL +SELECT 'wide', COUNT(*) FROM t_wide GROUP BY a HAVING COUNT(*) > 1; +which rows_in_group +narrow 2 +wide 2 +# +# 7. A UNION of the two is a VARCHAR of the wider declaration +# +CREATE TABLE t_union ENGINE=MEMORY AS +SELECT a FROM t_narrow UNION ALL SELECT a FROM t_wide; +SHOW CREATE TABLE t_union; +Table Create Table +t_union CREATE TABLE `t_union` ( + `a` varchar(200) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL +) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +DROP TABLE t_union; +# +# 8. Indexes on a promoted column keep VARCHAR key semantics: the +# padded duplicate is rejected, and NULL is still distinct. +# +CREATE TABLE t_uniq (a VARCHAR(200), UNIQUE KEY (a)) ENGINE=MEMORY +CHARACTER SET latin1; +INSERT INTO t_uniq VALUES ('ab'); +INSERT INTO t_uniq VALUES ('ab '); +ERROR 23000: Duplicate entry 'ab ' for key 'a' +INSERT INTO t_uniq VALUES ('AB'); +ERROR 23000: Duplicate entry 'AB' for key 'a' +INSERT INTO t_uniq VALUES (NULL), (NULL); +SELECT COUNT(*) AS rows_held FROM t_uniq; +rows_held +3 +SELECT a FROM t_uniq WHERE a='ab'; +a +ab +DROP TABLE t_uniq; +# +# 9. A BTREE index over a promoted column ranges correctly +# +CREATE TABLE t_btree (a VARCHAR(200), KEY USING BTREE (a)) ENGINE=MEMORY +CHARACTER SET latin1; +INSERT INTO t_btree SELECT CONCAT('v', LPAD(seq, 6, '0')) FROM seq_1_to_50; +SELECT COUNT(*) AS in_range FROM t_btree +WHERE a BETWEEN 'v000010' AND 'v000019'; +in_range +10 +SELECT a FROM t_btree WHERE a='v000042'; +a +v000042 +DROP TABLE t_btree; +# +# 10. NULL and the empty string stay distinct from each other +# +CREATE TABLE t_null (a VARCHAR(200)) ENGINE=MEMORY CHARACTER SET latin1; +INSERT INTO t_null VALUES (NULL), (''), (' '); +SELECT COUNT(*) AS n_null FROM t_null WHERE a IS NULL; +n_null +1 +SELECT COUNT(*) AS n_empty FROM t_null WHERE a=''; +n_empty +2 +SELECT a IS NULL AS is_null, LENGTH(a) AS len FROM t_null ORDER BY is_null, len; +is_null len +0 0 +0 2 +1 NULL +DROP TABLE t_null; +DROP TABLE t_narrow, t_wide; diff --git a/mysql-test/suite/heap/promotion_transparent.test b/mysql-test/suite/heap/promotion_transparent.test new file mode 100644 index 0000000000000..f47a8fb3cb8dd --- /dev/null +++ b/mysql-test/suite/heap/promotion_transparent.test @@ -0,0 +1,174 @@ +--source include/have_sequence.inc + +--echo # +--echo # A VARCHAR wide enough for the engine to store out of line must be +--echo # indistinguishable from a narrow one at the SQL layer. Every table +--echo # below is declared CHARACTER SET latin1 so that the declared width +--echo # in characters equals the width in bytes, and the threshold is +--echo # reached exactly where the declaration says it is. +--echo # +--echo # t_narrow stays inline. t_wide is stored as a blob by the engine. +--echo # + +CREATE TABLE t_narrow (a VARCHAR(8)) ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE t_wide (a VARCHAR(200)) ENGINE=MEMORY CHARACTER SET latin1; + +--echo # +--echo # 0. Promotion is in effect. +--echo # +--echo # Everything after this point is a check that promotion changes +--echo # nothing, and would pass just as well if the engine never promoted +--echo # anything. This is the check that it does. +--echo # +--echo # An inline VARCHAR(60000) reserves its declared width in every row, +--echo # so at a 1MB ceiling the table holds about 17 rows whatever the +--echo # values are. Stored out of line it holds the 200 short rows below +--echo # with room to spare. Without promotion this INSERT is refused. +--echo # + +SET @save_max_heap_table_size= @@max_heap_table_size; +SET max_heap_table_size= 1024*1024; +CREATE TABLE t_proof (a VARCHAR(60000)) ENGINE=MEMORY CHARACTER SET latin1; +INSERT INTO t_proof SELECT CONCAT('row', seq) FROM seq_1_to_200; +SELECT COUNT(*) AS rows_held FROM t_proof; +SELECT COUNT(*) AS intact FROM t_proof WHERE a = CONCAT('row', SUBSTR(a, 4)); +DROP TABLE t_proof; +SET max_heap_table_size= @save_max_heap_table_size; + +--echo # +--echo # 1. Declared type is unchanged +--echo # +SHOW CREATE TABLE t_narrow; +SHOW CREATE TABLE t_wide; + +SELECT column_name, data_type, column_type, + character_maximum_length, character_octet_length +FROM information_schema.columns +WHERE table_schema='test' AND table_name IN ('t_narrow','t_wide') +ORDER BY table_name, column_name; + +--echo # +--echo # 2. Type reported to the client is unchanged +--echo # +INSERT INTO t_narrow VALUES ('abc'); +INSERT INTO t_wide VALUES ('abc'); +--disable_ps_protocol +--enable_metadata +SELECT a FROM t_narrow; +SELECT a FROM t_wide; +--disable_metadata +--enable_ps_protocol + +--echo # +--echo # 3. A table built from the column keeps the type +--echo # +CREATE TABLE t_ctas ENGINE=MEMORY AS SELECT a FROM t_wide; +SHOW CREATE TABLE t_ctas; +DROP TABLE t_ctas; + +--echo # +--echo # 4. Width is the only difference across the threshold. At +--echo # CHARACTER SET latin1 the promotion threshold is 32 bytes, so +--echo # these two straddle it and must still agree on everything but +--echo # the declared width. +--echo # +CREATE TABLE t_below (a VARCHAR(32)) ENGINE=MEMORY CHARACTER SET latin1; +CREATE TABLE t_above (a VARCHAR(33)) ENGINE=MEMORY CHARACTER SET latin1; +SELECT table_name, data_type, character_maximum_length +FROM information_schema.columns +WHERE table_schema='test' AND table_name IN ('t_below','t_above') +ORDER BY table_name; +DROP TABLE t_below, t_above; + +--echo # +--echo # 5. VARCHAR comparison semantics, which differ from a blob's. +--echo # latin1_swedish_ci pads, so trailing spaces must not make a +--echo # value distinct, and comparison must stay case insensitive. +--echo # +DELETE FROM t_narrow; +DELETE FROM t_wide; +INSERT INTO t_narrow VALUES ('ab'), ('AB '), ('cd'); +INSERT INTO t_wide VALUES ('ab'), ('AB '), ('cd'); + +SELECT 'narrow' AS which, COUNT(DISTINCT a) AS distinct_vals FROM t_narrow +UNION ALL +SELECT 'wide', COUNT(DISTINCT a) FROM t_wide; + +SELECT 'narrow' AS which, COUNT(*) AS eq_padded FROM t_narrow WHERE a='ab' +UNION ALL +SELECT 'wide', COUNT(*) FROM t_wide WHERE a='ab'; + +SELECT 'narrow' AS which, LENGTH(a) AS len, CHAR_LENGTH(a) AS clen +FROM t_narrow WHERE a='cd' +UNION ALL +SELECT 'wide', LENGTH(a), CHAR_LENGTH(a) FROM t_wide WHERE a='cd'; + +--echo # +--echo # 6. Ordering and grouping agree. +--echo # +--echo # 'ab' and 'AB ' collate equal, so their order relative to each +--echo # other is a tie that neither table is obliged to break the same +--echo # way. BINARY breaks it deterministically without displacing the +--echo # collation from the primary sort. For grouping, the same tie +--echo # makes the representative value arbitrary, so count the groups +--echo # rather than name them. +--echo # +SELECT a FROM t_narrow ORDER BY a, BINARY a; +SELECT a FROM t_wide ORDER BY a, BINARY a; + +SELECT 'narrow' AS which, COUNT(*) AS groups +FROM (SELECT a FROM t_narrow GROUP BY a) g +UNION ALL +SELECT 'wide', COUNT(*) FROM (SELECT a FROM t_wide GROUP BY a) g; + +SELECT 'narrow' AS which, COUNT(*) AS rows_in_group FROM t_narrow + GROUP BY a HAVING COUNT(*) > 1 +UNION ALL +SELECT 'wide', COUNT(*) FROM t_wide GROUP BY a HAVING COUNT(*) > 1; + +--echo # +--echo # 7. A UNION of the two is a VARCHAR of the wider declaration +--echo # +CREATE TABLE t_union ENGINE=MEMORY AS + SELECT a FROM t_narrow UNION ALL SELECT a FROM t_wide; +SHOW CREATE TABLE t_union; +DROP TABLE t_union; + +--echo # +--echo # 8. Indexes on a promoted column keep VARCHAR key semantics: the +--echo # padded duplicate is rejected, and NULL is still distinct. +--echo # +CREATE TABLE t_uniq (a VARCHAR(200), UNIQUE KEY (a)) ENGINE=MEMORY + CHARACTER SET latin1; +INSERT INTO t_uniq VALUES ('ab'); +--error ER_DUP_ENTRY +INSERT INTO t_uniq VALUES ('ab '); +--error ER_DUP_ENTRY +INSERT INTO t_uniq VALUES ('AB'); +INSERT INTO t_uniq VALUES (NULL), (NULL); +SELECT COUNT(*) AS rows_held FROM t_uniq; +SELECT a FROM t_uniq WHERE a='ab'; +DROP TABLE t_uniq; + +--echo # +--echo # 9. A BTREE index over a promoted column ranges correctly +--echo # +CREATE TABLE t_btree (a VARCHAR(200), KEY USING BTREE (a)) ENGINE=MEMORY + CHARACTER SET latin1; +INSERT INTO t_btree SELECT CONCAT('v', LPAD(seq, 6, '0')) FROM seq_1_to_50; +SELECT COUNT(*) AS in_range FROM t_btree + WHERE a BETWEEN 'v000010' AND 'v000019'; +SELECT a FROM t_btree WHERE a='v000042'; +DROP TABLE t_btree; + +--echo # +--echo # 10. NULL and the empty string stay distinct from each other +--echo # +CREATE TABLE t_null (a VARCHAR(200)) ENGINE=MEMORY CHARACTER SET latin1; +INSERT INTO t_null VALUES (NULL), (''), (' '); +SELECT COUNT(*) AS n_null FROM t_null WHERE a IS NULL; +SELECT COUNT(*) AS n_empty FROM t_null WHERE a=''; +SELECT a IS NULL AS is_null, LENGTH(a) AS len FROM t_null ORDER BY is_null, len; +DROP TABLE t_null; + +DROP TABLE t_narrow, t_wide; diff --git a/mysql-test/suite/heap/row_limit.result b/mysql-test/suite/heap/row_limit.result new file mode 100644 index 0000000000000..549d3a79bc214 --- /dev/null +++ b/mysql-test/suite/heap/row_limit.result @@ -0,0 +1,74 @@ +# +# A MAX_ROWS limit counts rows. A row whose value is held out of +# line occupies several storage records, and those records must not +# be charged against the row limit. +# +SET @save_max_heap_table_size= @@max_heap_table_size; +SET max_heap_table_size= 64*1024*1024; +# A wide VARCHAR, which the engine keeps out of line +CREATE TABLE t1 (a VARCHAR(3000)) ENGINE=MEMORY CHARACTER SET latin1 +MAX_ROWS=1000; +INSERT INTO t1 SELECT REPEAT('z', 2900) FROM seq_1_to_1000; +SELECT COUNT(*) AS rows_held FROM t1; +rows_held +1000 +DROP TABLE t1; +# A declared blob column +CREATE TABLE t2 (b BLOB) ENGINE=MEMORY MAX_ROWS=1000; +INSERT INTO t2 SELECT REPEAT('z', 60000) FROM seq_1_to_100; +SELECT COUNT(*) AS rows_held FROM t2; +rows_held +100 +DROP TABLE t2; +# A narrow inline column, where a row has always been one record +CREATE TABLE t3 (a INT) ENGINE=MEMORY MAX_ROWS=1000; +INSERT INTO t3 SELECT seq FROM seq_1_to_1000; +SELECT COUNT(*) AS rows_held FROM t3; +rows_held +1000 +DROP TABLE t3; +# +# The limit is still a limit: the row past the last one is refused, +# whether or not the column is held out of line. +# +CREATE TABLE t4 (a VARCHAR(3000)) ENGINE=MEMORY CHARACTER SET latin1 +MAX_ROWS=10; +INSERT INTO t4 SELECT REPEAT('z', 2900) FROM seq_1_to_10; +SELECT COUNT(*) AS rows_held FROM t4; +rows_held +10 +INSERT INTO t4 VALUES (REPEAT('z', 2900)); +ERROR HY000: The table 't4' is full +SELECT COUNT(*) AS rows_held FROM t4; +rows_held +10 +DROP TABLE t4; +CREATE TABLE t5 (a INT) ENGINE=MEMORY MAX_ROWS=10; +INSERT INTO t5 SELECT seq FROM seq_1_to_10; +SELECT COUNT(*) AS rows_held FROM t5; +rows_held +10 +INSERT INTO t5 VALUES (11); +ERROR HY000: The table 't5' is full +SELECT COUNT(*) AS rows_held FROM t5; +rows_held +10 +DROP TABLE t5; +# +# A deleted row frees its place under the limit. +# +CREATE TABLE t6 (a VARCHAR(3000)) ENGINE=MEMORY CHARACTER SET latin1 +MAX_ROWS=10; +INSERT INTO t6 SELECT REPEAT('z', 2900) FROM seq_1_to_10; +DELETE FROM t6 LIMIT 4; +SELECT COUNT(*) AS rows_held FROM t6; +rows_held +6 +INSERT INTO t6 SELECT REPEAT('y', 2900) FROM seq_1_to_4; +SELECT COUNT(*) AS rows_held FROM t6; +rows_held +10 +INSERT INTO t6 VALUES (REPEAT('y', 2900)); +ERROR HY000: The table 't6' is full +DROP TABLE t6; +SET max_heap_table_size= @save_max_heap_table_size; diff --git a/mysql-test/suite/heap/row_limit.test b/mysql-test/suite/heap/row_limit.test new file mode 100644 index 0000000000000..17881182b8092 --- /dev/null +++ b/mysql-test/suite/heap/row_limit.test @@ -0,0 +1,68 @@ +--source include/have_sequence.inc + +--echo # +--echo # A MAX_ROWS limit counts rows. A row whose value is held out of +--echo # line occupies several storage records, and those records must not +--echo # be charged against the row limit. +--echo # + +SET @save_max_heap_table_size= @@max_heap_table_size; +SET max_heap_table_size= 64*1024*1024; + +--echo # A wide VARCHAR, which the engine keeps out of line +CREATE TABLE t1 (a VARCHAR(3000)) ENGINE=MEMORY CHARACTER SET latin1 + MAX_ROWS=1000; +INSERT INTO t1 SELECT REPEAT('z', 2900) FROM seq_1_to_1000; +SELECT COUNT(*) AS rows_held FROM t1; +DROP TABLE t1; + +--echo # A declared blob column +CREATE TABLE t2 (b BLOB) ENGINE=MEMORY MAX_ROWS=1000; +INSERT INTO t2 SELECT REPEAT('z', 60000) FROM seq_1_to_100; +SELECT COUNT(*) AS rows_held FROM t2; +DROP TABLE t2; + +--echo # A narrow inline column, where a row has always been one record +CREATE TABLE t3 (a INT) ENGINE=MEMORY MAX_ROWS=1000; +INSERT INTO t3 SELECT seq FROM seq_1_to_1000; +SELECT COUNT(*) AS rows_held FROM t3; +DROP TABLE t3; + +--echo # +--echo # The limit is still a limit: the row past the last one is refused, +--echo # whether or not the column is held out of line. +--echo # + +CREATE TABLE t4 (a VARCHAR(3000)) ENGINE=MEMORY CHARACTER SET latin1 + MAX_ROWS=10; +INSERT INTO t4 SELECT REPEAT('z', 2900) FROM seq_1_to_10; +SELECT COUNT(*) AS rows_held FROM t4; +--error ER_RECORD_FILE_FULL +INSERT INTO t4 VALUES (REPEAT('z', 2900)); +SELECT COUNT(*) AS rows_held FROM t4; +DROP TABLE t4; + +CREATE TABLE t5 (a INT) ENGINE=MEMORY MAX_ROWS=10; +INSERT INTO t5 SELECT seq FROM seq_1_to_10; +SELECT COUNT(*) AS rows_held FROM t5; +--error ER_RECORD_FILE_FULL +INSERT INTO t5 VALUES (11); +SELECT COUNT(*) AS rows_held FROM t5; +DROP TABLE t5; + +--echo # +--echo # A deleted row frees its place under the limit. +--echo # + +CREATE TABLE t6 (a VARCHAR(3000)) ENGINE=MEMORY CHARACTER SET latin1 + MAX_ROWS=10; +INSERT INTO t6 SELECT REPEAT('z', 2900) FROM seq_1_to_10; +DELETE FROM t6 LIMIT 4; +SELECT COUNT(*) AS rows_held FROM t6; +INSERT INTO t6 SELECT REPEAT('y', 2900) FROM seq_1_to_4; +SELECT COUNT(*) AS rows_held FROM t6; +--error ER_RECORD_FILE_FULL +INSERT INTO t6 VALUES (REPEAT('y', 2900)); +DROP TABLE t6; + +SET max_heap_table_size= @save_max_heap_table_size; diff --git a/mysql-test/suite/heap/tmp_table_convert_dedup.result b/mysql-test/suite/heap/tmp_table_convert_dedup.result index 966f3b88eeaad..0917518c11a17 100644 --- a/mysql-test/suite/heap/tmp_table_convert_dedup.result +++ b/mysql-test/suite/heap/tmp_table_convert_dedup.result @@ -15,37 +15,50 @@ # therefore never reaches that path. # # -# Setup: 1000 distinct pairs of ~1 KB values, each pair present twice, -# with the two copies of a pair adjacent, so that the write which -# overflows a deduplicating temporary table is always a duplicate of a -# row the conversion has already copied. +# Setup: 1000 distinct rows, each present twice, with the two copies +# adjacent, so that the write which overflows a deduplicating +# temporary table is always a duplicate of a row the conversion has +# already copied. # -# The columns are wide enough that the deduplicating key does not fit -# into a key of the on-disk temporary engine, so deduplication goes -# through a unique constraint rather than through a unique index. +# k is what makes the rows distinct. v and w are wide enough that the +# deduplicating key does not fit into a key of the on-disk temporary +# engine, so deduplication goes through a unique constraint rather +# than through a unique index, and they are left empty because that +# is all they are here for. +# +# Empty is also what keeps the overflow where this test needs it. A +# column that wide is stored out of line, so a row holding a value in +# one takes a record slot and a place for the value as well, and the +# write that finds the table full is then as likely to be the one +# storing the value as the duplicate that follows it. With nothing to +# store out of line a row costs one record slot, the table fills at +# the end of a write that stored a row, and the write that finds it +# full is the duplicate of that row. +# +# The limit is chosen so that the table holds well over the hundred +# rows the path above needs by the time it fills. # set @save_tmp_memory_table_size=@@tmp_memory_table_size; set @save_max_heap_table_size=@@max_heap_table_size; -CREATE TABLE t1 (id INT, v VARCHAR(1024), w VARCHAR(1024)) ENGINE=MyISAM; +CREATE TABLE t1 (id INT, k VARCHAR(6), v VARCHAR(1024), w VARCHAR(1024)) +ENGINE=MyISAM; INSERT INTO t1 -SELECT (seq+1) DIV 2, -CONCAT('v', LPAD((seq+1) DIV 2, 6, '0')), -CONCAT('w', LPAD((seq+1) DIV 2, 6, '0')) +SELECT (seq+1) DIV 2, LPAD((seq+1) DIV 2, 6, '0'), '', '' FROM seq_1_to_2000; -SELECT COUNT(*), COUNT(DISTINCT v, w) FROM t1; -COUNT(*) COUNT(DISTINCT v, w) +SELECT COUNT(*), COUNT(DISTINCT k, v, w) FROM t1; +COUNT(*) COUNT(DISTINCT k, v, w) 2000 1000 # # ================================================================ # Run 1: the temporary tables overflow and are converted # ================================================================ # -set @@tmp_memory_table_size=1024*1024*2; -set @@max_heap_table_size=1024*1024*2; +set @@tmp_memory_table_size=16384; +set @@max_heap_table_size=16384; # --- SELECT DISTINCT (end_write path) --- FLUSH STATUS; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT DISTINCT v, w FROM t1) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT DISTINCT k, v, w FROM t1) dt; rows_returned distinct_rows 1000 1000 SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED @@ -55,8 +68,8 @@ CONVERTED ON # --- SELECT DISTINCT with ORDER BY --- FLUSH STATUS; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT DISTINCT v, w FROM t1 ORDER BY v, w) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT DISTINCT k, v, w FROM t1 ORDER BY k, v, w) dt; rows_returned distinct_rows 1000 1000 SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED @@ -66,8 +79,8 @@ CONVERTED ON # --- GROUP BY (end_update path) --- FLUSH STATUS; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT v, w FROM t1 GROUP BY v, w) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT k, v, w FROM t1 GROUP BY k, v, w) dt; rows_returned distinct_rows 1000 1000 SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED @@ -77,8 +90,8 @@ CONVERTED ON # --- UNION DISTINCT (select_unit::write_record path) --- FLUSH STATUS; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM ((SELECT v, w FROM t1) UNION (SELECT v, w FROM t1)) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM ((SELECT k, v, w FROM t1) UNION (SELECT k, v, w FROM t1)) dt; rows_returned distinct_rows 1000 1000 SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED @@ -88,9 +101,10 @@ CONVERTED ON # --- INSERT SELECT DISTINCT: the duplicate reaches a user table --- FLUSH STATUS; -CREATE TABLE t2 (v VARCHAR(1024), w VARCHAR(1024)) ENGINE=MyISAM; -INSERT INTO t2 SELECT DISTINCT v, w FROM t1; -SELECT COUNT(*) AS rows_stored, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows +CREATE TABLE t2 (k VARCHAR(6), v VARCHAR(1024), w VARCHAR(1024)) +ENGINE=MyISAM; +INSERT INTO t2 SELECT DISTINCT k, v, w FROM t1; +SELECT COUNT(*) AS rows_stored, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows FROM t2; rows_stored distinct_rows 1000 1000 @@ -108,25 +122,26 @@ DROP TABLE t2; set @@tmp_memory_table_size=1024*1024*512; set @@max_heap_table_size=1024*1024*512; FLUSH STATUS; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT DISTINCT v, w FROM t1) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT DISTINCT k, v, w FROM t1) dt; rows_returned distinct_rows 1000 1000 -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT DISTINCT v, w FROM t1 ORDER BY v, w) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT DISTINCT k, v, w FROM t1 ORDER BY k, v, w) dt; rows_returned distinct_rows 1000 1000 -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT v, w FROM t1 GROUP BY v, w) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT k, v, w FROM t1 GROUP BY k, v, w) dt; rows_returned distinct_rows 1000 1000 -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM ((SELECT v, w FROM t1) UNION (SELECT v, w FROM t1)) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM ((SELECT k, v, w FROM t1) UNION (SELECT k, v, w FROM t1)) dt; rows_returned distinct_rows 1000 1000 -CREATE TABLE t2 (v VARCHAR(1024), w VARCHAR(1024)) ENGINE=MyISAM; -INSERT INTO t2 SELECT DISTINCT v, w FROM t1; -SELECT COUNT(*) AS rows_stored, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows +CREATE TABLE t2 (k VARCHAR(6), v VARCHAR(1024), w VARCHAR(1024)) +ENGINE=MyISAM; +INSERT INTO t2 SELECT DISTINCT k, v, w FROM t1; +SELECT COUNT(*) AS rows_stored, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows FROM t2; rows_stored distinct_rows 1000 1000 diff --git a/mysql-test/suite/heap/tmp_table_convert_dedup.test b/mysql-test/suite/heap/tmp_table_convert_dedup.test index f3993c14891d1..7ac4ea6674a29 100644 --- a/mysql-test/suite/heap/tmp_table_convert_dedup.test +++ b/mysql-test/suite/heap/tmp_table_convert_dedup.test @@ -23,27 +23,40 @@ --disable_ps2_protocol --echo # ---echo # Setup: 1000 distinct pairs of ~1 KB values, each pair present twice, ---echo # with the two copies of a pair adjacent, so that the write which ---echo # overflows a deduplicating temporary table is always a duplicate of a ---echo # row the conversion has already copied. +--echo # Setup: 1000 distinct rows, each present twice, with the two copies +--echo # adjacent, so that the write which overflows a deduplicating +--echo # temporary table is always a duplicate of a row the conversion has +--echo # already copied. --echo # ---echo # The columns are wide enough that the deduplicating key does not fit ---echo # into a key of the on-disk temporary engine, so deduplication goes ---echo # through a unique constraint rather than through a unique index. +--echo # k is what makes the rows distinct. v and w are wide enough that the +--echo # deduplicating key does not fit into a key of the on-disk temporary +--echo # engine, so deduplication goes through a unique constraint rather +--echo # than through a unique index, and they are left empty because that +--echo # is all they are here for. +--echo # +--echo # Empty is also what keeps the overflow where this test needs it. A +--echo # column that wide is stored out of line, so a row holding a value in +--echo # one takes a record slot and a place for the value as well, and the +--echo # write that finds the table full is then as likely to be the one +--echo # storing the value as the duplicate that follows it. With nothing to +--echo # store out of line a row costs one record slot, the table fills at +--echo # the end of a write that stored a row, and the write that finds it +--echo # full is the duplicate of that row. +--echo # +--echo # The limit is chosen so that the table holds well over the hundred +--echo # rows the path above needs by the time it fills. --echo # set @save_tmp_memory_table_size=@@tmp_memory_table_size; set @save_max_heap_table_size=@@max_heap_table_size; -CREATE TABLE t1 (id INT, v VARCHAR(1024), w VARCHAR(1024)) ENGINE=MyISAM; +CREATE TABLE t1 (id INT, k VARCHAR(6), v VARCHAR(1024), w VARCHAR(1024)) +ENGINE=MyISAM; INSERT INTO t1 -SELECT (seq+1) DIV 2, - CONCAT('v', LPAD((seq+1) DIV 2, 6, '0')), - CONCAT('w', LPAD((seq+1) DIV 2, 6, '0')) +SELECT (seq+1) DIV 2, LPAD((seq+1) DIV 2, 6, '0'), '', '' FROM seq_1_to_2000; -SELECT COUNT(*), COUNT(DISTINCT v, w) FROM t1; +SELECT COUNT(*), COUNT(DISTINCT k, v, w) FROM t1; --echo # --echo # ================================================================ @@ -51,46 +64,47 @@ SELECT COUNT(*), COUNT(DISTINCT v, w) FROM t1; --echo # ================================================================ --echo # -set @@tmp_memory_table_size=1024*1024*2; -set @@max_heap_table_size=1024*1024*2; +set @@tmp_memory_table_size=16384; +set @@max_heap_table_size=16384; --echo # --- SELECT DISTINCT (end_write path) --- FLUSH STATUS; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT DISTINCT v, w FROM t1) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT DISTINCT k, v, w FROM t1) dt; SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED FROM INFORMATION_SCHEMA.SESSION_STATUS WHERE VARIABLE_NAME = 'CREATED_TMP_DISK_TABLES'; --echo # --- SELECT DISTINCT with ORDER BY --- FLUSH STATUS; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT DISTINCT v, w FROM t1 ORDER BY v, w) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT DISTINCT k, v, w FROM t1 ORDER BY k, v, w) dt; SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED FROM INFORMATION_SCHEMA.SESSION_STATUS WHERE VARIABLE_NAME = 'CREATED_TMP_DISK_TABLES'; --echo # --- GROUP BY (end_update path) --- FLUSH STATUS; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT v, w FROM t1 GROUP BY v, w) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT k, v, w FROM t1 GROUP BY k, v, w) dt; SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED FROM INFORMATION_SCHEMA.SESSION_STATUS WHERE VARIABLE_NAME = 'CREATED_TMP_DISK_TABLES'; --echo # --- UNION DISTINCT (select_unit::write_record path) --- FLUSH STATUS; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM ((SELECT v, w FROM t1) UNION (SELECT v, w FROM t1)) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM ((SELECT k, v, w FROM t1) UNION (SELECT k, v, w FROM t1)) dt; SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED FROM INFORMATION_SCHEMA.SESSION_STATUS WHERE VARIABLE_NAME = 'CREATED_TMP_DISK_TABLES'; --echo # --- INSERT SELECT DISTINCT: the duplicate reaches a user table --- FLUSH STATUS; -CREATE TABLE t2 (v VARCHAR(1024), w VARCHAR(1024)) ENGINE=MyISAM; -INSERT INTO t2 SELECT DISTINCT v, w FROM t1; -SELECT COUNT(*) AS rows_stored, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows +CREATE TABLE t2 (k VARCHAR(6), v VARCHAR(1024), w VARCHAR(1024)) +ENGINE=MyISAM; +INSERT INTO t2 SELECT DISTINCT k, v, w FROM t1; +SELECT COUNT(*) AS rows_stored, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows FROM t2; SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED FROM INFORMATION_SCHEMA.SESSION_STATUS @@ -107,17 +121,18 @@ set @@tmp_memory_table_size=1024*1024*512; set @@max_heap_table_size=1024*1024*512; FLUSH STATUS; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT DISTINCT v, w FROM t1) dt; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT DISTINCT v, w FROM t1 ORDER BY v, w) dt; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM (SELECT v, w FROM t1 GROUP BY v, w) dt; -SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows -FROM ((SELECT v, w FROM t1) UNION (SELECT v, w FROM t1)) dt; -CREATE TABLE t2 (v VARCHAR(1024), w VARCHAR(1024)) ENGINE=MyISAM; -INSERT INTO t2 SELECT DISTINCT v, w FROM t1; -SELECT COUNT(*) AS rows_stored, COUNT(DISTINCT CONCAT(v,'#',w)) AS distinct_rows +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT DISTINCT k, v, w FROM t1) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT DISTINCT k, v, w FROM t1 ORDER BY k, v, w) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM (SELECT k, v, w FROM t1 GROUP BY k, v, w) dt; +SELECT COUNT(*) AS rows_returned, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows +FROM ((SELECT k, v, w FROM t1) UNION (SELECT k, v, w FROM t1)) dt; +CREATE TABLE t2 (k VARCHAR(6), v VARCHAR(1024), w VARCHAR(1024)) +ENGINE=MyISAM; +INSERT INTO t2 SELECT DISTINCT k, v, w FROM t1; +SELECT COUNT(*) AS rows_stored, COUNT(DISTINCT CONCAT(k,'#',v,'#',w)) AS distinct_rows FROM t2; SELECT IF(VARIABLE_VALUE > 0, 'ON', 'OFF') AS CONVERTED FROM INFORMATION_SCHEMA.SESSION_STATUS diff --git a/mysql-test/suite/perfschema/r/memory_table_io.result b/mysql-test/suite/perfschema/r/memory_table_io.result index a29bd7f32b138..825461c8d0c46 100644 --- a/mysql-test/suite/perfschema/r/memory_table_io.result +++ b/mysql-test/suite/perfschema/r/memory_table_io.result @@ -96,6 +96,7 @@ wait/io/table/sql/handler TABLE test no_index_tab fetch 1 wait/io/table/sql/handler TABLE test no_index_tab fetch 1 wait/io/table/sql/handler TABLE test no_index_tab delete 1 wait/io/table/sql/handler TABLE test no_index_tab fetch 1 +wait/io/table/sql/handler TABLE test no_index_tab fetch 1 wait/io/table/sql/handler TABLE test marker insert 1 wait/io/table/sql/handler TABLE test no_index_tab fetch 2 wait/io/table/sql/handler TABLE test marker insert 1 diff --git a/mysql-test/suite/versioning/r/partition.result b/mysql-test/suite/versioning/r/partition.result index c2a09cf7794a8..2eff4b78f7402 100644 --- a/mysql-test/suite/versioning/r/partition.result +++ b/mysql-test/suite/versioning/r/partition.result @@ -1145,8 +1145,11 @@ f varchar(45000) ) charset=latin1 with system versioning engine=memory partition by system_time interval 1 year (partition p1 history, partition pn current); -# fill the table until full -insert into t1 () values (),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(); +# fill the table until full. The rows must carry their full width +# as data: an engine that stores a wide VARCHAR out of line charges +# for the bytes present, so rows left at the default would occupy +# almost nothing and the table would never fill. +insert into t1 (f) select repeat('a', 45000) from seq_1_to_21; insert into t1 (f) select f from t1; ERROR HY000: The table 't1' is full # leave space for exactly one record in current partition @@ -1162,7 +1165,7 @@ f varchar(45000) ) charset=latin1 with system versioning engine=memory partition by system_time interval 1 year (partition p1 history, partition pn current); -insert into t1 () values (),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(); +insert into t1 (f) select repeat('a', 45000) from seq_1_to_18; select * into outfile 'MDEV-17891.data' from t1; load data infile 'MDEV-17891.data' replace into table t1; load data infile 'MDEV-17891.data' replace into table t1; diff --git a/mysql-test/suite/versioning/t/partition.test b/mysql-test/suite/versioning/t/partition.test index 47f1b5f43f1cf..e1dd32775066f 100644 --- a/mysql-test/suite/versioning/t/partition.test +++ b/mysql-test/suite/versioning/t/partition.test @@ -999,8 +999,11 @@ create or replace table t1 ( partition by system_time interval 1 year (partition p1 history, partition pn current); ---echo # fill the table until full -insert into t1 () values (),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(); +--echo # fill the table until full. The rows must carry their full width +--echo # as data: an engine that stores a wide VARCHAR out of line charges +--echo # for the bytes present, so rows left at the default would occupy +--echo # almost nothing and the table would never fill. +insert into t1 (f) select repeat('a', 45000) from seq_1_to_21; --error ER_RECORD_FILE_FULL insert into t1 (f) select f from t1; --echo # leave space for exactly one record in current partition @@ -1018,7 +1021,7 @@ create or replace table t1 ( partition by system_time interval 1 year (partition p1 history, partition pn current); -insert into t1 () values (),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(),(); +insert into t1 (f) select repeat('a', 45000) from seq_1_to_18; select * into outfile 'MDEV-17891.data' from t1; load data infile 'MDEV-17891.data' replace into table t1; diff --git a/sql/create_tmp_table.h b/sql/create_tmp_table.h index 8af7f70890476..e9cfa5e8f4110 100644 --- a/sql/create_tmp_table.h +++ b/sql/create_tmp_table.h @@ -47,6 +47,16 @@ class Create_tmp_table: public Data_type_statistics uint m_null_count[2]; // counter for distinct/other blob fields uint m_blobs_count[2]; + /* + Whether any distinct column is declared as a blob. A declared blob + has no maximum width, so a unique index over one has to be a hash. A + column whose data is out of line only because it was promoted keeps + its declared width and can be indexed normally. + + Unlike the counters beside it this is not wanted per counter: nothing + asks how many there are, and nothing asks about the other columns. + */ + bool m_distinct_has_unbounded_blob; // counter for "tails" of bit fields which do not fit in a byte uint m_uneven_bit[2]; diff --git a/sql/field.cc b/sql/field.cc index 5a6c55de06894..8053e4d20ef76 100644 --- a/sql/field.cc +++ b/sql/field.cc @@ -2530,7 +2530,7 @@ uint Field::fill_cache_field(CACHE_FIELD *copy) copy->str= ptr; copy->length= pack_length_in_rec(); copy->field= this; - if (flags & BLOB_FLAG) + if (data_is_out_of_line()) { copy->type= CACHE_BLOB; copy->length-= portable_sizeof_char_ptr; @@ -8152,26 +8152,111 @@ bool Field_varstring::memcpy_field_possible(const Field *from) const { return (Field_str::memcpy_field_possible(from) && !compression_method() == !from->compression_method() && + !data_is_out_of_line() && !from->data_is_out_of_line() && length_bytes == ((Field_varstring*) from)->length_bytes && (table->file && !(table->file->ha_table_flags() & HA_RECORD_MUST_BE_CLEAN_ON_WRITE))); } +bool Field_varstring::copy() +{ + DBUG_ASSERT(promoted); + if (value.copy((const char*) get_data(), get_length(), charset())) + { + Field_varstring::reset(); + return true; + } + set_data_ptr((const uchar*) value.ptr()); + return false; +} + + int Field_varstring::store(const char *from,size_t length,CHARSET_INFO *cs) { DBUG_ASSERT(marked_for_write_or_computed()); uint copy_length; + size_t new_length; + char buff[STRING_BUFFER_USUAL_SIZE]; + String tmpstr(buff, sizeof(buff), &my_charset_bin); int rc; - rc= well_formed_copy_with_check((char*) get_data(), field_length, + if (!promoted) + { + rc= well_formed_copy_with_check((char*) ptr + length_bytes, field_length, + cs, from, length, + Field_varstring::char_length(), + true, ©_length); + store_length(copy_length); + return rc; + } + + /* + The payload of a promoted field lives in value, and that is also + where a reader of this field is pointed, so a statement that stores + what it just read from this same column -- UPDATE t SET c = c -- has + its source and its destination in one buffer. Take the source + somewhere else first, the way Field_blob::store() does. + */ + if (from >= value.ptr() && from <= value.ptr() + value.length()) + { + if (tmpstr.copy(from, length, cs)) + goto oom_error; + from= tmpstr.ptr(); + } + + /* + Size the buffer to the value rather than to the declared width: a + conversion produces at most mbmaxlen bytes per source byte, and the + column cannot hold more than it was declared to. + */ + new_length= MY_MIN((size_t) field_length, mbmaxlen() * length); + if (value.alloc(new_length)) + goto oom_error; + + rc= well_formed_copy_with_check((char*) value.ptr(), new_length, cs, from, length, Field_varstring::char_length(), true, ©_length); + value.length(copy_length); - store_length(copy_length); + if (table && table->blob_storage) + { + /* + GROUP_CONCAT with ORDER BY or DISTINCT keeps many rows alive at + once and sorts them afterwards, so each row needs bytes of its own: + value is one buffer per Field and would leave every row pointing at + whichever value was stored last. + + Field_blob::store() sends its values through + Field_blob::handle_group_concat(), which first cuts them to + group_concat_max_len, because a blob has no declared width and the + storage would otherwise grow without bound. This column does have + one and the copy above has already applied it, so the value is + stored whole and its cut mark is clear. Cutting here as well would + change what a wide VARCHAR reports: the answer is assembled from + values that were never shortened, so the row at which the answer + overflows, and whether any value in it was cut, both stay what they + were before the column's payload moved out of the record. + */ + char *kept= table->blob_storage->store(value.ptr(), copy_length, false); + if (!kept) + { + reset(); + return -1; + } + store_length(copy_length); + set_data_ptr((const uchar*) kept); + return rc; + } + store_length(copy_length); + set_data_ptr((const uchar*) value.ptr()); return rc; + +oom_error: + reset(); + return -1; } @@ -8247,6 +8332,13 @@ bool Field_varstring::send(Protocol *protocol) void Field_varstring::mark_unused_memory_as_defined() { + /* + A promoted field has no slack: the record holds a pointer, and the + bytes it points at are the engine's, of which only the value itself + is ours to reason about. + */ + if (promoted) + return; uint used_length __attribute__((unused)) = get_length(); MEM_MAKE_DEFINED(get_data() + used_length, field_length - used_length); } @@ -8270,8 +8362,8 @@ int Field_varstring::cmp(const uchar *a_ptr, const uchar *b_ptr) const } set_if_smaller(a_length, field_length); set_if_smaller(b_length, field_length); - diff= field_charset()->strnncollsp(a_ptr + length_bytes, a_length, - b_ptr + length_bytes, b_length); + diff= field_charset()->strnncollsp(get_data(a_ptr), a_length, + get_data(b_ptr), b_length); return diff; } @@ -8297,9 +8389,9 @@ int Field_varstring::cmp_prefix(const uchar *a_ptr, const uchar *b_ptr, b_length= uint2korr(b_ptr); } return field_charset()->coll->strnncollsp_nchars(field_charset(), - a_ptr + length_bytes, + get_data(a_ptr), a_length, - b_ptr + length_bytes, + get_data(b_ptr), b_length, prefix_char_len, 0); @@ -8316,11 +8408,11 @@ int Field_varstring::key_cmp(const uchar *key_ptr, uint max_key_length) const size_t length= length_bytes == 1 ? (uint) *ptr : uint2korr(ptr); size_t local_char_length= max_key_length / mbmaxlen(); - local_char_length= field_charset()->charpos(ptr + length_bytes, - ptr + length_bytes + length, + local_char_length= field_charset()->charpos(get_data(), + get_data() + length, local_char_length); set_if_smaller(length, local_char_length); - return field_charset()->strnncollsp(ptr + length_bytes, + return field_charset()->strnncollsp(get_data(), length, key_ptr + HA_KEY_BLOB_LENGTH, uint2korr(key_ptr)); @@ -8449,7 +8541,7 @@ uchar *Field_varstring::pack(uchar *to, const uchar *from) const /* Store bytes of string */ if (length > 0) - memcpy(to, from+length_bytes, length); + memcpy(to, get_data(from), length); return to+length; } @@ -8493,12 +8585,30 @@ Field_varstring::unpack(uchar *to, const uchar *from, const uchar *from_end, to[0]= *from++; to[1]= *from++; } - if (length) + if (from + length > from_end || length > field_length) + return 0; // Error in data + if (promoted) { - if (from + length > from_end || length > field_length) - return 0; // Error in data - memcpy(to+ length_bytes, from, length); + /* + A promoted record slot holds a pointer where an inline one holds + the value, and has no room for the value at all. Point it at the + row being unpacked, the way Field_blob::unpack() points a blob at + it: the row outlives the read, and a caller that needs the bytes + for longer asks copy() for a set of its own. A zero length leaves + nothing to point at, so the slot takes a null pointer, which + get_data() reads as the empty string. + */ + const uchar *data= length ? from : NULL; + /* + A reader takes the length and the pointer from the same slot, so + the prefix written above has to describe the bytes pointed at + here, whichever width the row carried its own prefix in. + */ + DBUG_ASSERT(get_length(to) == length); + set_data_ptr(to, data); } + else if (length) + memcpy(to+ length_bytes, from, length); return from+length; } @@ -8513,6 +8623,16 @@ uint Field_varstring::packed_col_length() const uint Field_varstring::max_packed_col_length(uint max_length) const { + /* + pack() writes a length prefix and then the value itself, wherever + the record keeps the value. Callers ask this question with + pack_length(), which for a promoted field describes the pointer in + the record instead, so answer a promoted field from its declared + width, which is what pack() is bounded by and what pack_length() + reports for every other VARCHAR. + */ + if (promoted) + max_length= inline_pack_length(); return (max_length > 255 ? 2 : 1)+max_length; } @@ -8572,7 +8692,7 @@ int Field_varstring::cmp_binary(const uchar *a_ptr, const uchar *b_ptr, set_if_smaller(b_length, max_length); if (a_length != b_length) return 1; - return memcmp(a_ptr+length_bytes, b_ptr+length_bytes, a_length); + return memcmp(get_data(a_ptr), get_data(b_ptr), a_length); } @@ -8585,7 +8705,16 @@ Field *Field_varstring::make_new_field(MEM_ROOT *root, TABLE *new_table, keep_type, param); if (res) + { res->length_bytes= length_bytes; + /* + Promotion belongs to the table whose record layout was computed + with it, so the new table decides for itself. The value buffer + that came with the copied bytes has already been disowned by + reset_fields(). + */ + res->promoted= false; + } return res; } @@ -8680,7 +8809,7 @@ void Field_varstring::hash_not_null(Hasher *hasher) DBUG_ASSERT(marked_for_read()); DBUG_ASSERT(!is_null()); uint len= length_bytes == 1 ? (uint) *ptr : uint2korr(ptr); - hasher->add(charset(), ptr + length_bytes, len); + hasher->add(charset(), get_data(), len); } @@ -8995,14 +9124,13 @@ int Field_blob::handle_group_concat(const char *from, size_t length, tmp= table->blob_storage->store(from, new_length, cut); if (!tmp) - goto oom_error; + { + reset(); + return -1; + } Field_blob::store_length(new_length); bmove(ptr + packlength, (uchar*) &tmp, sizeof(char*)); return 0; - -oom_error: - reset(); - return -1; } diff --git a/sql/field.h b/sql/field.h index d8c22cd026d16..18b74685618fe 100644 --- a/sql/field.h +++ b/sql/field.h @@ -527,7 +527,9 @@ inline bool is_temporal_type_with_date(enum_field_types type) /* Only needed for calc_group_buffer(), where we have an enum_field_types but no Field object. - In all other cases use field->flags & BLOB_FLAG. + In all other cases ask the Field: `flags & BLOB_FLAG' for whether the + column is declared as a blob, data_is_out_of_line() for where its + payload lives. */ static inline bool is_any_blob_field_type(enum_field_types type) { @@ -1227,6 +1229,26 @@ class Field: public Value_source table, which is located on disk). */ virtual uint32 pack_length_in_rec() const { return pack_length(); } + /* + True when the payload does not live inside the record buffer: the + record holds a length and a pointer to the bytes instead. A record + copied wholesale therefore shares the payload rather than owning a + copy of it, and code that saves a row for longer than the reading + handler keeps its buffer alive must materialise the bytes first. + + Ask this, not `flags & BLOB_FLAG', wherever the question is where the + data lives. BLOB_FLAG answers a different question -- whether the + column is declared as a blob -- and `sql_select.cc' asserts that it + agrees with type(), so it cannot be set on anything reported as a + VARCHAR. + */ + virtual bool data_is_out_of_line() const + { return (flags & BLOB_FLAG) != 0; } + /* + Width of the length prefix the record holds in front of the payload, + zero for a column that has none. + */ + virtual uint length_size() const { return 0; } virtual bool compatible_field_size(uint metadata, const Relay_log_info *rli, uint16 mflags, int *order) const; virtual uint pack_length_from_metadata(uint field_metadata) const @@ -1626,6 +1648,34 @@ class Field: public Value_source virtual void sort_string(uchar *buff,uint length)=0; virtual bool optimize_range(uint idx, uint part) const; virtual void free() {} + /* + For a field whose data is out of line: replace whatever the record + points at with a copy this Field owns, so that the value outlives the + buffer it was read from. Returns true on out-of-memory. A field + that keeps its data in the record has nothing to do. + */ + virtual bool copy() { return false; } + + /* + A field whose data is out of line keeps a length and a pointer to + the payload in the record. The first two read that pair out of any + image of this field's record slot -- record[0], another record + buffer, or a key -- and reach the payload it names. The third takes + such an image, the length prefix followed by the payload, and writes + the pair into record[0], pointing it at the payload where it already + lies rather than copying it. + + A field that keeps its data in the record answers none of them, so + ask data_is_out_of_line() first. + */ + virtual const uchar *out_of_line_data(const uchar *rec) const + { DBUG_ASSERT(0); return NULL; } + virtual uint32 out_of_line_length(const uchar *rec) const + { DBUG_ASSERT(0); return 0; } + virtual void set_out_of_line_image(const uchar *image) + { DBUG_ASSERT(0); } + const uchar *out_of_line_data() const { return out_of_line_data(ptr); } + uint32 out_of_line_length() const { return out_of_line_length(ptr); } /* Creates a copy of this field which can be added to any table, and the @@ -2341,7 +2391,6 @@ class Field_str :public Field { bool val_bool() override { return val_real() != 0e0; } bool str_needs_quotes() const override { return true; } bool eq_cmp_as_binary() override { return MY_TEST(flags & BINARY_FLAG); } - virtual uint length_size() const { return 0; } double pos_in_interval(Field *min, Field *max) override { return pos_in_interval_val_str(min, max, length_size()); @@ -2403,6 +2452,7 @@ class Field_longstr :public Field_str uint max_length, uint *out_length, CHARSET_INFO *cs, size_t nchars); + String *uncompress(String *val_buffer, String *val_ptr, const uchar *from, uint from_length) const; public: @@ -4289,6 +4339,19 @@ class Field_varstring :public Field_longstr { } const uchar *get_data(const uchar *ptr_arg) const { + if (promoted) + { + const uchar *data= *((uchar* const *) (ptr_arg + length_bytes)); + /* + A record slot that has not been stored into holds a null + pointer, beside the zero length reset() left there. An inline + VARCHAR always has an address to offer for an empty value, and + the collation functions a reader goes on to call require one, + so offer an address here too. Field_blob::val_str() takes the + same precaution. + */ + return data ? data : (const uchar*) ""; + } return ptr_arg + length_bytes; } uint get_length() const @@ -4316,6 +4379,25 @@ class Field_varstring :public Field_longstr { static const uint MAX_SIZE; /* Store number of bytes used to store length (1 or 2) */ uint32 length_bytes; + /* + Promotion moves the payload out of the record: the slot holds the + length prefix followed by a pointer, the same shape a blob has, and + the bytes live in the storage engine's memory or in own_data below. + Only the storage geometry changes. type(), type_handler() and + sql_type() keep answering VARCHAR, so nothing above the Field can + tell the difference. + */ + bool promoted; + /* + Where a value stored through this Field is kept while a promoted + record points at it. A value read from the engine points into the + engine's own memory instead, until copy() below is asked to give the + record a copy it owns. This is the same arrangement Field_blob has, + and it carries the same rule: one buffer per Field, so a caller that + needs two rows alive at once must take its copy of the first before + reading the second. + */ + String value; Field_varstring(uchar *ptr_arg, uint32 len_arg, uint length_bytes_arg, uchar *null_ptr_arg, uchar null_bit_arg, @@ -4323,7 +4405,7 @@ class Field_varstring :public Field_longstr { TABLE_SHARE *share, const DTCollation &collation) :Field_longstr(ptr_arg, len_arg, null_ptr_arg, null_bit_arg, unireg_check_arg, field_name_arg, collation), - length_bytes(length_bytes_arg) + length_bytes(length_bytes_arg), promoted(false) { share->varchar_fields++; } @@ -4332,7 +4414,7 @@ class Field_varstring :public Field_longstr { TABLE_SHARE *share, const DTCollation &collation) :Field_longstr((uchar*) 0,len_arg, maybe_null_arg ? (uchar*) "": 0, 0, NONE, field_name_arg, collation), - length_bytes(len_arg < 256 ? 1 :2) + length_bytes(len_arg < 256 ? 1 :2), promoted(false) { share->varchar_fields++; } @@ -4340,21 +4422,106 @@ class Field_varstring :public Field_longstr { const Type_handler *type_handler() const override; en_fieldtype tmp_engine_column_type(bool use_packed_rows) const override { - return FIELD_VARCHAR; + /* + What the engine must think the column is, which is not what the + user is told it is. A promoted record slot holds a length and a + pointer, so an engine handed FIELD_VARCHAR would read the pointer + as though it were the value. + */ + return promoted ? FIELD_BLOB : FIELD_VARCHAR; } enum ha_base_keytype key_type() const override; - uint16 key_part_flag() const override { return HA_VAR_LENGTH_PART; } + uint16 key_part_flag() const override + { + /* + A promoted column reaches the engine's key code as a length prefix + followed by a pointer, which is what HA_BLOB_PART announces. The + prefix is still the VARCHAR's own one or two bytes, not a blob's + four, so the segment keeps its VARTEXT type alongside the flag. + */ + return promoted ? (HA_VAR_LENGTH_PART | HA_BLOB_PART) + : HA_VAR_LENGTH_PART; + } uint16 key_part_length_bytes() const override { return HA_KEY_BLOB_LENGTH; } uint row_pack_length() const override { return field_length; } bool zero_pack() const override { return false; } - int reset() override { bzero(ptr,field_length+length_bytes); return 0; } + int reset() override { bzero(ptr, pack_length()); return 0; } + bool data_is_out_of_line() const override { return promoted; } + /* + Move this field's payload out of the record. Called once, while the + table's record layout is still being computed, so that pack_length() + below already answers for the new shape. + */ + void promote() + { + DBUG_ASSERT(!promoted); + promoted= true; + } + /* Point a promoted record slot at bytes the field does not own. */ + void set_data_ptr(uchar *rec, const uchar *data) + { + DBUG_ASSERT(promoted); + memcpy(rec + length_bytes, &data, portable_sizeof_char_ptr); + } + void set_data_ptr(const uchar *data) { set_data_ptr(ptr, data); } + const uchar *out_of_line_data(const uchar *rec) const override + { + DBUG_ASSERT(promoted); + return get_data(rec); + } + uint32 out_of_line_length(const uchar *rec) const override + { + DBUG_ASSERT(promoted); + return get_length(rec); + } + void set_out_of_line_image(const uchar *image) override + { + DBUG_ASSERT(promoted); + memcpy(ptr, image, length_bytes); + set_data_ptr(image + length_bytes); + } + /* + Give the record a copy of the value it currently points at, so that + it survives the engine moving on to the next row. + */ + bool copy() override; + void free() override { value.free(); } + /* + A field made with make_new_field() gets this one's bytes, the + pointer inside value among them, but must not inherit the buffer: + the field it was copied from is still using it. Forget the buffer + without freeing it. Field_blob resets its own value here for the + same reason. + */ + void reset_fields() override + { + bzero((uchar*) &value, sizeof value); + value.set_charset(Field_str::charset()); + } uint32 max_data_length() const override { return field_length + (field_length > 255 ? 2 : 1); } + /* What the record slot costs while it holds the value inline. */ + uint32 inline_pack_length() const + { return (uint32) field_length + length_bytes; } uint32 pack_length() const override - { return (uint32) field_length+length_bytes; } + { + return promoted ? (uint32) length_bytes + portable_sizeof_char_ptr + : inline_pack_length(); + } uint32 key_length() const override { return (uint32) field_length; } + uint32 key_pack_length() const override + { + /* + A key part holds the value inline whether or not the record does, + so it stays as wide as the column was declared. The default is + pack_length(), which for a promoted field describes the pointer in + the record instead. Field_blob_key overrides this for the same + reason. + */ + return inline_pack_length(); + } uint32 sort_length() const override { return (uint32) field_length + sort_suffix_length(); @@ -4365,8 +4532,28 @@ class Field_varstring :public Field_longstr { } Copy_func *get_copy_func(const Field *from) const override; bool memcpy_field_possible(const Field *from) const override; + bool eq_def(const Field *field) const override + { + /* + Field::eq_def asks whether two columns hold the same values and + answers with pack_length(), which for a promoted field describes + the pointer in the record instead. Two VARCHARs hold the same + values when they are declared the same width and carry the same + length prefix, wherever either one keeps its bytes. + */ + return real_type() == field->real_type() && + charset() == field->charset() && + field_length == field->field_length && + length_bytes == ((const Field_varstring*) field)->length_bytes; + } void update_data_type_statistics(Data_type_statistics *st) const override { + /* + A promoted field occupies a slot in blob_field[], so it has to be + counted where that array's length comes from. + */ + if (promoted) + st->m_blob_count++; st->m_variable_string_count++; st->m_variable_string_total_length+= pack_length(); } @@ -4735,6 +4922,12 @@ class Field_blob :public Field_longstr { memcpy(ptr,length,packlength); memcpy(ptr+packlength, &data,sizeof(char*)); } + const uchar *out_of_line_data(const uchar *rec) const override + { return get_ptr(rec); } + uint32 out_of_line_length(const uchar *rec) const override + { return get_length(rec); } + void set_out_of_line_image(const uchar *image) override + { set_ptr((uchar*) image, (uchar*) image + packlength); } void set_ptr_offset(my_ptrdiff_t ptr_diff, uint32 length, const uchar *data) { uchar *ptr_ofs= ADD_TO_PTR(ptr,ptr_diff,uchar*); @@ -4765,7 +4958,7 @@ class Field_blob :public Field_longstr { @retval true Memory allocation error @retval false Success */ - bool copy() + bool copy() override { uchar *tmp= get_ptr(); if (value.copy((char*) tmp, get_length(), charset())) @@ -6229,6 +6422,25 @@ bool TABLE::vers_implicit() const return vers_end_field()->invisible == INVISIBLE_SYSTEM; } +/* + Does a HEAP table gain from keeping this column's payload outside the + record rather than inline? See HEAP_CONVERT_IF_BIGGER_TO_BLOB for why + the threshold is what it is. + + Two places move a column out of line and both ask this: the SQL layer + for an internal temporary table (Create_tmp_table::add_field()), and + the engine for a user ENGINE=MEMORY table + (heap_prepare_hp_create_info()). They act on disjoint columns, since + the engine skips what the SQL layer has already moved, but they have to + agree on which columns are worth moving. +*/ +static inline bool heap_wants_out_of_line(const Field *field) +{ + return field->type() == MYSQL_TYPE_VARCHAR && + !field->compression_method() && + field->field_length > HEAP_CONVERT_IF_BIGGER_TO_BLOB; +} + double pos_in_interval_for_string(CHARSET_INFO *cset, const uchar *midp_val, uint32 midp_len, const uchar *min_val, uint32 min_len, diff --git a/sql/field_conv.cc b/sql/field_conv.cc index 8b22127e7d312..60485bce464a3 100644 --- a/sql/field_conv.cc +++ b/sql/field_conv.cc @@ -821,6 +821,14 @@ Field::Copy_func *Field_varstring::get_copy_func(const Field *from) const { if (from->type() == MYSQL_TYPE_BIT) return do_field_int; + /* + A promoted record slot holds a pointer where the bytes would be, so + the inline copiers chosen below would move the pointer and leave two + records sharing one value. Copying through the value instead is + correct whichever side is promoted. + */ + if (data_is_out_of_line() || from->data_is_out_of_line()) + return do_field_string; /* Detect copy from pre 5.0 varbinary to varbinary as of 5.0 and use special copy function that removes trailing spaces and thus diff --git a/sql/item_buff.cc b/sql/item_buff.cc index 1079394e83025..c9570e60adfb7 100644 --- a/sql/item_buff.cc +++ b/sql/item_buff.cc @@ -39,7 +39,7 @@ Cached_item *new_Cached_item(THD *thd, Item *item, bool pass_through_ref) { if (pass_through_ref && item->real_item()->type() == Item::FIELD_ITEM && - !(((Item_field *) (item->real_item()))->field->flags & BLOB_FLAG)) + !((Item_field *) (item->real_item()))->field->data_is_out_of_line()) { Item_field *real_item= (Item_field *) item->real_item(); Field *cached_field= real_item->field; diff --git a/sql/item_sum.cc b/sql/item_sum.cc index 1b6440ba672b8..8d0a8332fc8d0 100644 --- a/sql/item_sum.cc +++ b/sql/item_sum.cc @@ -3955,11 +3955,16 @@ int dump_leaf_key(void* key_arg, element_count count __attribute__((unused)), what was cut, which val_str() reports as a warning. A value cut for a row that never gets here may have changed nothing. */ - if (table->blob_storage && (field->flags & BLOB_FLAG)) + if (table->blob_storage && field->data_is_out_of_line()) { - /* A NULL blob was never stored, so there is no mark to read. */ + /* + A NULL value was never stored, so there is no mark to read. + The question is where the value lives, not whether the column + was declared a blob: a VARCHAR wide enough to be kept out of + the record carries the same mark. + */ const uchar *rec= key + offset + item->get_null_bytes(); - const uchar *val= ((Field_blob*) field)->get_ptr(rec); + const uchar *val= field->out_of_line_data(rec); if (val && Blob_mem_storage::was_cut((const char*) val)) item->value_cut_in_result= true; } diff --git a/sql/key.cc b/sql/key.cc index de863b4e0a6db..0653fa6d8931b 100644 --- a/sql/key.cc +++ b/sql/key.cc @@ -225,7 +225,28 @@ void key_restore(uchar *to_record, const uchar *from_key, KEY *key_info, used_uneven_bits= 1; } } - if (key_part->key_part_flag & HA_BLOB_PART) + /* + A VARCHAR whose payload is kept outside the record announces both + HA_VAR_LENGTH_PART and HA_BLOB_PART: the first says what the value + is, the second says the record holds a pointer to it. It is the + first that decides how to put the value back, so ask about it + first. Only a column declared as a blob reaches the branch below, + and only it may be cast to Field_blob. + */ + if (key_part->key_part_flag & HA_VAR_LENGTH_PART) + { + Field *field= key_part->field; + my_ptrdiff_t ptrdiff= to_record - field->table->record[0]; + field->move_field_offset(ptrdiff); + key_length-= HA_KEY_BLOB_LENGTH; + length= MY_MIN(key_length, key_part->length); + MY_BITMAP *old_map= dbug_tmp_use_all_columns(field->table, &field->table->write_set); + field->set_key_image(from_key, length); + dbug_tmp_restore_column_map(&field->table->write_set, old_map); + from_key+= HA_KEY_BLOB_LENGTH; + field->move_field_offset(-ptrdiff); + } + else if (key_part->key_part_flag & HA_BLOB_PART) { /* This in fact never happens, as we have only partial BLOB @@ -242,19 +263,6 @@ void key_restore(uchar *to_record, const uchar *from_key, KEY *key_info, (ulong) blob_length, from_key); length= key_part->length; } - else if (key_part->key_part_flag & HA_VAR_LENGTH_PART) - { - Field *field= key_part->field; - my_ptrdiff_t ptrdiff= to_record - field->table->record[0]; - field->move_field_offset(ptrdiff); - key_length-= HA_KEY_BLOB_LENGTH; - length= MY_MIN(key_length, key_part->length); - MY_BITMAP *old_map= dbug_tmp_use_all_columns(field->table, &field->table->write_set); - field->set_key_image(from_key, length); - dbug_tmp_restore_column_map(&field->table->write_set, old_map); - from_key+= HA_KEY_BLOB_LENGTH; - field->move_field_offset(-ptrdiff); - } else { length= MY_MIN(key_length, key_part->length); diff --git a/sql/sql_const.h b/sql/sql_const.h index e51d4d18807e4..8a3cca02dcf02 100644 --- a/sql/sql_const.h +++ b/sql/sql_const.h @@ -70,6 +70,28 @@ #define MAX_FIELD_BLOBLENGTH UINT_MAX32 /* cf field_blob::get_length() */ #define CONVERT_IF_BIGGER_TO_BLOB 512 /* Threshold *in characters* */ +/* + A VARCHAR in a HEAP table whose declared width exceeds this many bytes + keeps its payload outside the record. + + Heap records are fixed width, so an inline VARCHAR(N) reserves its full + declared width in every row whether or not the row uses it. N counts + characters, so that width is between N and 4N bytes depending on the + character set: the same VARCHAR(100) reserves 100 bytes in latin1 and + 400 in utf8mb4. Out of line it costs a length prefix and a pointer in + the record, plus the bytes actually present. The threshold is + therefore compared against the byte width, not against N. + + Below it promotion loses: a non-empty out-of-line value costs at least + one whole continuation record in the engine, so a narrow column pays + more for the run than it saves on the record. + + Setting this to 0 promotes every VARCHAR and must keep working; it is + the configuration the tests use to reach the promoted paths without + needing wide columns. +*/ +#define HEAP_CONVERT_IF_BIGGER_TO_BLOB 32 /* Threshold *in bytes* */ + /* Max column width +1 */ #define MAX_FIELD_WIDTH (MAX_FIELD_CHARLENGTH*MAX_MBWIDTH+1) diff --git a/sql/sql_expression_cache.cc b/sql/sql_expression_cache.cc index bf2f92cf3e05c..35b20a6ed1700 100644 --- a/sql/sql_expression_cache.cc +++ b/sql/sql_expression_cache.cc @@ -148,8 +148,12 @@ void Expression_cache_tmptable::init() value would not affect the key. However, it matches the pre-blob behavior where blobs forced Aria, which failed the heap_hton check above and disabled the cache anyway. + + A VARCHAR whose payload sits outside the record is not one of these: + make_new_field() gives the key field its payload back in the record, + so the key format is the one the SQL layer expects. */ - if (cache_table->s->blob_fields) + if (cache_table->has_unbounded_blob_field()) { DBUG_PRINT("error", ("blob fields not supported in heap expression cache")); goto error; diff --git a/sql/sql_join_cache.cc b/sql/sql_join_cache.cc index c8948995c6abf..24a0241277f97 100644 --- a/sql/sql_join_cache.cc +++ b/sql/sql_join_cache.cc @@ -1327,13 +1327,13 @@ uint JOIN_CACHE::write_record_data(uchar * link, bool *is_full) CACHE_FIELD **copy_ptr_end= copy_ptr+blobs; for ( ; copy_ptr < copy_ptr_end; copy_ptr++) { - Field_blob *blob_field= (Field_blob *) (*copy_ptr)->field; + Field *blob_field= (*copy_ptr)->field; if (!blob_field->is_null()) { - uint blob_len= blob_field->get_length(); + uint blob_len= blob_field->out_of_line_length(); (*copy_ptr)->blob_length= blob_len; len+= blob_len; - (*copy_ptr)->str= blob_field->get_ptr(); + (*copy_ptr)->str= (uchar*) blob_field->out_of_line_data(); } } } @@ -1408,7 +1408,7 @@ uint JOIN_CACHE::write_record_data(uchar * link, bool *is_full) switch (copy->type) { case CACHE_BLOB: { - Field_blob *blob_field= (Field_blob *) copy->field; + Field *blob_field= copy->field; if (last_record) { last_rec_blob_data_is_in_rec_buff= 1; @@ -1877,7 +1877,7 @@ uint JOIN_CACHE::read_record_field(CACHE_FIELD *copy, bool blob_in_rec_buff) switch (copy->type) { case CACHE_BLOB: { - Field_blob *blob_field= (Field_blob *) copy->field; + Field *blob_field= copy->field; /* Copy the length and the pointer to data but not the blob data itself to the record buffer @@ -1890,8 +1890,8 @@ uint JOIN_CACHE::read_record_field(CACHE_FIELD *copy, bool blob_in_rec_buff) } else { - blob_field->set_ptr(pos, pos+copy->length); - len= copy->length + blob_field->get_length(); + blob_field->set_out_of_line_image(pos); + len= copy->length + blob_field->out_of_line_length(); } } break; diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 4489466ae3fe6..b18a793325ca2 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -12546,7 +12546,7 @@ void JOIN_TAB::calc_used_field_length(bool max_fl) uint flags=field->flags; fields++; rec_length+=field->pack_length(); - if (flags & BLOB_FLAG) + if (field->data_is_out_of_line()) blobs++; if (!(flags & NOT_NULL_FLAG)) null_fields++; @@ -22046,6 +22046,7 @@ Create_tmp_table::Create_tmp_table(ORDER *group, bool distinct, m_null_count[Create_tmp_table::other]= 0; m_blobs_count[Create_tmp_table::distinct]= 0; m_blobs_count[Create_tmp_table::other]= 0; + m_distinct_has_unbounded_blob= false; m_uneven_bit[Create_tmp_table::distinct]= 0; m_uneven_bit[Create_tmp_table::other]= 0; } @@ -22066,13 +22067,30 @@ void Create_tmp_table::add_field(TABLE *table, Field *field, uint fieldnr, if (!(field->flags & NOT_NULL_FLAG)) m_null_count[current_counter]++; + /* + A wide VARCHAR costs its full declared width in every record of a + heap table, so move its payload out of the record before the layout + is measured below. The column stays a VARCHAR to everything above + the Field; only where the bytes live changes. + + This decides the record layout, so it may read the column list and + nothing else. Two temporary tables built from one column list are + written from each other's record buffer -- a recursive CTE fills its + increment table that way -- and they agree on the layout only while + every column decides the same way in both. + */ + if (m_heap_expected && heap_wants_out_of_line(field)) + ((Field_varstring*) field)->promote(); + table->s->reclength+= field->pack_length(); // Assign it here, before update_data_type_statistics() changes m_blob_count - if (field->flags & BLOB_FLAG) + if (field->data_is_out_of_line()) { table->s->blob_field[m_blob_count]= fieldnr; m_blobs_count[current_counter]++; + if ((field->flags & BLOB_FLAG) && current_counter == distinct) + m_distinct_has_unbounded_blob= true; } table->field[fieldnr]= field; @@ -22578,6 +22596,16 @@ bool Create_tmp_table::finalize(THD *thd, MEM_ROOT *mem_root_save= thd->mem_root; thd->mem_root= &table->mem_root; + /* + From here on the union in TABLE holds blob_storage rather than + group_concat. Both readers of the bool -- create_tmp_field() and + Field_blob::make_new_field() -- have run by now, while the default + values copied in below are stored through Field::store(), which asks + the table for its blob storage. Left as the bool that question is + answered with the address 1. + */ + table->blob_storage= NULL; + DBUG_ASSERT(m_alloced_field_count >= share->fields); DBUG_ASSERT(m_alloced_field_count >= share->blob_fields); @@ -22853,11 +22881,11 @@ bool Create_tmp_table::finalize(THD *thd, (MAX_BLOB_WIDTH - 4 - 1)); /* Verify that the group buffer has room for this blob key - field. For native blob columns calc_group_buffer() sees - the blob type from the start and always allocates enough - space. This can only overflow when a varchar is promoted - to blob after calc_group_buffer() has already sized the - buffer (varchar-to-blob promotion path). + field. Only a declared blob reaches here, and + calc_group_buffer() sees its type from the start and always + allocates enough space, so this cannot overflow. The check + stays because a mis-sized group buffer would otherwise be a + silent overwrite. */ uint32 need= key_field_length + 4 /* length_bytes */ + MY_TEST(maybe_null); @@ -22876,6 +22904,11 @@ bool Create_tmp_table::finalize(THD *thd, original SQL-layer value (HA_VAR_LENGTH_PART for varchar), not HA_BLOB_PART. This prevents rebuild_key_from_group_buff() from being called on a key buffer that has varchar format. + + It describes the table field, which is where the engine reads + the value from. cur_group->field addresses the group buffer + instead, and that buffer always holds the value inline, so the + two disagree for a promoted column by design. */ m_key_part_info->key_part_flag= field->key_part_flag(); @@ -22901,6 +22934,13 @@ bool Create_tmp_table::finalize(THD *thd, /* Tell engine to that this key includes a blob */ keyinfo->flags|= HA_BLOB_PART_KEY; } + /* + A promoted VARCHAR needs nothing here. HA_BLOB_PART_KEY says + the key has a segment of unbounded length and so cannot be a + key at all once the table converts to Aria; a promoted column + is still as wide as it was declared, and the engine learns + about the indirection from the key part's own HA_BLOB_PART. + */ /* Verify key_part_info consistency with the GROUP BY key field. @@ -22917,8 +22957,6 @@ bool Create_tmp_table::finalize(THD *thd, HA_KEYTYPE_VARBINARY4 || (ha_base_keytype) m_key_part_info->type == HA_KEYTYPE_VARTEXT4)); - DBUG_ASSERT(!(m_key_part_info->key_part_flag & HA_BLOB_PART) || - (cur_group->field->flags & BLOB_FLAG)); /* Set store_length for all GROUP BY key parts so rebuild_key_from_group_buff() can advance through the key buffer. @@ -22968,12 +23006,18 @@ bool Create_tmp_table::finalize(THD *thd, DBUG_PRINT("info",("hidden_field_count: %d", param->hidden_field_count)); keyinfo->flags= 0; - if (m_blobs_count[distinct]) + if (m_distinct_has_unbounded_blob) { /* Special mode for index creation in MyISAM used to support unique indexes on blobs with arbitrary length. Such indexes cannot be used for lookups. + + Only a declared blob needs it. A promoted VARCHAR is reached + through a pointer too, but its width is still the declared one, + so it is indexed like any other VARCHAR -- and it has to be, + because the optimizer decided from that declared type that this + table could be looked up by key. */ keyinfo->flags|= HA_UNIQUE_HASH; } @@ -23103,6 +23147,14 @@ bool Create_tmp_table::finalize(THD *thd, m_key_part_info->type= (uint8) field->key_type(); m_key_part_info->key_type= field->binary() ? FIELDFLAG_BINARY : 0; + /* + Only a declared blob comes here. A promoted VARCHAR keeps the + key part it would have had inline: a VARTEXT of the declared + width, whose HA_BLOB_PART tells the engine to follow the + pointer. It must not add HA_BLOB_PART_KEY, which would turn + this key into a unique constraint the moment the table converted + to Aria, after the optimizer had planned a lookup on it. + */ if (field->flags & BLOB_FLAG) { /* @@ -23119,9 +23171,9 @@ bool Create_tmp_table::finalize(THD *thd, HA_KEYTYPE_VARTEXT4); } - DBUG_ASSERT(!(m_key_part_info->key_part_flag & HA_BLOB_PART) || + DBUG_ASSERT(!(field->flags & BLOB_FLAG) || m_key_part_info->length == 4 + portable_sizeof_char_ptr); - DBUG_ASSERT(!(m_key_part_info->key_part_flag & HA_BLOB_PART) || + DBUG_ASSERT(!(field->flags & BLOB_FLAG) || ((ha_base_keytype) m_key_part_info->type == HA_KEYTYPE_VARBINARY4 || (ha_base_keytype) m_key_part_info->type == @@ -23614,6 +23666,30 @@ bool Virtual_tmp_table::sp_save_in_target_list(THD *thd, return false; } +/* + Describe a key segment over a column whose payload lives outside the + record. Aria and MyISAM read one the same way: the record holds a + length prefix followed by a pointer, `bit_start' says how wide that + prefix is, and HA_BLOB_PART tells the engine to follow the pointer. + + A declared blob has no bounded width, so a unique constraint over one + covers the whole value and the segment length is zero. A promoted + VARCHAR is still as wide as it was declared and keeps the length the + caller assigned, because a real key over it would otherwise have + nothing in it. +*/ + +static void setup_out_of_line_keyseg(HA_KEYSEG *seg, const Field *field, + const KEY_PART_INFO *key_part) +{ + seg->type= (key_part->key_type & FIELDFLAG_BINARY) ? + HA_KEYTYPE_VARBINARY2 : HA_KEYTYPE_VARTEXT2; + seg->bit_start= (uint8) field->length_size(); + seg->flag= HA_BLOB_PART; + if (field->flags & BLOB_FLAG) + seg->length= 0; // Whole blob in unique constraint +} + #ifdef USE_ARIA_FOR_TMP_TABLES /* Create internal (MyISAM or Maria) temporary table @@ -23739,16 +23815,8 @@ bool create_internal_tmp_table(TABLE *table, KEY *org_keyinfo, seg->language= field->charset()->number; seg->length= keyinfo->key_part[i].length; seg->start= keyinfo->key_part[i].offset; - if (field->flags & BLOB_FLAG) - { - seg->type= - ((keyinfo->key_part[i].key_type & FIELDFLAG_BINARY) ? - HA_KEYTYPE_VARBINARY2 : HA_KEYTYPE_VARTEXT2); - seg->bit_start= (uint8)(field->pack_length() - - portable_sizeof_char_ptr); - seg->flag= HA_BLOB_PART; - seg->length=0; // Whole blob in unique constraint - } + if (field->data_is_out_of_line()) + setup_out_of_line_keyseg(seg, field, keyinfo->key_part + i); else { seg->type= keyinfo->key_part[i].type; @@ -23930,15 +23998,8 @@ bool create_internal_tmp_table(TABLE *table, KEY *org_keyinfo, seg->language= field->charset()->number; seg->length= keyinfo->key_part[i].length; seg->start= keyinfo->key_part[i].offset; - if (field->flags & BLOB_FLAG) - { - seg->type= - ((keyinfo->key_part[i].key_type & FIELDFLAG_BINARY) ? - HA_KEYTYPE_VARBINARY2 : HA_KEYTYPE_VARTEXT2); - seg->bit_start= (uint8) ((Field_blob*) field)->pack_length_no_ptr(); - seg->flag= HA_BLOB_PART; - seg->length=0; // Whole blob in unique constraint - } + if (field->data_is_out_of_line()) + setup_out_of_line_keyseg(seg, field, keyinfo->key_part + i); else { seg->type= keyinfo->key_part[i].type; @@ -24082,11 +24143,16 @@ int Window_rowid_remapper::materialize_pending_blobs(TABLE *from) uint *bf_end= from->s->blob_field + from->s->blob_fields; DBUG_ENTER("Window_rowid_remapper::materialize_pending_blobs"); + /* + Each value is written as the record image of its own column: the + length prefix that column carries, then the payload, which is what + set_out_of_line_data() reads back. + */ for (uint *bf= from->s->blob_field; bf < bf_end; bf++) { - Field_blob *fb= (Field_blob*) from->field[*bf]; + Field *fb= from->field[*bf]; if (!fb->is_null()) - total+= fb->get_length(); + total+= fb->length_size() + fb->out_of_line_length(); } if (!total) DBUG_RETURN(0); @@ -24098,13 +24164,15 @@ int Window_rowid_remapper::materialize_pending_blobs(TABLE *from) pos= pending_blob_buf; for (uint *bf= from->s->blob_field; bf < bf_end; bf++) { - Field_blob *fb= (Field_blob*) from->field[*bf]; + Field *fb= from->field[*bf]; uint32 length; - if (fb->is_null() || !(length= fb->get_length())) + if (fb->is_null()) continue; - memcpy(pos, fb->get_ptr(), length); - fb->set_ptr(length, pos); - pos+= length; + length= fb->out_of_line_length(); + memcpy(pos, fb->ptr, fb->length_size()); + memcpy(pos + fb->length_size(), fb->out_of_line_data(), length); + fb->set_out_of_line_image(pos); + pos+= fb->length_size() + length; } DBUG_RETURN(0); } @@ -28938,8 +29006,8 @@ static bool copy_blobs(Field **ptr) { for (; *ptr ; ptr++) { - if ((*ptr)->flags & BLOB_FLAG) - if (((Field_blob *) (*ptr))->copy()) + if ((*ptr)->data_is_out_of_line()) + if ((*ptr)->copy()) return 1; // Error } return 0; @@ -28949,8 +29017,8 @@ static void free_blobs(Field **ptr) { for (; *ptr ; ptr++) { - if ((*ptr)->flags & BLOB_FLAG) - ((Field_blob *) (*ptr))->free(); + if ((*ptr)->data_is_out_of_line()) + (*ptr)->free(); } } @@ -29063,8 +29131,13 @@ JOIN_TAB::remove_duplicates() fields, sort_length() returns UINT_MAX32, making the key buffer impractically large. Fall back to the row-by-row compare path for tables with blobs. + + The reason is the missing maximum width, not where the payload is + kept: a VARCHAR whose payload has moved out of the record still + answers a bounded sort_length(), and make_sort_key_part() reads it + through the pointer like any other value. */ - if (!table->s->blob_fields && + if (!table->has_unbounded_blob_field() && (table->s->db_type() == heap_hton || ((ALIGN_SIZE(keylength) + HASH_OVERHEAD) * table->file->stats.records < thd->variables.sortbuff_size))) @@ -30359,7 +30432,7 @@ setup_copy_fields(THD *thd, TMP_TABLE_PARAM *param, item->name= ref->name; } pos= item; - if (item->field->flags & BLOB_FLAG) + if (item->field->data_is_out_of_line()) { if (!(pos= new (thd->mem_root) Item_copy_string(thd, pos))) goto err; diff --git a/sql/sql_show.cc b/sql/sql_show.cc index ec9d08c6f0e2e..708012d8ec56f 100644 --- a/sql/sql_show.cc +++ b/sql/sql_show.cc @@ -9766,7 +9766,7 @@ bool optimize_schema_tables_memory_usage(List &tables) } else { - bool was_blob= field->flags & BLOB_FLAG; + bool was_blob= field->data_is_out_of_line(); field= new (thd->mem_root) Field_string(cur, 0, field->null_ptr, field->null_bit, Field::NONE, &field->field_name, field->dtcollation()); diff --git a/sql/table.cc b/sql/table.cc index 69cca8939d813..0423bb1b5585d 100644 --- a/sql/table.cc +++ b/sql/table.cc @@ -3522,7 +3522,7 @@ int TABLE_SHARE::init_from_binary_frm_image(THD *thd, bool write, goto err; for (k=0, ptr= share->field ; *ptr ; ptr++, k++) { - if ((*ptr)->flags & BLOB_FLAG) + if ((*ptr)->data_is_out_of_line()) (*save++)= k; } } @@ -8557,6 +8557,15 @@ void TABLE::restore_blob_values(String *blob_storage) } +bool TABLE::has_unbounded_blob_field() const +{ + for (uint *bf= s->blob_field, *end= bf + s->blob_fields; bf < end; bf++) + if (field[*bf]->flags & BLOB_FLAG) + return true; + return false; +} + + /** @brief Allocate space for keys @@ -9949,6 +9958,20 @@ bool TABLE::insert_all_rows_into_tmp_table(THD *thd, DBUG_ENTER("TABLE::insert_all_rows_into_tmp_table"); + /* + This table's handler reads straight into the destination's record + buffer below, with no conversion step, so the two have to agree on + the record layout. They are built from one column list, but from + two calls with their own options, and the options decide which + engine is expected and so whether a wide VARCHAR keeps its payload + in the record. A column that disagrees costs at least the width + that put it over HEAP_CONVERT_IF_BIGGER_TO_BLOB, so the record + lengths cannot match if any column does. + select_union_recursive::send_data() asserts the same thing for the + other direction of the same copy. + */ + DBUG_ASSERT(s->reclength == tmp_table->s->reclength); + if (with_cleanup) { if ((write_err= tmp_table->file->ha_delete_all_rows())) diff --git a/sql/table.h b/sql/table.h index 8406dc6e60749..d8affdd67681d 100644 --- a/sql/table.h +++ b/sql/table.h @@ -1360,9 +1360,12 @@ class Blob_mem_storage: public Sql_alloc } /* Whether the value at 'ptr' was cut on its way in. 'ptr' must be a - pointer this storage handed out, which is the case for every blob in - a table that has a Blob_mem_storage: Field_blob::store() sends all of - them through Field_blob::handle_group_concat(). + pointer this storage handed out, which is the case for every column + kept out of the record in a table that has a Blob_mem_storage: a + declared blob through Field_blob::handle_group_concat(), and a + VARCHAR whose payload has moved out of the record directly from + Field_varstring::store(). Only the former is ever cut here, because + only it has no declared width of its own. */ static bool was_cut(const char *ptr) { return ptr[-1] != 0; } void set_truncated_value(bool is_truncated_value) @@ -1818,6 +1821,14 @@ struct TABLE bool init_expr_arena(MEM_ROOT *mem_root); + /* + Does this table hold a column the user declared as a blob, one with + no maximum width? s->blob_fields counts those together with the + columns whose payload merely sits outside the record, and the two are + different questions: a decision taken because a value can be + arbitrarily wide must ask this one. + */ + bool has_unbounded_blob_field() const; bool alloc_keys(uint key_count); bool check_tmp_key(uint key, uint key_parts, uint (*next_field_no) (uchar *), uchar *arg); diff --git a/storage/heap/CMakeLists.txt b/storage/heap/CMakeLists.txt index 4e75796778d8e..3f540fb376430 100644 --- a/storage/heap/CMakeLists.txt +++ b/storage/heap/CMakeLists.txt @@ -34,7 +34,7 @@ IF(WITH_UNIT_TESTS) TARGET_LINK_LIBRARIES(hp_test2 heap mysys dbug strings) MY_ADD_TESTS(hp_test_hash hp_test_scan hp_test_freelist hp_test_concurrent hp_test_block_size hp_test_blob_alias hp_test_update - hp_test_write_dup hp_test_unlock_check + hp_test_write_dup hp_test_unlock_check hp_test_rectest LINK_LIBRARIES heap mysys dbug strings) INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/sql diff --git a/storage/heap/_check.c b/storage/heap/_check.c index 361ae778faa3b..76c4cc30041be 100644 --- a/storage/heap/_check.c +++ b/storage/heap/_check.c @@ -19,7 +19,7 @@ #include "heapdef.h" static int check_one_key(HP_INFO *, HP_KEYDEF *, uint, ulong, ulong, my_bool); -static int check_one_rb_key(const HP_INFO *, uint, ulong, my_bool); +static int check_one_rb_key(HP_INFO *, uint, ulong, my_bool); /* @@ -44,7 +44,7 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status) int error; uint key; ulong records=0, deleted=0, cont_count=0, pos, next_block; - ulong del_link_count; + ulong del_link_count, del_entry_count; uchar *del_ptr; my_bool block_count_error_printed= FALSE; HP_SHARE *share=info->s; @@ -56,7 +56,8 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status) for (error=key= 0 ; key < share->keys ; key++) { if (share->keydef[key].algorithm == HA_KEY_ALG_BTREE) - error|= check_one_rb_key(info, key, share->records, print_status); + error|= check_one_rb_key((HP_INFO*) info, key, share->records, + print_status); else error|= check_one_key((HP_INFO*) info, share->keydef + key, key, share->records, share->blength, print_status); @@ -73,10 +74,16 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status) error= 1; } - /* Verify free list record count matches share->deleted */ - del_link_count= 0; + /* + Verify both free list counts: records against share->deleted, and + entries against share->deleted_entries. A block counts once as an + entry however many records it spans, which is what a scan pays for + it, so the two counters diverge as soon as anything coalesces. + */ + del_link_count= del_entry_count= 0; for (del_ptr= share->del_link; del_ptr; ) { + del_entry_count++; if (hp_is_free_block_end(del_ptr)) { uchar *first= hp_free_block_first(del_ptr); @@ -95,6 +102,13 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status) del_link_count, (ulong) share->deleted)); error= 1; } + if (del_entry_count != share->deleted_entries) + { + DBUG_PRINT("error",("free list entry count %lu != " + "share->deleted_entries %lu", + del_entry_count, (ulong) share->deleted_entries)); + error= 1; + } /* This is basicly the same code as in hp_scan, but we repeat it here to @@ -215,10 +229,10 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status) desc_end= desc + share->blob_count; desc < desc_end; desc++) { - if (hp_blob_length(desc, current_ptr) > 0) + if (hp_blob_stored_length(desc, current_ptr) > 0) { uchar *chain; - memcpy(&chain, current_ptr + desc->offset + desc->packlength, + memcpy(&chain, current_ptr + desc->store_offset + desc->packlength, sizeof(chain)); if (chain) { @@ -333,25 +347,50 @@ static int check_one_key(HP_INFO *info, HP_KEYDEF *keydef, uint keynr, } -static int check_one_rb_key(const HP_INFO *info, uint keynr, ulong records, +static int check_one_rb_key(HP_INFO *info, uint keynr, ulong records, my_bool print_status) { - HP_KEYDEF *keydef= info->s->keydef + keynr; + HP_SHARE *share= info->s; + HP_KEYDEF *keydef= share->keydef + keynr; int error= 0; ulong found= 0; - uchar *key, *recpos; + uchar *key, *recpos, *unpacked= 0; uint key_length; uint not_used[2]; TREE_ELEMENT **last_pos; TREE_ELEMENT *parents[MAX_TREE_HEIGHT+1]; + my_bool saved_zerocopy= info->has_zerocopy_blobs; + + /* + The rb-tree holds pointers to stored records, while hp_rb_make_key() + reads a record in the SQL layer's layout. The two are the same when + nothing is promoted; otherwise the stored record has to be expanded + first, which also materializes the promoted values the key is built + from. + */ + if (share->promoted_count && + !(unpacked= (uchar*) my_safe_alloca(share->reclength))) + return 1; if ((key= tree_search_edge(&keydef->rb_tree, parents, &last_pos, offsetof(TREE_ELEMENT, left)))) { do { + uchar *rec; memcpy(&recpos, key + (*keydef->get_key_length)(keydef,key), sizeof(uchar*)); - key_length= hp_rb_make_key(keydef, info->recbuf, recpos, 0); + rec= recpos; + if (unpacked) + { + hp_unpack_record(share, unpacked, recpos); + if (hp_read_blobs(info, unpacked, recpos)) + { + error= 1; + break; + } + rec= unpacked; + } + key_length= hp_rb_make_key(keydef, info->recbuf, rec, 0); if (ha_key_cmp(keydef->seg, (uchar*) info->recbuf, (uchar*) key, key_length, SEARCH_FIND | SEARCH_SAME, not_used)) { @@ -371,6 +410,14 @@ static int check_one_rb_key(const HP_INFO *info, uint keynr, ulong records, DBUG_PRINT("error",("Found %lu of %lu records", found, records)); error= 1; } + if (unpacked) + my_safe_afree(unpacked, share->reclength); + /* + hp_read_blobs() above reports whether the record it just produced + aliases heap memory. That answer belongs to the caller's last read, + not to this check, so it is put back. + */ + info->has_zerocopy_blobs= saved_zerocopy; if (print_status) printf("Key: %d records: %ld\n", keynr, records); return error; diff --git a/storage/heap/_rectest.c b/storage/heap/_rectest.c index f611ad55ad312..56f50bce35df3 100644 --- a/storage/heap/_rectest.c +++ b/storage/heap/_rectest.c @@ -19,13 +19,130 @@ #include "heapdef.h" + +/* + Whether a continuation chain holds something other than the data_len + bytes at 'data'. + + The mirror of hp_copy_chain_data(): the same three run layouts, comparing + rather than copying, and stopping at the first difference. +*/ + +static my_bool hp_chain_data_differs(const uchar *chain, uint32 data_len, + const uchar *data, uint visible, + uint recbuffer) +{ + uint32 remaining; + + if (hp_is_single_rec(chain, visible)) + return memcmp(data, chain, data_len) != 0; /* Case A: data at offset 0 */ + if (hp_is_zerocopy(chain, visible)) + return memcmp(data, chain + recbuffer, /* Case B: past the header */ + data_len) != 0; + + /* Case C: walk the runs */ + remaining= data_len; + while (chain && remaining > 0) + { + uint16 run_rec_count= hp_cont_rec_count(chain); + const uchar *next_cont= hp_cont_next(chain); + const uchar *rec_end= chain + (uint32) run_rec_count * recbuffer; + const uchar *rec_ptr; + uint32 chunk; + + /* First record payload (after header) */ + chunk= visible - HP_CONT_HEADER_SIZE; + if (chunk > remaining) + chunk= remaining; + if (memcmp(data, chain + HP_CONT_HEADER_SIZE, chunk)) + return TRUE; + data+= chunk; + remaining-= chunk; + + /* Inner records: recbuffer stride, no flags byte */ + for (rec_ptr= chain + recbuffer; rec_ptr < rec_end; rec_ptr+= recbuffer) + { + chunk= recbuffer; + if (chunk > remaining) + chunk= remaining; + if (memcmp(data, rec_ptr, chunk)) + return TRUE; + data+= chunk; + remaining-= chunk; + } + + chain= next_cont; + } + return remaining != 0; +} + + int hp_rectest(register HP_INFO *info, register const uchar *old) { + HP_SHARE *share= info->s; + const uchar *pos= info->current_ptr; + const HP_BLOB_DESC *desc, *desc_end; + uint visible= share->visible, recbuffer= share->block.recbuffer; + uint sql_pos= 0, store_pos= 0; DBUG_ENTER("hp_rectest"); - if (memcmp(info->current_ptr,old,(size_t) info->s->reclength)) + /* + Walk the record, stopping at each column stored out of line. What the + two buffers hold there does not match and is not meant to: the stored + record has a pointer to the continuation chain, while the record buffer + has either the value itself, for a VARCHAR the engine promoted, or a + pointer to wherever the read handed the value out, for a blob. So each + such column is compared through its length and then its data, and only + the ranges between them are compared as bytes. + + A table with no column stored out of line runs the loop zero times and + compares the whole record in one memcmp, which is what this did before + out-of-line storage existed. + */ + for (desc= share->blob_descs, desc_end= desc + share->blob_count; + desc < desc_end; desc++) { - DBUG_RETURN((my_errno=HA_ERR_RECORD_CHANGED)); /* Record have changed */ + /* The range up to and including the length prefix */ + uint gap= desc->offset + desc->packlength; + uint32 length; + const uchar *data, *chain; + + DBUG_ASSERT(gap >= sql_pos); /* Descriptors ascend by offset */ + if (memcmp(pos + store_pos, old + sql_pos, (size_t) (gap - sql_pos))) + goto changed; + store_pos+= gap - sql_pos; + sql_pos= gap; + DBUG_ASSERT(store_pos == desc->store_offset + desc->packlength); + + /* The prefixes just compared equal, so one length describes both */ + length= hp_blob_length(desc, old); + memcpy(&chain, pos + store_pos, sizeof(chain)); + + if (desc->promoted) + { + data= old + sql_pos; + sql_pos+= desc->length; + store_pos+= (uint) sizeof(uchar*); + } + else + { + memcpy(&data, old + sql_pos, sizeof(data)); + sql_pos+= portable_sizeof_char_ptr; + store_pos+= portable_sizeof_char_ptr; + } + + if (length && + hp_chain_data_differs(chain, length, data, visible, recbuffer)) + goto changed; } + + if (memcmp(pos + store_pos, old + sql_pos, + (size_t) (share->reclength - sql_pos))) + goto changed; + DBUG_ASSERT(store_pos + (share->reclength - sql_pos) == + share->stored_reclength); DBUG_RETURN(0); + +changed: + DBUG_RETURN((my_errno= HA_ERR_RECORD_CHANGED)); /* Record have changed */ } /* _heap_rectest */ diff --git a/storage/heap/ha_heap.cc b/storage/heap/ha_heap.cc index e73bf841e2a6a..5c8e8416840f1 100644 --- a/storage/heap/ha_heap.cc +++ b/storage/heap/ha_heap.cc @@ -92,7 +92,8 @@ static handler *heap_create_handler(handlerton *hton, ha_heap::ha_heap(handlerton *hton, TABLE_SHARE *table_arg) :handler(hton, table_arg), file(0), int_table_flags2(0), - records_changed(0), key_stat_version(0), internal_table(0) + records_changed(0), deleted_entries(0), key_stat_version(0), + internal_table(0) { } @@ -288,9 +289,21 @@ IO_AND_CPU_COST ha_heap::keyread_time(uint index, ulong ranges, ha_rows rows, IO_AND_CPU_COST ha_heap::scan_time() { - /* The caller ha_scan_time() handles stats.records */ - - return {0, (double) stats.deleted * HEAP_ROW_NEXT_FIND_COST }; + /* + The caller ha_scan_time() handles stats.records; what is left to + charge for is the free records a scan steps over on its way past + them. + + That is one step per free list entry, not one per free record. + heap_scan() reads a coalesced block's length from its first record + and skips the whole block in a single step, so a row whose + out-of-line data freed a run of a thousand records costs a later + scan exactly what a row that freed one record costs it. Charging + per record would price a table with out-of-line columns at its + promotion ratio above what it is worth, and it is stored records, + not rows, that a chain multiplies. + */ + return {0, (double) deleted_entries * HEAP_ROW_NEXT_FIND_COST }; } @@ -490,13 +503,12 @@ int ha_heap::info(uint flag) errkey= hp_info.errkey; stats.records= hp_info.records; stats.deleted= hp_info.deleted; + deleted_entries= hp_info.deleted_entries; stats.mean_rec_length= hp_info.reclength; stats.data_file_length= hp_info.data_length; stats.index_file_length= hp_info.index_length; - stats.max_data_file_length= (hp_info.max_records == NO_LIMIT_RECORDS ? - ~(my_off_t) 0 : - hp_info.max_records * hp_info.reclength); - stats.delete_length= hp_info.deleted * hp_info.reclength; + stats.max_data_file_length= hp_info.max_data_length; + stats.delete_length= hp_info.delete_length; stats.create_time= (ulong) hp_info.create_time; if (flag & HA_STATUS_AUTO) stats.auto_increment_value= hp_info.auto_increment; @@ -750,11 +762,43 @@ ha_rows ha_heap::records_in_range(uint inx, const key_range *min_key, } +/* + Should the engine store this column as a blob rather than inline? + + heap_wants_out_of_line() decides which columns are worth moving; what + is left here is that a column the SQL layer has already moved is not + moved again. + + Promoting here is invisible to the SQL layer. The Field stays a + VARCHAR, the record buffer keeps its shape, and nothing about the + column's type, metadata or comparison semantics changes -- only where + the engine puts the bytes. +*/ + +static bool hp_promote_to_blob(const Field *field) +{ + return !field->data_is_out_of_line() && heap_wants_out_of_line(field); +} + + +static int hp_cmp_blob_desc(const void *a, const void *b) +{ + uint oa= ((const HP_BLOB_DESC*) a)->offset; + uint ob= ((const HP_BLOB_DESC*) b)->offset; + return oa < ob ? -1 : (oa > ob ? 1 : 0); +} + + int heap_prepare_hp_create_info(TABLE *table_arg, bool internal_table, HP_CREATE_INFO *hp_create_info) { TABLE_SHARE *share= table_arg->s; uint key, parts, mem_per_row= 0, keys= share->keys; + /* + A promoted column gives back its declared payload and spends a chain + pointer instead, which is what shortens the stored record. + */ + uint stored_reclength= share->reclength; uint auto_key= 0, auto_key_type= 0; ha_rows max_rows; HP_KEYDEF *keydef; @@ -830,7 +874,21 @@ int heap_prepare_hp_create_info(TABLE *table_arg, bool internal_table, DBUG_ASSERT((seg->flag & HA_BLOB_PART) == (field->key_part_flag() & HA_BLOB_PART)); - if (seg->flag & HA_BLOB_PART) + /* + A promoted VARCHAR. Its record slot is the column's own one or + two byte length prefix followed by the chain pointer, so the + segment keeps the VARTEXT type it already has and heap_create() + derives the prefix width from that. Only the indirection, which + HA_BLOB_PART carries, is new. + */ + DBUG_ASSERT(!(seg->flag & HA_BLOB_PART) || + field->type() != MYSQL_TYPE_VARCHAR || + seg->type == HA_KEYTYPE_VARTEXT1 || + seg->type == HA_KEYTYPE_VARTEXT2 || + seg->type == HA_KEYTYPE_VARBINARY1 || + seg->type == HA_KEYTYPE_VARBINARY2); + + if (seg->flag & HA_BLOB_PART && field->type() != MYSQL_TYPE_VARCHAR) { /* Blob key segment: 4-byte length + pointer to data. @@ -862,7 +920,7 @@ int heap_prepare_hp_create_info(TABLE *table_arg, bool internal_table, DBUG_ASSERT(seg->type == HA_KEYTYPE_VARBINARY4 || seg->type == HA_KEYTYPE_VARTEXT4); } - seg->bit_start= ((Field_blob*) field)->length_size(); + seg->bit_start= field->length_size(); } if (field->flags & (ENUM_FLAG | SET_FLAG)) seg->charset= &my_charset_bin; @@ -905,33 +963,87 @@ int heap_prepare_hp_create_info(TABLE *table_arg, bool internal_table, found_real_auto_increment= share->next_number_key_offset == 0; } - /* Populate blob column descriptors */ - if (share->blob_fields) + /* + Populate the out-of-line column descriptors: the native blobs, and + the VARCHARs wide enough to be worth storing as blobs. + + Both kinds share one array because they share one mechanism -- a + length prefix and a continuation chain pointer in the stored record. + They differ only in what the SQL record buffer holds at that column: + a pointer for a blob, the value itself for a promoted VARCHAR. + */ { - HP_BLOB_DESC *blob_descs; - blob_descs= (HP_BLOB_DESC*) my_malloc(hp_key_memory_HP_BLOB, - share->blob_fields * - sizeof(HP_BLOB_DESC), - MYF(MY_WME | MY_THREAD_SPECIFIC)); - if (!blob_descs) + uint promoted= 0, desc_count; + + for (uint i= 0; i < share->fields; i++) { - my_free(keydef); - return my_errno; + Field *field= table_arg->field[i]; + if (hp_promote_to_blob(field)) + { + promoted++; + stored_reclength-= field->field_length; + stored_reclength+= (uint) sizeof(uchar*); + } } - for (uint blob_index= 0; blob_index < share->blob_fields; blob_index++) + desc_count= share->blob_fields + promoted; + + if (desc_count) { - Field *field= table_arg->field[share->blob_field[blob_index]]; - Field_blob *blob= (Field_blob*) field; + HP_BLOB_DESC *blob_descs; + uint n= 0; + + blob_descs= (HP_BLOB_DESC*) my_malloc(hp_key_memory_HP_BLOB, + desc_count * + sizeof(HP_BLOB_DESC), + MYF(MY_WME | MY_ZEROFILL | + MY_THREAD_SPECIFIC)); + if (!blob_descs) + { + my_free(keydef); + return my_errno; + } - DBUG_ASSERT(field->type() == MYSQL_TYPE_BLOB || - field->type() == MYSQL_TYPE_GEOMETRY); + for (uint i= 0; i < share->fields; i++) + { + Field *field= table_arg->field[i]; - blob_descs[blob_index].offset= - (uint) blob->offset(table_arg->record[0]); - blob_descs[blob_index].packlength= blob->length_size(); + if (field->data_is_out_of_line()) + { + /* + A declared blob, or a VARCHAR the SQL layer has already moved + out of the record. Both hold a length prefix followed by a + pointer, so the engine reads them the same way and neither + needs the record reshaped underneath it. + */ + blob_descs[n].offset= (uint) field->offset(table_arg->record[0]); + blob_descs[n].packlength= field->length_size(); + blob_descs[n].promoted= FALSE; + n++; + } + else if (hp_promote_to_blob(field)) + { + blob_descs[n].offset= (uint) field->offset(table_arg->record[0]); + /* A VARCHAR's length prefix is one byte up to 255, else two */ + blob_descs[n].packlength= (field->pack_length() - + field->field_length); + blob_descs[n].length= field->field_length; + blob_descs[n].promoted= TRUE; + n++; + } + } + DBUG_ASSERT(n == desc_count); + + /* + The record layout is built by walking the descriptors in record + order, and nothing guarantees the field array is in that order. + */ + my_qsort(blob_descs, desc_count, sizeof(HP_BLOB_DESC), + hp_cmp_blob_desc); + + hp_create_info->blob_descs= blob_descs; + hp_create_info->blob_count= desc_count; + hp_create_info->stored_reclength= stored_reclength; } - hp_create_info->blob_descs= blob_descs; - hp_create_info->blob_count= share->blob_fields; } hp_create_info->auto_key= auto_key; @@ -945,7 +1057,7 @@ int heap_prepare_hp_create_info(TABLE *table_arg, bool internal_table, hp_create_info->with_auto_increment= found_real_auto_increment; hp_create_info->internal_table= internal_table; - max_rows= hp_rows_in_memory(share->reclength, mem_per_row, + max_rows= hp_rows_in_memory(stored_reclength, mem_per_row, hp_create_info->max_table_size); #ifdef GIVE_ERROR_IF_NOT_MEMORY_TO_INSERT_ONE_ROW /* We do not give the error now but instead give an error on first insert */ @@ -956,7 +1068,14 @@ int heap_prepare_hp_create_info(TABLE *table_arg, bool internal_table, if (share->max_rows && share->max_rows < max_rows) max_rows= share->max_rows; + /* + max_records only sizes the blocks. share->max_rows is the row limit + proper and travels separately, because the engine enforces it by + counting rows: a row with out-of-line columns occupies several + records, so the two cannot share a field. + */ hp_create_info->max_records= (ulong) MY_MIN(max_rows, ULONG_MAX); + hp_create_info->max_rows= (ulong) MY_MIN(share->max_rows, ULONG_MAX); hp_create_info->min_records= (ulong) MY_MIN(share->min_rows, ULONG_MAX); hp_create_info->keys= share->keys; hp_create_info->reclength= share->reclength; @@ -1058,7 +1177,7 @@ int ha_heap::find_unique_row(uchar *record, uint unique_idx) file->current_hash_ptr= pos; file->current_ptr= pos->ptr_to_rec; file->update= HA_STATE_AKTIV; - memcpy(record, file->current_ptr, (size_t) share->reclength); + hp_unpack_record(share, record, file->current_ptr); DBUG_RETURN(0); } } while ((pos= pos->next_key)); @@ -1084,7 +1203,7 @@ int ha_heap::find_unique_row(uchar *record, uint unique_idx) if (pos->hash_of_key != rec_hash) continue; - memcpy(record, pos->ptr_to_rec, (size_t) share->reclength); + hp_unpack_record(share, record, pos->ptr_to_rec); if (hp_read_blobs(file, record, pos->ptr_to_rec)) { result= -1; /* my_errno is set to HA_ERR_OUT_OF_MEM */ diff --git a/storage/heap/ha_heap.h b/storage/heap/ha_heap.h index a8efdd65c9ba3..9fe6d7fd837da 100644 --- a/storage/heap/ha_heap.h +++ b/storage/heap/ha_heap.h @@ -28,6 +28,13 @@ class ha_heap final : public handler ulonglong int_table_flags2; /* number of records changed since last statistics update */ ulong records_changed; + /* + Free list entries as of the last info() call, refreshed alongside + stats. Held here rather than read from the share because + estimate_scan_time() reaches scan_time() while 'file' can still be + NULL, which is why the rest of the cost model reads stats too. + */ + ulong deleted_entries; ulong saved_current_record; /* for remember_rnd_pos() / restart_rnd_next() */ uint key_stat_version; my_bool internal_table; diff --git a/storage/heap/heapdef.h b/storage/heap/heapdef.h index c9a60c218ca9c..e67f4d9eba199 100644 --- a/storage/heap/heapdef.h +++ b/storage/heap/heapdef.h @@ -201,18 +201,46 @@ static inline uint16 hp_free_block_start_count(const uchar *pos) return uint2korr(pos + HP_DEL_COUNT_OFFSET); } +/* + Record length up to which clearing dark records contiguously beats + clearing them at a stride. + + A record's two write sites, del_link at the start and the flags byte + at 'visible', land in different cache lines for any record this short, + so the strided loop dirties every line in the range anyway and gains + nothing by touching fewer bytes of each. Measured on Zen 4 (AVX-512), + Broadwell (AVX2) and Denverton (no AVX): at recbuffer 16 a contiguous + clear is 2.6 to 4.7 times faster, at 32 it is 1.3 to 2.3 times faster, + and the two cross over between 48 and 96 depending on the machine. + + A promoted VARCHAR gives a stored record of a length prefix and a + chain pointer, so its recbuffer is 16 and it sits at the top of that + range. +*/ +#define HP_CLEAR_DARK_MEMSET_MAX 32 + /* Clear the metadata bytes of dark records (records between block-start and block-end that are not individually on the free list). - Uses a strided loop so only the essential bytes per record are touched. - For short record lengths a contiguous bzero over the full range would - be faster, but the crossover point has not been measured. + + Clearing a short record whole rather than just its metadata is safe. + A record reaches here only once it is free, and the strided form below + already overwrites the first nine bytes of every one of them, so + nothing may rely on a dark record's contents either way. Callers pass + whole-record boundaries, so the two forms cover the same records. */ static inline void hp_clear_dark_records(uchar *from, uchar *to, uint recbuffer, uint visible) { uchar *pos; + + if (recbuffer <= HP_CLEAR_DARK_MEMSET_MAX) + { + if (to > from) + bzero(from, (size_t) (to - from)); + return; + } for (pos= from; pos < to; pos+= recbuffer) { *((uchar**) pos)= NULL; @@ -234,6 +262,7 @@ static inline void hp_push_free_record(HP_SHARE *share, uchar *pos) share->del_link= pos; pos[share->visible]= 0; share->deleted++; + share->deleted_entries++; share->total_records--; } @@ -284,9 +313,10 @@ static inline uchar *hp_pop_free_record(HP_SHARE *share) if (!hp_is_free_block_end(pos)) { - /* Single record */ + /* Single record: the whole entry goes */ share->del_link= *((uchar**) pos); share->deleted--; + share->deleted_entries--; share->total_records++; return pos; } @@ -375,6 +405,27 @@ static inline uint32 hp_blob_length(const HP_BLOB_DESC *desc, { return (uint32) read_lowendian(record + desc->offset, desc->packlength); } + +/* + The same length, read from a stored record. Promoted columns move + under compaction, so the stored descriptor is not at desc->offset. +*/ + +static inline uint32 hp_blob_stored_length(const HP_BLOB_DESC *desc, + const uchar *pos) +{ + return (uint32) read_lowendian(pos + desc->store_offset, desc->packlength); +} + +/* Address of the chain pointer slot inside a stored record */ + +static inline uchar *hp_blob_chain_slot(const HP_BLOB_DESC *desc, uchar *pos) +{ + return pos + desc->store_offset + desc->packlength; +} + +extern void hp_pack_record(HP_SHARE *share, uchar *pos, const uchar *record); +extern void hp_unpack_record(HP_SHARE *share, uchar *record, const uchar *pos); extern int hp_write_one_blob(HP_SHARE *share, const uchar *data_ptr, uint32 data_len, uchar **first_run_out); extern int hp_write_blobs(HP_INFO *info, const uchar *record, uchar *pos); diff --git a/storage/heap/hp_blob.c b/storage/heap/hp_blob.c index 3763b949ced09..cb56747b64bb7 100644 --- a/storage/heap/hp_blob.c +++ b/storage/heap/hp_blob.c @@ -26,6 +26,27 @@ This design amortizes the per-run header overhead across many records, giving near-100% space efficiency for typical blob sizes (150 KB and above), even when recbuffer is very small (e.g. 16 bytes). + + This file also owns the translation between the SQL record buffer and + the stored record, because the two are halves of one operation: a row + is moved with hp_pack_record() or hp_unpack_record() and then its + out-of-line columns are settled by hp_write_blobs() or hp_read_blobs(). + + The two layouts differ when the table has promoted columns -- VARCHARs + the engine stores as blobs. Such a column occupies a length prefix + followed by its declared payload in record[0], where the SQL layer + reaches the data at a fixed offset, but only a length prefix and a + chain pointer in the stored record. That shortens the stored record + and shifts every column after the first promoted one, so the layouts + need an explicit map rather than a single memcpy. + + The map is share->copy_spans, the list of byte ranges identical in both + layouts. Everything that is not a promoted column's reserved payload + belongs to a span: the null bytes, the fixed-width columns, native blob + descriptors, and the promoted columns' own length prefixes. The gaps + between spans are exactly the promoted payloads. A table with no + promoted column has one span covering the whole record, so it costs the + single memcpy it cost before promotion existed. */ #include "heapdef.h" @@ -106,6 +127,8 @@ void hp_shrink_tail(HP_SHARE *share) tail_pos-= reclaim_count * recbuffer; block_pos-= reclaim_count; share->deleted-= reclaim_count; + /* One whole entry leaves the list per iteration, block or single */ + share->deleted_entries--; /* When the current leaf block becomes empty (block_pos has reached 0), @@ -191,6 +214,19 @@ void hp_flush_unaliased_blob_free(HP_INFO *info, const uchar *record) if (!*chain_pos) continue; + if (desc->promoted) + { + /* + A promoted column holds its value inline in the record buffer, so + it can never be sourcing the parked chain and there is no pointer + to test. hp_write_blobs() reaches the same conclusion for the + same reason and will not adopt this chain. + */ + hp_free_run_chain(share, *chain_pos); + *chain_pos= NULL; + continue; + } + data_len= hp_blob_length(desc, record); memcpy(&data_ptr, record + desc->offset + desc->packlength, sizeof(data_ptr)); @@ -637,6 +673,63 @@ int hp_write_one_blob(HP_SHARE *share, const uchar *data_ptr, @return 0 on success, my_errno on failure */ +/* + Copy the SQL record buffer into a stored record. + + Leaves each promoted column's chain pointer slot untouched; the caller + runs hp_write_blobs() afterwards to allocate the chains and store the + pointers. A promoted column's length prefix is carried by a span and is + already in place when this returns. +*/ + +void hp_pack_record(HP_SHARE *share, uchar *pos, const uchar *record) +{ + const HP_COPY_SPAN *span, *span_end; + + for (span= share->copy_spans, span_end= span + share->copy_span_count; + span < span_end; span++) + memcpy(pos + span->store_offset, record + span->offset, span->length); +} + + +/* + Expand a stored record back into the SQL record buffer. + + Leaves each promoted column's payload untouched; the caller runs + hp_read_blobs() afterwards to copy the bytes out of the continuation + chain. A promoted column cannot be read without that copy: the SQL + layer reaches its data at a fixed offset from the length prefix, so + unlike a native blob there is no pointer slot to aim at heap memory. + + The reserved bytes past the value's length are never read by the SQL + layer and nothing writes them here, so they hold whatever the previous + row left behind. A bulk record comparison would still trip a memory + checker over them, so they are annotated as defined, the way Aria and + InnoDB annotate their VARCHAR slack. +*/ + +void hp_unpack_record(HP_SHARE *share, uchar *record, const uchar *pos) +{ + const HP_COPY_SPAN *span, *span_end; + const HP_BLOB_DESC *desc, *desc_end; + + for (span= share->copy_spans, span_end= span + share->copy_span_count; + span < span_end; span++) + memcpy(record + span->offset, pos + span->store_offset, span->length); + + if (!share->promoted_count) + return; + + for (desc= share->blob_descs, desc_end= desc + share->blob_count; + desc < desc_end; desc++) + { + if (desc->promoted) + MEM_MAKE_DEFINED(record + desc->offset + desc->packlength, + desc->length); + } +} + + int hp_write_blobs(HP_INFO *info, const uchar *record, uchar *pos) { HP_SHARE *share= info->s; @@ -662,13 +755,24 @@ int hp_write_blobs(HP_INFO *info, const uchar *record, uchar *pos) if (data_len == 0) { - bzero(pos + desc->offset + desc->packlength, sizeof(char*)); + bzero(pos + desc->store_offset + desc->packlength, sizeof(char*)); continue; } has_blob_data= TRUE; - memcpy(&data_ptr, record + desc->offset + desc->packlength, - sizeof(data_ptr)); + if (desc->promoted) + { + /* + A promoted column carries its value inline, so the bytes to write + are in the record buffer itself rather than behind a pointer. + They can never alias heap memory, which is why the parked chain + below is never adopted for one. + */ + data_ptr= record + desc->offset + desc->packlength; + } + else + memcpy(&data_ptr, record + desc->offset + desc->packlength, + sizeof(data_ptr)); if (parked && parked[desc - share->blob_descs] && hp_blob_sources_chain(share, data_ptr, @@ -692,17 +796,17 @@ int hp_write_blobs(HP_INFO *info, const uchar *record, uchar *pos) for (rd= share->blob_descs; rd < desc; rd++) { uchar *chain; - memcpy(&chain, pos + rd->offset + rd->packlength, sizeof(chain)); + memcpy(&chain, pos + rd->store_offset + rd->packlength, sizeof(chain)); if (chain && (!parked || chain != parked[rd - share->blob_descs])) hp_free_run_chain(share, chain); - bzero(pos + rd->offset + rd->packlength, sizeof(char*)); + bzero(pos + rd->store_offset + rd->packlength, sizeof(char*)); } hp_shrink_tail(share); - bzero(pos + desc->offset + desc->packlength, sizeof(char*)); + bzero(pos + desc->store_offset + desc->packlength, sizeof(char*)); DBUG_RETURN(my_errno); } - memcpy(pos + desc->offset + desc->packlength, &first_run, + memcpy(pos + desc->store_offset + desc->packlength, &first_run, sizeof(first_run)); } @@ -717,7 +821,7 @@ int hp_write_blobs(HP_INFO *info, const uchar *record, uchar *pos) uchar *chain; if (!*chain_pos) continue; - memcpy(&chain, pos + desc->offset + desc->packlength, sizeof(chain)); + memcpy(&chain, pos + desc->store_offset + desc->packlength, sizeof(chain)); if (chain == *chain_pos) *chain_pos= NULL; else @@ -785,10 +889,28 @@ static void hp_reassemble_chain(const uchar *chain, uint32 data_len, } +/* + Copy a chain's data into a caller-supplied buffer, whichever of the + three run layouts it uses. hp_reassemble_chain() handles the multi-run + case; this wraps the two contiguous ones around it. +*/ + +static void hp_copy_chain_data(const uchar *chain, uint32 data_len, + uchar *dest, uint visible, uint recbuffer) +{ + if (hp_is_single_rec(chain, visible)) + memcpy(dest, chain, data_len); /* Case A: data at offset 0 */ + else if (hp_is_zerocopy(chain, visible)) + memcpy(dest, chain + recbuffer, data_len); /* Case B: past the header */ + else + hp_reassemble_chain(chain, data_len, dest, visible, recbuffer); +} + + /* Read blob data from continuation runs into the reassembly buffer. - After memcpy(record, pos, reclength), blob descriptor pointers in + After hp_unpack_record(), blob descriptor pointers in record[] point into HP_BLOCK continuation run chains. This function walks each chain, reassembles blob data into info->blob_buff, and rewrites the pointers in record[] to point into blob_buff. @@ -829,11 +951,19 @@ int hp_read_blobs(HP_INFO *info, uchar *record, const uchar *pos) uint32 data_len; const uchar *chain; - data_len= hp_blob_length(desc, record); + /* + A promoted column goes straight into the record buffer, where the + SQL layer expects its value inline, so it never occupies blob_buff. + */ + if (desc->promoted) + continue; + + data_len= hp_blob_stored_length(desc, pos); if (data_len == 0) continue; - memcpy(&chain, record + desc->offset + desc->packlength, sizeof(chain)); + memcpy(&chain, pos + desc->store_offset + desc->packlength, + sizeof(chain)); if (!force_copy && !hp_is_multi_run(chain, visible)) { @@ -866,11 +996,26 @@ int hp_read_blobs(HP_INFO *info, uchar *record, const uchar *pos) uint32 data_len; const uchar *chain, *blob_data= buff_ptr; - data_len= hp_blob_length(desc, record); + data_len= hp_blob_stored_length(desc, pos); if (data_len == 0) continue; - memcpy(&chain, record + desc->offset + desc->packlength, sizeof(chain)); + memcpy(&chain, pos + desc->store_offset + desc->packlength, + sizeof(chain)); + + if (desc->promoted) + { + /* + Materialize inline. The SQL layer reaches a VARCHAR's data at a + fixed offset from its length prefix, so unlike a native blob + there is no pointer slot to aim at heap memory, and therefore no + zero-copy form of this read. + */ + hp_copy_chain_data(chain, data_len, + record + desc->offset + desc->packlength, + visible, recbuffer); + continue; + } if (hp_is_single_rec(chain, visible)) { @@ -984,9 +1129,9 @@ void hp_free_blobs(HP_SHARE *share, uchar *pos) { uchar *chain; - if (hp_blob_length(desc, pos) == 0) + if (hp_blob_stored_length(desc, pos) == 0) continue; - memcpy(&chain, pos + desc->offset + desc->packlength, sizeof(chain)); + memcpy(&chain, pos + desc->store_offset + desc->packlength, sizeof(chain)); hp_free_run_chain(share, chain); } diff --git a/storage/heap/hp_clear.c b/storage/heap/hp_clear.c index 8db3da68387c1..3d4094f28bd8a 100644 --- a/storage/heap/hp_clear.c +++ b/storage/heap/hp_clear.c @@ -40,6 +40,7 @@ void hp_clear(HP_SHARE *info) info->block.high_water_allocated=0; hp_clear_keys(info); info->records= info->deleted= info->total_records= 0; + info->deleted_entries= 0; info->data_length= 0; info->blength=1; info->changed=0; diff --git a/storage/heap/hp_create.c b/storage/heap/hp_create.c index a7a62ce3d26a9..df75ac1b2ddb3 100644 --- a/storage/heap/hp_create.c +++ b/storage/heap/hp_create.c @@ -22,6 +22,135 @@ static void init_block(HP_BLOCK *block, size_t reclength, ulong min_records, const ulong max_records); +/* + Map an offset in the SQL record buffer to its position in a stored + record. + + Defined for every offset outside a promoted column's reserved payload, + which is every offset the engine ever addresses: the null bytes, the + fixed-width columns, and each descriptor's length prefix. An offset + inside a payload has no stored counterpart, because that is the space + promotion removes. +*/ + +static uint hp_stored_offset(const HP_SHARE *share, uint offset) +{ + const HP_COPY_SPAN *span, *span_end; + + for (span= share->copy_spans, span_end= span + share->copy_span_count; + span < span_end; span++) + { + if (offset >= span->offset && offset < span->offset + span->length) + return span->store_offset + (offset - span->offset); + } + DBUG_ASSERT(0); /* Offset inside a promoted payload */ + return offset; +} + + +/* + Build the map between the SQL record buffer and the stored record. + + The spans are the ranges between promoted columns' reserved payloads. + A promoted column's length prefix rides along at the end of the span + before it, so only the payload is a gap, and the stored record spends + a chain pointer there instead. + + Once the spans exist, every descriptor's stored position follows from + them -- including the native blobs', whose shape does not change but + whose position does, because compaction moves everything after the + first promoted column. + + ha_heap.cc computes stored_reclength independently while deciding what + to promote. Re-deriving it here and asserting the two agree is what + catches a promotion decision that does not match the layout it implies. +*/ + +static void hp_setup_record_layout(HP_SHARE *share, uint reclength, + uint stored_reclength) +{ + HP_COPY_SPAN *span= share->copy_spans; + uint i, sql_pos= 0, store_pos= 0; + + for (i= 0; i < share->blob_count; i++) + { + HP_BLOB_DESC *desc= share->blob_descs + i; + uint gap; + + if (!desc->promoted) + continue; + gap= desc->offset + desc->packlength; + DBUG_ASSERT(gap >= sql_pos); /* Descriptors ascend by offset */ + span->offset= sql_pos; + span->store_offset= store_pos; + span->length= gap - sql_pos; + store_pos+= span->length + (uint) sizeof(uchar*); + sql_pos= gap + desc->length; + span++; + } + span->offset= sql_pos; + span->store_offset= store_pos; + span->length= reclength - sql_pos; + + DBUG_ASSERT((uint) (span - share->copy_spans) + 1 == share->copy_span_count); + DBUG_ASSERT(store_pos + span->length == stored_reclength); + + for (i= 0; i < share->blob_count; i++) + { + HP_BLOB_DESC *desc= share->blob_descs + i; + desc->store_offset= hp_stored_offset(share, desc->offset); + } + share->stored_reclength= stored_reclength; +} + + +/* + Derive a key's stored segments from its SQL segments. + + Mostly this is re-addressing: compaction has moved every segment that + follows the first promoted column. A segment over a promoted column is + additionally marked HA_BLOB_PART, which in a stored segment means the + data is not inline but in a continuation chain, exactly as it is for a + native blob. + + The segment keeps its VARCHAR type, so hashing and comparison keep + applying VARCHAR rules -- prefix truncation, PAD/NOPAD, the multi-byte + character position walk. Only where the bytes are read from changes. + Promotion must not change what a key means. +*/ + +static void hp_make_stored_keysegs(HP_SHARE *share, HA_KEYSEG *seg, + HA_KEYSEG *stored, uint keysegs) +{ + uint i, j; + + memcpy(stored, seg, sizeof(*stored) * keysegs); + for (i= 0; i < keysegs; i++) + { + stored[i].start= hp_stored_offset(share, seg[i].start); + stored[i].bit_pos= hp_stored_offset(share, seg[i].bit_pos); + /* + null_pos needs no translation, and gets none: the null bitmap sits + at the front of the record, ahead of every field, so no promoted + column can precede it and compaction cannot move it. hp_hash.c + reads a stored record at this offset, so assert what that relies on + rather than leaving the one untranslated offset unexplained. + */ + DBUG_ASSERT(!seg[i].null_bit || + hp_stored_offset(share, seg[i].null_pos) == seg[i].null_pos); + for (j= 0; j < share->blob_count; j++) + { + const HP_BLOB_DESC *desc= share->blob_descs + j; + if (desc->promoted && desc->offset == seg[i].start) + { + stored[i].flag|= HA_BLOB_PART; + break; + } + } + } +} + + /* In how many parts are we going to do allocations of memory and indexes If we assign 1M to the heap table memory, we will allocate roughly @@ -56,20 +185,32 @@ int heap_create(const char *name, HP_CREATE_INFO *create_info, { uint i, key_segs, max_length, length; HP_SHARE *share= 0; - HA_KEYSEG *keyseg; + HA_KEYSEG *keyseg, *stored_keyseg; HP_KEYDEF *keydef= create_info->keydef; uint reclength= create_info->reclength; + /* + Promoted columns make the stored record shorter than record[0]. + Everything about the block geometry -- the record stride, the + visibility byte offset, the rows that fit in the table ceiling -- + follows the stored length, which is the whole point of promoting. + */ + uint stored_reclength= (create_info->stored_reclength ? + create_info->stored_reclength : reclength); + uint copy_spans= 1, promoted= 0; uint keys= create_info->keys; ulong min_records= create_info->min_records; ulong max_records= create_info->max_records; uint visible_offset; /* - max_records is this function's row limit and 0 means "no limit". - The share stores an explicit ceiling instead, writing "no limit" as - NO_LIMIT_RECORDS, so hp_alloc_from_tail() tests one value with no - special case. That leaves 0 free to mean what it says on the - share, a table that accepts no rows. Block sizing needs a concrete - row count rather than the ceiling, so derive one here. + max_records is an expected record count used to size blocks, not a + limit; 0 means the caller has no expectation. Block sizing needs a + concrete number, so derive one here. + + The row limit is create_info->max_rows, where 0 still means "no + limit". The share stores an explicit ceiling instead, writing "no + limit" as NO_LIMIT_ROWS, so heap_write() tests one value with no + special case. That leaves 0 free to mean what it says on the share, + a table that accepts no rows. */ ulong block_max_records= (max_records ? max_records : MY_MAX(min_records, 1000)); @@ -103,7 +244,21 @@ int heap_create(const char *name, HP_CREATE_INFO *create_info, the flags byte at offset 'visible'. This also satisfies the blob continuation header requirement (HP_CONT_HEADER_SIZE + 1). */ - visible_offset= MY_MAX(reclength, HP_DEL_METADATA_SIZE); + visible_offset= MY_MAX(stored_reclength, HP_DEL_METADATA_SIZE); + + /* + One verbatim range per promoted column's payload gap, plus the + trailing one. A table with no promoted column keeps a single span + covering the whole record. + */ + for (i= 0; i < create_info->blob_count; i++) + { + if (create_info->blob_descs[i].promoted) + { + promoted++; + copy_spans++; + } + } for (i= key_segs= max_length= 0, keyinfo= keydef; i < keys; i++, keyinfo++) { @@ -144,13 +299,6 @@ int heap_create(const char *name, HP_CREATE_INFO *create_info, /* fall through */ case HA_KEYTYPE_VARTEXT1: keyinfo->flag|= HA_VAR_LENGTH_KEY; - /* - Real blob fields always enter as VARTEXT4/VARBINARY4, never - as VARTEXT1/VARBINARY1. Strip any spurious HA_BLOB_PART - (e.g. from uninitialized key_part_flag in SJ weedout tables). - */ - DBUG_ASSERT(!(keyseg->flag & HA_BLOB_PART)); - keyseg->flag&= ~HA_BLOB_PART; /* For BTREE algorithm, key length, greater than or equal to 255, is packed on 3 bytes. @@ -236,8 +384,11 @@ int heap_create(const char *name, HP_CREATE_INFO *create_info, sizeof(HP_SHARE)+ keys*sizeof(HP_KEYDEF)+ key_segs*sizeof(HA_KEYSEG)+ + (promoted ? + key_segs*sizeof(HA_KEYSEG) : 0)+ create_info->blob_count* - sizeof(HP_BLOB_DESC), + sizeof(HP_BLOB_DESC)+ + copy_spans*sizeof(HP_COPY_SPAN), MYF(MY_ZEROFILL | (create_info->internal_table ? MY_THREAD_SPECIFIC : 0))))) @@ -245,14 +396,29 @@ int heap_create(const char *name, HP_CREATE_INFO *create_info, share->keydef= (HP_KEYDEF*) (share + 1); share->key_stat_version= 1; keyseg= (HA_KEYSEG*) (share->keydef + keys); - if (create_info->blob_count) + /* + Without promotion the stored segments are the SQL ones, so nothing + extra is allocated and every lookup through seg_stored below is the + same pointer it always was. + */ + stored_keyseg= promoted ? keyseg + key_segs : keyseg; { - share->blob_descs= (HP_BLOB_DESC*) (keyseg + key_segs); - memcpy(share->blob_descs, create_info->blob_descs, - create_info->blob_count * sizeof(HP_BLOB_DESC)); - share->blob_count= create_info->blob_count; + uchar *tail= (uchar*) (keyseg + key_segs + + (promoted ? key_segs : 0)); + if (create_info->blob_count) + { + share->blob_descs= (HP_BLOB_DESC*) tail; + memcpy(share->blob_descs, create_info->blob_descs, + create_info->blob_count * sizeof(HP_BLOB_DESC)); + share->blob_count= create_info->blob_count; + tail= (uchar*) (share->blob_descs + create_info->blob_count); + } + share->copy_spans= (HP_COPY_SPAN*) tail; + share->copy_span_count= copy_spans; + share->promoted_count= promoted; + hp_setup_record_layout(share, reclength, stored_reclength); } - init_block(&share->block, hp_memory_needed_per_row(reclength), + init_block(&share->block, hp_memory_needed_per_row(stored_reclength), min_records, block_max_records); /* Fix keys */ memcpy(share->keydef, keydef, (size_t) (sizeof(keydef[0]) * keys)); @@ -261,7 +427,16 @@ int heap_create(const char *name, HP_CREATE_INFO *create_info, keyinfo->seg= keyseg; memcpy(keyseg, keydef[i].seg, (size_t) (sizeof(keyseg[0]) * keydef[i].keysegs)); + if (promoted) + { + keyinfo->seg_stored= stored_keyseg; + hp_make_stored_keysegs(share, keyseg, stored_keyseg, + keydef[i].keysegs); + } + else + keyinfo->seg_stored= keyseg; keyseg+= keydef[i].keysegs; + stored_keyseg+= keydef[i].keysegs; if (keydef[i].algorithm == HA_KEY_ALG_BTREE) { @@ -271,6 +446,7 @@ int heap_create(const char *name, HP_CREATE_INFO *create_info, keyseg->flag= 0; keyseg->null_bit= 0; keyseg++; + stored_keyseg++; /* Keep the two arrays in lockstep */ init_tree(&keyinfo->rb_tree, 0, 0, sizeof(uchar*), keys_compare, NULL, NULL, @@ -292,8 +468,11 @@ int heap_create(const char *name, HP_CREATE_INFO *create_info, } share->min_records= min_records; share->max_records= max_records ? max_records : NO_LIMIT_RECORDS; + share->max_rows= (create_info->max_rows ? create_info->max_rows : + NO_LIMIT_ROWS); share->max_table_size= create_info->max_table_size; share->data_length= share->index_length= 0; + share->deleted_entries= 0; share->reclength= reclength; share->visible= visible_offset; share->blength= 1; diff --git a/storage/heap/hp_delete.c b/storage/heap/hp_delete.c index e5a33ed54228c..4f3f8bbaf8059 100644 --- a/storage/heap/hp_delete.c +++ b/storage/heap/hp_delete.c @@ -47,6 +47,7 @@ void hp_push_free_block(HP_SHARE *share, uchar *first, uint16 count) share->del_link= last; share->deleted+= count; + share->deleted_entries++; share->total_records-= count; } @@ -57,6 +58,10 @@ void hp_push_free_block(HP_SHARE *share, uchar *first, uint16 count) Normalizes the head by treating a single record as a block of count 1, then checks adjacency in two directions (above/below). Falls back to hp_push_free_block/hp_push_free_record when no adjacency. + + Both merge branches leave share->deleted_entries alone: the head entry + grows to cover the new range instead of a second entry appearing. Only + the two fall-back calls add one, and they maintain it themselves. */ void hp_push_free_block_coalesce(HP_SHARE *share, uchar *first, @@ -197,13 +202,13 @@ int heap_delete(HP_INFO *info, const uchar *record) uint i; for (i= 0, desc= share->blob_descs; i < share->blob_count; i++, desc++) { - if (hp_blob_length(desc, pos) == 0) + if (hp_blob_stored_length(desc, pos) == 0) { info->pending_blob_chains[i]= NULL; continue; } memcpy(&info->pending_blob_chains[i], - pos + desc->offset + desc->packlength, sizeof(uchar*)); + pos + desc->store_offset + desc->packlength, sizeof(uchar*)); } info->has_pending_blob_free= TRUE; } diff --git a/storage/heap/hp_hash.c b/storage/heap/hp_hash.c index 83eff09b79702..a935c8bc6852a 100644 --- a/storage/heap/hp_hash.c +++ b/storage/heap/hp_hash.c @@ -338,12 +338,107 @@ ulong hp_hashnr(HP_KEYDEF *keydef, const uchar *key) simultaneously -- key_blob_buff holds only one blob at a time. */ +/* + Resolve a VARCHAR key segment to the value's bytes and its length. + + In a stored record a promoted segment holds a length prefix and a + continuation chain instead of the value, so the bytes must be + materialized before they can be hashed or compared. Nothing else about + the segment changes: the collation, the prefix length and the PAD + semantics are applied to the materialized bytes exactly as they are to + inline ones. That is what keeps a promoted column's key meaning + identical to an unpromoted one's. + + Only segments in a stored key array carry HA_BLOB_PART for this + purpose, so the same call serves both an input record and a stored one. + + @return TRUE if the value could not be materialized +*/ + +static my_bool hp_varchar_seg_data(HP_INFO *info, const HA_KEYSEG *seg, + const uchar *rec, const uchar **data, + size_t *length) +{ + const uchar *pos= rec + seg->start; + uint pack_length= seg->bit_start; + size_t len= (pack_length == 1 ? (size_t) *pos : (size_t) uint2korr(pos)); + + if ((seg->flag & HA_BLOB_PART) && len) + { + const uchar *out_of_line; + memcpy(&out_of_line, pos + pack_length, sizeof(out_of_line)); + if (!info) + { + /* + An SQL record. Its pointer addresses the value itself, laid out + contiguously, because nothing has packed it into a continuation + chain yet. + */ + *data= out_of_line; + } + else if (!(*data= hp_materialize_one_blob(info, out_of_line, + (uint32) len))) + return TRUE; + } + else + *data= pos + pack_length; + *length= len; + return FALSE; +} + + +/* + The width of a VARCHAR segment's value that takes part in a key. + + An inline value is capped at the segment's declared width, which is what + seg->length carries: a key over a VARCHAR(N) compares at most N + characters' worth of bytes. + + An out-of-line value is fetched whole by hp_varchar_seg_data(), and it + cannot be wider than the column was declared, so the cap has nothing to + cut. Say so with an early return rather than leave a clamp that can + never fire. The blob segments do the same. + + seg must come from the SQL segments. Only there does HA_BLOB_PART mean + "the SQL record holds a length and a pointer": hp_make_stored_keysegs() + also sets it on a stored segment whose column the engine itself packed + out of line, where the SQL record still holds the value inline. +*/ + +static size_t hp_varchar_seg_width(const HA_KEYSEG *seg, + const uchar *data, size_t length) +{ + CHARSET_INFO *cs= seg->charset; + + if (seg->flag & HA_BLOB_PART) + return length; + if (cs->mbmaxlen > 1) + { + size_t char_length= hp_charpos(cs, data, data + length, + seg->length / cs->mbmaxlen); + set_if_smaller(length, char_length); + } + else + set_if_smaller(length, seg->length); + return length; +} + + ulong hp_rec_hashnr(HP_INFO *info, HP_KEYDEF *keydef, const uchar *rec) { my_hasher_st hasher= my_hasher_mysql5x(); - HA_KEYSEG *seg,*endseg; + HA_KEYSEG *seg,*endseg,*sql_seg; - for (seg=keydef->seg,endseg=seg+keydef->keysegs ; seg < endseg ; seg++) + /* + A stored record is addressed by the stored segments: promotion has + compacted the record, moving everything after the first promoted + column. Without promotion the two arrays are the same pointer. + sql_seg walks the SQL segments in step, for the one decision that + describes the column rather than the record being hashed. + */ + seg= info ? keydef->seg_stored : keydef->seg; + sql_seg= keydef->seg; + for (endseg= seg + keydef->keysegs ; seg < endseg ; seg++, sql_seg++) { const uchar *pos= rec+seg->start; const uchar *end= pos+seg->length; @@ -370,23 +465,14 @@ ulong hp_rec_hashnr(HP_INFO *info, HP_KEYDEF *keydef, const uchar *rec) else if (seg->type == HA_KEYTYPE_VARTEXT1) /* Any VARCHAR segments */ { CHARSET_INFO *cs= seg->charset; - size_t pack_length= seg->bit_start; - size_t length= (pack_length == 1 ? - (size_t) *(uchar*) pos : - uint2korr(pos)); - DBUG_ASSERT(!(seg->flag & HA_BLOB_PART)); + const uchar *data; + size_t length; - if (cs->mbmaxlen > 1) - { - size_t char_length; - char_length= hp_charpos(cs, pos + pack_length, - pos + pack_length + length, - seg->length/cs->mbmaxlen); - set_if_smaller(length, char_length); - } - else - set_if_smaller(length, seg->length); - my_ci_hash_sort(&hasher, cs, pos+pack_length, length); + if (hp_varchar_seg_data(info, seg, rec, &data, &length)) + return 0; + + length= hp_varchar_seg_width(sql_seg, data, length); + my_ci_hash_sort(&hasher, cs, data, length); } else if (seg->type == HA_KEYTYPE_VARTEXT4 || seg->type == HA_KEYTYPE_VARBINARY4) @@ -459,14 +545,22 @@ ulong hp_rec_hashnr(HP_INFO *info, HP_KEYDEF *keydef, const uchar *rec) int hp_rec_key_cmp(HP_KEYDEF *keydef, const uchar *rec1, const uchar *rec2, HP_INFO *info) { - HA_KEYSEG *seg,*endseg; + HA_KEYSEG *seg,*endseg,*seg2; - for (seg=keydef->seg,endseg=seg+keydef->keysegs ; seg < endseg ; seg++) + /* + rec1 is always an input record, so it is addressed by the SQL + segments. rec2 is a stored record whenever info is given, and then + needs the stored segments: promotion has compacted it. The two + arrays are the same pointer for a table with nothing promoted. + */ + seg2= info ? keydef->seg_stored : keydef->seg; + for (seg=keydef->seg,endseg=seg+keydef->keysegs ; seg < endseg ; + seg++, seg2++) { if (seg->null_bit) { if ((rec1[seg->null_pos] & seg->null_bit) != - (rec2[seg->null_pos] & seg->null_bit)) + (rec2[seg2->null_pos] & seg2->null_bit)) return 1; if (rec1[seg->null_pos] & seg->null_bit) continue; @@ -477,7 +571,7 @@ int hp_rec_key_cmp(HP_KEYDEF *keydef, const uchar *rec1, const uchar *rec2, size_t char_length1; size_t char_length2; uchar *pos1= (uchar*)rec1 + seg->start; - uchar *pos2= (uchar*)rec2 + seg->start; + uchar *pos2= (uchar*)rec2 + seg2->start; if (cs->mbmaxlen > 1) { size_t char_length= seg->length / cs->mbmaxlen; @@ -504,7 +598,7 @@ int hp_rec_key_cmp(HP_KEYDEF *keydef, const uchar *rec1, const uchar *rec2, */ uint packlength= seg->bit_start; uchar *pos1= (uchar*) rec1 + seg->start; - uchar *pos2= (uchar*) rec2 + seg->start; + uchar *pos2= (uchar*) rec2 + seg2->start; uint32 len1= hp_blob_key_length(packlength, pos1); uint32 len2= hp_blob_key_length(packlength, pos2); const uchar *data1; @@ -542,29 +636,25 @@ int hp_rec_key_cmp(HP_KEYDEF *keydef, const uchar *rec1, const uchar *rec2, } else if (seg->type == HA_KEYTYPE_VARTEXT1) /* Any VARCHAR segments */ { - uchar *pos1= (uchar*) rec1 + seg->start; - uchar *pos2= (uchar*) rec2 + seg->start; + const uchar *pos1, *pos2; size_t len1, len2; - size_t pack_length= seg->bit_start; CHARSET_INFO *cs= seg->charset; - if (pack_length == 1) - { - len1= (size_t) *(uchar*) pos1++; - len2= (size_t) *(uchar*) pos2++; - } - else - { - len1= uint2korr(pos1); - len2= uint2korr(pos2); - pos1+= 2; - pos2+= 2; - } + + /* + rec2's value may be out of line when it is a stored record whose + column was promoted; rec1's never is. Both are compared below by + the same VARCHAR rules regardless. + */ + if (hp_varchar_seg_data(NULL, seg, rec1, &pos1, &len1) || + hp_varchar_seg_data(info, seg2, rec2, &pos2, &len2)) + return 1; /* We're not using my_ci_strnncollsp_nchars() here for NOPAD collations because some virtual implementations do not work correctly. For details see: https://jira.mariadb.org/browse/MDEV-38712 */ - if (cs->mbmaxlen > 1 && !(cs->state & MY_CS_NOPAD)) + if (!(seg->flag & HA_BLOB_PART) && + cs->mbmaxlen > 1 && !(cs->state & MY_CS_NOPAD)) { size_t nchars= seg->length / cs->mbmaxlen; if (my_ci_strnncollsp_nchars(cs, @@ -576,24 +666,9 @@ int hp_rec_key_cmp(HP_KEYDEF *keydef, const uchar *rec1, const uchar *rec2, } else { - size_t char_length1= len1; - size_t char_length2= len2; + size_t char_length1= hp_varchar_seg_width(seg, pos1, len1); + size_t char_length2= hp_varchar_seg_width(seg, pos2, len2); - if (cs->mbmaxlen > 1) - { - size_t safe_length1= char_length1; - size_t safe_length2= char_length2; - size_t char_length= seg->length / cs->mbmaxlen; - char_length1= hp_charpos(cs, pos1, pos1 + char_length1, char_length); - set_if_smaller(char_length1, safe_length1); - char_length2= hp_charpos(cs, pos2, pos2 + char_length2, char_length); - set_if_smaller(char_length2, safe_length2); - } - else - { - set_if_smaller(char_length1, seg->length); - set_if_smaller(char_length2, seg->length); - } if (my_ci_strnncollsp(seg->charset, pos1, char_length1, pos2, char_length2)) @@ -607,13 +682,13 @@ int hp_rec_key_cmp(HP_KEYDEF *keydef, const uchar *rec1, const uchar *rec2, { uchar bits1= get_rec_bits(rec1 + seg->bit_pos, seg->bit_start, seg->bit_length); - uchar bits2= get_rec_bits(rec2 + seg->bit_pos, - seg->bit_start, seg->bit_length); + uchar bits2= get_rec_bits(rec2 + seg2->bit_pos, + seg2->bit_start, seg2->bit_length); if (bits1 != bits2) return 1; dec= 1; } - if (bcmp(rec1 + seg->start, rec2 + seg->start, seg->length - dec)) + if (bcmp(rec1 + seg->start, rec2 + seg2->start, seg->length - dec)) return 1; } } @@ -625,15 +700,21 @@ int hp_rec_key_cmp(HP_KEYDEF *keydef, const uchar *rec1, const uchar *rec2, int hp_key_cmp(HP_KEYDEF *keydef, const uchar *rec, const uchar *key, HP_INFO *info) { - HA_KEYSEG *seg,*endseg; + HA_KEYSEG *seg,*endseg,*seg2; + /* + The key tuple is laid out by the SQL segments, while rec is a stored + record whenever info is given and then needs the stored segments. + The two arrays are the same pointer with nothing promoted. + */ + seg2= info ? keydef->seg_stored : keydef->seg; for (seg=keydef->seg,endseg=seg+keydef->keysegs ; seg < endseg ; - key+= (seg++)->length) + seg2++, key+= (seg++)->length) { if (seg->null_bit) { - int found_null= MY_TEST(rec[seg->null_pos] & seg->null_bit); + int found_null= MY_TEST(rec[seg2->null_pos] & seg2->null_bit); if (found_null != (int) *key++) return 1; if (found_null) @@ -648,7 +729,7 @@ int hp_key_cmp(HP_KEYDEF *keydef, const uchar *rec, const uchar *key, CHARSET_INFO *cs= seg->charset; size_t char_length_key; size_t char_length_rec; - uchar *pos= (uchar*) rec + seg->start; + uchar *pos= (uchar*) rec + seg2->start; if (cs->mbmaxlen > 1) { size_t char_length= seg->length / cs->mbmaxlen; @@ -676,7 +757,7 @@ int hp_key_cmp(HP_KEYDEF *keydef, const uchar *rec, const uchar *key, from hp_make_key(): always 4-byte length + data pointer. */ uint packlength= seg->bit_start; - uchar *pos= (uchar*) rec + seg->start; + uchar *pos= (uchar*) rec + seg2->start; uint32 rec_blob_len= hp_blob_key_length(packlength, pos); uint32 key_blob_len= uint4korr(key); const uchar *key_data; @@ -705,14 +786,15 @@ int hp_key_cmp(HP_KEYDEF *keydef, const uchar *rec, const uchar *key, } else if (seg->type == HA_KEYTYPE_VARTEXT1) /* Any VARCHAR segments */ { - uchar *pos= (uchar*) rec + seg->start; + const uchar *pos; CHARSET_INFO *cs= seg->charset; - size_t pack_length= seg->bit_start; - size_t char_length_rec= (pack_length == 1 ? (size_t) *(uchar*) pos : - uint2korr(pos)); + size_t char_length_rec; /* Key segments are always packed with 2 bytes */ size_t char_length_key= uint2korr(key); - pos+= pack_length; + + /* rec's value is out of line when its column was promoted */ + if (hp_varchar_seg_data(info, seg2, rec, &pos, &char_length_rec)) + return 1; key+= 2; /* skip key pack length */ if (cs->mbmaxlen > 1 && !(cs->state & MY_CS_NOPAD)) { @@ -728,15 +810,11 @@ int hp_key_cmp(HP_KEYDEF *keydef, const uchar *rec, const uchar *key, { if (cs->mbmaxlen > 1) { - size_t char_length1, char_length2; - char_length1= char_length2= seg->length / cs->mbmaxlen; - char_length1= hp_charpos(cs, key, key + char_length_key, char_length1); + size_t char_length1= hp_charpos(cs, key, key + char_length_key, + seg->length / cs->mbmaxlen); set_if_smaller(char_length_key, char_length1); - char_length2= hp_charpos(cs, pos, pos + char_length_rec, char_length2); - set_if_smaller(char_length_rec, char_length2); } - else - set_if_smaller(char_length_rec, seg->length); + char_length_rec= hp_varchar_seg_width(seg, pos, char_length_rec); if (my_ci_strnncollsp(seg->charset, pos, char_length_rec, @@ -749,14 +827,14 @@ int hp_key_cmp(HP_KEYDEF *keydef, const uchar *rec, const uchar *key, uint dec= 0; if (seg->type == HA_KEYTYPE_BIT && seg->bit_length) { - uchar bits= get_rec_bits(rec + seg->bit_pos, - seg->bit_start, seg->bit_length); + uchar bits= get_rec_bits(rec + seg2->bit_pos, + seg2->bit_start, seg2->bit_length); if (bits != (*key)) return 1; dec= 1; } - if (bcmp(rec + seg->start, key + dec, seg->length - dec)) + if (bcmp(rec + seg2->start, key + dec, seg->length - dec)) return 1; } } diff --git a/storage/heap/hp_info.c b/storage/heap/hp_info.c index 47b1ed6e8469e..5c47ccb73387d 100644 --- a/storage/heap/hp_info.c +++ b/storage/heap/hp_info.c @@ -33,10 +33,50 @@ int heap_info(reg1 HP_INFO *info,reg2 HEAPINFO *x, int flag ) DBUG_ENTER("heap_info"); x->records = info->s->records; x->deleted = info->s->deleted; + /* + Free list entries, a coalesced block counting once. A scan skips a + whole block in one step, so this and not 'deleted' is what the free + records cost it: freeing a row whose blob data spanned a thousand + records adds one step, not a thousand. + */ + x->deleted_entries = info->s->deleted_entries; x->reclength = info->s->reclength; x->data_length = info->s->data_length; + /* + The free space, in the bytes data_length counts. A free record is a + slot of the stored row width, which is the SQL row width only while + nothing is stored out of line; multiplying the free record count by + reclength once a wide VARCHAR is promoted reports more free space + than the table has ever allocated. + */ + x->delete_length = (ulonglong) info->s->deleted * + info->s->block.recbuffer; x->index_length = info->s->index_length; x->max_records = info->s->max_records; + /* + The ceiling data_length counts toward, in the bytes data_length is + counted in. Records are refused once data_length + index_length + reaches max_table_size, so that bound holds whatever the rows look + like -- and it is the only one that can, because a row with + out-of-line columns occupies a data-dependent number of records. + + A row limit binds first where the rows it admits cannot reach that + ceiling. reclength is the widest a row is as the SQL layer lays it + out, and storing a column out of line moves those bytes into + continuation records rather than adding to them, so max_rows * + reclength bounds the rows either way. + + Deliberately not max_records * reclength: max_records counts record + slots, and a slot holds a whole row only while nothing is stored out + of line. Multiplying the two once a wide VARCHAR is promoted + reports a table's ceiling as its slot count times a row width it no + longer stores, which overstates it by the promotion ratio. + */ + x->max_data_length = info->s->max_table_size; + if (info->s->max_rows != NO_LIMIT_ROWS && + (ulonglong) info->s->max_rows <= ULONGLONG_MAX / info->s->reclength) + set_if_smaller(x->max_data_length, + (ulonglong) info->s->max_rows * info->s->reclength); x->errkey = info->errkey; x->create_time = info->s->create_time; if (flag & HA_STATUS_AUTO) diff --git a/storage/heap/hp_rfirst.c b/storage/heap/hp_rfirst.c index 13b236e0274a8..fc70d9c435ba4 100644 --- a/storage/heap/hp_rfirst.c +++ b/storage/heap/hp_rfirst.c @@ -39,7 +39,7 @@ int heap_rfirst(HP_INFO *info, uchar *record, int inx) memcpy(&pos, pos + (*keyinfo->get_key_length)(keyinfo, pos), sizeof(uchar*)); info->current_ptr = pos; - memcpy(record, pos, (size_t)share->reclength); + hp_unpack_record(share, record, pos); if (share->blob_count && hp_read_blobs(info, record, pos)) DBUG_RETURN(my_errno); /* diff --git a/storage/heap/hp_rkey.c b/storage/heap/hp_rkey.c index f0abee4ae9a8a..6e47fdf39e158 100644 --- a/storage/heap/hp_rkey.c +++ b/storage/heap/hp_rkey.c @@ -71,7 +71,7 @@ int heap_rkey(HP_INFO *info, uchar *record, int inx, const uchar *key, if ((keyinfo->flag & (HA_NOSAME | HA_NULL_PART_KEY)) != HA_NOSAME) memcpy(info->lastkey, key, (size_t) keyinfo->length); } - memcpy(record, pos, (size_t) share->reclength); + hp_unpack_record(share, record, pos); if (share->blob_count && hp_read_blobs(info, record, pos)) DBUG_RETURN(my_errno); info->update= HA_STATE_AKTIV; diff --git a/storage/heap/hp_rlast.c b/storage/heap/hp_rlast.c index 2e0df8665f2f9..9b614a11394d2 100644 --- a/storage/heap/hp_rlast.c +++ b/storage/heap/hp_rlast.c @@ -39,7 +39,7 @@ int heap_rlast(HP_INFO *info, uchar *record, int inx) memcpy(&pos, pos + (*keyinfo->get_key_length)(keyinfo, pos), sizeof(uchar*)); info->current_ptr = pos; - memcpy(record, pos, (size_t)share->reclength); + hp_unpack_record(share, record, pos); if (share->blob_count && hp_read_blobs(info, record, pos)) DBUG_RETURN(my_errno); info->update = HA_STATE_AKTIV; diff --git a/storage/heap/hp_rnext.c b/storage/heap/hp_rnext.c index f10d1ff7fe1f1..a273ddb049e03 100644 --- a/storage/heap/hp_rnext.c +++ b/storage/heap/hp_rnext.c @@ -128,7 +128,7 @@ int heap_rnext(HP_INFO *info, uchar *record) my_errno=HA_ERR_END_OF_FILE; DBUG_RETURN(my_errno); } - memcpy(record,pos,(size_t) share->reclength); + hp_unpack_record(share, record, pos); if (share->blob_count && hp_read_blobs(info, record, pos)) DBUG_RETURN(my_errno); info->update=HA_STATE_AKTIV | HA_STATE_NEXT_FOUND; diff --git a/storage/heap/hp_rprev.c b/storage/heap/hp_rprev.c index feda190da4e06..caf2601c11432 100644 --- a/storage/heap/hp_rprev.c +++ b/storage/heap/hp_rprev.c @@ -95,7 +95,7 @@ int heap_rprev(HP_INFO *info, uchar *record) my_errno=HA_ERR_END_OF_FILE; DBUG_RETURN(my_errno); } - memcpy(record,pos,(size_t) share->reclength); + hp_unpack_record(share, record, pos); if (share->blob_count && hp_read_blobs(info, record, pos)) DBUG_RETURN(my_errno); info->update=HA_STATE_AKTIV | HA_STATE_PREV_FOUND; diff --git a/storage/heap/hp_rrnd.c b/storage/heap/hp_rrnd.c index 7ce640ac03477..d75847526b6ac 100644 --- a/storage/heap/hp_rrnd.c +++ b/storage/heap/hp_rrnd.c @@ -45,7 +45,7 @@ int heap_rrnd(register HP_INFO *info, uchar *record, uchar *pos) DBUG_RETURN(my_errno=HA_ERR_RECORD_DELETED); } info->update=HA_STATE_PREV_FOUND | HA_STATE_NEXT_FOUND | HA_STATE_AKTIV; - memcpy(record,info->current_ptr,(size_t) share->reclength); + hp_unpack_record(share, record, info->current_ptr); if (share->blob_count && hp_read_blobs(info, record, info->current_ptr)) DBUG_RETURN(my_errno); DBUG_PRINT("exit", ("found record at %p", info->current_ptr)); diff --git a/storage/heap/hp_rsame.c b/storage/heap/hp_rsame.c index cbc373e410404..231265f4d59fc 100644 --- a/storage/heap/hp_rsame.c +++ b/storage/heap/hp_rsame.c @@ -50,7 +50,7 @@ int heap_rsame(register HP_INFO *info, uchar *record, int inx) DBUG_RETURN(my_errno); } } - memcpy(record,info->current_ptr,(size_t) share->reclength); + hp_unpack_record(share, record, info->current_ptr); if (share->blob_count && hp_read_blobs(info, record, info->current_ptr)) DBUG_RETURN(my_errno); DBUG_RETURN(0); diff --git a/storage/heap/hp_scan.c b/storage/heap/hp_scan.c index caa4914bd4de9..c2577f960b4b6 100644 --- a/storage/heap/hp_scan.c +++ b/storage/heap/hp_scan.c @@ -134,7 +134,7 @@ int heap_scan(register HP_INFO *info, uchar *record) goto retry; } info->update= HA_STATE_PREV_FOUND | HA_STATE_NEXT_FOUND | HA_STATE_AKTIV; - memcpy(record,info->current_ptr,(size_t) share->reclength); + hp_unpack_record(share, record, info->current_ptr); if (share->blob_count && hp_read_blobs(info, record, info->current_ptr)) DBUG_RETURN(my_errno); info->current_hash_ptr=0; /* Can't use read_next */ diff --git a/storage/heap/hp_test_freelist-t.c b/storage/heap/hp_test_freelist-t.c index 6215f5a5d0fb5..d18a5a9143af7 100644 --- a/storage/heap/hp_test_freelist-t.c +++ b/storage/heap/hp_test_freelist-t.c @@ -498,8 +498,14 @@ static void test_tail_reclaim_cross_block(void) last_alloc_before= (ulong) share->block.last_allocated; first_block= share->block.level_info[0].last_blocks; - /* Allow exactly 2 blocks, fail at 3rd */ - share->max_records= (ulong)(2 * rib - 1); + /* + Allow exactly one more block, so the blob fills the second and is + refused the third. The memory ceiling is the only gate on record + allocation: a record limit could not express this, because the + records a blob needs depend on its length. + */ + share->max_table_size= (share->data_length + share->index_length + + share->block.alloc_size); /* Blob large enough to need more than 4 + rib continuation records. @@ -601,8 +607,9 @@ static void test_tail_reclaim_three_blocks(void) last_alloc_before= (ulong) share->block.last_allocated; first_block= share->block.level_info[0].last_blocks; - /* Allow exactly 3 blocks, fail at 4th */ - share->max_records= (ulong)(3 * rib - 1); + /* Allow exactly two more blocks, so the 4th is refused */ + share->max_table_size= (share->data_length + share->index_length + + 2 * share->block.alloc_size); /* Blob needs more than 4 + 2*rib continuation records to span @@ -695,8 +702,9 @@ static void test_block_reuse_after_reclaim(void) last_alloc_before= (ulong) share->block.last_allocated; - /* Allow exactly 3 blocks, fail at 4th */ - share->max_records= (ulong)(3 * rib - 1); + /* Allow exactly two more blocks, so the 4th is refused */ + share->max_table_size= (share->data_length + share->index_length + + 2 * share->block.alloc_size); /* Blob that spans 3 blocks then fails */ blob_len= (uint32)((2 * rib + 20) * 16); @@ -715,8 +723,8 @@ static void test_block_reuse_after_reclaim(void) data_len_after_shrink= share->data_length; - /* Remove max_records limit so we can fill freely */ - share->max_records= NO_LIMIT_RECORDS; + /* Lift the memory ceiling so the tail can be refilled freely */ + share->max_table_size= ~(ulonglong) 0; /* Insert 2*rib non-blob rows. The first reuses the free-list slot @@ -1868,11 +1876,154 @@ static void test_reclaim_over_ceiling(void) } +/* + Test: hp_clear_dark_records() honours its contract at both record + lengths. + + Short records are cleared contiguously and long ones at a stride, for + the performance reason given at HP_CLEAR_DARK_MEMSET_MAX. The two + forms must leave the same three metadata fields cleared in every + record of the range, and must leave the records on either side of it + untouched, or free-list walking breaks. + + The rest of the file exercises only the short-record branch, because + its tables are built with REC_LENGTH 15, giving a recbuffer of 16. +*/ + +static void check_dark_range(uint recbuffer, uint visible) +{ + const uint records= 6; + uchar *buf= (uchar*) my_malloc(PSI_NOT_INSTRUMENTED, + records * recbuffer, MYF(0)); + uint i, cleared= 0, guarded= 0; + + memset(buf, 0xff, records * recbuffer); + + /* Clear records 1..4, leaving 0 and 5 as the untouched neighbours. */ + hp_clear_dark_records(buf + recbuffer, buf + 5 * recbuffer, + recbuffer, visible); + + for (i= 1; i <= 4; i++) + { + uchar *pos= buf + i * recbuffer; + if (*((uchar**) pos) == NULL && + pos[HP_DEL_FLAG_OFFSET] == 0 && + pos[visible] == 0) + cleared++; + } + ok(cleared == 4, + "recbuffer %u: all 4 dark records cleared (got %u)", recbuffer, + cleared); + + for (i= 0; i < records; i+= 5) + { + uchar *pos= buf + i * recbuffer; + if (pos[0] == 0xff && pos[HP_DEL_FLAG_OFFSET] == 0xff && + pos[visible] == 0xff) + guarded++; + } + ok(guarded == 2, + "recbuffer %u: records outside the range untouched (got %u)", + recbuffer, guarded); + + /* An empty range must clear nothing at all. */ + memset(buf, 0xff, records * recbuffer); + hp_clear_dark_records(buf + recbuffer, buf + recbuffer, recbuffer, + visible); + ok(buf[recbuffer] == 0xff, + "recbuffer %u: empty range is a no-op", recbuffer); + + my_free(buf); +} + + +static void test_clear_dark_records(void) +{ + /* 16 takes the contiguous branch, 64 the strided one. */ + check_dark_range(16, 15); + check_dark_range(64, 63); +} + + +/* + Test: share->deleted_entries counts free list entries, not records. + + A scan reads a coalesced block's length from its first record and + steps over the whole block at once, so what the free records cost it + is the number of entries. Taking part of a block therefore leaves the + entry standing; only taking its last record removes one. +*/ + +static void test_deleted_entries_counter(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar blob_data_big[100]; + uchar blob_data_small[50]; + ulong deleted_after_block; + uint id; + + memset(blob_data_big, 'G', sizeof(blob_data_big)); + memset(blob_data_small, 'S', sizeof(blob_data_small)); + + if (create_and_open("test_del_entries", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(9, "setup failed"); + return; + } + + ok(share->deleted_entries == 0, "new table has no free list entries"); + + build_record(rec, 1, blob_data_big, sizeof(blob_data_big)); + ok(heap_write(info, rec) == 0, "insert 100-byte blob"); + build_record(rec, 2, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert guard row"); + + { + uchar key[4]; + int4store(key, 1); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found blob row"); + ok(heap_delete(info, rec) == 0, "deleted blob row"); + } + hp_flush_pending_blob_free(info); + + deleted_after_block= (ulong) share->deleted; + ok(deleted_after_block > 1 && share->deleted_entries == 1, + "a run of %lu freed records is one entry", deleted_after_block); + + /* Take part of the block: records drop, the entry stays */ + build_record(rec, 3, blob_data_small, sizeof(blob_data_small)); + ok(heap_write(info, rec) == 0, "insert 50-byte blob out of the block"); + ok(share->deleted < deleted_after_block && share->deleted_entries == 1, + "partial take keeps the entry: deleted %lu, entries %lu", + (ulong) share->deleted, (ulong) share->deleted_entries); + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap agrees with both free list counters"); + + /* Consume what is left of it one record at a time */ + for (id= 4; share->deleted && id < 200; id++) + { + build_record(rec, id, (const uchar*) "", 0); + if (heap_write(info, rec)) + break; + } + ok(share->deleted_entries == 0, + "entry is gone once its last record is taken"); + + heap_drop_table(info); + heap_close(info); +} + + int main(int argc __attribute__((unused)), char **argv __attribute__((unused))) { MY_INIT("hp_test_freelist"); - plan(258); + plan(274); diag("Test 1: free-list contiguity detects groups > 2 records"); test_freelist_contiguity_multirecord(); @@ -1940,6 +2091,12 @@ int main(int argc __attribute__((unused)), diag("Test 22: reclaimed leaf handed back over the memory ceiling"); test_reclaim_over_ceiling(); + diag("Test 23: dark-record clear, both record-length branches"); + test_clear_dark_records(); + + diag("Test 24: deleted_entries counts free list entries, not records"); + test_deleted_entries_counter(); + my_end(0); return exit_status(); } diff --git a/storage/heap/hp_test_helpers.h b/storage/heap/hp_test_helpers.h index e52571b5a8637..badaae65d655d 100644 --- a/storage/heap/hp_test_helpers.h +++ b/storage/heap/hp_test_helpers.h @@ -64,6 +64,7 @@ static int create_and_open_ceiling(const char *name, keydef.flag= HA_NOSAME; keydef.length= 4; + memset(&blob_desc, 0, sizeof(blob_desc)); blob_desc.offset= BLOB_OFFSET; blob_desc.packlength= BLOB_PACKLEN; diff --git a/storage/heap/hp_test_rectest-t.c b/storage/heap/hp_test_rectest-t.c new file mode 100644 index 0000000000000..313443e066b6b --- /dev/null +++ b/storage/heap/hp_test_rectest-t.c @@ -0,0 +1,338 @@ +/* Copyright (c) 2026, MariaDB Corporation. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA */ + +/* + Unit tests for hp_rectest(), the check heap_update() and heap_delete() + run while READ_CHECK_USED is set. It answers one question: is the record + the caller read still what the table holds? + + Every other consumer of the heap C API turns the flag off -- the server + does it in handler::ha_open(), and the other unit tests do it in their + setup -- so these tests are the only place the answer is observed. + + A column stored out of line makes the question harder than comparing the + two buffers. The record buffer holds a length and then either the value + itself, for a VARCHAR the engine promoted, or a pointer to wherever the + read handed the value out, for a blob. The stored record holds the same + length and then a pointer to the continuation chain. So the buffers do + not agree byte for byte even when nothing has changed, and the payload + the question is about is not in the stored record at all. + + Record layout, one promoted VARCHAR and one blob: + + byte 0 null bitmap + bytes 1-4 int4, the key + bytes 5-6 promoted length prefix + bytes 7-38 promoted payload, 32 declared bytes + bytes 39-40 blob length prefix + bytes 41-48 blob data pointer + + reclength is 49. Stored, the 32 payload bytes give way to a chain + pointer, so stored_reclength is 49 - 32 + sizeof(uchar*). +*/ + +#include +#include +#include +#include +#include "heap.h" +#include "heapdef.h" + +#define REC_LENGTH 49 +#define INT_OFFSET 1 +#define PROM_OFFSET 5 +#define PROM_PACKLEN 2 +#define PROM_LENGTH 32 +#define BLOB_OFFSET 39 +#define BLOB_PACKLEN 2 +#define STORED_LENGTH (REC_LENGTH - PROM_LENGTH + (uint) sizeof(uchar*)) + +/* Long enough that the chain needs more than one continuation record */ +#define LONG_BLOB_LEN 200 +#define SHORT_BLOB_LEN 4 + + +static void build_record(uchar *rec, int32 int_val, + const uchar *prom_data, uint16 prom_len, + const uchar *blob_data, uint16 blob_len) +{ + memset(rec, 0, REC_LENGTH); + int4store(rec + INT_OFFSET, int_val); + int2store(rec + PROM_OFFSET, prom_len); + memcpy(rec + PROM_OFFSET + PROM_PACKLEN, prom_data, prom_len); + int2store(rec + BLOB_OFFSET, blob_len); + memcpy(rec + BLOB_OFFSET + BLOB_PACKLEN, &blob_data, sizeof(blob_data)); +} + + +static void fill_pattern(uchar *buf, uint len, uchar seed) +{ + uint i; + for (i= 0; i < len; i++) + buf[i]= (uchar) (seed + (i % 251)); +} + + +static int create_and_open_two(const char *name, HP_SHARE **share, + HP_INFO **info1, HP_INFO **info2) +{ + HP_KEYDEF keydef; + HA_KEYSEG keyseg; + HP_CREATE_INFO ci; + HP_BLOB_DESC blob_descs[2]; + my_bool unused; + + memset(&keyseg, 0, sizeof(keyseg)); + keyseg.type= HA_KEYTYPE_BINARY; + keyseg.start= INT_OFFSET; + keyseg.length= 4; + keyseg.charset= &my_charset_bin; + + memset(&keydef, 0, sizeof(keydef)); + keydef.keysegs= 1; + keydef.seg= &keyseg; + keydef.algorithm= HA_KEY_ALG_HASH; + keydef.flag= HA_NOSAME; + keydef.length= 4; + + memset(blob_descs, 0, sizeof(blob_descs)); + blob_descs[0].offset= PROM_OFFSET; + blob_descs[0].packlength= PROM_PACKLEN; + blob_descs[0].length= PROM_LENGTH; + blob_descs[0].promoted= TRUE; + blob_descs[1].offset= BLOB_OFFSET; + blob_descs[1].packlength= BLOB_PACKLEN; + + memset(&ci, 0, sizeof(ci)); + ci.keys= 1; + ci.keydef= &keydef; + ci.reclength= REC_LENGTH; + ci.stored_reclength= STORED_LENGTH; + ci.max_records= 1000; + ci.min_records= 10; + ci.max_table_size= 1024 * 1024; + ci.blob_descs= blob_descs; + ci.blob_count= 2; + + if (heap_create(name, &ci, share, &unused)) + return 1; + /* + info1 keeps the READ_CHECK_USED that hp_open() sets, because it is the + handle whose read is being checked. info2 stands in for whoever + changed the row underneath it, and must not check anything itself. + */ + if (!(*info1= heap_open(name, 2))) + return 1; + if (!(*info2= heap_open(name, 2))) + return 1; + heap_extra(*info2, HA_EXTRA_NO_READCHECK); + return 0; +} + + +/* + Write one row, read it back through info1, and leave info1 positioned on + it with old_rec holding what the read handed out. +*/ + +static int seed_row(HP_INFO *info1, uchar *old_rec, + const uchar *prom, uint16 prom_len, + const uchar *blob, uint16 blob_len) +{ + uchar rec[REC_LENGTH]; + uchar key[4]; + + build_record(rec, 1, prom, prom_len, blob, blob_len); + if (heap_write(info1, rec)) + return 1; + int4store(key, 1); + return heap_rkey(info1, old_rec, 0, key, 4, HA_READ_KEY_EXACT) != 0; +} + + +/* Position info2 on the row and store rec over it */ + +static int change_row(HP_INFO *info2, const uchar *rec) +{ + uchar cur[REC_LENGTH]; + uchar key[4]; + + int4store(key, 1); + if (heap_rkey(info2, cur, 0, key, 4, HA_READ_KEY_EXACT)) + return 1; + return heap_update(info2, cur, rec) != 0; +} + + +typedef void (*mutate_fn)(uchar *rec, const uchar *prom_alt, + const uchar *blob_alt); + + +/* + One scenario: seed a row, let info2 change it as mutate says, then have + info1 update from the record it read before that change. expect_changed + is whether hp_rectest() is supposed to notice. +*/ + +static void run_case(const char *what, uint16 blob_len, + mutate_fn mutate, int expect_changed) +{ + HP_SHARE *share; + HP_INFO *info1, *info2; + uchar old_rec[REC_LENGTH], new_rec[REC_LENGTH], changed_rec[REC_LENGTH]; + uchar prom[PROM_LENGTH], prom_alt[PROM_LENGTH]; + uchar blob[LONG_BLOB_LEN], blob_alt[LONG_BLOB_LEN]; + char name[64]; + int rc; + + /* + Run every scenario at both blob sizes. Which of the three run layouts + hp_read_blobs() hands out decides whether the record buffer's blob + pointer happens to equal the stored chain pointer, and a scenario run + at only one size cannot tell a right answer from that coincidence. + */ + my_snprintf(name, sizeof(name), "test_rectest_%s_%s", what, + blob_len > SHORT_BLOB_LEN ? "multirun" : "onerec"); + + fill_pattern(prom, PROM_LENGTH, 1); + fill_pattern(prom_alt, PROM_LENGTH, 100); + fill_pattern(blob, LONG_BLOB_LEN, 7); + fill_pattern(blob_alt, LONG_BLOB_LEN, 200); + + if (create_and_open_two(name, &share, &info1, &info2)) + { + ok(0, "%s: setup failed: %d", name, my_errno); + skip(1, "setup failed"); + return; + } + + /* + A table that promoted nothing would answer every question below the + way a plain record does, and the tests would pass without covering + anything. Pin the layout that makes them mean something. + */ + ok(share->promoted_count == 1 && share->stored_reclength == STORED_LENGTH, + "%s: the table is laid out with one promoted column " + "(promoted_count %u, stored_reclength %u)", + name, share->promoted_count, share->stored_reclength); + + if (seed_row(info1, old_rec, prom, PROM_LENGTH, blob, blob_len)) + { + ok(0, "%s: could not seed the row: %d", name, my_errno); + goto done; + } + + if (mutate) + { + memcpy(changed_rec, old_rec, REC_LENGTH); + mutate(changed_rec, prom_alt, blob_alt); + if (change_row(info2, changed_rec)) + { + ok(0, "%s: the second handle could not change the row: %d", + name, my_errno); + goto done; + } + } + + /* + What info1 goes on to write does not matter -- hp_rectest() reads only + the record info1 claims to have read -- so write back what it read. + */ + memcpy(new_rec, old_rec, REC_LENGTH); + + rc= heap_update(info1, old_rec, new_rec); + if (expect_changed) + ok(rc == HA_ERR_RECORD_CHANGED, + "%s: the stale record is rejected (got %d)", name, rc); + else + ok(rc == 0, "%s: the unchanged record is accepted (got %d)", name, rc); + +done: + heap_close(info2); + heap_close(info1); + heap_delete_table(name); +} + + +static void mutate_prom_same_length(uchar *rec, const uchar *prom_alt, + const uchar *blob_alt) +{ + (void) blob_alt; + memcpy(rec + PROM_OFFSET + PROM_PACKLEN, prom_alt, PROM_LENGTH); +} + + +static void mutate_prom_length(uchar *rec, const uchar *prom_alt, + const uchar *blob_alt) +{ + (void) prom_alt; + (void) blob_alt; + int2store(rec + PROM_OFFSET, (uint16) (PROM_LENGTH - 1)); +} + + +static void mutate_blob_same_length(uchar *rec, const uchar *prom_alt, + const uchar *blob_alt) +{ + (void) prom_alt; + memcpy(rec + BLOB_OFFSET + BLOB_PACKLEN, &blob_alt, sizeof(blob_alt)); +} + + +static void mutate_int(uchar *rec, const uchar *prom_alt, + const uchar *blob_alt) +{ + (void) prom_alt; + (void) blob_alt; + int4store(rec + INT_OFFSET, (int32) 7); +} + + +int main(void) +{ + uint i; + const uint16 blob_lens[2]= { SHORT_BLOB_LEN, LONG_BLOB_LEN }; + + MY_INIT("hp_test_rectest-t"); + plan(20); + + for (i= 0; i < array_elements(blob_lens); i++) + { + uint16 blob_len= blob_lens[i]; + + /* + Nothing changed. The record buffer and the stored record disagree + at every column stored out of line by construction, so this is the + case a plain comparison of the two gets wrong the other way round. + */ + run_case("clean", blob_len, NULL, 0); + + /* + A promoted column's payload is not in the stored record, so a change + to it is invisible to anything that compares only what is. + */ + run_case("prom_value", blob_len, mutate_prom_same_length, 1); + run_case("prom_length", blob_len, mutate_prom_length, 1); + + /* A blob's payload is not in the stored record either */ + run_case("blob_value", blob_len, mutate_blob_same_length, 1); + + /* The inline case, which has always worked */ + run_case("inline", blob_len, mutate_int, 1); + } + + my_end(0); + return exit_status(); +} diff --git a/storage/heap/hp_test_scan-t.c b/storage/heap/hp_test_scan-t.c index 8b00992a4a38b..1358e41d1c8d8 100644 --- a/storage/heap/hp_test_scan-t.c +++ b/storage/heap/hp_test_scan-t.c @@ -339,6 +339,7 @@ static int create_and_open_blob_key(const char *name, keydef.flag= 0; keydef.length= keyseg.length; + memset(&blob_desc, 0, sizeof(blob_desc)); blob_desc.offset= BLOB_OFFSET; blob_desc.packlength= BLOB_PACKLEN; diff --git a/storage/heap/hp_test_write_dup-t.c b/storage/heap/hp_test_write_dup-t.c index fc39324af8568..e394eec5d1581 100644 --- a/storage/heap/hp_test_write_dup-t.c +++ b/storage/heap/hp_test_write_dup-t.c @@ -160,6 +160,7 @@ static int create_and_open(const char *name, uint keys, HP_KEYDEF *keydef, HP_BLOB_DESC blob_desc; my_bool unused; + memset(&blob_desc, 0, sizeof(blob_desc)); blob_desc.offset= BLOB_OFFSET; blob_desc.packlength= BLOB_PACKLEN; diff --git a/storage/heap/hp_update.c b/storage/heap/hp_update.c index 66e450fa4e944..132e5b9e10fc2 100644 --- a/storage/heap/hp_update.c +++ b/storage/heap/hp_update.c @@ -65,7 +65,7 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) detect changes. Unchanged blobs keep their existing chains. Changed blobs get new chains written before old ones are freed. - The bulk memcpy of heap_new into pos overwrites blob chain pointers + hp_pack_record() of heap_new into pos overwrites blob chain pointers with SQL-layer data pointers, so we save old chain pointers first and restore them for unchanged blobs afterward. */ @@ -88,7 +88,7 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) saved_chains[i]= NULL; if (had_cont) - memcpy(&saved_chains[i], pos + desc->offset + desc->packlength, + memcpy(&saved_chains[i], pos + desc->store_offset + desc->packlength, sizeof(saved_chains[i])); old_len= hp_blob_length(desc, old); @@ -101,17 +101,26 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) else { const uchar *old_data, *new_data; - memcpy(&old_data, old + desc->offset + desc->packlength, - sizeof(old_data)); - memcpy(&new_data, heap_new + desc->offset + desc->packlength, - sizeof(new_data)); + if (desc->promoted) + { + /* Value is inline in the record buffer, not behind a pointer */ + old_data= old + desc->offset + desc->packlength; + new_data= heap_new + desc->offset + desc->packlength; + } + else + { + memcpy(&old_data, old + desc->offset + desc->packlength, + sizeof(old_data)); + memcpy(&new_data, heap_new + desc->offset + desc->packlength, + sizeof(new_data)); + } blob_changed[i]= (old_data != new_data && memcmp(old_data, new_data, old_len) != 0); } any_changed|= blob_changed[i]; } - memcpy(pos, heap_new, (size_t) share->reclength); + hp_pack_record(share, pos, heap_new); /* Write new chains for changed blobs, restore old pointers for unchanged */ for (i= 0, desc= share->blob_descs; i < share->blob_count; i++, desc++) @@ -124,32 +133,35 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) as this may have been allocated from a segmented blob. When there is no saved chain (zero-length blob with no continuation - data), NULL out the pointer that memcpy(pos, heap_new) left behind. + data), NULL out the pointer that hp_pack_record() left behind. Without this, a stale SQL-layer pointer (e.g. from replication event buffer) would be interpreted as a chain head by hp_free_blobs(). */ if (saved_chains[i]) { - memcpy(pos + desc->offset + desc->packlength, + memcpy(pos + desc->store_offset + desc->packlength, &saved_chains[i], sizeof(saved_chains[i])); has_blob_data= TRUE; } else - bzero(pos + desc->offset + desc->packlength, sizeof(char*)); + bzero(pos + desc->store_offset + desc->packlength, sizeof(char*)); continue; } new_len= hp_blob_length(desc, heap_new); if (new_len == 0) - bzero(pos + desc->offset + desc->packlength, sizeof(char*)); + bzero(pos + desc->store_offset + desc->packlength, sizeof(char*)); else { const uchar *data_ptr; uchar *first_run; has_blob_data= TRUE; - memcpy(&data_ptr, heap_new + desc->offset + desc->packlength, - sizeof(data_ptr)); + if (desc->promoted) + data_ptr= heap_new + desc->offset + desc->packlength; + else + memcpy(&data_ptr, heap_new + desc->offset + desc->packlength, + sizeof(data_ptr)); if (hp_write_one_blob(share, data_ptr, new_len, &first_run)) { @@ -160,18 +172,18 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) if (blob_changed[j]) { uchar *chain; - memcpy(&chain, pos + share->blob_descs[j].offset + + memcpy(&chain, pos + share->blob_descs[j].store_offset + share->blob_descs[j].packlength, sizeof(chain)); if (chain) hp_free_run_chain(share, chain); } } hp_shrink_tail(share); - memcpy(pos, old, (size_t) share->reclength); + hp_pack_record(share, pos, old); if (had_cont) { for (j= 0; j < share->blob_count; j++) - memcpy(pos + share->blob_descs[j].offset + + memcpy(pos + share->blob_descs[j].store_offset + share->blob_descs[j].packlength, &saved_chains[j], sizeof(saved_chains[j])); pos[share->visible]|= HP_ROW_HAS_CONT; @@ -179,7 +191,7 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) my_safe_afree(saved_chains, alloc_size); goto err; } - memcpy(pos + desc->offset + desc->packlength, + memcpy(pos + desc->store_offset + desc->packlength, &first_run, sizeof(first_run)); } } @@ -238,7 +250,7 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) for (i= 0, desc= share->blob_descs; i < share->blob_count; i++, desc++) { uchar *chain; - memcpy(&chain, pos + desc->offset + desc->packlength, sizeof(chain)); + memcpy(&chain, pos + desc->store_offset + desc->packlength, sizeof(chain)); memcpy((uchar*) heap_new + desc->offset + desc->packlength, &chain, sizeof(chain)); } @@ -249,7 +261,7 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) } else { - memcpy(pos, heap_new, (size_t) share->reclength); + hp_pack_record(share, pos, heap_new); } if (++(share->records) == share->blength) share->blength+= share->blength; diff --git a/storage/heap/hp_write.c b/storage/heap/hp_write.c index 04f15d42071e0..bba3d8e031bf7 100644 --- a/storage/heap/hp_write.c +++ b/storage/heap/hp_write.c @@ -52,6 +52,19 @@ int heap_write(HP_INFO *info, const uchar *record) */ if (info->has_pending_blob_free) hp_flush_unaliased_blob_free(info, record); + /* + The row limit is tested here, where rows are counted, and not in the + record allocator. A row occupies one record plus however many its + blob data needs, so the number of records a table has allocated says + nothing about the number of rows it holds. Memory is bounded + separately, against max_table_size, in hp_alloc_from_tail(). + */ + if (share->records >= share->max_rows) + { + DBUG_PRINT("error", ("row limit reached. records: %lu max_rows: %lu", + share->records, share->max_rows)); + DBUG_RETURN(my_errno= HA_ERR_RECORD_FILE_FULL); + } if (!(pos=next_free_record_pos(share))) DBUG_RETURN(my_errno); info->changed= share->changed= 1; @@ -63,7 +76,7 @@ int heap_write(HP_INFO *info, const uchar *record) goto err; } - memcpy(pos,record,(size_t) share->reclength); + hp_pack_record(share, pos, record); if (share->blob_count) { if (hp_write_blobs(info, record, pos)) @@ -117,7 +130,7 @@ int heap_write(HP_INFO *info, const uchar *record) /* Do NOT call hp_free_blobs here: the err: label is reached when a key - write fails (line 52), which is BEFORE memcpy(pos, record, reclength) + write fails, which is BEFORE hp_pack_record() and hp_write_blobs(). The slot at pos still contains stale data from the delete list, so hp_free_blobs would chase garbage chain pointers. */ @@ -183,6 +196,12 @@ int hp_rb_write_key(HP_INFO *info, HP_KEYDEF *keyinfo, const uchar *record, total_records + deleted == block.last_allocated by incrementing both last_allocated and total_records by the allocated count. heap_scan() relies on this invariant. + + The only ceiling tested here is the table's memory ceiling, because + records are the unit memory is spent in. The row limit belongs to + heap_write(), which is where rows are counted; a row can need any + number of records, so a limit expressed in rows cannot be enforced by + counting them. */ uchar *hp_alloc_from_tail(HP_SHARE *info, uint *blocks) @@ -196,15 +215,6 @@ uchar *hp_alloc_from_tail(HP_SHARE *info, uint *blocks) if (!(block_pos= (uint)(info->block.last_allocated % info->block.records_in_block))) { - if (info->block.last_allocated > info->max_records) - { - DBUG_PRINT("error", - ("record file full. last_allocated: %lu max_records: %lu", - info->block.last_allocated, info->max_records)); - my_errno= HA_ERR_RECORD_FILE_FULL; - DBUG_RETURN(NULL); - } - if (info->block.last_allocated < info->block.high_water_allocated) { /* Block was freed by shrink_tail(). Reclaim block */ @@ -215,11 +225,12 @@ uchar *hp_alloc_from_tail(HP_SHARE *info, uint *blocks) { /* The table memory ceiling gates memory the table does not hold - yet, so it belongs here and not beside the max_records test. - The reclaim branch hands back a leaf that data_length already - counts, and hp_shrink_tail() reaches it whenever it empties the - tail of a table that has been over its ceiling since its first - row: index leaves are allocated without consulting the ceiling. + yet, so it belongs on this arm rather than above the reclaim + test. The reclaim branch hands back a leaf that data_length + already counts, and hp_shrink_tail() reaches it whenever it + empties the tail of a table that has been over its ceiling since + its first row: index leaves are allocated without consulting the + ceiling. Testing the ceiling there as well would make such a table refuse a row it held a moment earlier. */ @@ -282,8 +293,9 @@ uchar *hp_take_free_block(HP_SHARE *share, uint16 count) if (remaining == 0) { - /* Block fully consumed */ + /* Block fully consumed: this is the only branch that ends an entry */ share->del_link= *((uchar**) first); + share->deleted_entries--; } else if (remaining == 1) {