//---------------------------------------------------------------------------
//	Greenplum Database
//	Copyright (C) 2018 VMware, Inc. or its affiliates.
//
//	@filename:
//		CBucketTest.cpp
//
//	@doc:
//		Testing operations on histogram buckets
//---------------------------------------------------------------------------

#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif

#include "unittest/dxl/statistics/CBucketTest.h"

#include <stdint.h>

#include "gpos/io/COstreamString.h"
#include "gpos/string/CWStringDynamic.h"

#include "naucrates/base/CDatumBoolGPDB.h"
#include "naucrates/base/CDatumGenericGPDB.h"
#include "naucrates/base/CDatumInt4GPDB.h"
#include "naucrates/dxl/CDXLUtils.h"
#include "naucrates/md/CMDTypeGenericGPDB.h"
#include "naucrates/md/IMDType.h"
#include "naucrates/statistics/CBucket.h"
#include "naucrates/statistics/CPoint.h"
#include "naucrates/statistics/CStatisticsUtils.h"

#include "unittest/base.h"
#include "unittest/dxl/statistics/CCardinalityTestUtils.h"
#include "unittest/gpopt/CTestUtils.h"

using namespace gpopt;

// unittest for statistics objects
GPOS_RESULT
CBucketTest::EresUnittest()
{
	// tests that use shared optimization context
	CUnittest rgutSharedOptCtxt[] = {
		GPOS_UNITTEST_FUNC(CBucketTest::EresUnittest_CBucketInt4),
		GPOS_UNITTEST_FUNC(CBucketTest::EresUnittest_CBucketBool),
		GPOS_UNITTEST_FUNC(CBucketTest::EresUnittest_CBucketScale),
		GPOS_UNITTEST_FUNC(CBucketTest::EresUnittest_CBucketDifference),
		GPOS_UNITTEST_FUNC(CBucketTest::EresUnittest_CBucketIntersect),
		GPOS_UNITTEST_FUNC(
			CBucketTest::EresUnittest_CBucketMergeCommutativityUnion),
		GPOS_UNITTEST_FUNC(
			CBucketTest::EresUnittest_CBucketMergeCommutativitySameLowerBounds),
		GPOS_UNITTEST_FUNC(
			CBucketTest::EresUnittest_CBucketMergeCommutativitySameUpperBounds),
		GPOS_UNITTEST_FUNC(
			CBucketTest::EresUnittest_CBucketMergeCommutativityUnionAll),
		GPOS_UNITTEST_FUNC(
			CBucketTest::EresUnittest_CBucketMergeCommutativityDoubleDatum),
		GPOS_UNITTEST_FUNC(
			CBucketTest::
				EresUnittest_CBucketMergeCommutativityDoubleDatumSameLowerBounds),
		GPOS_UNITTEST_FUNC(
			CBucketTest::
				EresUnittest_CBucketMergeCommutativityDoubleDatumSameUpperBounds),
	};

	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// setup a file-based provider
	CMDProviderMemory *pmdp = CTestUtils::m_pmdpf;
	pmdp->AddRef();
	CMDAccessor mda(mp, CMDCache::Pcache(), CTestUtils::m_sysidDefault, pmdp);

	// install opt context in TLS
	CAutoOptCtxt aoc(mp, &mda, nullptr /* pceeval */,
					 CTestUtils::GetCostModel(mp));

	return CUnittest::EresExecute(rgutSharedOptCtxt,
								  GPOS_ARRAY_SIZE(rgutSharedOptCtxt));
}

