#!/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_truncate02.py
#       session level operations on tables
#

import wiredtiger, wttest
from wtdataset import SimpleDataSet
from wtscenario import make_scenarios

# test_truncate_fast_delete
#       When deleting leaf pages that aren't in memory, we set transactional
# information in the page's WT_REF structure, which results in interesting
# issues.
class test_truncate_fast_delete(wttest.WiredTigerTestCase):
    name = 'test_truncate'
    nentries = 10000

    # Use a small page size and lots of keys because we want to create lots
    # of individual pages in the file.
    types = [
        ('file', dict(type='file:', config=\
            'allocation_size=512,leaf_page_max=512')),
    ]

    # This is all about testing the btree layer, not the schema layer, test
    # files and ignore tables.
    keyfmt = [
        ('integer', dict(keyfmt='i')),
        ('recno', dict(keyfmt='r')),
        ('string', dict(keyfmt='S')),
        ]

    # Overflow records force pages to be instantiated, blocking fast delete.
    overflow = [
        ('ovfl1', dict(overflow=False)),
        ('ovfl2', dict(overflow=True)),
        ]

    # Random reads and writes force pages to be instantiated and potentially
    # create update structures, blocking fast delete and changing how fast
    # delete rollback works.
    reads = [
        ('read1', dict(readafter=False,readbefore=False)),
        ('read2', dict(readafter=True,readbefore=False)),
        ('read3', dict(readafter=False,readbefore=True)),
        ('read4', dict(readafter=True,readbefore=True)),
        ]
    writes = [
        ('write1', dict(writeafter=False,writebefore=False)),
        ('write2', dict(writeafter=True,writebefore=False)),
        ('write3', dict(writeafter=False,writebefore=True)),
        ('write4', dict(writeafter=True,writebefore=True)),
        ]

    # Test both commit and abort of the truncate transaction.
    txn = [
        ('txn1', dict(commit=True)),
        ('txn2', dict(commit=False)),
        ]

    scenarios = make_scenarios(types, keyfmt, overflow, reads, writes, txn,
                               prune=20, prunelong=1000)

    # Return the number of records visible to the cursor; test both forward
    # and backward iteration, they are different code paths in this case.
    def cursor_count(self, cursor, expected):
        count = 0
        while cursor.next() == 0:
            count += 1
        self.assertEqual(count, expected)
        cursor.reset()
        count = 0
        while cursor.prev() == 0:
            count += 1
        self.assertEqual(count, expected)

    # Open a cursor in a new session and confirm how many records it sees.
    def outside_count(self, isolation, expected):
        s = self.conn.open_session()
        s.begin_transaction(isolation)
        cursor = s.open_cursor(self.type + self.name, None)
        self.cursor_count(cursor, expected)
        s.close()

    # Trigger fast delete and test cursor counts.
    def test_truncate_fast_delete(self):
        uri = self.type + self.name

        '''
        print '===== run:'
        print 'config:', self.config + self.keyfmt, \
            'overflow=', self.overflow, \
            'readafter=', self.readafter, 'readbefore=', self.readbefore, \
            'writeafter=', self.writeafter, 'writebefore=', self.writebefore, \
            'commit=', self.commit
        '''

        # Create the object.
        ds = SimpleDataSet(self, uri, self.nentries,
                           config=self.config, key_format=self.keyfmt)
        ds.populate()

        # Optionally add a few overflow records so we block fast delete on
        # those pages.
        if self.overflow:
            cursor = self.session.open_cursor(uri, None, 'overwrite=false')
            for i in range(1, self.nentries, 3123):
                cursor.set_key(ds.key(i))
                cursor.set_value(ds.value(i))
                cursor.update()
            cursor.close()

        # Close and re-open it so we get a disk image, not an insert skiplist.
        self.reopen_conn()

        # Optionally read/write a few rows before truncation.
        if self.readbefore or self.writebefore:
            cursor = self.session.open_cursor(uri, None, 'overwrite=false')
            if self.readbefore:
                    for i in range(1, self.nentries, 737):
                        cursor.set_key(ds.key(i))
                        cursor.search()
            if self.writebefore:
                    for i in range(1, self.nentries, 988):
                        cursor.set_key(ds.key(i))
                        cursor.set_value(ds.value(i + 100))
                        cursor.update()
            cursor.close()

        # Begin a transaction, and truncate a big range of rows.
        self.session.begin_transaction(None)
        start = self.session.open_cursor(uri, None)
        start.set_key(ds.key(10))
        end = self.session.open_cursor(uri, None)
        end.set_key(ds.key(self.nentries - 10))
        self.session.truncate(None, start, end, None)
        start.close()
        end.close()

        # Optionally read/write a few rows after truncation.
        if self.readafter or self.writeafter:
            cursor = self.session.open_cursor(uri, None, 'overwrite=false')
            if self.readafter:
                    for i in range(1, self.nentries, 1123):
                        cursor.set_key(ds.key(i))
                        cursor.search()
            if self.writeafter:
                    for i in range(1, self.nentries, 621):
                        cursor.set_key(ds.key(i))
                        cursor.set_value(ds.value(i + 100))
                        cursor.update()
            cursor.close()

        # A cursor involved in the transaction should see the deleted records.
        # The number 19 comes from deleting row 10 (inclusive), to row N - 10,
        # exclusive, or 9 + 10 == 19.
        remaining = 19
        cursor = self.session.open_cursor(uri, None)
        self.cursor_count(cursor, remaining)
        cursor.close()

        # A separate, read_committed cursor should not see the deleted records.
        self.outside_count("isolation=read-committed", self.nentries)

        # A separate, read_uncommitted cursor should see the deleted records.
        self.outside_count("isolation=read-uncommitted", remaining)

        # Commit/rollback the transaction.
        if self.commit:
                self.session.commit_transaction()
        else:
                self.session.rollback_transaction()

        # Check a read_committed cursor sees the right records.
        cursor = self.session.open_cursor(uri, None)
        if self.commit:
                self.cursor_count(cursor, remaining)
        else:
                self.cursor_count(cursor, self.nentries)
        cursor.close()

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