--[[----------------------------------------------------------------------------

WFRxIndexStoreTest.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2015 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

------------------------------------------------------------------------------]]

	-- Rx
local Rx = require 'Rx'
local Observable = Rx.Observable

	-- WF.core.os
local KSPathUtils = require 'KSPathUtils'
local WFFileUtils = require 'WFFileUtils'
local KSIdGenerator = require 'KSIdGenerator'

	-- WF.core.rxlua
local WFRxPromiseBridge = require 'WFRxPromiseBridge'

	-- WF.core.self_test
local WFAsyncTestCase = require 'WFAsyncTestCase'
local WFRxTestCase = require 'WFRxTestCase'

	-- WF.core.utils
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFCapabilities = require 'WFCapabilities'
local WFLogger = require 'WFLogger'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFUnknown = require 'WFUnknown'

	-- WF.sqlite.doc_store
local WFRxDocStore = require 'WFRxDocStore'
local WFRxIndexStore = require 'WFRxIndexStore'
local WFRxIndexStoreSchemaVersionList = require 'WFRxIndexStoreSchemaVersionList'
local WFSQLiteDatabase = require "WFSQLiteDatabase"
local WFSQLiteDatabaseSchema = require "WFSQLiteDatabaseSchema"

--------------------------------------------------------------------------------

local asKSPromise = WFRxPromiseBridge

--============================================================================--

local WFRxIndexStoreTest = {}

--------------------------------------------------------------------------------

local docStore, indexStore, coveragePath

local randomDocId = '7206c3ae9a094d69861ee8983e95085d'
local randomDocId2 = '8b572d0e423c4654b078893ddd41c230'

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.setUp( inCoveragePath )

	coveragePath = inCoveragePath

end

--------------------------------------------------------------------------------