// basic int4 bucket tests;
GPOS_RESULT
CBucketTest::EresUnittest_CBucketInt4()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// generate integer points
	CPoint *point1 = CTestUtils::PpointInt4(mp, 1);
	CPoint *point2 = CTestUtils::PpointInt4(mp, 2);
	CPoint *point3 = CTestUtils::PpointInt4(mp, 3);
	CPoint *point4 = CTestUtils::PpointInt4(mp, 4);
	CPoint *point10 = CTestUtils::PpointInt4(mp, 10);

	// bucket [1,1]
	CBucket *bucket1 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 1, 1, CDouble(1.0), CDouble(1.0));
	CCardinalityTestUtils::PrintBucket(mp, "b1", bucket1);

	GPOS_UNITTEST_ASSERT_MSG(bucket1->Contains(point1), "[1,1] must contain 1");
	GPOS_UNITTEST_ASSERT_MSG(
		CDouble(1.0) ==
			bucket1->GetOverlapPercentage(point1, true /*include_point*/),
		"overlap of 1 in [1,1] must 1.0");

	GPOS_UNITTEST_ASSERT_MSG(!bucket1->Contains(point2),
							 "[1,1] must not contain 2");

	// bucket [1,3)
	CBucket *bucket2 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 1, 3, CDouble(1.0), CDouble(10.0));
	CCardinalityTestUtils::PrintBucket(mp, "b2", bucket2);

	// overlap of [1,2) w.r.t [1,3) should be about 50%
	CDouble overlap =
		bucket2->GetOverlapPercentage(point2, false /*include_point*/);
	GPOS_UNITTEST_ASSERT(0.49 <= overlap && overlap <= 0.51);

	// bucket [1,10)
	CBucket *bucket3 = GPOS_NEW(mp)
		CBucket(CTestUtils::PpointInt4(mp, 1), CTestUtils::PpointInt4(mp, 10),
				true /* is_lower_closed */, true /* is_upper_closed */,
				CDouble(1.0), CDouble(10.0));
	CCardinalityTestUtils::PrintBucket(mp, "b3", bucket3);
	// bucket (1, 5)
	CBucket *bucket4 = GPOS_NEW(mp)
		CBucket(CTestUtils::PpointInt4(mp, 1), CTestUtils::PpointInt4(mp, 5),
				false /* is_lower_closed */, false /* is_upper_closed */,
				CDouble(1.0), CDouble(5.0));
	CCardinalityTestUtils::PrintBucket(mp, "b4", bucket4);
	// bucket [1, 5)
	CBucket *bucket5 = GPOS_NEW(mp)
		CBucket(CTestUtils::PpointInt4(mp, 1), CTestUtils::PpointInt4(mp, 5),
				true /* is_lower_closed */, false /* is_upper_closed */,
				CDouble(1.0), CDouble(5.0));
	CCardinalityTestUtils::PrintBucket(mp, "b5", bucket5);

	// overlap of [1,4) w.r.t [1,10] should be about 30%
	CDouble overlap_open =
		bucket3->GetOverlapPercentage(point4, false /*include_point*/);
	GPOS_UNITTEST_ASSERT(0.29 <= overlap_open && overlap_open <= 0.31);
	// overlap of [1,4] w.r.t [1,10] should be about 40%
	CDouble overlap_closed =
		bucket3->GetOverlapPercentage(point4, true /*include_point*/);
	GPOS_UNITTEST_ASSERT(0.39 <= overlap_closed && overlap_closed <= 0.41);
	// overlap of [1,10) w.r.t [1,10] should be about 90%
	CDouble overlap_bound =
		bucket3->GetOverlapPercentage(point10, false /*include_point*/);
	GPOS_UNITTEST_ASSERT(0.89 <= overlap_bound && overlap_bound <= 0.91);
	// overlap of (1,3) w.r.t (1,5) should be about 33%
	CDouble overlap_open2 =
		bucket4->GetOverlapPercentage(point3, false /*include_point*/);
	GPOS_UNITTEST_ASSERT(0.32 <= overlap_open2 && overlap_open2 <= 0.34);
	// overlap of [1,3) w.r.t [1,5) should be about 50%
	CDouble overlap_closed2 =
		bucket5->GetOverlapPercentage(point3, false /*include_point*/);
	GPOS_UNITTEST_ASSERT(0.49 <= overlap_closed2 && overlap_closed2 <= 0.51);

	// subsumption
	GPOS_UNITTEST_ASSERT(bucket1->Subsumes(bucket1));
	GPOS_UNITTEST_ASSERT(bucket2->Subsumes(bucket1));

	// width
	CDouble width = bucket1->Width();
	GPOS_UNITTEST_ASSERT(0.99 <= width && width <= 1.01);

	// bucket [1,2] and (2,4)
	CBucket *pbucket3 = CCardinalityTestUtils::PbucketInteger(
		mp, 1, 2, true, true, CDouble(1.0), CDouble(1.0));
	CBucket *pbucket4 = CCardinalityTestUtils::PbucketInteger(
		mp, 2, 4, false, false, CDouble(1.0), CDouble(1.0));

	// point IsBefore
	GPOS_UNITTEST_ASSERT_MSG(pbucket4->IsBefore(point2),
							 "2 must be before (2,4)");

	// bucket IsBefore
	GPOS_UNITTEST_ASSERT_MSG(pbucket3->IsBefore(pbucket4),
							 "[1,2] must be before (2,4)");

	point1->Release();
	point2->Release();
	point3->Release();
	point4->Release();
	point10->Release();
	GPOS_DELETE(bucket1);
	GPOS_DELETE(bucket2);
	GPOS_DELETE(bucket3);
	GPOS_DELETE(bucket4);
	GPOS_DELETE(bucket5);
	GPOS_DELETE(pbucket3);
	GPOS_DELETE(pbucket4);

	return GPOS_OK;
}

