From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: From: Vladimir Davydov Subject: [PATCH 1/8] vinyl: move update optimization from write iterator to tx Date: Sun, 14 Oct 2018 21:16:45 +0300 Message-Id: In-Reply-To: References: In-Reply-To: References: To: kostja@tarantool.org Cc: tarantool-patches@freelists.org List-ID: An UPDATE operation is written as DELETE + REPLACE to secondary indexes. We write those statements to the memory level even if the UPDATE doesn't actually update columns indexed by a secondary key. We filter them out in the write iterator when the memory level is dumped. That's what we use vy_stmt_column_mask for. Actually, there's no point to keep those statements until dump - we could as well filter them out when the transaction is committed. This would even save some memory. This wouldn't hurt read operations, because point lookup doesn't work for secondary indexes by design and so we have to read all sources, including disk, on every read from a secondary index. That said, let's move update optimization from the write iterator to vy_tx_commit. This is a step towards removing vy_stmt_column_mask. --- src/box/vy_tx.c | 11 +++- src/box/vy_write_iterator.c | 44 +++++----------- src/box/vy_write_iterator.h | 26 ++------- test/unit/vy_cache.c | 9 ++-- test/unit/vy_iterators_helper.c | 22 +++----- test/unit/vy_iterators_helper.h | 19 ++----- test/unit/vy_point_lookup.c | 2 +- test/unit/vy_write_iterator.c | 105 ++----------------------------------- test/unit/vy_write_iterator.result | 54 ++++++++----------- 9 files changed, 64 insertions(+), 228 deletions(-) diff --git a/src/box/vy_tx.c b/src/box/vy_tx.c index 957c87f0..f83f9981 100644 --- a/src/box/vy_tx.c +++ b/src/box/vy_tx.c @@ -50,6 +50,7 @@ #include "trigger.h" #include "trivia/util.h" #include "tuple.h" +#include "column_mask.h" #include "vy_cache.h" #include "vy_lsm.h" #include "vy_mem.h" @@ -651,6 +652,12 @@ vy_tx_prepare(struct vy_tx *tx) if (v->is_overwritten) continue; + /* Skip statements which don't change this secondary key. */ + if (lsm->index_id > 0 && + key_update_can_be_skipped(lsm->key_def->column_mask, + vy_stmt_column_mask(v->stmt))) + continue; + if (lsm->index_id > 0 && repsert == NULL && delete == NULL) { /* * This statement is for a secondary index, @@ -1001,7 +1008,7 @@ vy_tx_set(struct vy_tx *tx, struct vy_lsm *lsm, struct tuple *stmt) if (old != NULL && vy_stmt_type(stmt) != IPROTO_UPSERT) { /* * Inherit the column mask of the overwritten statement - * so as not to skip both statements on dump. + * so as not to skip both statements on commit. */ uint64_t column_mask = vy_stmt_column_mask(stmt); if (column_mask != UINT64_MAX) @@ -1020,7 +1027,7 @@ vy_tx_set(struct vy_tx *tx, struct vy_lsm *lsm, struct tuple *stmt) * second field, but the column mask will say it does. * * To discard DELETE statements in the write iterator - * (see optimization #6), we turn a REPLACE into an + * (see optimization #5), we turn a REPLACE into an * INSERT in case the REPLACE was generated by an * update that changed secondary key fields. So we * can't tolerate inaccuracy in a column mask. diff --git a/src/box/vy_write_iterator.c b/src/box/vy_write_iterator.c index 1b54e539..17b80685 100644 --- a/src/box/vy_write_iterator.c +++ b/src/box/vy_write_iterator.c @@ -32,7 +32,6 @@ #include "vy_mem.h" #include "vy_run.h" #include "vy_upsert.h" -#include "column_mask.h" #include "fiber.h" #define HEAP_FORWARD_DECLARATION @@ -632,7 +631,7 @@ vy_write_iterator_deferred_delete(struct vy_write_iterator *stream, /** * Build the history of the current key. - * Apply optimizations 1, 2 and 3 (@sa vy_write_iterator.h). + * Apply optimizations 1 and 2 (@sa vy_write_iterator.h). * When building a history, some statements can be * skipped (e.g. multiple REPLACE statements on the same key), * but nothing can be merged yet, since we don't know the first @@ -690,7 +689,6 @@ vy_write_iterator_build_history(struct vy_write_iterator *stream, int current_rv_i = 0; int64_t current_rv_lsn = vy_write_iterator_get_vlsn(stream, 0); int64_t merge_until_lsn = vy_write_iterator_get_vlsn(stream, 1); - uint64_t key_mask = stream->cmp_def->column_mask; while (true) { *is_first_insert = vy_stmt_type(src->tuple) == IPROTO_INSERT; @@ -702,9 +700,7 @@ vy_write_iterator_build_history(struct vy_write_iterator *stream, * generated by an update operation, it can be * turned into an INSERT. */ - uint64_t stmt_mask = vy_stmt_column_mask(src->tuple); - if (stmt_mask != UINT64_MAX && - !key_update_can_be_skipped(stmt_mask, key_mask)) + if (vy_stmt_column_mask(src->tuple) != UINT64_MAX) *is_first_insert = true; } @@ -758,6 +754,12 @@ vy_write_iterator_build_history(struct vy_write_iterator *stream, goto next_lsn; } + rc = vy_write_iterator_push_rv(stream, src->tuple, + current_rv_i); + if (rc != 0) + break; + ++*count; + /* * Optimization 2: skip statements overwritten * by a REPLACE or DELETE. @@ -765,34 +767,12 @@ vy_write_iterator_build_history(struct vy_write_iterator *stream, if (vy_stmt_type(src->tuple) == IPROTO_REPLACE || vy_stmt_type(src->tuple) == IPROTO_INSERT || vy_stmt_type(src->tuple) == IPROTO_DELETE) { - uint64_t stmt_mask = vy_stmt_column_mask(src->tuple); - /* - * Optimization 3: skip statements which - * do not change this secondary key. - */ - if (!stream->is_primary && - key_update_can_be_skipped(key_mask, stmt_mask)) - goto next_lsn; - - rc = vy_write_iterator_push_rv(stream, src->tuple, - current_rv_i); - if (rc != 0) - break; - ++*count; current_rv_i++; current_rv_lsn = merge_until_lsn; merge_until_lsn = vy_write_iterator_get_vlsn(stream, current_rv_i + 1); - goto next_lsn; } - - assert(vy_stmt_type(src->tuple) == IPROTO_UPSERT); - rc = vy_write_iterator_push_rv(stream, src->tuple, - current_rv_i); - if (rc != 0) - break; - ++*count; next_lsn: rc = vy_write_iterator_merge_step(stream); if (rc != 0) @@ -844,7 +824,7 @@ vy_read_view_merge(struct vy_write_iterator *stream, struct tuple *hint, assert(rv->history != NULL); struct vy_write_history *h = rv->history; /* - * Optimization 5: discard a DELETE statement referenced + * Optimization 4: discard a DELETE statement referenced * by a read view if it is preceded by another DELETE for * the same key. */ @@ -923,7 +903,7 @@ vy_read_view_merge(struct vy_write_iterator *stream, struct tuple *hint, } if (is_first_insert && vy_stmt_type(rv->tuple) == IPROTO_DELETE) { /* - * Optimization 6: discard the first DELETE if + * Optimization 5: discard the first DELETE if * the oldest statement for the current key among * all sources is an INSERT and hence there's no * statements for this key in older runs or the @@ -939,11 +919,11 @@ vy_read_view_merge(struct vy_write_iterator *stream, struct tuple *hint, * If the oldest statement among all sources is an * INSERT, convert the first REPLACE to an INSERT * so that if the key gets deleted later, we will - * be able invoke optimization #6 to discard the + * be able invoke optimization #5 to discard the * DELETE statement. * * Otherwise convert the first INSERT to a REPLACE - * so as not to trigger optimization #6 on the next + * so as not to trigger optimization #5 on the next * compaction. */ uint32_t size; diff --git a/src/box/vy_write_iterator.h b/src/box/vy_write_iterator.h index 3b9b535a..4ae3815d 100644 --- a/src/box/vy_write_iterator.h +++ b/src/box/vy_write_iterator.h @@ -117,27 +117,7 @@ * skip keep skip merge * * --------------------------------------------------------------- - * Optimization #3: when compacting runs of a secondary key, skip - * statements, which do not update this key. - * - * -------- - * SAME KEY - * -------- - * VLSN(i) VLSN(i+1) - * Masks | | - * intersection:| not 0 0 0 not 0 not 0 | - * | ANY DELETE REPLACE ANY ... REPLACE | - * \______/\_______________/\___________________/ - * merge skip merge - * - * Details: when UPDATE is executed by Tarantool, it is - * transformed into DELETE + REPLACE or a single REPLACE. But it - * is only necessary to write anything into the secondary key if - * such UPDATE changes any field, which is part of the key. - * All other UPDATEs can be simply skipped. - * - * --------------------------------------------------------------- - * Optimization #4: use older REPLACE/DELETE to apply UPSERTs and + * Optimization #3: use older REPLACE/DELETE to apply UPSERTs and * convert them into a single REPLACE. When compaction includes * the last level, absence of REPLACE or DELETE is equivalent * to a DELETE, and UPSERT can be converted to REPLACE as well. @@ -166,7 +146,7 @@ * vy_write_iterator_build_read_views. * * --------------------------------------------------------------- - * Optimization #5: discard a tautological DELETE statement, i.e. + * Optimization #4: discard a tautological DELETE statement, i.e. * a statement that was not removed from the history because it * is referenced by read view, but that is preceeded by another * DELETE and hence not needed. @@ -182,7 +162,7 @@ * skip keep skip discard * * --------------------------------------------------------------- - * Optimization #6: discard the first DELETE if the oldest + * Optimization #5: discard the first DELETE if the oldest * statement for the current key among all sources is an INSERT. * Rationale: if a key's history starts from an INSERT, there is * either no statements for this key in older runs or the latest diff --git a/test/unit/vy_cache.c b/test/unit/vy_cache.c index 5d296aa6..d46d6c3f 100644 --- a/test/unit/vy_cache.c +++ b/test/unit/vy_cache.c @@ -18,8 +18,7 @@ test_basic() struct tuple_format *format; create_test_cache(fields, types, lengthof(fields), &cache, &key_def, &format); - struct tuple *select_all = vy_new_simple_stmt(format, NULL, - &key_template); + struct tuple *select_all = vy_new_simple_stmt(format, &key_template); struct mempool history_node_pool; mempool_create(&history_node_pool, cord_slab_cache(), @@ -96,7 +95,7 @@ test_basic() for (int i = 0; i < 4; ++i) vy_cache_iterator_next(&itr, &history, &unused); ret = vy_history_last_stmt(&history); - ok(vy_stmt_are_same(ret, &chain1[3], format, NULL), + ok(vy_stmt_are_same(ret, &chain1[3], format), "next_key * 4"); /* @@ -115,11 +114,11 @@ test_basic() * the last_stmt. So restore on chain1[0], but the result * must be chain1[1]. */ - struct tuple *last_stmt = vy_new_simple_stmt(format, NULL, &chain1[0]); + struct tuple *last_stmt = vy_new_simple_stmt(format, &chain1[0]); ok(vy_cache_iterator_restore(&itr, last_stmt, &history, &unused) >= 0, "restore"); ret = vy_history_last_stmt(&history); - ok(vy_stmt_are_same(ret, &chain1[1], format, NULL), + ok(vy_stmt_are_same(ret, &chain1[1], format), "restore on position after last"); tuple_unref(last_stmt); diff --git a/test/unit/vy_iterators_helper.c b/test/unit/vy_iterators_helper.c index 40d8d6a1..58eaa220 100644 --- a/test/unit/vy_iterators_helper.c +++ b/test/unit/vy_iterators_helper.c @@ -42,7 +42,6 @@ vy_iterator_C_test_finish() struct tuple * vy_new_simple_stmt(struct tuple_format *format, - struct tuple_format *format_with_colmask, const struct vy_stmt_template *templ) { if (templ == NULL) @@ -59,9 +58,6 @@ vy_new_simple_stmt(struct tuple_format *format, ++i; } size += mp_sizeof_array(i); - fail_if(templ->optimize_update && templ->type == IPROTO_UPSERT); - if (templ->optimize_update) - format = format_with_colmask; /* Encode the statement. */ char *buf = (char *) malloc(size); @@ -119,7 +115,6 @@ vy_new_simple_stmt(struct tuple_format *format, ops = mp_encode_int(ops, templ->upsert_value); operations[0].iov_base = tmp; operations[0].iov_len = ops - tmp; - fail_if(templ->optimize_update); ret = vy_stmt_new_upsert(format, buf, pos, operations, 1); fail_if(ret == NULL); break; @@ -137,16 +132,13 @@ vy_new_simple_stmt(struct tuple_format *format, free(buf); vy_stmt_set_lsn(ret, templ->lsn); vy_stmt_set_flags(ret, templ->flags); - if (templ->optimize_update) - vy_stmt_set_column_mask(ret, 0); return ret; } const struct tuple * vy_mem_insert_template(struct vy_mem *mem, const struct vy_stmt_template *templ) { - struct tuple *stmt = vy_new_simple_stmt(mem->format, - mem->format_with_colmask, templ); + struct tuple *stmt = vy_new_simple_stmt(mem->format, templ); struct tuple *region_stmt = vy_stmt_dup_lsregion(stmt, &mem->env->allocator, mem->generation); assert(region_stmt != NULL); @@ -166,12 +158,12 @@ vy_cache_insert_templates_chain(struct vy_cache *cache, const struct vy_stmt_template *key_templ, enum iterator_type order) { - struct tuple *key = vy_new_simple_stmt(format, NULL, key_templ); + struct tuple *key = vy_new_simple_stmt(format, key_templ); struct tuple *prev_stmt = NULL; struct tuple *stmt = NULL; for (uint i = 0; i < length; ++i) { - stmt = vy_new_simple_stmt(format, NULL, &chain[i]); + stmt = vy_new_simple_stmt(format, &chain[i]); vy_cache_add(cache, stmt, prev_stmt, key, order); if (i != 0) tuple_unref(prev_stmt); @@ -187,7 +179,7 @@ void vy_cache_on_write_template(struct vy_cache *cache, struct tuple_format *format, const struct vy_stmt_template *templ) { - struct tuple *written = vy_new_simple_stmt(format, NULL, templ); + struct tuple *written = vy_new_simple_stmt(format, templ); vy_cache_on_write(cache, written, NULL); tuple_unref(written); } @@ -250,13 +242,11 @@ destroy_test_cache(struct vy_cache *cache, struct key_def *def, bool vy_stmt_are_same(const struct tuple *actual, const struct vy_stmt_template *expected, - struct tuple_format *format, - struct tuple_format *format_with_colmask) + struct tuple_format *format) { if (vy_stmt_type(actual) != expected->type) return false; - struct tuple *tmp = vy_new_simple_stmt(format, format_with_colmask, - expected); + struct tuple *tmp = vy_new_simple_stmt(format, expected); fail_if(tmp == NULL); uint32_t a_len, b_len; const char *a, *b; diff --git a/test/unit/vy_iterators_helper.h b/test/unit/vy_iterators_helper.h index 24641df3..9690f684 100644 --- a/test/unit/vy_iterators_helper.h +++ b/test/unit/vy_iterators_helper.h @@ -43,13 +43,10 @@ #define vyend 99999999 #define MAX_FIELDS_COUNT 100 #define STMT_TEMPLATE(lsn, type, ...) \ -{ { __VA_ARGS__, vyend }, IPROTO_##type, lsn, false, 0, 0, 0 } - -#define STMT_TEMPLATE_OPTIMIZED(lsn, type, ...) \ -{ { __VA_ARGS__, vyend }, IPROTO_##type, lsn, true, 0, 0, 0 } +{ { __VA_ARGS__, vyend }, IPROTO_##type, lsn, 0, 0, 0 } #define STMT_TEMPLATE_FLAGS(lsn, type, flags, ...) \ -{ { __VA_ARGS__, vyend }, IPROTO_##type, lsn, false, flags, 0, 0 } +{ { __VA_ARGS__, vyend }, IPROTO_##type, lsn, flags, 0, 0 } #define STMT_TEMPLATE_DEFERRED_DELETE(lsn, type, ...) \ STMT_TEMPLATE_FLAGS(lsn, type, VY_STMT_DEFERRED_DELETE, __VA_ARGS__) @@ -83,11 +80,6 @@ struct vy_stmt_template { enum iproto_type type; /** Statement lsn. */ int64_t lsn; - /* - * True, if statement must have column mask, that allows - * to skip it in the write_iterator. - */ - bool optimize_update; /** Statement flags. */ uint8_t flags; /* @@ -103,15 +95,12 @@ struct vy_stmt_template { * Create a new vinyl statement using the specified template. * * @param format - * @param format_with_colmask Format for statements with a - * colmask. * @param templ Statement template. * * @return Created statement. */ struct tuple * vy_new_simple_stmt(struct tuple_format *format, - struct tuple_format *format_with_colmask, const struct vy_stmt_template *templ); /** @@ -210,15 +199,13 @@ destroy_test_cache(struct vy_cache *cache, struct key_def *def, * @param stmt Actual value. * @param templ Expected value. * @param format Template statement format. - * @param format_with_colmask Template statement format with colmask. * * @retval stmt === template. */ bool vy_stmt_are_same(const struct tuple *actual, const struct vy_stmt_template *expected, - struct tuple_format *format, - struct tuple_format *format_with_colmask); + struct tuple_format *format); #if defined(__cplusplus) } diff --git a/test/unit/vy_point_lookup.c b/test/unit/vy_point_lookup.c index dd33bbec..642cb4ce 100644 --- a/test/unit/vy_point_lookup.c +++ b/test/unit/vy_point_lookup.c @@ -274,7 +274,7 @@ test_basic() struct vy_stmt_template tmpl_key = STMT_TEMPLATE(0, SELECT, i); struct tuple *key = vy_new_simple_stmt(format, - pk->mem_format_with_colmask, &tmpl_key); + &tmpl_key); struct tuple *res; rc = vy_point_lookup(pk, NULL, &prv, key, &res); tuple_unref(key); diff --git a/test/unit/vy_write_iterator.c b/test/unit/vy_write_iterator.c index 337e27ac..bb0eb8d3 100644 --- a/test/unit/vy_write_iterator.c +++ b/test/unit/vy_write_iterator.c @@ -119,8 +119,7 @@ compare_write_iterator_results(const struct vy_stmt_template *content, if (ret == NULL) break; fail_if(i >= expected_count); - ok(vy_stmt_are_same(ret, &expected[i], mem->format, - mem->format_with_colmask), + ok(vy_stmt_are_same(ret, &expected[i], mem->format), "stmt %d is correct", i); ++i; } while (ret != NULL); @@ -129,7 +128,7 @@ compare_write_iterator_results(const struct vy_stmt_template *content, for (i = 0; i < handler.count; i++) { fail_if(i >= deferred_count); ok(vy_stmt_are_same(handler.stmt[i], &deferred[i], - handler.format, NULL), + handler.format), "deferred stmt %d is correct", i); } if (deferred != NULL) { @@ -149,7 +148,7 @@ void test_basic(void) { header(); - plan(66); + plan(58); { /* * STATEMENT: REPL REPL REPL DEL REPL REPL REPL REPL REPL REPL @@ -312,54 +311,6 @@ test_basic(void) } { /* - * STATEMENT: REPL DEL REPL REPL - * LSN: 5 6 6 7 - * READ VIEW: * - * \_______________/\_______/ - * \_____/\______/ - * merge skip as - * optimized - * update - * DEL and REPL with lsn 6 can be skipped for read view 6 for - * secondary index, because they do not change secondary key. - */ - const struct vy_stmt_template content[] = { - STMT_TEMPLATE(5, REPLACE, 1, 1), - STMT_TEMPLATE_OPTIMIZED(6, DELETE, 1), - STMT_TEMPLATE_OPTIMIZED(6, REPLACE, 1, 2), - STMT_TEMPLATE(7, REPLACE, 1, 3) - }; - const struct vy_stmt_template expected[] = { content[3], content[0] }; - const int vlsns[] = {6}; - int content_count = sizeof(content) / sizeof(content[0]); - int expected_count = sizeof(expected) / sizeof(expected[0]); - int vlsns_count = sizeof(vlsns) / sizeof(vlsns[0]); - compare_write_iterator_results(content, content_count, - expected, expected_count, NULL, 0, - vlsns, vlsns_count, false, true); -} -{ -/* - * STATEMENT: DEL REPL - * LSN: 6 6 - * \______/ - * skip both as optimized update - */ - const struct vy_stmt_template content[] = { - STMT_TEMPLATE_OPTIMIZED(6, DELETE, 1), - STMT_TEMPLATE_OPTIMIZED(6, REPLACE, 1, 2), - }; - const struct vy_stmt_template expected[] = {}; - const int vlsns[] = {}; - int content_count = sizeof(content) / sizeof(content[0]); - int expected_count = sizeof(expected) / sizeof(expected[0]); - int vlsns_count = sizeof(vlsns) / sizeof(vlsns[0]); - compare_write_iterator_results(content, content_count, - expected, expected_count, NULL, 0, - vlsns, vlsns_count, false, false); -} -{ -/* * STATEMENT: UPS UPS UPS REPL * LSN: 6 7 8 9 * READ VIEW: * @@ -415,56 +366,6 @@ test_basic(void) } { /* - * STATEMENT: REPL DEL REPL - * LSN: 6 7 7 - * \___/\__________/ - * merge skip as optimized update - * - * last_level = false. - * Check if the key is not fully skipped in a case of optimized - * update as the newest version. - */ - const struct vy_stmt_template content[] = { - STMT_TEMPLATE(6, REPLACE, 1, 1), - STMT_TEMPLATE_OPTIMIZED(7, DELETE, 1), - STMT_TEMPLATE_OPTIMIZED(7, REPLACE, 1, 2), - }; - const struct vy_stmt_template expected[] = { content[0] }; - const int vlsns[] = {}; - int content_count = sizeof(content) / sizeof(content[0]); - int expected_count = sizeof(expected) / sizeof(expected[0]); - int vlsns_count = sizeof(vlsns) / sizeof(vlsns[0]); - compare_write_iterator_results(content, content_count, - expected, expected_count, NULL, 0, - vlsns, vlsns_count, false, false); -} -{ -/* - * STATEMENT: REPL DEL REPL - * LSN: 6 7 7 - * \_________/|\___/ - * skip last level | skip as optimized - * delete. | update. - * - * last_level = true. First apply 'last level DELETE' optimization - * and only then the 'optimized UPDATE'. - */ - const struct vy_stmt_template content[] = { - STMT_TEMPLATE(6, REPLACE, 1, 1), - STMT_TEMPLATE_OPTIMIZED(7, DELETE, 1), - STMT_TEMPLATE_OPTIMIZED(7, REPLACE, 1, 2), - }; - const struct vy_stmt_template expected[] = { content[2] }; - const int vlsns[] = {}; - int content_count = sizeof(content) / sizeof(content[0]); - int expected_count = sizeof(expected) / sizeof(expected[0]); - int vlsns_count = sizeof(vlsns) / sizeof(vlsns[0]); - compare_write_iterator_results(content, content_count, - expected, expected_count, NULL, 0, - vlsns, vlsns_count, true, false); -} -{ -/* * STATEMENT: REPL DEL REPL DEL REPL DEL * LSN: 4 5 6 7 8 9 * READ VIEW: * * * diff --git a/test/unit/vy_write_iterator.result b/test/unit/vy_write_iterator.result index 4f95aeb9..a8011685 100644 --- a/test/unit/vy_write_iterator.result +++ b/test/unit/vy_write_iterator.result @@ -1,5 +1,5 @@ *** test_basic *** -1..66 +1..58 ok 1 - stmt 0 is correct ok 2 - stmt 1 is correct ok 3 - stmt 2 is correct @@ -24,46 +24,38 @@ ok 21 - correct results count ok 22 - stmt 0 is correct ok 23 - stmt 1 is correct ok 24 - correct results count -ok 25 - correct results count -ok 26 - stmt 0 is correct -ok 27 - stmt 1 is correct +ok 25 - stmt 0 is correct +ok 26 - stmt 1 is correct +ok 27 - stmt 2 is correct ok 28 - correct results count ok 29 - stmt 0 is correct -ok 30 - stmt 1 is correct -ok 31 - stmt 2 is correct -ok 32 - correct results count -ok 33 - stmt 0 is correct +ok 30 - correct results count +ok 31 - stmt 0 is correct +ok 32 - stmt 1 is correct +ok 33 - stmt 2 is correct ok 34 - correct results count ok 35 - stmt 0 is correct -ok 36 - correct results count -ok 37 - stmt 0 is correct +ok 36 - stmt 1 is correct +ok 37 - stmt 2 is correct ok 38 - correct results count ok 39 - stmt 0 is correct ok 40 - stmt 1 is correct ok 41 - stmt 2 is correct ok 42 - correct results count -ok 43 - stmt 0 is correct -ok 44 - stmt 1 is correct -ok 45 - stmt 2 is correct -ok 46 - correct results count -ok 47 - stmt 0 is correct -ok 48 - stmt 1 is correct -ok 49 - stmt 2 is correct +ok 43 - deferred stmt 0 is correct +ok 44 - deferred stmt 1 is correct +ok 45 - deferred stmt 2 is correct +ok 46 - deferred stmt 3 is correct +ok 47 - correct deferred stmt count +ok 48 - stmt 0 is correct +ok 49 - stmt 1 is correct ok 50 - correct results count -ok 51 - deferred stmt 0 is correct -ok 52 - deferred stmt 1 is correct -ok 53 - deferred stmt 2 is correct -ok 54 - deferred stmt 3 is correct +ok 51 - correct deferred stmt count +ok 52 - stmt 0 is correct +ok 53 - stmt 1 is correct +ok 54 - correct results count ok 55 - correct deferred stmt count ok 56 - stmt 0 is correct -ok 57 - stmt 1 is correct -ok 58 - correct results count -ok 59 - correct deferred stmt count -ok 60 - stmt 0 is correct -ok 61 - stmt 1 is correct -ok 62 - correct results count -ok 63 - correct deferred stmt count -ok 64 - stmt 0 is correct -ok 65 - correct results count -ok 66 - correct deferred stmt count +ok 57 - correct results count +ok 58 - correct deferred stmt count *** test_basic: done *** -- 2.11.0