local function tearDown()

	local obs = {}
	if coveragePath then
		if indexStore then
			obs[ #obs + 1 ] = indexStore:DEBUG_dumpCoverage( coveragePath )
		end
		coveragePath = nil
	end

	if indexStore then
		local oldIndexStore = indexStore
		obs[ #obs + 1 ] = indexStore:haltAndDelete()
			:tapOnCompleted( function()
				oldIndexStore:close()
			end )
		indexStore = nil
	end

	if docStore then
		local oldDocStore = docStore
		obs[ #obs + 1 ] = docStore:haltAndDelete()
			:tapOnCompleted( function()
				oldDocStore:close()
			end )
		docStore = nil
	end

	return Rx.Observable.concat( unpack( obs ) )
		:call( require 'WFRxRunUntilDone' )


end

WFRxIndexStoreTest.tearDown = tearDown

--------------------------------------------------------------------------------

local function createDocStore( rxTest, docStorePath )

	if not docStorePath then
		docStorePath = KSPathUtils.getStandardFilePath( "temp" )
		docStorePath = KSPathUtils.child( docStorePath, rxTest:testName() )
		WFFileUtils.delete( docStorePath )
	end

	return WFRxDocStore( KSPathUtils.child( docStorePath, 'Managed Catalog.mcat' ) ), docStorePath

end

--------------------------------------------------------------------------------

local function createIndexStoreWithNoReadOnlyThreads( rxTest, docStore, indexSpecScript, optEnablePlatformIndexStore )

	return WFRxIndexStore( docStore, indexSpecScript, 0, optEnablePlatformIndexStore )
end

--------------------------------------------------------------------------------

local function createIndexStoreWith1ReadOnlyThreads( rxTest, docStore, indexSpecScript, optEnablePlatformIndexStore )

	return WFRxIndexStore( docStore, indexSpecScript, 1, optEnablePlatformIndexStore )

end

--------------------------------------------------------------------------------

local WFRxIndexStoreTestFunctions = {}

function WFRxIndexStoreTestFunctions.rxTestSimpleQueryRx( rxTest, createIndexStoreFn )
	rxTest:setTimeLimit( 10 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local aspectRatios = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, aspectRatio FROM aspectRatio
					ORDER BY aspectRatio DESC]] } )

	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	aspectRatios:expectData { { 1, 1.5 } }

	local keywords = rxTest:observe( indexStore:query {
		sql = [[SELECT name FROM keyword]]
	})

	keywords:expectData {}

	aspectRatios:dispose()

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = 'af21cdb6d2fa4d1c8099d08a6ba1d7c1',
			height = 130,
			width = 100,
			keyword = 'mushroom',
		},

		':ignore:' )

	keywords:expectData { { 'mushroom' } }

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestQueryOnce( rxTest, createIndexStoreFn )
	rxTest:setTimeLimit( 10 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	-- Test - Does not compose with the calm setting
	rxTest:expectOnlyError( indexStore:queryOnce {
			sql = [[SELECT docId, aspectRatio FROM aspectRatio]],
			calm = true,
		}, "Calm is not allowed when using the queryOnce API." )


	local aspectRatios = rxTest:observe( indexStore:queryOnce {
		sql = [[SELECT docId, aspectRatio FROM aspectRatio
					ORDER BY aspectRatio DESC]] } )

	-- Test Returns the correct values on DocStoreWrite
	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	aspectRatios:expectData { { 1, 1.5 } }

	-- Cleanup
	rxTest:finally(function()
		aspectRatios:dispose()
	end)
end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestQueryThenDebounce( rxTest, createIndexStoreFn )
	rxTest:setTimeLimit( 10 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	-- Test - Does not compose with the calm setting
	rxTest:expectOnlyError( indexStore:queryThenDebounce( 0.2, {
				sql = [[SELECT docId, aspectRatio FROM aspectRatio]],
				calm = true,
			}), "Calm is not allowed when using the queryThenDebounce API." )

	-- Test - Does not allow a throttle below our threshold
	rxTest:expectOnlyError( indexStore:queryThenDebounce( 0.0, {
				sql = [[SELECT docId, aspectRatio FROM aspectRatio]],
			}), "To prevent misuse, the lower threshold for receiving updates with this API is 0.125 seconds" )

	-- Test - Does not allow setting throttle time directly
	rxTest:expectOnlyError( indexStore:queryThenDebounce( 0.2, {
				sql = [[SELECT docId, aspectRatio FROM aspectRatio]],
				throttleTime = 2,
			}), "Please use queryThenDebounce instead of setting throttleTime directly" )

	-- Test Returns the correct initial values and remains open for updates
	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	local aspectRatios = rxTest:observe( indexStore:queryThenDebounce(0.2, {
		sql = [[SELECT docId, aspectRatio FROM aspectRatio
					ORDER BY aspectRatio DESC]] }) )


	aspectRatios:expectData { { 1, 1.5 } }

	-- Test Returns the correct values on DocStoreWrite
	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	aspectRatios:expectDataWithinTime( 0.3, { { 1, 1.50 } } )

	-- Cleanup
	aspectRatios:dispose()
end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestSingleColumnQueryRx( rxTest, createIndexStoreFn )
	rxTest:setTimeLimit( 10 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local aspectRatios = rxTest:observe( indexStore:query {
		sql = [[SELECT docId FROM aspectRatio
					ORDER BY aspectRatio DESC]],
		singleColumnQuery = true } )

	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	aspectRatios:expectData { 1 }

	local keywords = rxTest:observe( indexStore:query {
		sql = [[SELECT name FROM keyword]],
		singleColumnQuery = true
	} )

	keywords:expectData {}

	aspectRatios:dispose()

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = 'af21cdb6d2fa4d1c8099d08a6ba1d7c1',
			height = 130,
			width = 100,
			keyword = 'mushroom',
		},

		':ignore:' )

	keywords:expectData { 'mushroom' }

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = 'bf21cdb6d2fa4d1c8099d08a6ba1d7c1',
			height = 120,
			width = 100,
			keyword = 'egg',
		},

		':ignore:' )

	keywords:expectData { 'mushroom', 'egg' }

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = 'cf21cdb6d2fa4d1c8099d08a6ba1d7c1',
			height = 140,
			width = 100,
			keyword = 'pepper',
		},

		':ignore:' )

	keywords:expectData { 'mushroom', 'egg', 'pepper' }

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestQueryNoChangeNoUpdate_Boolean( rxTest, createIndexStoreFn )
	-- WF-809 is a bug where index store tables with a boolean in them would
	-- fail the comparison because the emit fn sends a boolean, but the current
	-- row in the database is stored with a 1/0 since SQLite does not have a
	-- boolean type. This would result in the index store table being updated
	-- with the same data as well as any queries on that table refiring, even
	-- though they wouldn't get any new data in the results.
	rxTest:setTimeLimit( 10 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local timesFired = 0
	local boolResults = rxTest:observe( indexStore:query( {
		sql = [[SELECT julianday( 'now' ), string, number, bool, nil FROM simple_datatypes]] } )
		:map( function( rows )
			for _, row in ipairs( rows ) do
				-- strip out the now() value, it's just there to ensure the results
				-- are always different
				table.remove( row, 1 )
			end
			timesFired = timesFired + 1
			return rows
		end ) )

	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			abool = true,
			astring = "foo",
			anumber = 4.28
		},

		':ignore:' )

	boolResults:expectData { { "foo", 4.28, 1, nil } }

	rxTest:expectEqual( 1, timesFired )

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = randomDocId,
			abool = true,
			astring = "foo",
			anumber = 4.28,
			change = 'bar'
		},

		':ignore:' )

	-- we have to sleep here to give the index store time to index and possibly re-fire the query
	-- (it should not, but we need to give it enough time that we're confident it didn't)
	rxTest:sleep( 1 )

	boolResults:expectDataWithinTime( 1, { { "foo", 4.28, 1, nil } } )
	rxTest:expectEqual( 1, timesFired )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestQueryNoChangeNoUpdate( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 5 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local timesFired = 0

	local aspectRatios = rxTest:observe( indexStore:query(
		{
			sql = [[SELECT docId, aspectRatio FROM aspectRatio
						ORDER BY aspectRatio DESC]]
		} )
		
		:tap( function( rows )
			timesFired = timesFired + 1
		end ) )

	rxTest:waitFor(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		} )

	aspectRatios:expectData { { 1, 1.5 } }

	rxTest:expectEqual( 1, timesFired )

	rxTest:waitFor(

		docStore:applyOverlay {
			_id = randomDocId,
			foo = 'bar',
		} )

	-- we have to sleep here to give the index store time to index and possibly re-fire the query
	-- (it should not, but we need to give it enough time that we're confident it didn't)
	rxTest:sleep( 0.5 )

	rxTest:expectEqual( 1, timesFired )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestQueryNoChangeNoUpdate_testPerformance( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 300 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local timesFired = 0

	local ongoingQuery = rxTest:observe( indexStore:query(
		{
			sql = [[SELECT docId, string, number, bool FROM simple_datatypes]],
			calm = 0.001
		} )
		
		:map( function( rows )
			timesFired = timesFired + 1
			rows.timesFired = timesFired
			return rows
		end ) )

	local batchOfDocs = {}
	local expectedData = {}
	local docIds = {}
	
	local aNumber = 123456789
	local aString = 'Here is a sample string. It is not very long. But it is also not very short.'
	
	local totalDocs = 100
	
	for i = 1, totalDocs do
	
		local docId = KSIdGenerator.uuidInLowercaseHex()
	
		batchOfDocs[ #batchOfDocs + 1 ] = {
			_id = docId,
			astring = aString,
			anumber = aNumber,
			abool = true
		}
		
		expectedData[ #expectedData + 1 ] = { i, aString, aNumber, 1 }
		
		docIds[ #docIds + 1 ] = docId
		
	end
	
	expectedData.timesFired = 1
	
	rxTest:waitFor( docStore:bulkSetDocValue( batchOfDocs ) )

	ongoingQuery:expectData( expectedData )

	rxTest:expectEqual( 1, timesFired )

	rxTest:measurePerformance( function()
	
		local overlayCount = 100
	
		for i = 1, overlayCount do
		
			local newAString = string.format( "Here is a string: %s", KSIdGenerator.uuidInLowercaseHex() )
		
			local overlay = {
				astring = newAString
			}
			
			expectedData = {
				timesFired = i + 1
			}
			
			for j = 1, totalDocs do
				expectedData[ #expectedData + 1 ] = { j, newAString, aNumber, 1 }
			end
			
			rxTest:waitFor( docStore:applyOverlayToIds( overlay, docIds ) )
			
			ongoingQuery:expectDataWithinTime( 10, expectedData )
			
		end
	
	end )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestQueryCalming( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 10 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local aspectRatios = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, aspectRatio FROM aspectRatio
					ORDER BY aspectRatio DESC]],
		calm = 5,
		} )

	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	aspectRatios:expectDataWithinTime( 2, { { 1, 1.5 } } )

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = randomDocId,
			height = 200,
			width = 100,
		},

		':ignore:' )

	aspectRatios:expectDataWithinTime( 10, { { 1, 2 } } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestSimpleQueryOnceRx( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 10 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	rxTest:expectSingleValue(

		indexStore:queryOnce {
			sql = [[SELECT docId, aspectRatio FROM aspectRatio
						ORDER BY aspectRatio DESC]] ,
		},

		{ { 1, 1.5 } } )

	rxTest:expectSingleValue(

		indexStore:queryOnce {
			sql = [[SELECT name FROM keyword]],
		},

		{} )

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = 'af21cdb6d2fa4d1c8099d08a6ba1d7c1',
			height = 130,
			width = 100,
			keyword = 'mushroom',
		},

		':ignore:' )

	rxTest:expectSingleValue(

		indexStore:queryOnce {
			sql = [[SELECT name FROM keyword]],
		},

		{ { 'mushroom' } } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestSingleColumnQueryOnceRx( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 10 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	rxTest:expectSingleValue(

		indexStore:queryOnce {
			sql = [[SELECT docId FROM aspectRatio
						ORDER BY aspectRatio DESC]],
			singleColumnQuery = true
		},

		{ 1 } )

	rxTest:expectSingleValue(

		indexStore:queryOnce {
			sql = [[SELECT name FROM keyword]],
			singleColumnQuery = true
		},

		{} )

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = 'af21cdb6d2fa4d1c8099d08a6ba1d7c1',
			height = 130,
			width = 100,
			keyword = 'mushroom',
		},

		':ignore:' )

	rxTest:expectSingleValue(

		indexStore:queryOnce {
			sql = [[SELECT name FROM keyword]],
			singleColumnQuery = true
		},

		{ 'mushroom' } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestInvalidArguments( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:expectError( function()
		indexStore:query {
			sql = [[SELECT docId, aspectRatio FROM aspectRatio]],
			enqueue = 'invalid',
		}
	end )

	rxTest:expectError( function()
		indexStore:query {
			sql = [[SELECT docId, aspectRatio FROM aspectRatio]],
			quiesceUntilUpToDate = 'invalid',
		}
	end )

	rxTest:expectError( function()
		indexStore:query {
			sql = [[SELECT docId, aspectRatio FROM aspectRatio]],
			encoder = {},
		}
	end )

	rxTest:expectError( function()
		indexStore:query {
			sql = [[SELECT docId, aspectRatio FROM aspectRatio]],
			tempIds = false,
		}
	end )

	local query = rxTest:observe( indexStore:query {
			sql = [[SELECT 1]],
		} )

	query:expectError( ":ignore:" )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestSizeOfIndexQueue( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 10 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local sizeOfIndexQueue = rxTest:observe( indexStore:sizeOfIndexQueue() )

	sizeOfIndexQueue:expectDataWithinTime( 5, 0, 0 )

	rxTest:waitFor(
		docStore:setDocValue {
				_id = randomDocId,
				height = 150,
				width = 100,
			} )

	local queryObserver = rxTest:observe( indexStore:query {
		sql = [[SELECT 1 FROM aspectRatio;]],
		quiesceUntilUpToDate = true
	} )

	sizeOfIndexQueue:expectDataWithinTime( 5, 1, 1 )

	queryObserver:expectDataWithinTime{ 5, { 1 } }

	sizeOfIndexQueue:expectDataWithinTime( 5, 1, 1 )

end

--------------------------------------------------------------------------------

local function prepareIndexStore( rxTest, indexStore, useMultiLineTable )

	useMultiLineTable = useMultiLineTable or false

	local size = indexStore:sizeOfIndexQueue()
	local docResult
	local query

	if useMultiLineTable then
		docResult = docStore:setDocValue {
			_id = randomDocId,
			keywords = {
				foo = true,
				bar = true,
			}
		}

		query = indexStore:query {
			sql = [[SELECT count(*) FROM keywords]]
		}
	else
		docResult = docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		}

		query = indexStore:query {
			sql = [[SELECT count(*) FROM aspectRatio;]]
		}
	end

	local bringingUpToDate = true

	local bringUpToDate = Observable.combineLatest( docResult, query, size )
		:filter ( function( doc, query, count, size )
			return doc and query and count == size
		end )
		:map( function( doc, query, count, size )
			-- this does setup very similar to rxTestSizeOfIndexQueue, but it is
			-- important not to attach any validations involving observables from
			-- the indexStore to the rxTest or the rxTest will keep them alive
			-- and prevent the indexStore from being closed and re-opened cleanly
			return assert( count == 1 and size == 1 )
		end )
		:tap( function()
			bringingUpToDate = false
		end )

	local unsubscribe = bringUpToDate:subscribe()

	while bringingUpToDate do
		rxTest:sleep( 0.01 )
	end

	unsubscribe:dispose()

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestGetReindexingProgress( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 5 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestKeywords.lua' )

	-- the first time we just get completed, but this should ensure that the doc store/index store
	-- has initialized itself enough so that when we recreate it later in the test, it will actually
	-- hit reindexing
	rxTest:expectOnlyCompleted( indexStore:getReindexingProgress() )

	rxTest:expectOnlyCompleted( indexStore:haltAndDelete() )

	-- add a bunch of documents so that we have something to index
	local docs = {}
	for i = 1, 500 do
		table.insert( docs, {
			_id = KSIdGenerator.uuidInLowercaseHex(),
			keywords = {
				foo = true,
				bar = true,
			}
		} )
		table.insert( docs, {
			_id = KSIdGenerator.uuidInLowercaseHex(),
			height = 150,
			width = 100,
		} )
	end

	docStore:bulkSetDocValue( docs )

	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	-- we can't reliably track intermediate reindexing steps, but we can watch for the
	-- last reindexing step
	local reindexingProgress = rxTest:observe( indexStore:getReindexingProgress()
		:takeLast( 1 ) )

	reindexingProgress:expectData( {
		description = "reindexing tables",
		errorCount = 0,
		percentComplete = 1,
		processedCount = 11238.75,
		totalCount = 11238.75,
	} )

	-- once we've hit 100%, verify that the index queue says we've indexed all 1000 docs.
	local sizeOfIndexQueue = rxTest:observe( indexStore:sizeOfIndexQueue() )

	sizeOfIndexQueue:expectData( 1000, 1000 )
end


--------------------------------------------------------------------------------

-- FIXME [jandrew 2025-03-11]: Disabled due to instability.

function WFRxIndexStoreTestFunctions.DISABLED_rxTestReindexingWithConcurrentUpdates( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 50 )

	docStore = createDocStore( rxTest )
	local docStorePath = string.gsub( docStore:path(), '.mcat', '' )

	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestKeywords.lua' )
	prepareIndexStore( rxTest, indexStore, true )

	rxTest:expectOnlyCompleted( indexStore:halt() )

	-- distort with brute force the map function hash to simulate a map function change in 'RxTestKeywords.lua'

	local db = WFSQLiteDatabase( docStorePath .. '.wfindex', WFSQLiteDatabaseSchema( "1.0" ) )
	db:execOnce( [[UPDATE indexStatus SET mapFunctionHash = 'this_is_not_a_hash' WHERE indexTableName = 'keywords' ]] )
	db:close()
	db = nil

	-- schedule a change on the docStore and then fire a query

	rxTest:expectSingleValue(

	docStore:applyOverlay {
			_id = randomDocId,
			keywords = {
				bar = {
					_delete = true
				}
			}
		},

	':ignore:' )

	-- create new indexStore. Should blow away the keywords table

	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestKeywords.lua' )

	local query = rxTest:observe( indexStore:query {
		sql = [[SELECT count(*) FROM keywords;]],
		quiesceUntilUpToDate = true

	} )

	query:expectData{ { 1 } } -- only one line should survive

	query = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, keyword FROM keywords]],

	} )

	query:expectData{ { randomDocId, 'foo' } }

	rxTest:expectOnlyCompleted( indexStore:haltAndDelete() )
	indexStore = nil

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestIndexStoreDetectsIndexingError( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = randomDocId,
			keyword = '< generate error >',
		},

		':ignore:' )

	rxTest:observe( indexStore:query {
		sql = [[SELECT * FROM keyword;]]
	}:catch( function( errMsg )
		if errMsg:find( "error indexing table 'keyword': " ) then
			error( "recognized error" )
		else
			error( "unexpected error" )
		end
	end ) ):expectError( "recognized error" )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestIndexStoreErrorOnlyImpactsRelevantTables( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:setTimeLimit( 5 )

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = randomDocId,
			keyword = '< generate error >',
			height = 150,
			width = 100,
		},

		':ignore:' )

	rxTest:observe( indexStore:query {
		sql = [[SELECT * FROM keyword;]]
	} ):expectError( ":ignore:" )


	local safeQuery = indexStore:query {
		sql = [[SELECT aspectRatio FROM aspectRatio;]]
	}

	rxTest:observe( safeQuery ):expectDataWithinTime( 1, { { 1.5 } } )

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = randomDocId,
			keyword = '< generate error >',
			height = 200,
			width = 100,
		},

		':ignore:' )

	rxTest:sleep( 0.5 )

	rxTest:observe( indexStore:query {
		sql = [[SELECT * FROM keyword;]]
	} ):expectError( ":ignore:" )


	rxTest:observe( safeQuery ):expectDataWithinTime( 1, { { 2.0 } } )

	-- make another update after the erroring document to ensure that the error
	-- doesn't prevent subsequent documents from failing
	rxTest:expectSingleValue(
			docStore:applyOverlay {
				_id = randomDocId,
				height = 300,
				width = 100,
			},

			':ignore:' )

	rxTest:observe( indexStore:query {
		sql = [[SELECT * FROM keyword;]]
	} ):expectError( ":ignore:" )

	rxTest:observe( safeQuery ):expectDataWithinTime( 1, { { 3.0 } } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestDocValueFilter( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local filteredIndexResults = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, count FROM doc_value_filter]] } )

	local docId1, docId2 = KSIdGenerator.uuidInLowercaseHex(), KSIdGenerator.uuidInLowercaseHex()
	rxTest:expectSingleValue(
		docStore:setDocValue {
			_id = docId1,
			type = 'ok_docvalue_type',
			height = 150,
			width = 100,
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3,  { { 1, 1 } } )

	-- docValue that doesn't meet filter criteria shouldn't hit map fn
	rxTest:expectSingleValue(
		docStore:setDocValue {
			_id = docId2,
			type = 'not_ok_docvalue_type',
			height = 150,
			width = 100,
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3,  { { 1, 1 } } )

	-- docValue that does meet filter criteria should bump map fn counter again
	rxTest:expectSingleValue(
		docStore:applyOverlay {
			_id = docId1,
			height = 177,
			width = 100,
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3,  { { 1, 2 } } )

	-- deletes - no matter the filter criteria, should always go to the table
	-- this allows the table to clean up any entries that existed from before
	-- deletion when the doc did match the filter
	rxTest:expectSingleValue(
		docStore:applyOverlay {
			_id = docId2,
			_deleted = true
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3, { { 1, 2 }, { 2, 3 } } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestRelevantTypeSubtypeTuplesFiltering( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 10 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local filteredIndexResults = rxTest:observe( indexStore:query {
			sql = [[SELECT docId, count FROM asset_only_type_subtype_filter]],
			quiesceUntilUpToDate = true
		} )

	local docId1, docId2 = KSIdGenerator.uuidInLowercaseHex(), KSIdGenerator.uuidInLowercaseHex()
	
	rxTest:waitFor(
		docStore:setDocValue {
			_id = docId1,
			type = 'asset',
			height = 150,
			width = 100 } )

	filteredIndexResults:expectDataWithinTime( 5,  { { 1, 1 } } )

	-- docValue that doesn't meet filter criteria shouldn't hit map fn
	rxTest:waitFor(
		docStore:setDocValue {
			_id = docId2,
			type = 'album',
			height = 150,
			width = 100 } )

	filteredIndexResults:expectDataWithinTime( 5,  { { 1, 1 } } )

	-- docValue that does meet filter criteria should bump map fn counter again
	rxTest:waitFor(
		docStore:applyOverlay {
			_id = docId1,
			height = 177,
			width = 100 } )

	filteredIndexResults:expectDataWithinTime( 5,  { { 1, 2 } } )

	-- deletes - no matter the filter criteria, should always go to the table
	-- this allows the table to clean up any entries that existed from before
	-- deletion when the doc did match the filter
	rxTest:waitFor(
		docStore:applyOverlay {
			_id = docId2,
			_deleted = true } )
			
	filteredIndexResults:expectDataWithinTime( 5, { { 1, 2 }, { 2, 3 } } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestIncludeCatalogDocuments( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local filteredIndexResults = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, name FROM catalog_docs]] } )

	-- Standard Catalog documents should be indexed.

	local docId1, docId2 = KSIdGenerator.uuidInLowercaseHex(), KSIdGenerator.uuidInLowercaseHex()
	rxTest:expectSingleValue(
		docStore:setDocValue {
			_id = docId1,
			type = 'ok_docvalue_type',
			keyword = 'Aang',
			height = 150,
			width = 100,
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3,  { { 1, 'Aang' } } )

	-- Space-feed documents should be ignored.

	rxTest:expectSingleValue(
		docStore:setDocValue {
			_id = docId2,
			type = 'not_ok_docvalue_type',
			keyword = 'Bumi',
			height = 150,
			width = 100,
			_spaceId = KSIdGenerator.uuidInLowercaseHex()
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3,  { { 1, 'Aang' } } )

	-- Modify the catalog document.

	rxTest:expectSingleValue(
		docStore:applyOverlay {
			_id = docId1,
			height = 177,
			width = 100,
			keyword = 'Katara',
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3,  { { 1, 'Katara' } } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestIncludeSpaceFeedDocuments( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local filteredIndexResults = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, name FROM space_docs]] } )

	-- Space-feed documents should be indexed.

	local docId1, docId2 = KSIdGenerator.uuidInLowercaseHex(), KSIdGenerator.uuidInLowercaseHex()
	rxTest:expectSingleValue(
		docStore:setDocValue {
			_id = docId1,
			_spaceId = KSIdGenerator.uuidInLowercaseHex(),
			type = 'ok_docvalue_type',
			keyword = 'Aang',
			height = 150,
			width = 100,
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3,  { { 1, 'Aang' } } )

	-- Catalog documents should be ignored.

	rxTest:expectSingleValue(
		docStore:setDocValue {
			_id = docId2,
			type = 'not_ok_docvalue_type',
			keyword = 'Bumi',
			height = 150,
			width = 100,
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3,  { { 1, 'Aang' } } )

	-- Modify the space-feed document.

	rxTest:expectSingleValue(
		docStore:applyOverlay {
			_id = docId1,
			height = 177,
			width = 100,
			keyword = 'Katara',
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3,  { { 1, 'Katara' } } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestIncludeCatalogAndSpaceFeedDocuments( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local filteredIndexResults = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, name FROM space_and_catalog_docs]] } )

	-- Space-feed documents should be indexed.

	local docId1, docId2 = KSIdGenerator.uuidInLowercaseHex(), KSIdGenerator.uuidInLowercaseHex()
	rxTest:expectSingleValue(
		docStore:setDocValue {
			_id = docId1,
			_spaceId = KSIdGenerator.uuidInLowercaseHex(),
			type = 'ok_docvalue_type',
			keyword = 'Aang',
			height = 150,
			width = 100,
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3, { { 1, 'Aang' } } )

	-- Catalog documents should also be indexed.

	rxTest:expectSingleValue(
		docStore:setDocValue {
			_id = docId2,
			type = 'not_ok_docvalue_type',
			keyword = 'Bumi',
			height = 150,
			width = 100,
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3,  { { 1, 'Aang' }, { 2, 'Bumi' } } )

	-- Modify the space-feed document.

	rxTest:expectSingleValue(
		docStore:applyOverlay {
			_id = docId1,
			height = 177,
			width = 100,
			keyword = 'Katara',
		}, ':ignore:' )

	filteredIndexResults:expectDataWithinTime( 3,  { { 2, 'Bumi' }, { 1, 'Katara' } } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestRxReindexSingleDoc( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local aspectRatios = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, aspectRatio FROM aspectRatio
					ORDER BY aspectRatio DESC]] } )

	rxTest:expectSingleValue(

		docStore:setDocValue {
				_id = randomDocId,
				height = 150,
				width = 100,
			},

		':ignore:' )

	aspectRatios:expectData { { 1, 1.5 } }

	rxTest:expectSingleValue(

			docStore:applyOverlay {
				_id = randomDocId,
				height = 177,
				width = 100,
			},

		':ignore:' )

	aspectRatios:expectData { { 1, 1.77 } }

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestRxIndexRecordsForDeletedDocsGetDeleted( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local aspectRatios = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, aspectRatio FROM aspectRatio
					ORDER BY aspectRatio DESC]] } )

	local lastRev

	rxTest:expectSingleValue(

		docStore:setDocValue {
				_id = randomDocId,
				height = 150,
				width = 100,
			}
		:tap( function( revId )
			lastRev = revId
		end ),

		':ignore:' )

	aspectRatios:expectData { { 1, 1.5 } }

	rxTest:expectSingleValue(

		docStore:setDocValue {
				_id = randomDocId,
				_revId = lastRev,
			}
		:tap( function( revId )
			lastRev = revId
		end ),

		':ignore:' )

	aspectRatios:expectData { }

	rxTest:expectSingleValue(

		docStore:setDocValue {
				_id = randomDocId,
				_revId = lastRev,
				height = 150,
				width = 100,
			},

		':ignore:' )

	aspectRatios:expectData { { 1, 1.5 } }

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestRxMultipleResultsPerDoc( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestKeywords.lua' )

	local keywords = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, keyword FROM keywords
					ORDER BY docId, keyword]] } )

	rxTest:expectSingleValue(

		docStore:setDocValue {
				_id = randomDocId,
				keywords = {
					dog = true,
					cat = true,
				},
			},

		':ignore:' )

	keywords:expectData {
			{ randomDocId, "cat" },
			{ randomDocId, "dog" },
		}

	rxTest:expectSingleValue(

		docStore:applyOverlay {
				_id = randomDocId,
				keywords = {
					_replace = true,
					dog = true,
					bat = true,
				},
			},

		':ignore:' )

	keywords:expectData {
			{ randomDocId, "bat" },
			{ randomDocId, "dog" },
		}

	rxTest:expectSingleValue(

		docStore:applyOverlay {
				_id = randomDocId,
				keywords = {
					_delete = true,
				},
			},

		':ignore:' )

	keywords:expectData {
		}

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestRxDoesntSendRedundantUpdates( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local updateCount = 0

	local aspectRatios = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, aspectRatio FROM aspectRatio]]
	}:tap(function()
		updateCount = updateCount + 1
		return updateCount
	end))

	local keywords = rxTest:observe( indexStore:query {
		sql = [[SELECT name FROM keyword]]
	})

	rxTest:expectSingleValue(

		docStore:setDocValue {
				_id = randomDocId,
				height = 150,
				width = 100,
			},

		':ignore:' )

	aspectRatios:expectData { { 1, 1.5 } }

	assert( updateCount == 1, 'Extra updates?' )

	keywords:expectData {}

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = randomDocId,
			keyword = 'dog',
		},

		':ignore:' )

	keywords:expectData { { "dog" } }

	-- if enough time elapsed that the keyword index updated, we can assume
	-- that a faulty extra notification on aspectRatio would also have arrived

	assert( updateCount == 1, 'Redundant update notification sent!' )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestTempIds( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local function q( ... )
		return rxTest:observe( indexStore:query {
			sql = [[SELECT aspectRatio.docId, aspectRatio FROM aspectRatio
						JOIN TEMP_ids ON aspectRatio.docId = TEMP_ids.docId
						ORDER BY aspectRatio DESC]],
			tempIds = { ... },
		} )
	end

	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	q( 1 ):expectDataWithinTime( 1, { { 1, 1.5 } } )

	q( 1,
		'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
		'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
		'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
		'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'
		):expectDataWithinTime( 1, { { 1, 1.5 } } )

	q( 1,
		'b', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
		'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
		'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
		'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'
		):expectDataWithinTime( { { 1, 1.5 } } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestRebuildsForDifferentDocStore( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 60 )

	local function q( ... )
		return rxTest:observe( indexStore:queryOnce {
			sql = [[SELECT aspectRatio.docId, aspectRatio FROM aspectRatio
						ORDER BY aspectRatio DESC]],
		} )
	end

	local docStorePath = KSPathUtils.getStandardFilePath( "temp" )
	docStorePath = KSPathUtils.child( docStorePath, rxTest:testName() )
	docStorePath = WFFileUtils.ensureFileNameIsUnique( docStorePath )

	docStore = WFRxDocStore( docStorePath )

	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
			first = true,
		},

		':ignore:' )

	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	q( randomDocId ):expectData{ { 1, 1.5 } }

	rxTest:expectOnlyCompleted( indexStore:halt() )
	local docStorePath2 = KSPathUtils.parent( docStorePath )
	docStorePath2 = KSPathUtils.child( docStorePath2, KSIdGenerator.uuidInLowercaseHex() )
	rxTest:expectOnlyCompleted( docStore:haltAndDelete() )

	local indexStorePath = docStorePath:gsub( '%.[^./\\]*$', '' ) .. '.wfindex'
	local indexStorePath2 = docStorePath2:gsub( '%.[^./\\]*$', '' ) .. '.wfindex'
	local timeLimit = 2
		-- wait up to 2 s
	local pollInterval = 0.1
	local numberOfTries = math.ceil( timeLimit / pollInterval )

	while true do

		if WFFileUtils.exists( indexStorePath2 ) then
			pcall( WFFileUtils.delete, indexStorePath2 )
		end

		if pcall( WFFileUtils.move, indexStorePath, indexStorePath2 ) then

			break

		else
			if numberOfTries < 1 then

				error( "Tried but could not move file from "
					.. indexStorePath
					.. " to "
					.. indexStorePath2 )

			end
			rxTest:sleep( pollInterval )

		end

		numberOfTries = numberOfTries - 1

	end

	docStore = WFRxDocStore( docStorePath2 )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId2,
			height = 200,
			width = 100,
			second = true,
		},

		':ignore:' )

	q( randomDocId ):expectData{ { 1, 2.0 } }

	rxTest:expectOnlyCompleted( indexStore:halt() )
	indexStore = nil