// basic BOOL bucket tests;
GPOS_RESULT
CBucketTest::EresUnittest_CBucketBool()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// generate boolean points
	CPoint *p1 = CTestUtils::PpointBool(mp, true);
	CPoint *p2 = CTestUtils::PpointBool(mp, false);

	// bucket for true
	CBucket *bucket =
		CCardinalityTestUtils::PbucketSingletonBoolVal(mp, true, CDouble(1.0));

	GPOS_UNITTEST_ASSERT_MSG(bucket->Contains(p1),
							 "true bucket must contain true");
	GPOS_UNITTEST_ASSERT_MSG(CDouble(1.0) == bucket->GetOverlapPercentage(p1),
							 "overlap must 1.0");

	GPOS_UNITTEST_ASSERT_MSG(!bucket->Contains(p2),
							 "true bucket must not contain false");
	GPOS_UNITTEST_ASSERT_MSG(CDouble(0.0) == bucket->GetOverlapPercentage(p2),
							 "overlap must 0.0");

	p1->Release();
	p2->Release();

	GPOS_DELETE(bucket);

	return GPOS_OK;
}

// scaling a bucket
GPOS_RESULT
CBucketTest::EresUnittest_CBucketScale()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// generate integer point
	CPoint *point1 = CTestUtils::PpointInt4(mp, 10);

	// bucket [1,100)
	CBucket *bucket1 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 1, 100, CDouble(0.5), CDouble(20.0));

	CBucket *bucket2 =
		bucket1->MakeBucketScaleUpper(mp, point1, false /* include_upper */);

	// new bucket [1, 10) must not contain 10
	GPOS_UNITTEST_ASSERT(!bucket2->Contains(point1));

	// new bucket [1, 10) must contain 9
	CPoint *point2 = CTestUtils::PpointInt4(mp, 9);
	GPOS_UNITTEST_ASSERT(bucket2->Contains(point2));
	point2->Release();

	// new bucket's frequency and distinct values must be lesser than original
	GPOS_UNITTEST_ASSERT(bucket2->GetFrequency() < bucket1->GetFrequency());
	GPOS_UNITTEST_ASSERT(bucket2->GetNumDistinct() < bucket1->GetNumDistinct());

	// scale lower
	CBucket *pbucket3 =
		bucket1->MakeBucketScaleLower(mp, point1, true /* include_lower */);
	GPOS_UNITTEST_ASSERT(pbucket3->Contains(point1));

	// scale to a singleton
	CBucket *pbucket4 = bucket1->MakeBucketSingleton(mp, point1);
	GPOS_UNITTEST_ASSERT(pbucket4->GetNumDistinct() < 2.0);

	// clean up
	point1->Release();
	GPOS_DELETE(bucket1);
	GPOS_DELETE(bucket2);
	GPOS_DELETE(pbucket3);
	GPOS_DELETE(pbucket4);

	return GPOS_OK;
}

// difference operation on buckets
GPOS_RESULT
CBucketTest::EresUnittest_CBucketDifference()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// bucket [1,100)
	CBucket *bucket1 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 1, 100, CDouble(1.0), CDouble(1.0));

	// bucket [50,75)
	CBucket *bucket2 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 50, 60, CDouble(1.0), CDouble(1.0));

	// bucket [200, 300)
	CBucket *pbucket3 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 200, 300, CDouble(1.0), CDouble(1.0));

	CBucket *pbucket4 = nullptr;
	CBucket *pbucket5 = nullptr;
	bucket1->Difference(mp, bucket2, &pbucket4, &pbucket5);
	GPOS_UNITTEST_ASSERT(nullptr != pbucket4);
	GPOS_UNITTEST_ASSERT(nullptr != pbucket5);
	CCardinalityTestUtils::PrintBucket(mp, "pbucket4", pbucket4);
	CCardinalityTestUtils::PrintBucket(mp, "pbucket5", pbucket4);

	CBucket *pbucket6 = nullptr;
	CBucket *pbucket7 = nullptr;
	bucket1->Difference(mp, pbucket3, &pbucket6, &pbucket7);
	GPOS_UNITTEST_ASSERT(nullptr != pbucket6);
	GPOS_UNITTEST_ASSERT(nullptr == pbucket7);
	CCardinalityTestUtils::PrintBucket(mp, "pbucket6", pbucket6);

	GPOS_DELETE(bucket1);
	GPOS_DELETE(bucket2);
	GPOS_DELETE(pbucket3);
	GPOS_DELETE(pbucket4);
	GPOS_DELETE(pbucket5);
	GPOS_DELETE(pbucket6);

	return GPOS_OK;
}

