#!/usr/bin/env python
#
# Public Domain 2014-present MongoDB, Inc.
# Public Domain 2008-2014 WiredTiger, Inc.
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# test_timestamp06.py
#   Timestamps: multistep transactions
#

from helper import copy_wiredtiger_home
import random
from suite_subprocess import suite_subprocess
import wiredtiger, wttest
from wtscenario import make_scenarios

def timestamp_str(t):
    return '%x' % t

class test_timestamp06(wttest.WiredTigerTestCase, suite_subprocess):
    table_ts_log     = 'table:ts06_ts_logged'
    table_ts_nolog   = 'table:ts06_ts_nologged'

    types = [
        ('col_fix', dict(empty=1, extra_config=',key_format=r,value_format=8t')),
        ('col_var', dict(empty=0, extra_config=',key_format=r')),
        ('lsm', dict(empty=0, extra_config=',type=lsm')),
        ('row', dict(empty=0, extra_config='',)),
    ]
    ckpt = [
        ('ckpt_ts_def', dict(ckptcfg='', ckpt_ts=True)),
        ('ckpt_ts_false', dict(ckptcfg='use_timestamp=false', ckpt_ts=False)),
        ('ckpt_ts_true', dict(ckptcfg='use_timestamp=true', ckpt_ts=True)),
    ]

    conncfg = [
        ('nolog', dict(conn_config='create', using_log=False)),
        ('V1', dict(conn_config='create,log=(archive=false,enabled),compatibility=(release="2.9")', using_log=True)),
        ('V2', dict(conn_config='create,log=(archive=false,enabled)', using_log=True)),
    ]
    session_config = 'isolation=snapshot'

    scenarios = make_scenarios(conncfg, types, ckpt)

    # Check that a cursor (optionally started in a new transaction), sees the
    # expected values.
    def check(self, session, txn_config, tablename, expected):
        if txn_config:
            session.begin_transaction(txn_config)

        cur = session.open_cursor(tablename, None)
        actual = dict((k, v) for k, v in cur if v != 0)
        if actual != expected:
            print("missing: ", sorted(set(expected.items()) - set(actual.items())))
            print("extras: ", sorted(set(actual.items()) - set(expected.items())))
        self.assertTrue(actual == expected)
        # Search for the expected items as well as iterating
        for k, v in expected.items():
            self.assertEqual(cur[k], v, "for key " + str(k))
        cur.close()
        if txn_config:
            session.commit_transaction()

    # Take a backup of the database and verify that the value we want to
    # check exists in the tables the expected number of times.
    def backup_check(self, check_value, expected_ts_log, expected_ts_nolog):
        newdir = "BACKUP"
        copy_wiredtiger_home(self, '.', newdir, True)

        conn = self.setUpConnectionOpen(newdir)
        session = self.setUpSessionOpen(conn)
        cur_ts_log      = session.open_cursor(self.table_ts_log)
        cur_ts_nolog    = session.open_cursor(self.table_ts_nolog)
        # Count how many times the given value is present in the
        # logged timestamp table.
        actual_ts_log = 0
        for k, v in cur_ts_log:
            if check_value == v:
                actual_ts_log += 1
        cur_ts_log.close()
        # Count how many times the given value is present in the
        # not logged timestamp table
        actual_ts_nolog = 0
        for k, v in cur_ts_nolog:
            if check_value == v:
                actual_ts_nolog += 1
        cur_ts_nolog.close()
        conn.close()
        self.assertEqual(actual_ts_log, expected_ts_log)
        self.assertEqual(actual_ts_nolog, expected_ts_nolog)

    # Check that a cursor sees the expected values after a checkpoint.
    def ckpt_backup(self, check_value, valcnt_ts_log, valcnt_ts_nolog):
        # Take a checkpoint.  Make a copy of the database.  Open the
        # copy and verify whether or not the expected data is in there.
        self.pr("CKPT: " + self.ckptcfg)
        ckptcfg = self.ckptcfg

        self.session.checkpoint(ckptcfg)
        self.backup_check(check_value, valcnt_ts_log, valcnt_ts_nolog)

    def test_timestamp06(self):
        # Open two timestamp tables:
        # 1. Table is logged and uses timestamps.
        # 2. Table is not logged and uses timestamps.
        self.session.create(self.table_ts_log, 'key_format=i,value_format=i')
        cur_ts_log = self.session.open_cursor(self.table_ts_log)
        self.session.create(self.table_ts_nolog, 'key_format=i,value_format=i,log=(enabled=false)')
        cur_ts_nolog = self.session.open_cursor(self.table_ts_nolog)

        # Insert keys 1..100
        nkeys = 100
        orig_keys = list(range(1, nkeys+1))
        keys = orig_keys[:]
        random.shuffle(keys)

        self.session.begin_transaction()
        # Make three updates with different timestamps.
        self.session.timestamp_transaction('commit_timestamp=' + timestamp_str(1))
        for k in keys:
            cur_ts_log[k] = 1
            cur_ts_nolog[k] = 1

        self.session.timestamp_transaction('commit_timestamp=' + timestamp_str(101))
        for k in keys:
            cur_ts_log[k] = 2
            cur_ts_nolog[k] = 2

        self.session.timestamp_transaction('commit_timestamp=' + timestamp_str(201))
        for k in keys:
            cur_ts_log[k] = 3
            cur_ts_nolog[k] = 3

        self.session.commit_transaction('commit_timestamp=' + timestamp_str(301))

        # Scenario: 1
        # Check that we see all the latest values (i.e. 3) as per transaction
        # visibility when reading without the read timestamp.
        # All tables should see all the values.
        self.check(self.session, "", self.table_ts_log,
            dict((k, 3) for k in orig_keys))
        self.check(self.session, "", self.table_ts_nolog,
            dict((k, 3) for k in orig_keys))

        # Scenario: 2
        # Check that we see the values till correctly from checkpointed data
        # files in case of multistep transactions.
        # Set oldest and stable timestamps
        old_ts = timestamp_str(100)
        # Set the stable timestamp such that last update is beyond it.
        stable_ts = timestamp_str(200)
        self.conn.set_timestamp('oldest_timestamp=' + old_ts)
        self.conn.set_timestamp('stable_timestamp=' + stable_ts)

        # Check that we see the values correctly till stable timestamp.
        self.check(self.session, 'read_timestamp=' + stable_ts,
            self.table_ts_log, dict((k, 2) for k in orig_keys))
        self.check(self.session, 'read_timestamp=' + stable_ts,
            self.table_ts_nolog, dict((k, 2) for k in orig_keys))

        # For logged table we should see latest values (i.e. 3) when logging
        # is enabled.
        if self.using_log == True:
            valcnt_ts_log = nkeys
        else:
            # When logging is disabled, we should not see the values beyond the
            # stable timestamp with timestamped checkpoints.
            if self.ckpt_ts == True:
                valcnt_ts_log = 0
            else:
                valcnt_ts_log = nkeys

        # For non-logged table we should not see the values beyond the
        # stable timestamp with timestamped checkpoints.
        if self.ckpt_ts == True:
            valcnt_ts_nolog = 0
        else:
            valcnt_ts_nolog = nkeys

        # Check to see the count of latest values as expected from checkpoint.
        self.ckpt_backup(3, valcnt_ts_log, valcnt_ts_nolog)
        self.ckpt_backup(2, (nkeys - valcnt_ts_log), (nkeys - valcnt_ts_nolog))

        # Scenario: 3
        # Check we see all the data values correctly after rollback. Skip the case where the most
        # recent checkpoint wasn't based on the last stable timestamp, those can't be rolled back.
        if self.ckpt_ts == False:
                return
        self.conn.rollback_to_stable()

        # All tables should see the values correctly when read with
        # read timestamp as stable timestamp.
        self.check(self.session, 'read_timestamp=' + stable_ts,
            self.table_ts_nolog, dict((k, 2) for k in orig_keys))
        self.check(self.session, 'read_timestamp=' + stable_ts,
            self.table_ts_log, dict((k, 2) for k in orig_keys))

        # Scenario: 4
        # Check that we see the values correctly when read without any
        # timestamp.
        if self.using_log == True:
            # For logged table we should see latest values (i.e. 3) when logging
            # is enabled.
            self.check(self.session, "",
                self.table_ts_log, dict((k, 3) for k in orig_keys))
        else:
            # When logging is disabled, we should not see the values beyond the
            # stable timestamp with timestamped checkpoints.
            self.check(self.session, "",
                self.table_ts_log, dict((k, 2) for k in orig_keys))

        # For non-logged table we should not see the values beyond the
        # stable timestamp with timestamped checkpoints.
        self.check(self.session, "",
            self.table_ts_nolog, dict((k, 2) for k in orig_keys))

if __name__ == '__main__':
    wttest.run()