end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestBooleanLogic( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	for i = 1, 100 do

		rxTest:expectSingleValue(

			docStore:setDocValue {
				_id = string.format( "000000000000000000000000%08x", i ),
				height = 100 * i,
				width = 100,
			},

			':ignore:' )
	end

	local query = rxTest:observe( indexStore:querySetLogic {
		operator = 'or',
		{
			sql = 'SELECT docId FROM aspectRatio WHERE aspectRatio <= ?',
			5
		},
		{
			sql = 'SELECT docId FROM aspectRatio WHERE aspectRatio > ?',
			95
		},
	}
	:map( function( setObject )
		return setObject:toArray()
	end ) )
	query:expectDataWithinTime(1, {
		1, 2, 3, 4, 5, 96, 97, 98, 99, 100
	} )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestVirtualTableForSetQuery( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 30 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	for i = 1, 100 do

		rxTest:expectSingleValue(

			docStore:setDocValue {
				_id = string.format( "000000000000000000000000%08x", i ),
				height = ( 200 - i ) * i,
				width = 200 - i,
			},

			':ignore:' )
	end

	local virtualTable = indexStore:newVirtualTableForSetQuery {
		operator = 'or',
		{
			sql = 'SELECT docId FROM aspectRatio WHERE aspectRatio <= ?',
			3
		},
		{
			sql = 'SELECT docId FROM aspectRatio WHERE aspectRatio > ?',
			97
		},
	}

	local query = virtualTable:selectMany( function( tableName )
		return indexStore:query {
			sql = 'SELECT docId FROM temp.ResultSetVirtualTable_1 AS vt JOIN aspectRatio ON aspectRatio.docId == vt.id ORDER BY aspectRatio DESC'
		}
	end )
	local tableName = rxTest:observe( virtualTable )

	tableName:expectDataWithinTime( 1, 'temp.ResultSetVirtualTable_1', 1 )

	rxTest:observe( query )
	:expectData( {
		{ 100, }, { 99 }, { 98, }, { 3, }, { 2, }, { 1 }
	} )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestEncoder( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local aspectRatios = rxTest:observe( indexStore:query {
		sql = [[SELECT docId, aspectRatio, 'aaaabbbbaaaabbbbaaaabbbbaaaabbbb', 0, '2014-05-19T17:29:00.00Z', 1, NULL FROM aspectRatio
					ORDER BY aspectRatio DESC]],
		encoder = 'WFOzAssetInfo.encodeSql',
	}:map( function( result )
		local firstResult = result.photos:getAsset( 1 )
		return result.count, firstResult.aspectRatio, firstResult.thumbnailMD5
	end ) )

	rxTest:expectSingleValue(

		docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	aspectRatios:expectData( 1, 1.5, 'aaaabbbbaaaabbbbaaaabbbbaaaabbbb' )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestNMB886Regression( rxTest, createIndexStoreFn )

	-- The docStore/indexStore issue identified by NMB-886 was that the map server was requiring that,
	-- while indexing docs, the continueFrom value to track processing was less than the last doc's sequence #.
	-- The problem with this is that, when processing is fully complete the continueFrom value will actually
	-- be greater than the last value. This meant that if the continueFrom ends up equal to the last doc's
	-- sequence we would incorrectly stop rather than continuing to process the last document. So this test
	-- tries to test that condition - when we have exactly one document left to index, do we process it
	-- correctly?
	-- To test this we need to rely on the batch size and we will create batch size + 1 documents. When working
	-- correctly, the first batch should be indexed and then we should another batch of a single document. Before
	-- the fix to NMB-886 we would miss indexing the last document.
	rxTest:setTimeLimit( 10 )

	local hundredAndOneDocs = {}
	local i = 1
	while ( i <= 101 ) do
		i = i + 1
		local newDoc = {
			_id = KSIdGenerator.uuidInLowercaseHex(),
			height = 200,
			width = 100,
		}
		table.insert( hundredAndOneDocs, newDoc )
	end

	docStore = createDocStore( rxTest )
	docStore:bulkSetDocValue( hundredAndOneDocs )

	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:observe(
		indexStore:query( {
			sql = [[ SELECT COUNT( docId ) FROM aspectRatio ]]
		} )
		:map( function( result )
			local count = KSTableUtils.derefOpt( result, 1, 1 ) or 0
			return count
		end )
	):expectDataWithinTime( 8, 101 )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestHalt( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	local indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:expectOnlyCompleted( indexStore:halt() )

	rxTest:expectOnlyError( indexStore:query {
		sql = [[SELECT docId, aspectRatio FROM aspectRatio]]
	}, ':ignore:' )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestFaultyIndexing( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:expectSingleValue( docStore:applyOverlay {
		_id = KSIdGenerator.uuidInLowercaseHex(),
		type = 'badIndexing',
	}, ':ignore:' )

	rxTest:expectOnlyError( indexStore:query {
		sql = [[SELECT docId FROM bad_indexing]],
		quiesceUntilUpToDate = true,
	}, ':ignore:' )

end

--------------------------------------------------------------------------------

-- @mark Manual Table Tests

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestManualTableQuery( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 20 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local localDocId = 1

	local observer = rxTest
	:observe( indexStore:query{
		sql = [[SELECT aspectRatio
				FROM aspectRatio LEFT JOIN TEMP_search
				ON aspectRatio.docId == TEMP_search.docId
				ORDER BY TEMP_search.sortOrder]],
	})

	observer:expectData( {} )

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	observer:expectData( {  { 1.5 } } )

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = randomDocId,
			height = 200,
			width = 100,
		},

		':ignore:' )

	observer:expectData( { { 2.0 } } )

	local aspectRatio = rxTest
	:observe( indexStore:query{
		sql = [[SELECT aspectRatio
				FROM aspectRatio INNER JOIN TEMP_search
				ON aspectRatio.docId == TEMP_search.docId
				ORDER BY TEMP_search.sortOrder]],
	})

	aspectRatio:expectData( {} )

	rxTest:expectSingleValue(

		indexStore:bulkInsertManualTable( 'TEMP_search', {{ localDocId, 2 }} )

		, ':ignore:' )

	aspectRatio:expectData( { { 2.0 } } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestManualTableQueryWithQuiesceUntilUpToDate( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 20 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local observer = rxTest
	:observe( indexStore:query{
		sql = [[SELECT aspectRatio
				FROM aspectRatio LEFT JOIN TEMP_search
				ON aspectRatio.docId == TEMP_search.docId
				ORDER BY TEMP_search.sortOrder]],
		quiesceUntilUpToDate = true
	})

	observer:expectData( {} )

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = randomDocId,
			height = 150,
			width = 100,
		},

		':ignore:' )

	observer:expectData( {  { 1.5 } } )

	rxTest:expectSingleValue(

		docStore:applyOverlay {
			_id = randomDocId,
			height = 200,
			width = 100,
		},

		':ignore:' )

	observer:expectData( { { 2.0 } } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestBulkInsertManualTableName( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local assetId1 = KSIdGenerator.uuidInLowercaseHex()
	local assetId2 = KSIdGenerator.uuidInLowercaseHex()
	local assetId3 = KSIdGenerator.uuidInLowercaseHex()
	local assetId4 = KSIdGenerator.uuidInLowercaseHex()

	local result = rxTest
	:observe( indexStore:query{
		sql = [[SELECT docId, sortOrder
				FROM TEMP_search
				ORDER BY sortOrder]]
	})


	rxTest:expectSingleValue(

		indexStore:bulkInsertManualTable( 'TEMP_search', {{ assetId1, 1 }} )

		, ':ignore:' )

	result:expectData { { assetId1, 1 } }


	rxTest:expectSingleValue(

		indexStore:bulkInsertManualTable( 'TEMP_search', {{ assetId2, 2 }} )

		, ':ignore:' )

	result:expectData {
		{ assetId1, 1 },
		{ assetId2, 2 },
	}


	rxTest:expectSingleValue(

		indexStore:bulkInsertManualTable( 'TEMP_search', {{ assetId3, 3 }} )

		, ':ignore:' )

	result:expectData {
		{ assetId1, 1 },
		{ assetId2, 2 },
		{ assetId3, 3 },
	}


	rxTest:expectSingleValue(

		indexStore:bulkInsertManualTable( 'TEMP_search', {{ assetId4, 4 }} )

		, ':ignore:' )

	result:expectData {
		{ assetId1, 1 },
		{ assetId2, 2 },
		{ assetId3, 3 },
		{ assetId4, 4 },
	}

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestClearManualTableName( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	local assetId1 = KSIdGenerator.uuidInLowercaseHex()

	local result = rxTest
	:observe( indexStore:query{
		sql = [[SELECT docId, sortOrder
				FROM TEMP_search
				ORDER BY sortOrder]]
	})


	rxTest:expectSingleValue(

		indexStore:bulkInsertManualTable( 'TEMP_search', {{ assetId1, 1 }} )

		, ':ignore:' )

	result:expectData { { assetId1, 1 } }


	rxTest:expectSingleValue(

		indexStore:clearManualTable( 'TEMP_search' )

		, ':ignore:' )

	result:expectData { }

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestInvalidManualTableName( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:expectOnlyError( indexStore:bulkInsertManualTable( 'TEST_wrong', {{ 'invalid' }} ), ':ignore:' )

	rxTest:expectOnlyError( indexStore:clearManualTable( 'TEST_wrong' ), ':ignore:' )

	rxTest:expectSingleValue(
		indexStore:bulkInsertManualTable( 'TEMP_search', {{ 'invalid' }} ),
		{}
	)

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestInvalidBulkInsertManualTableArguments( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:expectSingleValue( indexStore:bulkInsertManualTable( 'TEMP_search', {{ 'invalid' }} ), {} )

	rxTest:expectError( function()

		indexStore:bulkInsertManualTable( 'TEMP_search', 'invalid' )

	end )

	rxTest:expectError( function()

		indexStore:bulkInsertManualTable( 'TEMP_search', 10 )

	end )

	rxTest:expectError( function()

		indexStore:bulkInsertManualTable( 10, {{ 'invalid' }} )

	end )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestInvalidClearManualTableArguments( rxTest, createIndexStoreFn )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	rxTest:expectError( function()

		indexStore:clearManualTable( 10 )

	end )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.testQueryPWithManyDocuments_testNightly( test )

	docStore = WFRxDocStore( ':memory:' )
	indexStore = WFRxIndexStore( docStore, 'RxTestAspectRatio.lua' )

	return WFAsyncTestCase( test, 12 )

	:waitFor( function()
		return indexStore:queryP {
			sql = [[SELECT docId, aspectRatio FROM aspectRatio
						ORDER BY aspectRatio DESC]],
		}
	end )

	:continueWhenValueMatches( 'data', {} )

	:waitFor( function()

		return Observable.range( 1, 400 )
			:selectMany( function( i )
				return docStore:setDocValue {
					_id = KSIdGenerator.uuidInLowercaseHex(),
					height = 100 + i,
					width = 100,
				}
			end )
		:call( asKSPromise )

	end )

	:continueWhenValueMatches( 'data', ":ignore:" )

	:waitFor( function()
		return indexStore:queryP( [[SELECT max( aspectRatio ) FROM aspectRatio]])
	end )

	:continueWhenValueMatches( 'data', { {5} } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.testSimpleQueryPviaRx( test )

	docStore = WFRxDocStore( ':memory:' )
	indexStore = WFRxIndexStore( docStore, 'RxTestAspectRatio.lua' )

	return WFAsyncTestCase( test )

	:waitFor( function()
		return indexStore:queryP {
			sql = [[SELECT docId, aspectRatio FROM aspectRatio
						ORDER BY aspectRatio DESC]],
		}
	end )

	:continueWhenValueMatches( 'data', {} )

	:waitFor( function()

		return docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		}
		:call( asKSPromise )

	end )

	:continueWhenValueMatches( 'data', ":ignore:" )

	:waitFor( function()
		return indexStore:queryP( [[SELECT docId, aspectRatio FROM aspectRatio]])
	end )

	:continueWhenValueMatches( 'data', { {1, 1.5} } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.testSizeOfIndexQueueB( test )

	docStore = WFRxDocStore( ':memory:' )
	indexStore = WFRxIndexStore( docStore, 'RxTestAspectRatio.lua' )

	local sizeOfIndexQueueB = indexStore:sizeOfIndexQueueB()

	return WFAsyncTestCase( test )

	:waitFor( sizeOfIndexQueueB )

	:continueWhenValueMatches( 0 )

	:waitFor( function()
		sizeOfIndexQueueB:close()
		local KSPromises = require 'KSPromises'
		return KSPromises.resolvedPromiseP( 'data', true )
	end )

	:continueWhenValueMatches( 'data', true )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.testSimpleQueryBviaRx( test )

	docStore = WFRxDocStore( ':memory:' )
	indexStore = WFRxIndexStore( docStore, 'RxTestAspectRatio.lua' )

	local aspectRatiosB = indexStore:queryB {
		sql = [[SELECT docId, aspectRatio FROM aspectRatio
					WHERE aspectRatio > ?
					ORDER BY aspectRatio DESC]],
		cacheKey = 'a',
		1,
	}

	return WFAsyncTestCase( test )

	:waitFor( function()

		return indexStore:queryB [[
			SELECT docId, aspectRatio FROM aspectRatio
		]]
		:closingMapB( function( rows )
			return WFUnknown.isUnknown( rows )
		end )

	end )

	:continueWhenValueMatches( true )

	:waitFor( function()

		return docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		}
		:call( asKSPromise )

	end )

	:continueWhenValueMatches( 'data', ':ignore:' )

	:waitFor( aspectRatiosB )

	:continueWhenValueMatches{ { 1, 1.5 } }

	:waitFor( function()

		return docStore:setDocValue {
			_id = 'af21cdb6d2fa4d1c8099d08a6ba1d7c1',
			height = 130,
			width = 100,
		}
		:call( asKSPromise )

	end )

	:continueWhenValueMatches( 'data', ':ignore:' )

	:waitFor( aspectRatiosB )

	:continueWhenValueMatches( { { 1, 1.5 }, { 2, 1.3 } } )

	:waitFor( function()
		local sawUnknown = false
		return indexStore:queryB {
			sql = [[SELECT docId, aspectRatio FROM aspectRatio
						WHERE aspectRatio > ?
						ORDER BY aspectRatio DESC]],
			cacheKey = 'a',
			1
		}
		:closingMapB( function( rows )
			sawUnknown = sawUnknown or WFUnknown.isUnknown( rows )
			return sawUnknown, rows
		end )
	end )

	:continueWhenValueMatches( false, {
		{ 1, 1.5 },
		{ 2, 1.3 },
	} )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.testDiagnosticLog_testNightly( test )

	docStore = WFRxDocStore( ':memory:' )
	indexStore = WFRxIndexStore( docStore, 'RxTestAspectRatio.lua' )
	local reportP
	local queryB

	return WFAsyncTestCase( test, 45 )

	:waitFor( function()

		return docStore:setDocValue {
			_id = randomDocId,
			height = 150,
			width = 100,
		}
		:call( asKSPromise )

	end )

	:waitFor( function()

		queryB = indexStore:queryB {
			sql = [[SELECT docId, aspectRatio FROM aspectRatio
						WHERE aspectRatio > ?]],
			1
		}
		:closingMapSwitchB( function()
			return indexStore:queryB {
				sql = [[SELECT docId, aspectRatio FROM aspectRatio
							WHERE aspectRatio < ?]],
				2
			}
		end )

		return queryB:relayB()

	end )

	:continueWhenValueMatches( { {1, 1.5} } )

	:waitFor( function()

		local WFDiagnosticLog = require 'WFDiagnosticLog'

		local progressB
		reportP, progressB = WFDiagnosticLog.generateHtmlReportP {

		}
		return progressB
	end )

	:continueWhenValueMatches( 1 )

	:waitFor( function()

		return reportP
		:mapDataP_new( function( report )
			return not not ( string.find( report, "SELECT docId, aspectRatio" ) )
				-- ensure our query from above is reported
		end )

	end )

	:continueWhenValueMatches( 'data', true )

	:waitFor( function()

		return queryB

	end )

	:continueWhenValueMatches( { {1, 1.5} } )

end

--------------------------------------------------------------------------------

for k, v in pairs( WFRxIndexStoreTest ) do
	if string.match( k, "^rxTest" ) then
		WFRxIndexStoreTest[ k ] = function( rxTest )
			rxTest:finally( tearDown )
			v( rxTest )
		end
	elseif string.match( k, "^test" ) then
		WFRxIndexStoreTest[ k ] = function( test )
			v( test )
			:waitFor( function()
				return tearDown()
					:call( asKSPromise )
			end )
			:continueWhenValueMatches( "data", true )
		end
	end
end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestQueryWithCarray( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 10 )

	docStore = createDocStore( rxTest )
	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestAspectRatio.lua' )

	-- Create 10 documents.

	local evenDocumentIds = {}
	local oddDocumentIds = {}

	for i = 1, 10 do

		if i % 2 == 0 then
			evenDocumentIds[ #evenDocumentIds + 1 ] = i
		else
			oddDocumentIds[ #oddDocumentIds + 1 ] = i
		end

		rxTest:waitFor( docStore:setDocValue {
				_id = KSIdGenerator.uuidInLowercaseHex(),
				height = 50,
				width = 100,
			} )

	end

	local evenDocIdAspectRatios = rxTest:observe( indexStore:query {
		sql = [[SELECT docId FROM aspectRatio
					WHERE docId IN CARRAY( ?, ?, 'int32' )
					ORDER BY docId ASC]],
		singleColumnQuery = true,
		evenDocumentIds,
		#evenDocumentIds
	} )

	evenDocIdAspectRatios:expectData { 2, 4, 6, 8, 10 }

	evenDocIdAspectRatios:dispose()

	local oddDocIdAspectRatios = rxTest:observe( indexStore:query {
		sql = [[SELECT docId FROM aspectRatio
					WHERE docId IN CARRAY( ?, ?, 'int32' )
					ORDER BY docId ASC]],
		singleColumnQuery = true,
		oddDocumentIds,
		#oddDocumentIds
	} )

	oddDocIdAspectRatios:expectData { 1, 3, 5, 7, 9 }

	oddDocIdAspectRatios:dispose()

end

--------------------------------------------------------------------------------

if WFPlatformInfo.isAndroid then

	function WFRxIndexStoreTestFunctions.rxTestPlatformIndexStoreDisabled( rxTest, createIndexStoreFn )

		docStore = createDocStore( rxTest )

		-- Init with platform based indexstore disabled
		indexStore = createIndexStoreFn( rxTest, docStore, 'LrmCatalogIndexDefinitions.lua' )

		-- Run a query to make sure init is done
		rxTest:waitFor(
			indexStore:queryOnce( {
				sql = [[SELECT count(*) FROM albums]],
				} ) )

		rxTest:expectEqual( false, indexStore:DEBUG_getPlatformIndexStore() )

	end

	--------------------------------------------------------------------------------

	function WFRxIndexStoreTestFunctions.rxTestPlatformIndexStoreEnabled( rxTest, createIndexStoreFn )

		docStore = createDocStore( rxTest )

		-- Init with platform based indexstore enabled
		indexStore = createIndexStoreFn( rxTest, docStore, 'LrmCatalogIndexDefinitions.lua', true )

		-- Run a query to make sure init is done
		rxTest:waitFor(
			indexStore:queryOnce( {
				sql = [[SELECT count(*) FROM albums]],
				} ) )

		rxTest:expectSingleValue( Rx.Observable.just( indexStore:DEBUG_getPlatformIndexStore() )
			:map( function( val )
				WFArguments.check( 'WFRxPlatformIndexStore' )
				return true
			end ), true )
	end

end

--------------------------------------------------------------------------------

-- @mark Version Schema Reindex Tests

--------------------------------------------------------------------------------

local function createDocsToReindex( rxTest, docStore )

	 --The following creates docs of types/counts:
	-- asset:image 10
	-- asset:video 10
	-- album_asset 10
	-- album:collection 1

	local docs = {}

	local albumCount = 1

	local albumDocId = KSIdGenerator.uuidInLowercaseHex()

	-- Albums
	local album = {
		_id = albumDocId,
		type = 'album',
		subtype = 'collection',
		name = KSIdGenerator.uuidInLowercaseHex()
	}

	table.insert( docs, album )

	local imageAndVideoAndAlbumAssetCount = 10

	for i = 1, imageAndVideoAndAlbumAssetCount do

		local imageDocId = KSIdGenerator.uuidInLowercaseHex()

		-- Images
		table.insert( docs, {
			_id = imageDocId,
			type = 'asset',
			subtype = 'image',
		} )

		-- Videos
		table.insert( docs, {
			_id = KSIdGenerator.uuidInLowercaseHex(),
			type = 'asset',
			subtype = 'video',
		} )

		-- Album Assets
		table.insert( docs, {
			_id = KSIdGenerator.uuidInLowercaseHex(),
			type = 'album_asset',
			asset = { id = imageDocId },
			album = { id = albumDocId }
		} )

	end

	return docs, imageAndVideoAndAlbumAssetCount, imageAndVideoAndAlbumAssetCount, imageAndVideoAndAlbumAssetCount, albumCount

end

--------------------------------------------------------------------------------

local function createAndInsertDocsToReindex( rxTest, docStore )

	local docs, imageCount, videoCount, albumAssetCount, albumCount = createDocsToReindex( rxTest, docStore )
	docs.label = 'oz' -- Add an Oz label, so the docs are more realistic.

	rxTest:waitFor( docStore:bulkSetDocValue( docs ) )

	-- Add a second label to a document, in order to verify proper handling of
	-- documents with more than one label.

	local doc = docs[ 1 ]

	rxTest:waitFor( docStore:docValue( doc._id )
		:flatMap( function( docValue )
			return docStore:setLabel( docValue._id, 'foo', docValue._revId )
		end ) )

	return imageCount, videoCount, albumAssetCount, albumCount

end

--------------------------------------------------------------------------------

local function overrideSchemaVersionList( rxTest, override )

	WFRxIndexStoreSchemaVersionList.TEST_setSchemaVersionList( rxTest, override )

end

--------------------------------------------------------------------------------

local function testSchemaVersionReindex( rxTest, createIndexStoreFn, initialSchemaVersionList,
											newSchemaVersionList, expectedReindexingResults,
											optArgs )

	rxTest:setTimeLimit( 10 )

	optArgs = optArgs or {}

	if optArgs.reindexStrategyOverride == 'bulkDocValue' then

		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaBulkDocValue', 'true' )

	elseif optArgs.reindexStrategyOverride == 'changedDocs' then

		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaChangedDocs', 'true' )

	end

	rxTest:finally( function()
		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaBulkDocValue', nil )
		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaChangedDocs', nil )
	end )

	docStore = createDocStore( rxTest )

	-- This function creates the following docs of types/counts:
	-- asset:image 10
	-- asset:video 10
	-- album_asset 10
	-- album:collection 1

	local imageCount, videoCount, albumAssetCount, albumCount = createAndInsertDocsToReindex( rxTest, docStore )

	-- Init and close the index store so that we have something to reindex.

	overrideSchemaVersionList( rxTest, initialSchemaVersionList )

	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )

	rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )

	local currentSequence = initialSchemaVersionList[ 1 ].sequence

	if optArgs.resumeReindexing then

		-- Testing resuming reindexing.

		if not optArgs.resumeReindexing.noop then

			-- Make it look like indexing is incomplete.
			local docStorePath = string.gsub( docStore:path(), '.mcat', '' )
			local db = WFSQLiteDatabase( docStorePath .. '.wfindex', WFSQLiteDatabaseSchema( "1.0" ) )
			db:execOnce( [[UPDATE indexStatus SET needsUpdateFrom = 1 WHERE indexTableName = 'assets' ]] )
			db:close()
			db = nil

		end

		currentSequence = optArgs.resumeReindexing.currentSequence
		local inProgressSequence = optArgs.resumeReindexing.inProgressSequence

		rxTest:expectOnlyCompleted( indexStore:DEBUG_setIndexStoreSchemaVersionInfo( currentSequence, inProgressSequence ) )

		rxTest:finally( function()
			indexStore:DEBUG_setIndexStoreSchemaVersionInfo( 0, 0 ):call( require 'WFRxRunUntilDone' )
		end )

	end

	if optArgs.unknownSequence then

		currentSequence = optArgs.unknownSequence.currentSequence
		local inProgressSequence = optArgs.unknownSequence.inProgressSequence

		rxTest:expectOnlyCompleted( indexStore:DEBUG_setIndexStoreSchemaVersionInfo( currentSequence, inProgressSequence ) )

		rxTest:finally( function()

			if indexStore then
				indexStore:DEBUG_setIndexStoreSchemaVersionInfo( 0, 0 ):call( require 'WFRxRunUntilDone' )
				indexStore:close()
				indexStore = nil
			end

		end )

		-- distort with brute force the map function hash to simulate a map function change in 'RxTestBasicTypesFoo.lua'

		if optArgs.unknownSequence.mapFnHashChange then

			local docStorePath = string.gsub( docStore:path(), '.mcat', '' )

			local db = WFSQLiteDatabase( docStorePath .. '.wfindex', WFSQLiteDatabaseSchema( "1.0" ) )
			db:execOnce( [[UPDATE indexStatus SET mapFunctionHash = 'this_is_not_a_hash' WHERE indexTableName = 'assets' ]] )
			db:close()
			db = nil

		end

	end

	rxTest:expectOnlyCompleted( indexStore:halt() )
	indexStore:close()
	indexStore = nil

	rxTest:sleep( 0.1 )
		-- Sleep before creating the new index store. This gives time for the previous index store
		-- thread to fully close.

	-- Now recreate the index store using a new schema version.

	local latestSequenceNumber = newSchemaVersionList[ #newSchemaVersionList ].sequence

	overrideSchemaVersionList( rxTest, newSchemaVersionList )

	if optArgs.resumeReindexing then
		indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )
	else
		indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesBar.lua' )
			-- Use modified index store map functions via RxTestBasicTypesBar.lua.
	end

	-- Wait for reindexing to complete.

	if KSTableUtils.deepEquals( newSchemaVersionList, initialSchemaVersionList ) or
			( optArgs.resumeReindexing and
			optArgs.resumeReindexing.noop and
			optArgs.resumeReindexing.inProgressSequence == latestSequenceNumber ) then

		-- If the version list hasn't changed, no reindexing should happen.

		-- If we're resuming reindexing, but there's no additional work, there should be no reindexing.

		rxTest:expectOnlyCompleted( indexStore:getReindexingProgress() )

	else

		rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )

	end

	-- Verify the sequence has been updated.

	rxTest:expectSingleValue( indexStore:DEBUG_getIndexStoreSchemaVersionInfo(), latestSequenceNumber, 0 )

	-- Verify what has been reindexed.

	rxTest:expectSingleValue( indexStore:DEBUG_getForcedReindexingStats(), expectedReindexingResults )

	-- Verify the correct documents were reindexed. Updated documents will have the string 'bar'
	-- in the fooOrBar column. This is because the newer index store table map functions
	-- emit 'bar' instead of 'foo'. See RxTestBasicTypesFoo.lua and RxTestBasicTypesBar.lua.

	local validTables = { 'albums', 'album_assets', 'assets', 'allDocs' }

	local _, toBeReindexedTables
			= WFRxIndexStoreSchemaVersionList.getTablesToReindex( currentSequence, validTables )

	local toBeReindexedTypes, toBeReindexTablesSet = {}, {}

	if type( toBeReindexedTables ) == 'table' then

		toBeReindexTablesSet = KSTableUtils.invert( toBeReindexedTables or {} )

		toBeReindexedTypes = rxTest:getFirstValue( indexStore:DEBUG_getTypeSubtypeMapForTables( toBeReindexedTables ) )

	end

	local verifyUpdateForTableAndTypeTuple = function( rows, tableName )

		local totalDocCount = 0

		for _, row in ipairs( rows ) do

			totalDocCount = totalDocCount + 1

			local docType = row[ 1 ]
			local docSubtype = row[ 2 ]
			local fooOrBar = row[ 3 ]

			local tableOfSubtypes = toBeReindexedTypes[ docType ] or {}
			local tableOfSubtypesSet = KSTableUtils.invert( tableOfSubtypes ) or {}

			if toBeReindexTablesSet[ tableName ] and
				toBeReindexedTypes[ docType ] and
				( ( not docSubtype and tableOfSubtypesSet[ 'NULL' ] ) or
					tableOfSubtypesSet[ docSubtype ] or
					tableOfSubtypesSet[ '*' ]  ) then

				-- We expect the document to have been reindexed.

				if optArgs.resumeReindexing then
					rxTest:expectEqual( 'foo', fooOrBar )
				else
					rxTest:expectEqual( 'bar', fooOrBar )
				end

			end

		end

		return totalDocCount

	end

	rxTest:expectSingleValue( indexStore:queryOnce
		{
			sql = [[SELECT type, subtype, fooOrBar
					FROM albums
					WHERE type = 'album' AND subtype = 'collection' ]],
		}
		:map( function( rows )
			return verifyUpdateForTableAndTypeTuple( rows, 'albums' )
		end ), albumCount )

	rxTest:expectSingleValue( indexStore:queryOnce
		{
			sql = [[SELECT type, subtype, fooOrBar
					FROM assets
					WHERE type = 'asset' AND subtype = 'image' ]],
		}
		:map( function( rows )
			return verifyUpdateForTableAndTypeTuple( rows, 'assets' )
		end ), imageCount )

	rxTest:expectSingleValue( indexStore:queryOnce
		{
			sql = [[SELECT type, subtype, fooOrBar
					FROM assets
					WHERE type = 'asset' AND subtype = 'video' ]],
		}
		:map( function( rows )
			return verifyUpdateForTableAndTypeTuple( rows, 'assets'  )
		end ), videoCount )

	rxTest:expectSingleValue( indexStore:queryOnce
		{
			sql = [[SELECT type, subtype, fooOrBar
					FROM album_assets
					WHERE type = 'album_asset' ]],
		}
		:map( function( rows )
			return verifyUpdateForTableAndTypeTuple( rows, 'album_assets'  )
		end ), albumAssetCount )

end

--------------------------------------------------------------------------------

local reindexingStrategies = {
	'bulkDocValue',
	'changedDocs'
}

--------------------------------------------------------------------------------

local function getMethodNameViaStrategy( strategy )

	return strategy == 'bulkDocValue'
			and 'fetchDocsByTypeAndBringUpToDate'
					or 'fetchChangedDocsByTypeAndBringUpToDate'

end

--------------------------------------------------------------------------------

for _, reindexStrategy in ipairs( reindexingStrategies ) do

	----------------------------------------------------------------------------

	local testName = string.format( "rxTestSchemaVersionReindex_%s_nothingToReindex_testSerially", reindexStrategy )

	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )

		-- Verifies the behavior when there's no new sequence; nothing should be reindexed.

		local initialSchemaVersionList = { { sequence = 0 } }

		local newSchemaVersionList = { { sequence = 0 } }

		local expectedReindexingResults = {}

		local optArgs = {
			reindexStrategyOverride = reindexStrategy
		}

		testSchemaVersionReindex( rxTest, createIndexStoreFn, initialSchemaVersionList,
									newSchemaVersionList, expectedReindexingResults,
									optArgs )

	end

	----------------------------------------------------------------------------

	testName = string.format( "rxTestSchemaVersionReindex_%s_oneTable_testSerially", reindexStrategy )

	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )

		-- Verifies the behavior of a new entry in the version schema that includes a
		-- new sequence with one table update.

		local initialSchemaVersionList = { { sequence = 0 } }

		local newSchemaVersionList = {
			{
				sequence = 0,
			},
			{
				sequence = 1,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "assets" }
			}
		}

		local expectedReindexingResults = {

			minNeedsUpdateFrom = 1,
			numberOfDocsToReindex = 20, -- 10 image assets, 10 video assets
			reindexedDocCount = 20,
			numberOfTablesToReindex = 1, -- assets
			method = getMethodNameViaStrategy( reindexStrategy )

		}

		local optArgs = {
			reindexStrategyOverride = reindexStrategy
		}

		testSchemaVersionReindex( rxTest, createIndexStoreFn, initialSchemaVersionList,
										newSchemaVersionList, expectedReindexingResults,
										optArgs )

	end

	----------------------------------------------------------------------------

	testName = string.format( "rxTestSchemaVersionReindex_%s_allTables_testSerially", reindexStrategy )

	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )

		-- Verifies the behavior of a new entry in the version schema that includes a
		-- new sequence with ALL tables.

		local initialSchemaVersionList = { { sequence = 0 } }

		local newSchemaVersionList = {
			{
				sequence = 0,
			},
			{
				sequence = 1,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "assets", "albums", "album_assets" }
			}
		}

		local expectedReindexingResults = {

			minNeedsUpdateFrom = 1,
			numberOfDocsToReindex = 31, -- 10 image assets, 10 video assets, 10 album assets, 1 album
			reindexedDocCount = 31,
			numberOfTablesToReindex = 3, -- assets, albums, album_assets
			method = getMethodNameViaStrategy( reindexStrategy )

		}

		local optArgs = {
			reindexStrategyOverride = reindexStrategy
		}

		testSchemaVersionReindex( rxTest, createIndexStoreFn, initialSchemaVersionList,
										newSchemaVersionList, expectedReindexingResults,
										optArgs )

	end

	----------------------------------------------------------------------------

	testName = string.format( "rxTestSchemaVersionReindex_%s_allTypeSubtypeTuplesOneTable_testSerially", reindexStrategy )

	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )

		-- Verifies the behavior of a new entry in the version schema that includes a
		-- a table that references all types and subtypes.

		local initialSchemaVersionList = { { sequence = 0 } }

		local newSchemaVersionList = {
			{
				sequence = 0,
			},
			{
				sequence = 1,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "allDocs" }
			}
		}

		local expectedReindexingResults = {

			minNeedsUpdateFrom = 1,
			numberOfDocsToReindex = 31, -- 10 image assets, 10 video assets, 10 album assets, 1 album
			reindexedDocCount = 31,
			numberOfTablesToReindex = 1, -- allDocs
			method = getMethodNameViaStrategy( reindexStrategy )

		}

		local optArgs = {
			reindexStrategyOverride = reindexStrategy,
			allTypeSubtypeTuples = true
		}

		testSchemaVersionReindex( rxTest, createIndexStoreFn, initialSchemaVersionList,
										newSchemaVersionList, expectedReindexingResults,
										optArgs )

	end

	----------------------------------------------------------------------------

	testName = string.format( "rxTestSchemaVersionReindex_%s_resumeReindexing_noop_testSerially", reindexStrategy )

	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )

		-- Verifies that reindexing can be resumed/completed, even when there's no
		-- additional work.

		local initialSchemaVersionList = { { sequence = 0 } }

		local newSchemaVersionList = {
			{
				sequence = 0,
			},
			{
				sequence = 1,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "assets" }
			}
		}

		local expectedReindexingResults = { method = getMethodNameViaStrategy( reindexStrategy ) }

		local optArgs = {
			reindexStrategyOverride = reindexStrategy
		}

		optArgs.resumeReindexing = {

			noop = true,
			currentSequence = 0,
			inProgressSequence = 1

		}

		testSchemaVersionReindex( rxTest, createIndexStoreFn, initialSchemaVersionList,
									newSchemaVersionList, expectedReindexingResults,
									optArgs )

	end

	----------------------------------------------------------------------------

	testName = string.format( "rxTestSchemaVersionReindex_%s_resumeReindexing_testSerially", reindexStrategy )

	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )

		-- Verifies that reindexing can be resumed/completed, even when there's no
		-- additional work.

		local initialSchemaVersionList = { { sequence = 0 } }

		local newSchemaVersionList = {
			{
				sequence = 0,
			},
			{
				sequence = 1,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "assets" }
			}
		}

		local expectedReindexingResults = {

			minNeedsUpdateFrom = 1,
			numberOfDocsToReindex = 20, -- 10 images, 10 videos
			reindexedDocCount = 20,
			numberOfTablesToReindex = 1, -- assets
			method = getMethodNameViaStrategy( reindexStrategy )

		}

		local optArgs = {
			reindexStrategyOverride = reindexStrategy
		}

		optArgs.resumeReindexing = {

			currentSequence = 0,
			inProgressSequence = 1

		}

		testSchemaVersionReindex( rxTest, createIndexStoreFn, initialSchemaVersionList,
									newSchemaVersionList, expectedReindexingResults,
									optArgs )

	end

	----------------------------------------------------------------------------

	testName = string.format( "rxTestSchemaVersionReindex_%s_resumeReindexingNewSequence_testSerially", reindexStrategy )

	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )

		-- Verifies that an in-progress reindex will be fully restarted if the
		-- in progress reindex does not match the most recent sequence number.

		local initialSchemaVersionList = { { sequence = 0 } }

		local newSchemaVersionList = {
			{
				sequence = 0,
			},
			{
				sequence = 1,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "assets" }
			},
			{
				sequence = 2,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "albums" }
			}
		}

		local expectedReindexingResults = {

			minNeedsUpdateFrom = 1,
			numberOfDocsToReindex = 21, -- 10 images, 10 videos, 1 album
			reindexedDocCount = 21,
			numberOfTablesToReindex = 2, -- assets, albums
			method = getMethodNameViaStrategy( reindexStrategy )

		}

		local optArgs = {
			reindexStrategyOverride = reindexStrategy
		}

		optArgs.resumeReindexing = {

			currentSequence = 0,
			inProgressSequence = 1

		}

		testSchemaVersionReindex( rxTest, createIndexStoreFn, initialSchemaVersionList,
									newSchemaVersionList, expectedReindexingResults,
									optArgs )

	end

	----------------------------------------------------------------------------

	testName = string.format( "rxTestSchemaVersionReindex_%s_resumeReindexingNewSequence_noop_testSerially", reindexStrategy )

	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )

		-- Verifies that an in-progress reindex will be fully restarted if the
		-- in progress reindex does not match the most recent sequence number.

		local initialSchemaVersionList = { { sequence = 0 } }

		local newSchemaVersionList = {
			{
				sequence = 0,
			},
			{
				sequence = 1,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "assets" }
			},
			{
				sequence = 2,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "albums" }
			}
		}

		local expectedReindexingResults = {

			minNeedsUpdateFrom = 1,
			numberOfDocsToReindex = 21, -- 10images, 10 videos, 1 album
			reindexedDocCount = 21,
			numberOfTablesToReindex = 2, -- assets, albums
			method = getMethodNameViaStrategy( reindexStrategy )

		}

		local optArgs = {
			reindexStrategyOverride = reindexStrategy
		}

		optArgs.resumeReindexing = {

			noop = true,
			currentSequence = 0,
			inProgressSequence = 1

		}

		testSchemaVersionReindex( rxTest, createIndexStoreFn, initialSchemaVersionList,
									newSchemaVersionList, expectedReindexingResults,
									optArgs )

	end

	----------------------------------------------------------------------------

	testName = string.format( "rxTestSchemaVersionReindex_%s_unknownSequence_hashReindexingFallback_testSerially", reindexStrategy )

	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )

		-- Verifies that we will fall back to hash-based reindexing in the event
		-- the index store is tracking an sequence number that is no longer in
		-- the schema version list.

		local initialSchemaVersionList = { { sequence = 0 } }

		local newSchemaVersionList = {
			{
				sequence = 2,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "assets" }
			}
		}

		local expectedReindexingResults = {
			method = "fetchChangedDocsAndBringUpToDate", -- hash-based method, should reindex all tables/docs.
			minNeedsUpdateFrom = 1,
			numberOfDocsToReindex = 31,
			numberOfTablesToReindex = 4,
			reindexedDocCount = 31,
		}

		local optArgs = {
			reindexStrategyOverride = reindexStrategy
		}

		optArgs.unknownSequence = {
			currentSequence = 1,
				-- An unknown sequence number should trigger a hash-based reindex.
			inProgressSequence = 0,
			mapFnHashChange = true
		}

		testSchemaVersionReindex( rxTest, createIndexStoreFn, initialSchemaVersionList,
									newSchemaVersionList, expectedReindexingResults,
									optArgs )

	end

	----------------------------------------------------------------------------