// intersection of buckets
GPOS_RESULT
CBucketTest::EresUnittest_CBucketIntersect()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	SBucketsIntersectTestElem rgBucketsIntersectTestElem[] = {
		{7, 203, true, true, CDouble(0.1), CDouble(100), 3, 213, true, true,
		 CDouble(0.1), CDouble(100), true, 7, 203, true, true,
		 CDouble(9.33e-05), CDouble(93.33)},  // overlaps
		{3, 213, true, true, CDouble(0.1), CDouble(100), 7, 203, true, true,
		 CDouble(0.1), CDouble(100), true, 7, 203, true, true,
		 CDouble(9.33e-05), CDouble(93.33)},  // same as above but reversed
		{13, 103, true, true, CDouble(0.1), CDouble(100), 2, 98, true, true,
		 CDouble(0.1), CDouble(100), true, 13, 98, true, true,
		 CDouble(8.85e-05), CDouble(88.54)},  // subsumes
		{2, 99, true, true, CDouble(0.1), CDouble(100.0), 13, 103, true, true,
		 CDouble(0.1), CDouble(100), true, 13, 99, true, true,
		 CDouble(8.87e-05), CDouble(88.66)},  // same as above but reversed
		{0, 5, true, true, CDouble(0.1), CDouble(100), 10, 15, false, true,
		 CDouble(0.1), CDouble(100), false, -1, -1, false, false, CDouble(0.1),
		 CDouble(100)},	 // negative
		{10, 15, true, true, CDouble(0.1), CDouble(100), 0, 5, false, true,
		 CDouble(0.1), CDouble(100), false, -1, -1, false, false, CDouble(0.1),
		 CDouble(100)},	 // same as above but reversed
		{0, 5, true, true, CDouble(0.1), CDouble(100), 5, 10, true, true,
		 CDouble(0.1), CDouble(100), true, 5, 5, true, true, CDouble(2.00e-05),
		 CDouble(
			 20)},	// ub of one bucket is the same as lb of the other and both bounds are closed
		{5, 10, true, true, CDouble(0.1), CDouble(100), 0, 5, true, true,
		 CDouble(0.1), CDouble(100), true, 5, 5, true, true, CDouble(2.00e-05),
		 CDouble(20)},	// same as above but reversed
		{0, 5, true, true, CDouble(0.1), CDouble(100), 5, 10, false, true,
		 CDouble(0.1), CDouble(100), false, -1, -1, false, false, CDouble(0.1),
		 CDouble(
			 100)},	 // ub of one bucket is the same as lb of the other but closing criteria are different
		{5, 10, false, true, CDouble(0.1), CDouble(100), 0, 5, true, true,
		 CDouble(0.1), CDouble(100), false, -1, -1, false, false, CDouble(0.1),
		 CDouble(100)},	 // same as above but reversed
		{0, 5, true, true, CDouble(0.1), CDouble(100), 0, 5, false, true,
		 CDouble(0.1), CDouble(100), true, 0, 5, false, true,
		 CDouble(0.00010000000000000002),
		 CDouble(100)},	 // exact match but only differ in closure of lb
		{0, 5, true, true, CDouble(0.1), CDouble(100), 0, 5, true, true,
		 CDouble(0.1), CDouble(100), true, 0, 5, true, true,
		 CDouble(0.00010000000000000002),
		 CDouble(100)},	 // exact match with all bounds closed
		{0, 5, true, false, CDouble(0.1), CDouble(100), 0, 5, true, false,
		 CDouble(0.1), CDouble(100), true, 0, 5, true, false,
		 CDouble(0.00010000000000000002),
		 CDouble(100)},	 // exact match with ubs open
		{0, 5, false, false, CDouble(0.1), CDouble(100), 0, 5, true, false,
		 CDouble(0.1), CDouble(100), true, 0, 5, false, false,
		 CDouble(0.00010000000000000002),
		 CDouble(100)},	 // exact match with lbs differ in closure
		{
			0,
			5,
			true,
			true,
			CDouble(0.1),
			CDouble(100),
			0,
			5,
			true,
			false,
			CDouble(0.1),
			CDouble(100),
			true,
			0,
			5,
			true,
			false,
			CDouble(0.00010000000000000002),
			CDouble(100),
		},	// exact match with ubs differ in closure
		{
			0, 5,	 true,	true,		CDouble(0), CDouble(0), 0,
			5, true, false, CDouble(0), CDouble(0), true,		0,
			5, true, false, CDouble(0), CDouble(0),
		},	// overlaps but both buckets are empty
		{
			0, 5,	 true,	true,		CDouble(0.1), CDouble(100), 0,
			5, true, false, CDouble(0), CDouble(0),	  true,			0,
			5, true, false, CDouble(0), CDouble(0),
		},
	};	// overlaps but one bucket is empty

	const ULONG length = GPOS_ARRAY_SIZE(rgBucketsIntersectTestElem);
	for (ULONG ul = 0; ul < length; ul++)
	{
		CBucket *bucket1 = CCardinalityTestUtils::PbucketInteger(
			mp, rgBucketsIntersectTestElem[ul].m_iLb1,
			rgBucketsIntersectTestElem[ul].m_iUb1,
			rgBucketsIntersectTestElem[ul].m_fLb1Closed,
			rgBucketsIntersectTestElem[ul].m_fUb1Closed,
			rgBucketsIntersectTestElem[ul].m_frequency1,
			rgBucketsIntersectTestElem[ul].m_distinct1);

		CBucket *bucket2 = CCardinalityTestUtils::PbucketInteger(
			mp, rgBucketsIntersectTestElem[ul].m_iLb2,
			rgBucketsIntersectTestElem[ul].m_iUb2,
			rgBucketsIntersectTestElem[ul].m_fLb2Closed,
			rgBucketsIntersectTestElem[ul].m_fUb2Closed,
			rgBucketsIntersectTestElem[ul].m_frequency2,
			rgBucketsIntersectTestElem[ul].m_distinct2);

		BOOL result = bucket1->Intersects(bucket2);

		GPOS_RESULT eres = GPOS_FAILED;
		if (rgBucketsIntersectTestElem[ul].fIntersect == result)
		{
			eres = GPOS_OK;
		}

		if (true == result)
		{
			CDouble dDummy1(0.0);
			CDouble dDummy2(0.0);
			CBucket *pbucketOuput =
				bucket1->MakeBucketIntersect(mp, bucket2, &dDummy1, &dDummy2);
			CBucket *pbucketExpected = CCardinalityTestUtils::PbucketInteger(
				mp, rgBucketsIntersectTestElem[ul].m_iLbOutput,
				rgBucketsIntersectTestElem[ul].m_iUbOutput,
				rgBucketsIntersectTestElem[ul].m_fLbOutputClosed,
				rgBucketsIntersectTestElem[ul].m_fUbOutputClosed,
				rgBucketsIntersectTestElem[ul].m_frequencyOutput,
				rgBucketsIntersectTestElem[ul].m_distinctOutput);

			BOOL fMatch = FMatchBucket(pbucketOuput, pbucketExpected);

			if (!fMatch)
			{
				eres = GPOS_FAILED;

				CWStringDynamic str(mp);
				COstreamString oss(&str);

				pbucketOuput->OsPrint(oss);
				oss << std::endl;
				pbucketExpected->OsPrint(oss);
				oss << std::endl;
				GPOS_TRACE(str.GetBuffer());
			}

			GPOS_DELETE(pbucketExpected);
			GPOS_DELETE(pbucketOuput);
		}
		// clean up
		GPOS_DELETE(bucket1);
		GPOS_DELETE(bucket2);

		if (GPOS_OK != eres)
		{
			return eres;
		}
	}

	return GPOS_OK;
}


