From 41a3619761b991a096144996223549cbaa51636f Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Wed, 9 Sep 2026 19:18:48 -0400 Subject: [PATCH 1/2] MDEV-40032 WIP: promote wide VARCHAR to BLOB inside the HEAP engine A VARCHAR whose declared width exceeds a threshold is stored out of line by the HEAP engine itself, in the blob continuation records that MDEV-38975 already built. The SQL layer is never told: the field stays a `Field_varstring`, so the declared type, the metadata sent to the client, and VARCHAR comparison and key semantics are all unchanged. This applies to user `ENGINE=MEMORY` tables as well as to internal temporary tables. Heap records are fixed width, so an inline `VARCHAR(N)` reserves its full declared width in every row whether the row uses it or not. N counts characters, so that width is between N and 4N bytes depending on the character set. Stored out of line the row pays a length prefix, a chain pointer, and only the bytes actually present. `HP_BLOB_DESC` carries the stored-side geometry and a `promoted` flag, `HP_SHARE` gains a `stored_reclength` and the byte ranges that are identical between the two layouts, and `hp_pack_record()` / `hp_unpack_record()` in `hp_blob.c` replace the whole-record memcpys. The three key functions that read a stored record use a parallel keyseg array. With nothing promoted every new pointer aliases the old one and the code path is what it was. The engine's limits are split three ways, each enforced in its own unit. Rows are limited by the new `HP_SHARE::max_rows`, tested in `heap_write()` where rows are counted; `share->records` had been incremented and compared against nothing, so HEAP had no row limit at all. Bytes are limited by the existing `max_table_size` test in `hp_alloc_from_tail()`, now the only ceiling there. `max_records` survives as the block-sizing estimate and as what `heap_info()` reports, and is documented as not being a limit: it was being tested against a slot counter, which a row using many continuation records exhausts long before its memory budget. This is also the first time LIMIT pushdown is applied as the row count it always was. That changes what a table accepts, which `heap.blob` records. Under `max_heap_table_size=65536` a table with a declared BLOB took one 30000-byte row and refused the second; it now takes two and refuses the third. The refusal came from the `max_records` test, which compared a slot counter against an estimate derived from the SQL row width, and only where a new block was about to be allocated. A row whose value spans many continuation records exhausts that count while using a fraction of the bytes it stands for, so the table filled at half its budget. With bytes limited where bytes are counted, it fills when it has spent them. `hp_clear_dark_records()` clears a short record whole rather than at a stride. Measured on Zen 4, Broadwell and Denverton: at a 16-byte record the 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 above that, so the switch is at 32. `heap.promotion_transparent` asserts that storing a VARCHAR out of line changes nothing. Such a test would pass just as well if the engine never promoted anything, so it opens by proving promotion is in effect: an inline `VARCHAR(60000)` holds about 17 rows under a 1MB ceiling while the promoted form holds 200. `heap.row_limit` covers the row limit on a promoted VARCHAR, a declared BLOB and a plain INT. `heap_info()` now reports the ceiling `data_length` counts toward rather than `max_records * reclength`. That product is a record count times a row width, which is a byte ceiling only while one record holds one row; once a column is stored out of line it overstates the ceiling by the promotion ratio, so a `VARCHAR(3000)` table under a 1MB limit advertised 196MB and read as 0.2% full at the point it refused a row. The number is `max_table_size`, or `max_rows * reclength` where a row limit admits fewer bytes than that. `heap.max_data_length` asserts a table reaches the ceiling it reports before it refuses a row. `ha_heap::scan_time()` prices the free records a scan steps over at one step per free list entry rather than one per free record. heap_scan() reads a coalesced block's length from its first record and skips the block in a single step, so a row whose out-of-line data freed a run of a thousand records costs a later scan what a row that freed one record costs it; charging per record priced such a table at its promotion ratio above what it is worth. `HP_SHARE` counts the entries alongside the records, maintained in the five functions that own the free list -- only a block's last record ends an entry, and a coalescing push extends one rather than adding another. `heap_check_heap()` counts both while walking the list and rejects a share whose counters disagree. Two existing tests observed the old storage layout. `versioning.partition` filled a `VARCHAR(45000)` MEMORY table with rows left at their default and relied on each one reserving its full width anyway. Stored out of line those rows occupy almost nothing and the table never fills, so the `ER_RECORD_FILE_FULL` the test is built around never arrives. The rows now carry their full width as data, which restores the fullness the test needs and holds whether the column is stored inline or not. `perfschema.memory_table_io` records one more fetch on the scan that follows a delete. A row stored out of line leaves a hole behind when it is updated, and a scan that lands in one reports a deleted record to the caller, which costs a further `rnd_next`. A declared BLOB produces exactly the same event sequence, so this is the existing behaviour of out-of-line storage rather than anything promotion introduces. --- include/heap.h | 98 +++++++- mysql-test/suite/heap/blob.result | 5 +- mysql-test/suite/heap/max_data_length.result | 53 +++++ mysql-test/suite/heap/max_data_length.test | 53 +++++ .../suite/heap/promotion_transparent.result | 221 ++++++++++++++++++ .../suite/heap/promotion_transparent.test | 174 ++++++++++++++ mysql-test/suite/heap/row_limit.result | 74 ++++++ mysql-test/suite/heap/row_limit.test | 68 ++++++ .../suite/perfschema/r/memory_table_io.result | 1 + .../suite/versioning/r/partition.result | 9 +- mysql-test/suite/versioning/t/partition.test | 9 +- storage/heap/_check.c | 69 +++++- storage/heap/_rectest.c | 17 +- storage/heap/ha_heap.cc | 173 +++++++++++--- storage/heap/ha_heap.h | 7 + storage/heap/heapdef.h | 81 ++++++- storage/heap/hp_blob.c | 175 ++++++++++++-- storage/heap/hp_clear.c | 1 + storage/heap/hp_create.c | 207 ++++++++++++++-- storage/heap/hp_delete.c | 9 +- storage/heap/hp_hash.c | 145 ++++++++---- storage/heap/hp_info.c | 31 +++ storage/heap/hp_rfirst.c | 2 +- storage/heap/hp_rkey.c | 2 +- storage/heap/hp_rlast.c | 2 +- storage/heap/hp_rnext.c | 2 +- storage/heap/hp_rprev.c | 2 +- storage/heap/hp_rrnd.c | 2 +- storage/heap/hp_rsame.c | 2 +- storage/heap/hp_scan.c | 2 +- storage/heap/hp_test_freelist-t.c | 175 +++++++++++++- storage/heap/hp_test_helpers.h | 1 + storage/heap/hp_test_scan-t.c | 1 + storage/heap/hp_test_write_dup-t.c | 1 + storage/heap/hp_update.c | 50 ++-- storage/heap/hp_write.c | 46 ++-- 36 files changed, 1782 insertions(+), 188 deletions(-) create mode 100644 mysql-test/suite/heap/max_data_length.result create mode 100644 mysql-test/suite/heap/max_data_length.test create mode 100644 mysql-test/suite/heap/promotion_transparent.result create mode 100644 mysql-test/suite/heap/promotion_transparent.test create mode 100644 mysql-test/suite/heap/row_limit.result create mode 100644 mysql-test/suite/heap/row_limit.test diff --git a/include/heap.h b/include/heap.h index d2ccfc7a3d110..8680b21c6d0d4 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,11 @@ 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; ulonglong index_length; uint reclength; /* Length of one record */ int errkey; @@ -130,6 +140,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 +163,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 +221,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 +268,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 +326,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/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/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_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/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/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..bd5f3b5edf52f 100644 --- a/storage/heap/_rectest.c +++ b/storage/heap/_rectest.c @@ -21,11 +21,24 @@ int hp_rectest(register HP_INFO *info, register const uchar *old) { + HP_SHARE *share= info->s; + const HP_COPY_SPAN *span, *span_end; DBUG_ENTER("hp_rectest"); - if (memcmp(info->current_ptr,old,(size_t) info->s->reclength)) + /* + Compare the ranges the stored record and the record buffer share. + A promoted column's payload is not in the stored record at all, so + there is nothing to compare it against. Everything else, native blob + descriptors included, is covered exactly as before. + */ + for (span= share->copy_spans, span_end= span + share->copy_span_count; + span < span_end; span++) { - DBUG_RETURN((my_errno=HA_ERR_RECORD_CHANGED)); /* Record have changed */ + if (memcmp(info->current_ptr + span->store_offset, old + span->offset, + (size_t) span->length)) + { + DBUG_RETURN((my_errno=HA_ERR_RECORD_CHANGED)); /* Record have changed */ + } } DBUG_RETURN(0); } /* _heap_rectest */ diff --git a/storage/heap/ha_heap.cc b/storage/heap/ha_heap.cc index e73bf841e2a6a..a5386c6581f92 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,12 +503,11 @@ 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.max_data_file_length= hp_info.max_data_length; stats.delete_length= hp_info.deleted * hp_info.reclength; stats.create_time= (ulong) hp_info.create_time; if (flag & HA_STATUS_AUTO) @@ -750,11 +762,52 @@ ha_rows ha_heap::records_in_range(uint inx, const key_range *min_key, } +/* + Should this column be stored as a blob rather than inline? + + Only a VARCHAR is a candidate. Heap rows are fixed width, so an inline + VARCHAR(N) reserves its full declared width in every row whether or not + the row uses it, while a blob costs a length prefix and a chain pointer + in the row plus the bytes actually present in a continuation run. + + N counts characters, so the declared width is between N and 4N bytes + depending on the character set: the same VARCHAR(100) reserves 100 of + them in latin1 and 400 in utf8mb4. The threshold is compared against + field_length, which is that width already in bytes, because the waste + is in bytes. + + This 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->type() == MYSQL_TYPE_VARCHAR && + field->pack_length_in_rec() != 0 && + field->field_length > HEAP_CONVERT_IF_BIGGER_TO_BLOB); +} + + +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; @@ -905,33 +958,86 @@ 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->flags & BLOB_FLAG) + { + Field_blob *blob= (Field_blob*) field; + + DBUG_ASSERT(field->type() == MYSQL_TYPE_BLOB || + field->type() == MYSQL_TYPE_GEOMETRY); + + blob_descs[n].offset= (uint) blob->offset(table_arg->record[0]); + blob_descs[n].packlength= blob->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 +1051,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 +1062,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 +1171,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 +1197,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..c2aee1c57d070 100644 --- a/storage/heap/heapdef.h +++ b/storage/heap/heapdef.h @@ -34,6 +34,28 @@ C_MODE_START #define HP_MIN_RECORDS_IN_BLOCK 16 #define HP_MAX_RECORDS_IN_BLOCK 8192 +/* + A VARCHAR whose declared payload is wider than this many bytes is + stored as a blob instead of inline. + + 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. A blob costs a length prefix and a pointer in the row, + and only the bytes actually present in a continuation run. The + threshold is therefore compared against the byte width, not N. + + Below the threshold promotion loses: a non-empty promoted value costs + at least one whole continuation record, so a narrow column pays more + for the run than it saves on the row. + + Setting this to 0 promotes every VARCHAR and must keep working; it is + the configuration the tests use to reach the promoted paths without + wide columns. +*/ +#define HEAP_CONVERT_IF_BIGGER_TO_BLOB 32 + /* Flags stored in the 'visible' byte at end of each record */ #define HP_ROW_ACTIVE 1 /* Bit 0: record is active (not deleted) */ #define HP_ROW_HAS_CONT 2 /* Bit 1: primary record has continuation chain(s) */ @@ -201,18 +223,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 +284,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 +335,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 +427,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..701c144d773ec 100644 --- a/storage/heap/hp_create.c +++ b/storage/heap/hp_create.c @@ -22,6 +22,126 @@ 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); + 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 +176,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 +235,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++) { @@ -236,8 +382,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 +394,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 +425,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 +444,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 +466,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..87631d413b7b0 100644 --- a/storage/heap/hp_hash.c +++ b/storage/heap/hp_hash.c @@ -338,12 +338,58 @@ 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 *chain; + DBUG_ASSERT(info); /* Only a stored segment is out of line */ + memcpy(&chain, pos + pack_length, sizeof(chain)); + if (!(*data= hp_materialize_one_blob(info, chain, (uint32) len))) + return TRUE; + } + else + *data= pos + pack_length; + *length= len; + return FALSE; +} + + ulong hp_rec_hashnr(HP_INFO *info, HP_KEYDEF *keydef, const uchar *rec) { my_hasher_st hasher= my_hasher_mysql5x(); HA_KEYSEG *seg,*endseg; - 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. + */ + seg= info ? keydef->seg_stored : keydef->seg; + for (endseg= seg + keydef->keysegs ; seg < endseg ; seg++) { const uchar *pos= rec+seg->start; const uchar *end= pos+seg->length; @@ -370,23 +416,22 @@ 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 (hp_varchar_seg_data(info, seg, rec, &data, &length)) + return 0; if (cs->mbmaxlen > 1) { size_t char_length; - char_length= hp_charpos(cs, pos + pack_length, - pos + pack_length + length, + 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); - my_ci_hash_sort(&hasher, cs, pos+pack_length, length); + my_ci_hash_sort(&hasher, cs, data, length); } else if (seg->type == HA_KEYTYPE_VARTEXT4 || seg->type == HA_KEYTYPE_VARBINARY4) @@ -459,14 +504,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 +530,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 +557,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,23 +595,18 @@ 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: @@ -607,13 +655,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 +673,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 +702,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 +730,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 +759,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)) { @@ -749,14 +804,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..55360abaa3f04 100644 --- a/storage/heap/hp_info.c +++ b/storage/heap/hp_info.c @@ -33,10 +33,41 @@ 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; 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_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) { From f7c8e9586d6aaaf9aab4983fd4b22552255e3794 Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Thu, 10 Sep 2026 03:30:43 -0400 Subject: [PATCH 2/2] Make VARCHAR promotion zero-copy Promoting inside the engine is not zero-copy. `record[0]` keeps its full declared width, so the engine compacts the record on the way in and expands it again on the way out, and every read of a promoted column copies the payload back into the record. Promote the field instead, and the payload is not copied at all: a read is handed the address of the engine's own bytes, and a record copied wholesale copies that address rather than the value behind it. `Field_varstring` gains a `promoted` flag that changes only where the payload lives. The record slot becomes the column's own length prefix followed by a pointer, which is the shape a blob already has, and `pack_length()` answers for it. `type()`, `type_handler()` and `sql_type()` keep answering VARCHAR, so nothing above the Field can tell the difference. `Create_tmp_table::add_field()` takes the decision, before the record layout is measured. 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. `insert_all_rows_into_tmp_table()` asserts that the two record lengths match, the same thing `select_union_recursive::send_data()` already asserts for the other direction of that copy. A column that disagrees costs at least the width that put it over the threshold, so the record lengths cannot match if any column does. `Field::data_is_out_of_line()` is the question the rest of the server means where it asks `flags & BLOB_FLAG` today, and the two are not the same question. BLOB_FLAG says 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. Where the payload sits is a separate property, and a promoted VARCHAR now has it too. The sites that meant the latter are switched over: the `blob_field[]` array, join buffer sizing and its cache fields, the wholesale copy and free of out-of-line values, `Item_copy_string` substitution, `Cached_item` selection, and the key segment setup for both temporary table engines. Where a caller instead means "declared as a blob" and was reading `s->blob_fields`, which conflates the two, `TABLE::has_unbounded_blob_field()` answers the question it meant: duplicate removal by hash and the subquery expression cache both need a bounded width rather than an inline one, and a wide VARCHAR has it. Reaching an out-of-line payload becomes a set of `Field` virtuals rather than a cast. `out_of_line_data()` and `out_of_line_length()` read the length and pointer pair out of any image of the field's record slot, and `set_out_of_line_image()` writes such a pair back, pointing it at the payload where it already lies. With `copy()` and `free()` also virtual, the join buffer, the window function row remapper and the `GROUP_CONCAT` cut check no longer cast to `Field_blob` to reach a field that is not one. Keys are where the two questions diverge most. A declared blob has no maximum width, so a unique index over one has to be a hash, and its key is marked `HA_BLOB_PART_KEY`, which stops it being a key at all once the table converts to Aria. A promoted VARCHAR is still as wide as it was declared, so it keeps an ordinary bounded key part: a VARTEXT of that width, whose `HA_BLOB_PART` tells the engine to follow the pointer. It has to keep one, because the optimizer planned a lookup on it from the declared type. `Create_tmp_table` therefore tracks whether any distinct column is a declared blob separately from the columns that are merely out of line, and only the former reaches `HA_UNIQUE_HASH`. Carrying both flags also decides `key_restore()`, where the bounded question has to be asked first: it is `HA_VAR_LENGTH_PART` that says how to put the value back, and only a column declared as a blob may be cast to `Field_blob`. A value stored through a promoted field needs somewhere to live. `Field_varstring` gains the buffer `Field_blob` has, with the same rule of one per field, and `copy()` gives the record a set of bytes it owns when the value has to outlive the buffer it was read from. `store()` takes the source somewhere else first when it lies inside that buffer, the way `Field_blob::store()` does, so that `UPDATE t SET c = c` does not read a buffer it is writing. Where many rows are alive at once and sorted afterwards, as they are for `GROUP_CONCAT` with `ORDER BY` or `DISTINCT`, the value goes to the table's `Blob_mem_storage` instead, so each row has bytes of its own. It is stored whole there: a blob is cut to `group_concat_max_len` on the way in because it has no declared width, and a VARCHAR has one that the copy has already applied. `unpack()` points the slot at the row being read, the way `Field_blob::unpack()` does, and `max_packed_col_length()` answers from the declared width, since its callers ask with `pack_length()` and that now describes the pointer. The engine reads a promoted column exactly as it reads a declared blob, both being a length prefix and a pointer, so `heap_prepare_hp_create_info()` builds one descriptor for either and does not promote again what the SQL layer has already moved. Two of the heap key functions capped a VARCHAR segment at the segment's declared width; for an out-of-line column that width describes the descriptor in the record rather than the value, so the value is used whole, as the blob segments already are. Two places in the engine still counted records as though one held a row. `heap_info()` reports free space as the free record count times the record stride rather than times the SQL row width, which is a byte count only while nothing is stored out of line: a `VARCHAR(3000)` table reported more free space than it had ever allocated. And `hp_rectest()`, which answers whether the record a caller read is still what the table holds, compares each out-of-line column through its length and then its data. A promoted column's payload is not in the stored record to be compared at all, and the pointer beside a declared blob's is the continuation chain rather than wherever the read handed the value out, so comparing the two records byte for byte both missed a change and reported one that had not happened. Four tests observed the old storage layout. `main.gconcat_distinct_walk_fail` starved the duplicate filter with 600 rows of a `VARCHAR(100)`. Out of line the filter holds a pointer per value rather than the value itself, so it takes more distinct values to fill it. The row count and the number of distinct values go up, which is what fills the filter either way. `heap.count_distinct_blob_convert` described its last case as having no blob argument. Its `VARCHAR(64)` is now stored out of line and takes the same path a declared blob does, so that case is relabelled and a narrow column carrying the same values is added beside it as the control the old case used to be. `heap.tmp_table_convert_dedup` needs the write that finds the table full to be a duplicate of a row the conversion has already copied. Its two wide columns held values, and a row storing an out-of-line value costs a record slot and a place for the value, so the overflowing write was as likely to be the store as the duplicate that follows it. The wide columns are left empty, which is all they are needed for, and a narrow column carries what makes the rows distinct. `heap.blob_update_overflow` records different counters for the same overflow. The aggregate's own temporary tables are unchanged. What moved is the `information_schema.session_status` query the test reads the counters with: `VARIABLE_VALUE` is a `varchar(2048)`, 6144 bytes per row in utf8mb3, and under the shrunken ceiling the test sets it used to spill on its own. Five tests are added, each written against the question it answers rather than against the fix. `heap.promotion_layout_agreement` reads one recursive CTE three ways, one of them alongside a fulltext match, which is what can build the two tables of a recursion with different options. `heap.promotion_keeps_optimizations` measures the rows read by `SELECT DISTINCT` and the subquery cache hits for a narrow VARCHAR, a wide one and a declared TEXT. A wide column that lost an optimization to the BLOB_FLAG question reads the declared column's row count. `heap.group_concat_cut_reporting` pins which row each of the three reports as cut, a wide VARCHAR having a declared width to cut against where a TEXT has none. `heap.promotion_data_free` asserts that a table never reports more free space than it has allocated, which holds whatever the rows look like and does not pin a byte count. `hp_test_rectest-t` is the only heap unit test that leaves the read check on, the server turning it off for every table it opens. It runs each case at both a single-record and a multi-run chain, because which one a read hands out decides whether the record buffer's pointer happens to equal the stored one, and a case run at only one of them cannot tell a right answer from that coincidence. `main.information_schema` and `main.log_slow_innodb` are disabled rather than re-recorded. Keeping a temporary table in `MEMORY` where it previously converted to `Aria` exposes MDEV-41068: `filesort` tie-breaks equal sort keys on the rowid, and a `MEMORY` table's rowid is the address the record was allocated at, so the order of tied rows is not the same from one server start to the next. Both tests assert an order that is unspecified, so neither is stable here whatever is recorded for it. The fix is on `10.11`; both tests are re-enabled when it upmerges. --- include/heap.h | 2 + mysql-test/main/disabled.def | 2 + .../main/gconcat_distinct_walk_fail.result | 12 +- .../main/gconcat_distinct_walk_fail.test | 9 +- .../suite/heap/blob_update_overflow.result | 16 +- .../heap/count_distinct_blob_convert.result | 32 +- .../heap/count_distinct_blob_convert.test | 25 +- .../heap/group_concat_cut_reporting.result | 43 +++ .../heap/group_concat_cut_reporting.test | 36 ++ .../suite/heap/promotion_data_free.result | 41 +++ .../suite/heap/promotion_data_free.test | 50 +++ .../heap/promotion_keeps_optimizations.result | 71 ++++ .../heap/promotion_keeps_optimizations.test | 107 ++++++ .../heap/promotion_layout_agreement.result | 47 +++ .../heap/promotion_layout_agreement.test | 55 +++ .../suite/heap/tmp_table_convert_dedup.result | 89 +++-- .../suite/heap/tmp_table_convert_dedup.test | 87 +++-- sql/create_tmp_table.h | 10 + sql/field.cc | 172 +++++++-- sql/field.h | 230 +++++++++++- sql/field_conv.cc | 8 + sql/item_buff.cc | 2 +- sql/item_sum.cc | 11 +- sql/key.cc | 36 +- sql/sql_const.h | 22 ++ sql/sql_expression_cache.cc | 6 +- sql/sql_join_cache.cc | 14 +- sql/sql_select.cc | 161 ++++++--- sql/sql_show.cc | 2 +- sql/table.cc | 25 +- sql/table.h | 17 +- storage/heap/CMakeLists.txt | 2 +- storage/heap/_rectest.c | 124 ++++++- storage/heap/ha_heap.cc | 68 ++-- storage/heap/heapdef.h | 22 -- storage/heap/hp_create.c | 16 +- storage/heap/hp_hash.c | 103 +++--- storage/heap/hp_info.c | 9 + storage/heap/hp_test_rectest-t.c | 338 ++++++++++++++++++ 39 files changed, 1814 insertions(+), 308 deletions(-) create mode 100644 mysql-test/suite/heap/group_concat_cut_reporting.result create mode 100644 mysql-test/suite/heap/group_concat_cut_reporting.test create mode 100644 mysql-test/suite/heap/promotion_data_free.result create mode 100644 mysql-test/suite/heap/promotion_data_free.test create mode 100644 mysql-test/suite/heap/promotion_keeps_optimizations.result create mode 100644 mysql-test/suite/heap/promotion_keeps_optimizations.test create mode 100644 mysql-test/suite/heap/promotion_layout_agreement.result create mode 100644 mysql-test/suite/heap/promotion_layout_agreement.test create mode 100644 storage/heap/hp_test_rectest-t.c diff --git a/include/heap.h b/include/heap.h index 8680b21c6d0d4..9f902712b248c 100644 --- a/include/heap.h +++ b/include/heap.h @@ -68,6 +68,8 @@ typedef struct st_heapinfo /* Struct from heap_info */ 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; 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_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/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/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/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/_rectest.c b/storage/heap/_rectest.c index bd5f3b5edf52f..56f50bce35df3 100644 --- a/storage/heap/_rectest.c +++ b/storage/heap/_rectest.c @@ -19,26 +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 HP_COPY_SPAN *span, *span_end; + 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"); /* - Compare the ranges the stored record and the record buffer share. - A promoted column's payload is not in the stored record at all, so - there is nothing to compare it against. Everything else, native blob - descriptors included, is covered exactly as before. + 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 (span= share->copy_spans, span_end= span + share->copy_span_count; - span < span_end; span++) + for (desc= share->blob_descs, desc_end= desc + share->blob_count; + desc < desc_end; desc++) { - if (memcmp(info->current_ptr + span->store_offset, old + span->offset, - (size_t) span->length)) + /* 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 { - DBUG_RETURN((my_errno=HA_ERR_RECORD_CHANGED)); /* Record have changed */ + 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 a5386c6581f92..5c8e8416840f1 100644 --- a/storage/heap/ha_heap.cc +++ b/storage/heap/ha_heap.cc @@ -508,7 +508,7 @@ int ha_heap::info(uint flag) stats.data_file_length= hp_info.data_length; stats.index_file_length= hp_info.index_length; stats.max_data_file_length= hp_info.max_data_length; - stats.delete_length= hp_info.deleted * hp_info.reclength; + 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; @@ -763,30 +763,21 @@ ha_rows ha_heap::records_in_range(uint inx, const key_range *min_key, /* - Should this column be stored as a blob rather than inline? - - Only a VARCHAR is a candidate. Heap rows are fixed width, so an inline - VARCHAR(N) reserves its full declared width in every row whether or not - the row uses it, while a blob costs a length prefix and a chain pointer - in the row plus the bytes actually present in a continuation run. - - N counts characters, so the declared width is between N and 4N bytes - depending on the character set: the same VARCHAR(100) reserves 100 of - them in latin1 and 400 in utf8mb4. The threshold is compared against - field_length, which is that width already in bytes, because the waste - is in bytes. - - This 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. + 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->type() == MYSQL_TYPE_VARCHAR && - field->pack_length_in_rec() != 0 && - field->field_length > HEAP_CONVERT_IF_BIGGER_TO_BLOB); + return !field->data_is_out_of_line() && heap_wants_out_of_line(field); } @@ -883,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. @@ -915,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; @@ -1002,15 +1007,16 @@ int heap_prepare_hp_create_info(TABLE *table_arg, bool internal_table, { Field *field= table_arg->field[i]; - if (field->flags & BLOB_FLAG) + if (field->data_is_out_of_line()) { - Field_blob *blob= (Field_blob*) field; - - DBUG_ASSERT(field->type() == MYSQL_TYPE_BLOB || - field->type() == MYSQL_TYPE_GEOMETRY); - - blob_descs[n].offset= (uint) blob->offset(table_arg->record[0]); - blob_descs[n].packlength= blob->length_size(); + /* + 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++; } diff --git a/storage/heap/heapdef.h b/storage/heap/heapdef.h index c2aee1c57d070..e67f4d9eba199 100644 --- a/storage/heap/heapdef.h +++ b/storage/heap/heapdef.h @@ -34,28 +34,6 @@ C_MODE_START #define HP_MIN_RECORDS_IN_BLOCK 16 #define HP_MAX_RECORDS_IN_BLOCK 8192 -/* - A VARCHAR whose declared payload is wider than this many bytes is - stored as a blob instead of inline. - - 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. A blob costs a length prefix and a pointer in the row, - and only the bytes actually present in a continuation run. The - threshold is therefore compared against the byte width, not N. - - Below the threshold promotion loses: a non-empty promoted value costs - at least one whole continuation record, so a narrow column pays more - for the run than it saves on the row. - - Setting this to 0 promotes every VARCHAR and must keep working; it is - the configuration the tests use to reach the promoted paths without - wide columns. -*/ -#define HEAP_CONVERT_IF_BIGGER_TO_BLOB 32 - /* Flags stored in the 'visible' byte at end of each record */ #define HP_ROW_ACTIVE 1 /* Bit 0: record is active (not deleted) */ #define HP_ROW_HAS_CONT 2 /* Bit 1: primary record has continuation chain(s) */ diff --git a/storage/heap/hp_create.c b/storage/heap/hp_create.c index 701c144d773ec..df75ac1b2ddb3 100644 --- a/storage/heap/hp_create.c +++ b/storage/heap/hp_create.c @@ -129,6 +129,15 @@ static void hp_make_stored_keysegs(HP_SHARE *share, HA_KEYSEG *seg, { 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; @@ -290,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. diff --git a/storage/heap/hp_hash.c b/storage/heap/hp_hash.c index 87631d413b7b0..a935c8bc6852a 100644 --- a/storage/heap/hp_hash.c +++ b/storage/heap/hp_hash.c @@ -365,10 +365,19 @@ static my_bool hp_varchar_seg_data(HP_INFO *info, const HA_KEYSEG *seg, if ((seg->flag & HA_BLOB_PART) && len) { - const uchar *chain; - DBUG_ASSERT(info); /* Only a stored segment is out of line */ - memcpy(&chain, pos + pack_length, sizeof(chain)); - if (!(*data= hp_materialize_one_blob(info, chain, (uint32) 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 @@ -378,18 +387,58 @@ static my_bool hp_varchar_seg_data(HP_INFO *info, const HA_KEYSEG *seg, } +/* + 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; /* 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; - for (endseg= seg + keydef->keysegs ; seg < endseg ; 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; @@ -422,15 +471,7 @@ ulong hp_rec_hashnr(HP_INFO *info, HP_KEYDEF *keydef, const uchar *rec) if (hp_varchar_seg_data(info, seg, rec, &data, &length)) return 0; - if (cs->mbmaxlen > 1) - { - size_t char_length; - 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); + length= hp_varchar_seg_width(sql_seg, data, length); my_ci_hash_sort(&hasher, cs, data, length); } else if (seg->type == HA_KEYTYPE_VARTEXT4 || @@ -612,7 +653,8 @@ int hp_rec_key_cmp(HP_KEYDEF *keydef, const uchar *rec1, const uchar *rec2, 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, @@ -624,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)) @@ -783,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, diff --git a/storage/heap/hp_info.c b/storage/heap/hp_info.c index 55360abaa3f04..5c47ccb73387d 100644 --- a/storage/heap/hp_info.c +++ b/storage/heap/hp_info.c @@ -42,6 +42,15 @@ int heap_info(reg1 HP_INFO *info,reg2 HEAPINFO *x, int flag ) 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; /* 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(); +}