-- SPAM [jandrew 2021-08-12]: The following test is disabled because it only
-- works if you cherry-pick commit cd5dafdc0d0fe2e3bee3caf480efc73ea93914be from
-- the wf/releases/3.4 branch into main.

--	testName = string.format( "rxTestSchemaVersionReindex_%s_upgradeDowngradeUpgrade_testSerially", reindexStrategy )

--	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )
--
--		-- The following test verifies the upgrade behavior when upgrading from a downgraded
--		-- version of Lightroom. Example:
--
--		-- - User upgrades to MAX release
--		-- - User downgrades to Auguest release
--		-- - User upgrades to MAX release
--
--		-- Each of these events should trigger a hash-based reindexing, regardless of the presence
--		-- of a schema version list.
--
--		rxTest:setTimeLimit( 10 )
--
--		local verifyUpdateForTableAndTypeTuple = function( rows, tableName, fooOrBarExpected )
--
--			local totalDocCount = 0
--
--			for _, row in ipairs( rows ) do
--
--				totalDocCount = totalDocCount + 1
--
--				local fooOrBar = row[ 3 ]
--
--				rxTest:expectEqual( fooOrBarExpected, fooOrBar )
--
--			end
--
--			return totalDocCount
--
--		end
--
--		if reindexStrategy == 'bulkDocValue' then
--
--			WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaBulkDocValue', 'true' )
--
--		elseif reindexStrategy == 'changedDocs' then
--
--			WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaChangedDocs', 'true' )
--
--		end
--
--		rxTest:finally( function()
--			WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaBulkDocValue', nil )
--			WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaChangedDocs', nil )
--		end )
--
--		local docStorePath
--
--		docStore, docStorePath = createDocStore( rxTest )
--
--		-- This function creates the following docs of types/counts:
--		-- asset:image 10
--		-- asset:video 10
--		-- album_asset 10
--		-- album:collection 1
--
--		local imageCount, videoCount, albumAssetCount, albumCount = createAndInsertDocsToReindex( rxTest, docStore )
--
--		-- Init and close the index store so that we have something to reindex. This index store has
--		-- *no* schema version list sequences numbers.
--
--		-- We also close the doc store.
--
--		indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )
--
--		rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )
--
--		rxTest:expectSingleValue( indexStore:DEBUG_getIndexStoreSchemaVersionInfo(), nil, nil )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM albums
--						WHERE type = 'album' AND subtype = 'collection' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'albums', 'foo' )
--			end ), albumCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM assets
--						WHERE type = 'asset' AND subtype = 'image' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'assets', 'foo' )
--			end ), imageCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM assets
--						WHERE type = 'asset' AND subtype = 'video' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'assets', 'foo' )
--			end ), videoCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM album_assets
--						WHERE type = 'album_asset' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'album_assets', 'foo' )
--			end ), albumAssetCount )
--
--		rxTest:expectOnlyCompleted( indexStore:halt() )
--		indexStore:close()
--		indexStore = nil
--
--		rxTest:expectOnlyCompleted( docStore:halt() )
--		docStore:close()
--		docStore = nil
--
--		rxTest:sleep( 0.1 )
--			-- Sleep before creating the dbs.
--
--		-- UPGRADE
--
--		docStore = createDocStore( rxTest, docStorePath )
--
--		rxTest:expectSingleValue( docStore:countAllDocs(), 31 )
--
--		-- Now recreate the index store with a supplied schema version list.
--
--		local newSchemaVersionList = {
--			{
--				sequence = 0,
--			},
--			{
--				sequence = 1,
--				platforms = { WFPlatformInfo.getOS() },
--				tables = { "assets" }
--			}
--		}
--
--		overrideSchemaVersionList( rxTest, newSchemaVersionList )
--
--		indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesBar.lua' )
--
--		rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )
--
--		-- Verify the sequence has been updated.
--
--		rxTest:expectSingleValue( indexStore:DEBUG_getIndexStoreSchemaVersionInfo(), 1, 0 )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM albums
--						WHERE type = 'album' AND subtype = 'collection' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'albums', 'bar' )
--			end ), albumCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM assets
--						WHERE type = 'asset' AND subtype = 'image' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'assets', 'bar' )
--			end ), imageCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM assets
--						WHERE type = 'asset' AND subtype = 'video' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'assets', 'bar' )
--			end ), videoCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM album_assets
--						WHERE type = 'album_asset' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'album_assets', 'bar' )
--			end ), albumAssetCount )
--
--		-- Close the doc store & index store
--		rxTest:expectOnlyCompleted( indexStore:halt() )
--		indexStore:close()
--		indexStore = nil
--
--		rxTest:expectOnlyCompleted( docStore:halt() )
--		docStore:close()
--		docStore = nil
--
--		rxTest:sleep( 0.1 )
--			-- Sleep before creating the next dbs.
--
--		-- DOWNGRADE
--
--		docStore = createDocStore( rxTest, docStorePath )
--
--		rxTest:expectSingleValue( docStore:countAllDocs(), 31 )
--
--		newSchemaVersionList = {
--			{
--				sequence = 0,
--			}
--		}
--
--		overrideSchemaVersionList( rxTest, newSchemaVersionList )
--
--		indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )
--
--		rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )
--
--		rxTest:expectSingleValue( indexStore:DEBUG_getIndexStoreSchemaVersionInfo(), 1, 0 )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM albums
--						WHERE type = 'album' AND subtype = 'collection' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'albums', 'foo' )
--			end ), albumCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM assets
--						WHERE type = 'asset' AND subtype = 'image' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'assets', 'foo' )
--			end ), imageCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM assets
--						WHERE type = 'asset' AND subtype = 'video' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'assets', 'foo' )
--			end ), videoCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM album_assets
--						WHERE type = 'album_asset' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'album_assets', 'foo' )
--			end ), albumAssetCount )
--
--		-- Close the doc store & index store
--		rxTest:expectOnlyCompleted( indexStore:halt() )
--		indexStore:close()
--		indexStore = nil
--
--		rxTest:expectOnlyCompleted( docStore:halt() )
--		docStore:close()
--		docStore = nil
--
--		rxTest:sleep( 0.1 )
--			-- Sleep before creating the next dbs.
--
--		-- UPGRADE AGAIN
--
--		docStore = createDocStore( rxTest, docStorePath )
--
--		rxTest:expectSingleValue( docStore:countAllDocs(), 31 )
--
--		-- Now recreate the index store with a supplied schema version list.
--
--		newSchemaVersionList = {
--			{
--				sequence = 0,
--			},
--			{
--				sequence = 1,
--				platforms = { WFPlatformInfo.getOS() },
--				tables = { "assets" }
--			}
--		}
--
--		overrideSchemaVersionList( rxTest, newSchemaVersionList )
--
--		indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesBar.lua' )
--
--		rxTest:waitFor( indexStore:getReindexingProgress() )
--
--		-- Verify the sequence has been updated.
--
--		rxTest:expectSingleValue( indexStore:DEBUG_getIndexStoreSchemaVersionInfo(), 1, 0 )
--		
--		-- Verify the correct documents were reindexed. Updated documents will have the string 'bar'
--		-- in the fooOrBar column. This is because the newer index store table map functions
--		-- emit 'bar' instead of 'foo'. See RxTestBasicTypesFoo.lua and RxTestBasicTypesBar.lua.
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM albums
--						WHERE type = 'album' AND subtype = 'collection' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'albums', 'bar' )
--			end ), albumCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM assets
--						WHERE type = 'asset' AND subtype = 'image' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'assets', 'bar' )
--			end ), imageCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM assets
--						WHERE type = 'asset' AND subtype = 'video' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'assets', 'bar' )
--			end ), videoCount )
--
--		rxTest:expectSingleValue( indexStore:queryOnce
--			{
--				sql = [[SELECT type, subtype, fooOrBar
--						FROM album_assets
--						WHERE type = 'album_asset' ]],
--			}
--			:map( function( rows )
--				return verifyUpdateForTableAndTypeTuple( rows, 'album_assets', 'bar' )
--			end ), albumAssetCount )
--
--	end

	----------------------------------------------------------------------------

	testName = string.format( "rxTestSchemaVersionReindex_%s_noSequence_hashReindexingFallback_testSerially", reindexStrategy )

	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )

		-- The following test verifies the upgrade behavior when upgrading from a version
		-- of LrD that does not support the schema version list, to a version that does.

		-- Upon first launch of the schema-version-aware release, we should reindex ALL the
		-- tables specified in the schema version list, as well as all of the tables with
		-- any schema changes that have occurred since the previous installed version.
		-- Effectively we should fall back to hash-based reindexing.

		rxTest:setTimeLimit( 10 )

		if reindexStrategy == 'bulkDocValue' then

			WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaBulkDocValue', 'true' )

		elseif reindexStrategy == 'changedDocs' then

			WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaChangedDocs', 'true' )

		end

		rxTest:finally( function()
			WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaBulkDocValue', nil )
			WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaChangedDocs', nil )
		end )

		local docStorePath

		docStore, docStorePath = createDocStore( rxTest )

		-- This function creates the following docs of types/counts:
		-- asset:image 10
		-- asset:video 10
		-- album_asset 10
		-- album:collection 1

		local imageCount, videoCount, albumAssetCount, albumCount = createAndInsertDocsToReindex( rxTest, docStore )

		-- Init and close the index store so that we have something to reindex. This index store has
		-- *no* schema version list sequences numbers.
		
		-- We also close the doc store.

		indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )

		rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )
		
		rxTest:expectOnlyCompleted( indexStore:DEBUG_setIndexStoreSchemaVersionInfo( nil, nil ) )

		rxTest:expectSingleValue( indexStore:DEBUG_getIndexStoreSchemaVersionInfo(), nil, nil )

		rxTest:expectOnlyCompleted( indexStore:halt() )
		indexStore:close()
		indexStore = nil

		rxTest:expectOnlyCompleted( docStore:halt() )
		docStore:close()
		docStore = nil

		rxTest:sleep( 0.1 )
			-- Sleep before creating the dbs.

		-- Now recreate the doc store

		docStore = createDocStore( rxTest, docStorePath )

		rxTest:expectSingleValue( docStore:countAllDocs(), 31 )

		-- Now recreate the index store with a supplied schema version list.

		local newSchemaVersionList = {
			{
				sequence = 0,
			},
			{
				sequence = 1,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "assets" }
			}
		}

		overrideSchemaVersionList( rxTest, newSchemaVersionList )

		indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesBar.lua' )

		rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )

		-- Verify the sequence has been updated.

		rxTest:expectSingleValue( indexStore:DEBUG_getIndexStoreSchemaVersionInfo(), 1, 0 )

		-- Verify what has been reindexed. Because we've upgraded from a build that doesn't
		-- understand the schema version list, we need to reindex all tables.

		rxTest:expectSingleValue( indexStore:DEBUG_getForcedReindexingStats(), {
			method = "fetchChangedDocsAndBringUpToDate", -- hash-based method, should reindex all tables/docs.
			minNeedsUpdateFrom = 1,
			numberOfDocsToReindex = 31,
			numberOfTablesToReindex = 4,
			reindexedDocCount = 31,
		} )

		-- Verify the correct documents were reindexed. Updated documents will have the string 'bar'
		-- in the fooOrBar column. This is because the newer index store table map functions
		-- emit 'bar' instead of 'foo'. See RxTestBasicTypesFoo.lua and RxTestBasicTypesBar.lua.

		local verifyUpdateForTableAndTypeTuple = function( rows, tableName )

			local totalDocCount = 0

			for _, row in ipairs( rows ) do

				totalDocCount = totalDocCount + 1

				local fooOrBar = row[ 3 ]

				rxTest:expectEqual( 'bar', fooOrBar )

			end

			return totalDocCount

		end

		rxTest:expectSingleValue( indexStore:queryOnce
			{
				sql = [[SELECT type, subtype, fooOrBar
						FROM albums
						WHERE type = 'album' AND subtype = 'collection' ]],
			}
			:map( function( rows )
				return verifyUpdateForTableAndTypeTuple( rows, 'albums' )
			end ), albumCount )

		rxTest:expectSingleValue( indexStore:queryOnce
			{
				sql = [[SELECT type, subtype, fooOrBar
						FROM assets
						WHERE type = 'asset' AND subtype = 'image' ]],
			}
			:map( function( rows )
				return verifyUpdateForTableAndTypeTuple( rows, 'assets' )
			end ), imageCount )

		rxTest:expectSingleValue( indexStore:queryOnce
			{
				sql = [[SELECT type, subtype, fooOrBar
						FROM assets
						WHERE type = 'asset' AND subtype = 'video' ]],
			}
			:map( function( rows )
				return verifyUpdateForTableAndTypeTuple( rows, 'assets'  )
			end ), videoCount )

		rxTest:expectSingleValue( indexStore:queryOnce
			{
				sql = [[SELECT type, subtype, fooOrBar
						FROM album_assets
						WHERE type = 'album_asset' ]],
			}
			:map( function( rows )
				return verifyUpdateForTableAndTypeTuple( rows, 'album_assets'  )
			end ), albumAssetCount )

		-- Close the index store again:

		rxTest:expectOnlyCompleted( indexStore:halt() )
		indexStore:close()
		indexStore = nil

		rxTest:sleep( 0.1 )
			-- Sleep before creating it.

		-- Now recreate the index store using a new schema version list. This verifies
		-- that we will now reindex only the tables in the list.

		newSchemaVersionList = {
			{
				sequence = 0,
			},
			{
				sequence = 1,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "assets" }
			},
			{
				sequence = 2,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "albums" }
			},
		}

		overrideSchemaVersionList( rxTest, newSchemaVersionList )

		indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )

		rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )

		-- Verify the sequence has been updated.

		rxTest:expectSingleValue( indexStore:DEBUG_getIndexStoreSchemaVersionInfo(), 2, 0 )

		-- Verify what has been reindexed.

		local expectedMethod
		
		if reindexStrategy == 'bulkDocValue' then

			expectedMethod = 'fetchDocsByTypeAndBringUpToDate'

		elseif reindexStrategy == 'changedDocs' then
		
			expectedMethod = 'fetchChangedDocsByTypeAndBringUpToDate'
		
		end

		rxTest:expectSingleValue( indexStore:DEBUG_getForcedReindexingStats(), {
			method = expectedMethod,
			minNeedsUpdateFrom = 1,
			numberOfDocsToReindex = 1,
			numberOfTablesToReindex = 1,
			reindexedDocCount = 1,
		} )

	end

	----------------------------------------------------------------------------

	testName = string.format( "rxTestSchemaVersionReindex_%s_futureSequence_hashReindexingFallback_testSerially", reindexStrategy )

	WFRxIndexStoreTestFunctions[ testName ] = function( rxTest, createIndexStoreFn )

		-- Verifies that we will fall back to hash-based reindexing in the event
		-- the index store is tracking an unknown sequence number that is greater
		-- than the sequence head of the current list.

		local initialSchemaVersionList = { { sequence = 0 } }

		local newSchemaVersionList = {
			{
				sequence = 0,
			},
			{
				sequence = 1,
				platforms = { WFPlatformInfo.getOS() },
				tables = { "assets" }
			}
		}

		local expectedReindexingResults = {
			method = "fetchChangedDocsAndBringUpToDate", -- hash-based method, should reindex all tables/docs.
			minNeedsUpdateFrom = 1,
			numberOfDocsToReindex = 31,
			numberOfTablesToReindex = 4,
			reindexedDocCount = 31,
		}

		local optArgs = {
			reindexStrategyOverride = reindexStrategy
		}

		optArgs.unknownSequence = {
			currentSequence = 11,
				-- An unknown sequence number should trigger a hash-based reindex.
			inProgressSequence = 0,
			mapFnHashChange = true
		}

		testSchemaVersionReindex( rxTest, createIndexStoreFn, initialSchemaVersionList,
									newSchemaVersionList, expectedReindexingResults,
									optArgs )

	end

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestSchemaVersionReindex_droppedTableReferencedInSchema_testSerially( rxTest, createIndexStoreFn )

	rxTest:setTimeLimit( 10 )

	-- Verifies that reindexing will fail if the schema version list references
	-- a table that does not exist.

	local initialSchemaVersionList = { { sequence = 0 } }

	local newSchemaVersionList = {
		{
			sequence = 0,
		},
		{
			sequence = 1,
			platforms = { WFPlatformInfo.getOS() },
			tables = { "thisTableNoLongerExists" }
		},
		{
			sequence = 2,
			platforms = { WFPlatformInfo.getOS() },
			tables = { "assets" }
		}
	}

	docStore = createDocStore( rxTest )

	createAndInsertDocsToReindex( rxTest, docStore )

	-- Init and close the index store so that we have something to reindex.

	overrideSchemaVersionList( rxTest, initialSchemaVersionList )

	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )
	rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )
	rxTest:expectOnlyCompleted( indexStore:halt() )
	indexStore:close()
	indexStore = nil

	rxTest:sleep( 0.1 )
		-- Sleep before creating the new index store. This gives time for the previous index store
		-- thread to fully close.

	-- Now recreate the index store using a new schema version.

	overrideSchemaVersionList( rxTest, newSchemaVersionList )

	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )

	-- Ther should be no indication of reindexing progress because the unknown table will
	-- can an error.

	local progressCount = 0

	local progressSub = indexStore:getReindexingProgress()

		:subscribe(
			function()
				progressCount = progressCount + 1
			end,
			function()
				progressCount = progressCount + 1
			end,
			function()
				progressCount = progressCount + 1
			end )

	rxTest:sleep( 1 )
		-- Wait for any progress data to come through.

	rxTest:expectEqual( 0, progressCount )

	progressSub:dispose()

	-- Verify the sequence has not been updated.

	rxTest:expectSingleValue( indexStore:DEBUG_getIndexStoreSchemaVersionInfo(), 0, 0 )

	-- Verify what has been reindexed.

	rxTest:expectSingleValue( indexStore:DEBUG_getForcedReindexingStats(), {} )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTestFunctions.rxTestSchemaVersionReindex_deletedIndexStore_testSerially( rxTest, createIndexStoreFn )

	-- Verifies we will reindex everything in the event the index store is deleted.
	rxTest:setTimeLimit( 5 )

	docStore = createDocStore( rxTest )

	-- This function creates the following docs of types/counts:
	-- asset:image 10
	-- asset:video 10
	-- album_asset 10
	-- album:collection 1

	local imageCount, videoCount, albumAssetCount, albumCount = createAndInsertDocsToReindex( rxTest, docStore )

	-- Now recreate the index store, No index store previously exited, mimicking the
	-- deleted index store scenario.

	local newSchemaVersionList = {
		{
			sequence = 0,
		},
		{
			sequence = 1,
			platforms = { WFPlatformInfo.getOS() },
			tables = { "assets" }
		}
	}

	overrideSchemaVersionList( rxTest, newSchemaVersionList )

	indexStore = createIndexStoreFn( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )

	-- Wait for reindexing to complete.

	rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )

	-- Verify what has been reindexed. Everything should have been reindexed, and not just
	-- the tables/types listed in the schema version list.

	local expectedReindexingResults = {
		method = "fetchChangedDocsAndBringUpToDate", -- hash-based method, should reindex all tables/docs.
		minNeedsUpdateFrom = 1,
		numberOfDocsToReindex = 31,
		numberOfTablesToReindex = 4,
		reindexedDocCount = 31,
	}

	rxTest:expectSingleValue( indexStore:DEBUG_getForcedReindexingStats(), expectedReindexingResults )

	-- Verify the sequence has been updated.

	rxTest:expectSingleValue( indexStore:DEBUG_getIndexStoreSchemaVersionInfo(), 1, 0 )

	-- Verify Index Store Contents

	rxTest:expectSingleValue( indexStore:queryOnce
		{
			sql = [[SELECT COUNT( * ) FROM albums]],
			singleColumnQuery = true
		}, { albumCount } )

	rxTest:expectSingleValue( indexStore:queryOnce
		{
			sql = [[SELECT COUNT( * ) FROM album_assets]],
			singleColumnQuery = true
		}, { albumAssetCount } )

	rxTest:expectSingleValue( indexStore:queryOnce
		{
			sql = [[SELECT COUNT( * ) FROM assets WHERE subtype = 'image']],
			singleColumnQuery = true
		}, { imageCount } )

	rxTest:expectSingleValue( indexStore:queryOnce
		{
			sql = [[SELECT COUNT( * ) FROM assets WHERE subtype = 'video']],
			singleColumnQuery = true
		}, { videoCount } )