// do the bucket boundaries, frequencies and NDVs match
BOOL
CBucketTest::FMatchBucket(CBucket *bucket1, CBucket *bucket2)
{
	GPOS_UNITTEST_ASSERT(nullptr != bucket1);
	GPOS_UNITTEST_ASSERT(nullptr != bucket2);

	if (bucket1->IsLowerClosed() != bucket2->IsLowerClosed())
	{
		return false;
	}

	if (bucket1->IsUpperClosed() != bucket2->IsUpperClosed())
	{
		return false;
	}

	if (fabs((bucket1->GetFrequency() - bucket2->GetFrequency()).Get()) >
		CStatistics::Epsilon)
	{
		return false;
	}

	if (fabs((bucket1->GetNumDistinct() - bucket2->GetNumDistinct()).Get()) > 1)
	{
		return false;
	}

	if (bucket1->GetLowerBound()->Equals(bucket2->GetLowerBound()) &&
		bucket1->GetUpperBound()->Equals(bucket2->GetUpperBound()))
	{
		return true;
	}

	return false;
}

// basic merge commutativity test for union
GPOS_RESULT
CBucketTest::EresUnittest_CBucketMergeCommutativityUnion()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// 1000 rows
	CBucket *bucket1 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 0, 100, CDouble(0.6), CDouble(100.0));

	// 600 rows
	CBucket *bucket2 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 50, 150, CDouble(0.3), CDouble(100.0));

	CBucket *bucket1_new1 = nullptr;
	CBucket *bucket2_new1 = nullptr;
	CDouble result_rows1(0.0);

	CBucket *result1 = bucket1->SplitAndMergeBuckets(
		mp, bucket2, 1000, 600, &bucket1_new1, &bucket2_new1, &result_rows1,
		false /*is_union_all*/);


	CBucket *bucket1_new2 = nullptr;
	CBucket *bucket2_new2 = nullptr;
	CDouble result_rows2(0.0);
	CBucket *result2 = bucket2->SplitAndMergeBuckets(
		mp, bucket1, 600, 1000, &bucket1_new2, &bucket2_new2, &result_rows2,
		false /*is_union_all*/);

	GPOS_UNITTEST_ASSERT(result1->Equals(result2));

	if (nullptr != bucket1_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket1_new1->Equals(bucket2_new2));
	}
	else if (nullptr != bucket2_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket2_new1->Equals(bucket1_new2));
	}

	GPOS_DELETE(bucket1);
	GPOS_DELETE(bucket2);
	GPOS_DELETE(result1);
	GPOS_DELETE(result2);
	GPOS_DELETE(bucket1_new1);
	GPOS_DELETE(bucket2_new1);
	GPOS_DELETE(bucket1_new2);
	GPOS_DELETE(bucket2_new2);

	return GPOS_OK;
}