end

--------------------------------------------------------------------------------

for rxTestName, rxTestFunction in pairs( WFRxIndexStoreTestFunctions ) do

	WFRxIndexStoreTest[ rxTestName ] = function( rxTest )
		return rxTestFunction( rxTest, createIndexStoreWithNoReadOnlyThreads )
	end

	local withReadOnlyThreadsTestName
	if string.find( rxTestName, "_testSerially" ) then
		withReadOnlyThreadsTestName = string.gsub( rxTestName, "_testSerially", "_WithReadOnlyThreads_testSerially")
	else
		withReadOnlyThreadsTestName = rxTestName .. "_WithReadOnlyThreads"
	end
	
	WFRxIndexStoreTest[ withReadOnlyThreadsTestName ] = function( rxTest )
		return rxTestFunction( rxTest, createIndexStoreWith1ReadOnlyThreads )
	end

end

--------------------------------------------------------------------------------

-- The following matches the document counts within Josh Haftel's catalog.

local assetImageCount = 293485
local assetVideoCount = 784
local albumAssetCount = 181393
local albumCollectionCount = 585
local assetPersonCount = 13025

local function createPsuedoJoshCatalogDocsToReindex( rxTest, docStore )

	-- The following creates docs of types/counts based on Josh's real world
	-- catalog. Other doc counts are small enough not to matter for the
	-- purposes of this test.

	local docs = {}

	for i=1, albumCollectionCount, 1 do

		table.insert( docs, {
			_id = KSIdGenerator.uuidInLowercaseHex(),
			type = 'album',
			subtype = 'collection',
			name = KSIdGenerator.uuidInLowercaseHex()
		} )

	end

	for i=1, assetImageCount, 1 do

		table.insert( docs, {
			_id = KSIdGenerator.uuidInLowercaseHex(),
			type = 'asset',
			subtype = 'image',
		} )

	end

	for i=1, assetVideoCount, 1 do

		table.insert( docs, {
			_id = KSIdGenerator.uuidInLowercaseHex(),
			type = 'asset',
			subtype = 'video',
		} )

	end

	for i=1, assetPersonCount, 1 do

		table.insert( docs, {
			_id = KSIdGenerator.uuidInLowercaseHex(),
			type = 'asset',
			subtype = 'person',
		} )

	end

	for i=1, albumAssetCount, 1 do

		table.insert( docs, {
			_id = KSIdGenerator.uuidInLowercaseHex(),
			type = 'album_asset',
			asset = { id = KSIdGenerator.uuidInLowercaseHex() },
			album = { id = KSIdGenerator.uuidInLowercaseHex() }
		} )

	end

	return docs, assetImageCount, assetVideoCount, albumAssetCount, albumCollectionCount, assetPersonCount