// merge commutativity test for union when lower bounds have same value but
// one is closed and the other is open
GPOS_RESULT
CBucketTest::EresUnittest_CBucketMergeCommutativitySameLowerBounds()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// 1000 rows
	// b1 = [0,100)
	CBucket *bucket1 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 0, 100, CDouble(0.6), CDouble(100.0));

	// 600 rows
	// b2 = (0,50)
	CPoint *ppLower = CTestUtils::PpointInt4(mp, 0);
	CPoint *ppUpper = CTestUtils::PpointInt4(mp, 50);
	CBucket *bucket2 = GPOS_NEW(mp)
		CBucket(ppLower, ppUpper, false /* is_lower_closed */,
				false /*is_upper_closed*/, CDouble(0.2), CDouble(50));

	CBucket *bucket1_new1 = nullptr;
	CBucket *bucket2_new1 = nullptr;
	CDouble result_rows1(0.0);

	// returns [0,0]
	// bucket1_new1 = (0,100)
	// bucket2_new1 = (0,50)
	CBucket *result1 = bucket1->SplitAndMergeBuckets(
		mp, bucket2, 1000, 600, &bucket1_new1, &bucket2_new1, &result_rows1,
		false /*is_union_all*/);

	GPOS_UNITTEST_ASSERT(bucket2->Equals(bucket2_new1));

	CBucket *bucket1_new2 = nullptr;
	CBucket *bucket2_new2 = nullptr;
	CDouble result_rows2(0.0);
	CBucket *result2 = bucket2->SplitAndMergeBuckets(
		mp, bucket1, 600, 1000, &bucket1_new2, &bucket2_new2, &result_rows2,
		false /*is_union_all*/);

	GPOS_UNITTEST_ASSERT(result1->Equals(result2));
	GPOS_UNITTEST_ASSERT(result1->IsSingleton());
	GPOS_UNITTEST_ASSERT(result2->IsSingleton());

	if (nullptr != bucket1_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket1_new1->Equals(bucket2_new2));
	}
	else if (nullptr != bucket2_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket2_new1->Equals(bucket1_new2));
	}

	GPOS_DELETE(bucket1);
	GPOS_DELETE(bucket2);
	GPOS_DELETE(result1);
	GPOS_DELETE(result2);
	GPOS_DELETE(bucket1_new1);
	GPOS_DELETE(bucket2_new1);
	GPOS_DELETE(bucket1_new2);
	GPOS_DELETE(bucket2_new2);

	return GPOS_OK;
}

// merge commutativity test for union when lower bounds have same value but
// one is closed and the other is open
GPOS_RESULT
CBucketTest::EresUnittest_CBucketMergeCommutativitySameUpperBounds()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// 1000 rows
	// b1 = [0,100)
	CBucket *bucket1 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 0, 100, CDouble(0.6), CDouble(100.0));

	// 600 rows
	// b2 = [0,100]
	CPoint *ppLower = CTestUtils::PpointInt4(mp, 0);
	CPoint *ppUpper = CTestUtils::PpointInt4(mp, 100);
	CBucket *bucket2 = GPOS_NEW(mp)
		CBucket(ppLower, ppUpper, true /* is_lower_closed */,
				true /*is_upper_closed*/, CDouble(0.2), CDouble(50));

	CBucket *bucket1_new1 = nullptr;
	CBucket *bucket2_new1 = nullptr;
	CDouble result_rows1(0.0);

	// returns [0,100)
	// bucket1_new1 = NULL
	// bucket2_new1 = [100,100]
	CBucket *result1 = bucket1->SplitAndMergeBuckets(
		mp, bucket2, 1000, 600, &bucket1_new1, &bucket2_new1, &result_rows1,
		false /*is_union_all*/);

	GPOS_UNITTEST_ASSERT(bucket2_new1->IsSingleton());

	CBucket *bucket1_new2 = nullptr;
	CBucket *bucket2_new2 = nullptr;
	CDouble result_rows2(0.0);
	CBucket *result2 = bucket2->SplitAndMergeBuckets(
		mp, bucket1, 600, 1000, &bucket1_new2, &bucket2_new2, &result_rows2,
		false /*is_union_all*/);

	GPOS_UNITTEST_ASSERT(bucket1_new2->IsSingleton());

	GPOS_UNITTEST_ASSERT(result1->Equals(result2));

	if (nullptr != bucket1_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket1_new1->Equals(bucket2_new2));
	}
	else if (nullptr != bucket2_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket2_new1->Equals(bucket1_new2));
	}

	GPOS_DELETE(bucket1);
	GPOS_DELETE(bucket2);
	GPOS_DELETE(result1);
	GPOS_DELETE(result2);
	GPOS_DELETE(bucket1_new1);
	GPOS_DELETE(bucket2_new1);
	GPOS_DELETE(bucket1_new2);
	GPOS_DELETE(bucket2_new2);

	return GPOS_OK;
}

// basic merge commutativity test for union all
GPOS_RESULT
CBucketTest::EresUnittest_CBucketMergeCommutativityUnionAll()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// 1000 rows
	CBucket *bucket1 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 0, 100, CDouble(0.6), CDouble(100.0));

	// 600 rows
	CBucket *bucket2 = CCardinalityTestUtils::PbucketIntegerClosedLowerBound(
		mp, 50, 150, CDouble(0.3), CDouble(100.0));

	CBucket *bucket1_new1 = nullptr;
	CBucket *bucket2_new1 = nullptr;
	CDouble result_rows1(0.0);

	CBucket *result1 = bucket1->SplitAndMergeBuckets(
		mp, bucket2, 1000, 600, &bucket1_new1, &bucket2_new1, &result_rows1,
		true /*is_union_all*/);


	CBucket *bucket1_new2 = nullptr;
	CBucket *bucket2_new2 = nullptr;
	CDouble result_rows2(0.0);
	CBucket *result2 = bucket2->SplitAndMergeBuckets(
		mp, bucket1, 600, 1000, &bucket1_new2, &bucket2_new2, &result_rows2,
		true /*is_union_all*/);

	GPOS_UNITTEST_ASSERT(result1->Equals(result2));

	if (nullptr != bucket1_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket1_new1->Equals(bucket2_new2));
	}
	else if (nullptr != bucket2_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket2_new1->Equals(bucket1_new2));
	}

	GPOS_DELETE(bucket1);
	GPOS_DELETE(bucket2);
	GPOS_DELETE(result1);
	GPOS_DELETE(result2);
	GPOS_DELETE(bucket1_new1);
	GPOS_DELETE(bucket2_new1);
	GPOS_DELETE(bucket1_new2);
	GPOS_DELETE(bucket2_new2);

	return GPOS_OK;
}

// basic merge commutativity test for double datum
GPOS_RESULT
CBucketTest::EresUnittest_CBucketMergeCommutativityDoubleDatum()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// [0.0, 100.0)
	CPoint *ppLower1 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(0.0));
	CPoint *ppUpper1 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(100.0));

	CBucket *bucket1 = GPOS_NEW(mp)
		CBucket(ppLower1, ppUpper1, true /* is_lower_closed */,
				false /*is_upper_closed*/, CDouble(0.2), CDouble(50));

	// [50.0, 150.0)
	CPoint *ppLower2 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(50.0));
	CPoint *ppUpper2 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(150.0));
	CBucket *bucket2 = GPOS_NEW(mp)
		CBucket(ppLower2, ppUpper2, true /* is_lower_closed */,
				false /*is_upper_closed*/, CDouble(0.2), CDouble(50));

	CBucket *bucket1_new1 = nullptr;
	CBucket *bucket2_new1 = nullptr;
	CDouble result_rows1(0.0);

	CBucket *result1 = bucket1->SplitAndMergeBuckets(
		mp, bucket2, 1000, 600, &bucket1_new1, &bucket2_new1, &result_rows1,
		false /*is_union_all*/);


	CBucket *bucket1_new2 = nullptr;
	CBucket *bucket2_new2 = nullptr;
	CDouble result_rows2(0.0);
	CBucket *result2 = bucket2->SplitAndMergeBuckets(
		mp, bucket1, 600, 1000, &bucket1_new2, &bucket2_new2, &result_rows2,
		false /*is_union_all*/);

	GPOS_UNITTEST_ASSERT(result1->Equals(result2));

	if (nullptr != bucket1_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket1_new1->Equals(bucket2_new2));
	}
	else if (nullptr != bucket2_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket2_new1->Equals(bucket1_new2));
	}

	GPOS_DELETE(bucket1);
	GPOS_DELETE(bucket2);
	GPOS_DELETE(result1);
	GPOS_DELETE(result2);
	GPOS_DELETE(bucket1_new1);
	GPOS_DELETE(bucket2_new1);
	GPOS_DELETE(bucket1_new2);
	GPOS_DELETE(bucket2_new2);

	return GPOS_OK;
}