end

-------------------------------------------------------------------------------

local function testRealWorldSchemaVersionReindexPerformance( rxTest, initialSchemaVersionList,
															newSchemaVersionList, expectedReindexingResults,
															maybeReindexViaChangedDocs, maybeReindexViaBulkDocValue )

	rxTest:setTimeLimit( 1000 )

	if maybeReindexViaChangedDocs then
		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaChangedDocs', 'true' )
	elseif maybeReindexViaBulkDocValue then
		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaBulkDocValue', 'true' )
	end

	rxTest:finally( function()
		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaBulkDocValue', nil )
		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaChangedDocs', nil )
	end )

	docStore = createDocStore( rxTest )

	local docs = createPsuedoJoshCatalogDocsToReindex( rxTest, docStore )

	local batchOfDocs = {}

	for _, doc in ipairs( docs ) do

		batchOfDocs[ #batchOfDocs + 1 ] = doc

		if #batchOfDocs >= 1000 then

			batchOfDocs.label = 'oz'
			rxTest:waitFor( docStore:bulkSetDocValue( batchOfDocs ) )
			batchOfDocs = {}

		end

	end

	if #batchOfDocs > 0 then

		batchOfDocs.label = 'oz'
		rxTest:waitFor( docStore:bulkSetDocValue( batchOfDocs ) )
		batchOfDocs = {}

	end

	-- Init and close the index store so that we have something to reindex.

	overrideSchemaVersionList( rxTest, initialSchemaVersionList )

	indexStore = createIndexStoreWithNoReadOnlyThreads( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )
	rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )
	rxTest:expectOnlyCompleted( indexStore:halt() )
	indexStore:close()
	indexStore = nil

	rxTest:sleep( 1 )
		-- Sleep before creating the new index store. This gives time for the previous index store
		-- thread to fully close.

	-- Now recreate the index store using a new schema version.

	overrideSchemaVersionList( rxTest, newSchemaVersionList )

	rxTest:measurePerformance( function()

		indexStore = createIndexStoreWithNoReadOnlyThreads( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )

		-- Wait for reindexing to complete.

		rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )

	end )

end

--------------------------------------------------------------------------------

local function testRealWorldHashBasedReindexPerformance( rxTest, tablesToReindex )

	rxTest:setTimeLimit( 1000 )

	docStore = createDocStore( rxTest )

	local docs = createPsuedoJoshCatalogDocsToReindex( rxTest, docStore )

	local batchOfDocs = {}

	for _, doc in ipairs( docs ) do

		batchOfDocs[ #batchOfDocs + 1 ] = doc

		if #batchOfDocs >= 1000 then

			batchOfDocs.label = 'oz'
			rxTest:waitFor( docStore:bulkSetDocValue( batchOfDocs ) )
			batchOfDocs = {}

		end

	end

	if #batchOfDocs > 0 then

		batchOfDocs.label = 'oz'
		rxTest:waitFor( docStore:bulkSetDocValue( batchOfDocs ) )
		batchOfDocs = {}

	end

	-- Init and close the index store so that we have something to reindex.

	indexStore = createIndexStoreWithNoReadOnlyThreads( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )
	rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )

	-- Force override the map function hash to cause a reindexing for each specified table:

	local docStorePath = string.gsub( docStore:path(), '.mcat', '' )
	local db = WFSQLiteDatabase( docStorePath .. '.wfindex', WFSQLiteDatabaseSchema( "1.0" ) )

	for _, tableName in ipairs( tablesToReindex ) do

		db:execOnce( string.format( [[UPDATE indexStatus SET mapFunctionHash = 'this_is_not_a_hash' WHERE indexTableName =  '%s' ]], tableName ) )

	end

	db:close()
	db = nil

	rxTest:expectOnlyCompleted( indexStore:halt() )
	indexStore:close()
	indexStore = nil

	rxTest:sleep( 1 )
		-- Sleep before creating the new index store. This gives time for the previous index store
		-- thread to fully close.

	rxTest:measurePerformance( function()

		indexStore = createIndexStoreWithNoReadOnlyThreads( rxTest, docStore, 'RxTestBasicTypesFoo.lua' )

		-- Wait for reindexing to complete.

		rxTest:expectSingleValue( indexStore:getReindexingProgress():takeLast( 1 ):pluck( 'percentComplete' ), 1 )

	end )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.rxTestSchemaVersionReindex_bulkDocValue_imageAssets_testPerformance( rxTest )

	local initialSchemaVersionList = { { sequence = 0 } }

	local newSchemaVersionList = {
		{
			sequence = 0,
		},
		{
			sequence = 1,
			platforms = { WFPlatformInfo.getOS() },
			tables = { "assets" }
		}
	}

	local expectedReindexingResults = {

		minNeedsUpdateFrom = 1,
		numberOfDocsToReindex = assetImageCount + assetVideoCount,
		reindexedDocCount = assetImageCount + assetVideoCount,
		numberOfTablesToReindex = 1,

	}

	testRealWorldSchemaVersionReindexPerformance( rxTest, initialSchemaVersionList,
								newSchemaVersionList, expectedReindexingResults,
								false, true )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.rxTestSchemaVersionReindex_bulkDocValue_albumCollections_testPerformance( rxTest )

	local initialSchemaVersionList = { { sequence = 0 } }

	local newSchemaVersionList = {
		{
			sequence = 0,
		},
		{
			sequence = 1,
			platforms = { WFPlatformInfo.getOS() },
			tables = { "albums" }
		}
	}

	local expectedReindexingResults = {

		minNeedsUpdateFrom = 1,
		numberOfDocsToReindex = albumCollectionCount,
		reindexedDocCount = albumCollectionCount,
		numberOfTablesToReindex = 1,

	}

	testRealWorldSchemaVersionReindexPerformance( rxTest, initialSchemaVersionList,
								newSchemaVersionList, expectedReindexingResults,
								false, true )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.rxTestSchemaVersionReindex_bulkDocValue_allDocs_testPerformance( rxTest )

	local initialSchemaVersionList = { { sequence = 0 } }

	local newSchemaVersionList = {
		{
			sequence = 0,
		},
		{
			sequence = 1,
			platforms = { WFPlatformInfo.getOS() },
			tables = { "albums", 'assets', 'album_assets' }
		}
	}

	local expectedReindexingResults = {

		minNeedsUpdateFrom = 1,
		numberOfDocsToReindex = albumCollectionCount + assetImageCount + assetVideoCount + albumAssetCount,
		reindexedDocCount = albumCollectionCount + assetImageCount + assetVideoCount + albumAssetCount,
		numberOfTablesToReindex = 3,

	}

	testRealWorldSchemaVersionReindexPerformance( rxTest, initialSchemaVersionList,
								newSchemaVersionList, expectedReindexingResults,
								false, true )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.rxTestSchemaVersionReindex_changedDocs_imageAssets_testPerformance( rxTest )

	local initialSchemaVersionList = { { sequence = 0 } }

	local newSchemaVersionList = {
		{
			sequence = 0,
		},
		{
			sequence = 1,
			platforms = { WFPlatformInfo.getOS() },
			tables = { "assets" },
		}
	}

	local expectedReindexingResults = {

		minNeedsUpdateFrom = 1,
		numberOfDocsToReindex = assetImageCount + assetVideoCount,
		reindexedDocCount = assetImageCount + assetVideoCount,
		numberOfTablesToReindex = 1,

	}

	testRealWorldSchemaVersionReindexPerformance( rxTest, initialSchemaVersionList,
								newSchemaVersionList, expectedReindexingResults,
								true, false )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.rxTestSchemaVersionReindex_changedDocs_albumCollections_testPerformance( rxTest )

	local initialSchemaVersionList = { { sequence = 0 } }

	local newSchemaVersionList = {
		{
			sequence = 0,
		},
		{
			sequence = 1,
			platforms = { WFPlatformInfo.getOS() },
			tables = { "albums" }
		}
	}

	local expectedReindexingResults = {

		minNeedsUpdateFrom = 1,
		numberOfDocsToReindex = albumCollectionCount,
		reindexedDocCount = albumCollectionCount,
		numberOfTablesToReindex = 1,

	}

	testRealWorldSchemaVersionReindexPerformance( rxTest, initialSchemaVersionList,
								newSchemaVersionList, expectedReindexingResults,
								true, false )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.rxTestSchemaVersionReindex_changedDocs_allDocs_testPerformance( rxTest )

	local initialSchemaVersionList = { { sequence = 0 } }

	local newSchemaVersionList = {
		{
			sequence = 0,
		},
		{
			sequence = 1,
			platforms = { WFPlatformInfo.getOS() },
			tables = { "albums", 'assets', 'album_assets' }
		}
	}

	local expectedReindexingResults = {

		minNeedsUpdateFrom = 1,
		numberOfDocsToReindex = albumCollectionCount + assetImageCount + assetVideoCount + albumAssetCount,
		reindexedDocCount = albumCollectionCount + assetImageCount + assetVideoCount + albumAssetCount,
		numberOfTablesToReindex = 3,

	}

	testRealWorldSchemaVersionReindexPerformance( rxTest, initialSchemaVersionList,
								newSchemaVersionList, expectedReindexingResults,
								true, false )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.rxTestHashBasedReindex_imageAssets_testPerformance( rxTest )

	testRealWorldHashBasedReindexPerformance( rxTest, { 'assets' } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.rxTestHashBasedReindex_albumCollections_testPerformance( rxTest )

	testRealWorldHashBasedReindexPerformance( rxTest, { 'albums' } )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest.rxTestHashBasedReindex_allDocs_testPerformance( rxTest )

	testRealWorldHashBasedReindexPerformance( rxTest, { 'albums', 'assets', 'album_assets' } )

end

--------------------------------------------------------------------------------

local logger = WFLogger( 'ManualReindexingTest' )
--logger:enable( 'print' )

-- TODO [jandrew 2024-10-16]: It would be great to make the following tests leverage
-- database files hosted on artifactory.

local function testSchemaVersionReindex_manualDocStore_testPerformance( rxTest, useBulkDocValue )

	rxTest:setTimeLimit( 1000 )

	-- In order to run this test, you need a real doc store and index. Place the database
	-- files in some accessible folder, and then update the following paths:

	local manualDocStorePath = "/Users/jeffreyandrew/Desktop/small/db.mcat" -- Path to your doc store
	local manualIndexStorePath = "/Users/jeffreyandrew/Desktop/small/db.wfindex" -- Path to your doc store
	local dbPath = "/Users/jeffreyandrew/Desktop/reindex" -- Path to where the above dbs will be copied.

	local initialSchemaVersionList = { { sequence = 0 } }

	local newSchemaVersionList = WFRxIndexStoreSchemaVersionList.getSchemaVersionList()

	if not useBulkDocValue then
		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaChangedDocs', 'true' )
	else
		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaBulkDocValue', 'true' )
	end

	rxTest:finally( function()
		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaBulkDocValue', nil )
		WFCapabilities.setCapability( 'WFRxIndexStoreTest.testReindexViaChangedDocs', nil )
	end )

	logger:infof( "Initial Schema Version List: %s", initialSchemaVersionList )

	logger:infof( "Final Schema Version List: %s", newSchemaVersionList )

	WFFileUtils.deleteDirectory( dbPath )
	WFFileUtils.mkdir( dbPath )

	local docStorePath = KSPathUtils.child( dbPath, 'db.mcat' )
	local indexStorePath = KSPathUtils.child( dbPath, 'db.wfindex' )

	WFFileUtils.copy( manualIndexStorePath, indexStorePath )
	WFFileUtils.copy( manualDocStorePath, docStorePath )

	logger:infof( "Create Doc Store At Path: %s", docStorePath )

	docStore = WFRxDocStore( docStorePath )

	rxTest:finally( function()
		docStore:close()
		docStore = nil
	end )

	-- Init and close the index store so that we have something to reindex.

	logger:infof( "Overriding Initial Schema Version List" )

	overrideSchemaVersionList( rxTest, initialSchemaVersionList )

	logger:infof( "Spinning up Index Store" )

	indexStore = createIndexStoreWithNoReadOnlyThreads( rxTest, docStore, 'WFCatIndexDefinitions.lua' )
	rxTest:expectSingleValue( indexStore:getReindexingProgress()
		:tap( function( progress )
			logger:infof( "Reindexing Progress: %.2f", progress and progress.percentComplete or 0 )
		end )
		:takeLast( 1 )
		:pluck( 'percentComplete' ), 1 )

	rxTest:expectOnlyCompleted( indexStore:halt() )
	indexStore:close()
	indexStore = nil

	rxTest:sleep( 2 )
		-- Sleep before creating the new index store. This gives time for the previous index store
		-- thread to fully close.

 	--Now recreate the index store using a new schema version.

	overrideSchemaVersionList( rxTest, newSchemaVersionList )

	rxTest:measurePerformance( function()

		indexStore = createIndexStoreWithNoReadOnlyThreads( rxTest, docStore, 'WFCatIndexDefinitions.lua' )

		-- Wait for reindexing to complete.

			rxTest:expectSingleValue( indexStore:getReindexingProgress()
				:tap( function( progress )
					logger:infof( "Reindexing Progress: %.2f", progress and progress.percentComplete or 0 )
				end )
				:takeLast( 1 )
				:pluck( 'percentComplete' ), 1 )

	end )

	indexStore:close()
	indexStore = nil

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest._DISABLE_rxTestSchemaVersionReindex_changedDocs_manualDocStore_testPerformance( rxTest )

	testSchemaVersionReindex_manualDocStore_testPerformance( rxTest, false )

end

--------------------------------------------------------------------------------

function WFRxIndexStoreTest._DISABLE_rxTestSchemaVersionReindex_bulkDocValue_manualDocStore_testPerformance( rxTest )

	testSchemaVersionReindex_manualDocStore_testPerformance( rxTest, true )

end

--------------------------------------------------------------------------------

return WFRxTestCase.wrapSuite( 'WFRxIndexStoreTest', WFRxIndexStoreTest )