// merge commutativity test for union when lower bounds have same value but
// one is closed and the other is open for double datum
GPOS_RESULT
CBucketTest::EresUnittest_CBucketMergeCommutativityDoubleDatumSameLowerBounds()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// b1 = [0,100)
	CPoint *ppLower1 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(0.0));
	CPoint *ppUpper1 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(100.0));

	CBucket *bucket1 = GPOS_NEW(mp)
		CBucket(ppLower1, ppUpper1, true /* is_lower_closed */,
				false /*is_upper_closed*/, CDouble(0.2), CDouble(50));

	// b2 = (0,50)
	CPoint *ppLower2 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(0.0));
	CPoint *ppUpper2 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(50.0));
	CBucket *bucket2 = GPOS_NEW(mp)
		CBucket(ppLower2, ppUpper2, false /* is_lower_closed */,
				false /*is_upper_closed*/, CDouble(0.2), CDouble(50));

	CBucket *bucket1_new1 = nullptr;
	CBucket *bucket2_new1 = nullptr;
	CDouble result_rows1(0.0);

	CBucket *result1 = bucket1->SplitAndMergeBuckets(
		mp, bucket2, 1000, 600, &bucket1_new1, &bucket2_new1, &result_rows1,
		false /*is_union_all*/);


	CBucket *bucket1_new2 = nullptr;
	CBucket *bucket2_new2 = nullptr;
	CDouble result_rows2(0.0);
	CBucket *result2 = bucket2->SplitAndMergeBuckets(
		mp, bucket1, 600, 1000, &bucket1_new2, &bucket2_new2, &result_rows2,
		false /*is_union_all*/);

	GPOS_UNITTEST_ASSERT(result1->Equals(result2));

	if (nullptr != bucket1_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket1_new1->Equals(bucket2_new2));
	}
	else if (nullptr != bucket2_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket2_new1->Equals(bucket1_new2));
	}

	GPOS_DELETE(bucket1);
	GPOS_DELETE(bucket2);
	GPOS_DELETE(result1);
	GPOS_DELETE(result2);
	GPOS_DELETE(bucket1_new1);
	GPOS_DELETE(bucket2_new1);
	GPOS_DELETE(bucket1_new2);
	GPOS_DELETE(bucket2_new2);

	return GPOS_OK;
}

// merge commutativity test for union when upper bounds have same value but
// one is closed and the other is open for double datum
GPOS_RESULT
CBucketTest::EresUnittest_CBucketMergeCommutativityDoubleDatumSameUpperBounds()
{
	// create memory pool
	CAutoMemoryPool amp;
	CMemoryPool *mp = amp.Pmp();

	// b1 = [0,100)
	CPoint *ppLower1 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(0.0));
	CPoint *ppUpper1 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(100.0));

	CBucket *bucket1 = GPOS_NEW(mp)
		CBucket(ppLower1, ppUpper1, true /* is_lower_closed */,
				false /*is_upper_closed*/, CDouble(0.4), CDouble(50));

	// b2 = [0,100]
	CPoint *ppLower2 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(0.0));
	CPoint *ppUpper2 =
		CCardinalityTestUtils::PpointDouble(mp, GPDB_FLOAT8, CDouble(100.0));
	CBucket *bucket2 = GPOS_NEW(mp)
		CBucket(ppLower2, ppUpper2, true /* is_lower_closed */,
				true /*is_upper_closed*/, CDouble(0.2), CDouble(50));

	CBucket *bucket1_new1 = nullptr;
	CBucket *bucket2_new1 = nullptr;
	CDouble result_rows1(0.0);

	CBucket *result1 = bucket1->SplitAndMergeBuckets(
		mp, bucket2, 1000, 600, &bucket1_new1, &bucket2_new1, &result_rows1,
		false /*is_union_all*/);


	CBucket *bucket1_new2 = nullptr;
	CBucket *bucket2_new2 = nullptr;
	CDouble result_rows2(0.0);
	CBucket *result2 = bucket2->SplitAndMergeBuckets(
		mp, bucket1, 600, 1000, &bucket1_new2, &bucket2_new2, &result_rows2,
		false /*is_union_all*/);

	GPOS_UNITTEST_ASSERT(result1->Equals(result2));

	if (nullptr != bucket1_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket1_new1->Equals(bucket2_new2));
	}
	else if (nullptr != bucket2_new1)
	{
		GPOS_UNITTEST_ASSERT(bucket2_new1->Equals(bucket1_new2));
	}

	GPOS_DELETE(bucket1);
	GPOS_DELETE(bucket2);
	GPOS_DELETE(result1);
	GPOS_DELETE(result2);
	GPOS_DELETE(bucket1_new1);
	GPOS_DELETE(bucket2_new1);
	GPOS_DELETE(bucket1_new2);
	GPOS_DELETE(bucket2_new2);

	return GPOS_OK;
}
// EOF
