/*
 * src/bin/pgcopydb/compare.c
 *     Implementation of a CLI to copy a database between two Postgres instances
 */

#include <errno.h>
#include <inttypes.h>
#include <sys/wait.h>
#include <unistd.h>

#include "catalog.h"
#include "copydb.h"
#include "env_utils.h"
#include "file_utils.h"
#include "lock_utils.h"
#include "log.h"
#include "multi_db.h"
#include "progress.h"
#include "schema.h"
#include "signals.h"
#include "summary.h"


static bool compare_queue_table_hook(void *ctx, SourceTable *sourceTable);
static bool compare_schemas_table_hook(void *ctx, SourceTable *sourceTable);
static bool compare_schemas_index_hook(void *ctx, SourceIndex *sourceIndex);
static bool compare_schemas_seq_hook(void *ctx, SourceSequence *sourceSeq);
static bool compare_alldb_chksum_hook(void *ctx, SourceTable *table);


/*
 * compare_data fetches the schema on the source database and then compute the
 * rowcount and checksum of every selected table's contents to compare them.
 */
bool
compare_data(CopyDataSpec *copySpecs)
{
	Queue compareQueue = { 0 };

	/* use a queue to share the workload */
	if (!queue_create(&compareQueue, "compare"))
	{
		log_error("Failed to create the compare data process queue");
		return false;
	}

	/*
	 * Retrieve catalogs from the source database, the target is
	 * supposed to have the same objects.
	 */
	ConnStrings *dsn = &(copySpecs->connStrings);

	log_info("SOURCE: Connecting to \"%s\"", dsn->safeSourcePGURI.pguri);

	/*
	 * Reduce the catalog queries to the section we need here, and make sure we
	 * don't prepare the target catalogs.
	 */
	copySpecs->section = DATA_SECTION_TABLE_DATA;

	char *target_pguri = copySpecs->connStrings.target_pguri;
	copySpecs->connStrings.target_pguri = NULL;

	if (!copydb_fetch_schema_and_prepare_specs(copySpecs))
	{
		log_fatal("Failed to retrieve source database schema, "
				  "see above for details.");

		(void) queue_unlink(&compareQueue);
		return false;
	}

	/* cache invalidation for the computed checksums */
	DatabaseCatalog *sourceDB = &(copySpecs->catalogs.source);

	if (!catalog_init(sourceDB))
	{
		log_error("Failed to open internal catalogs in COPY worker process, "
				  "see above for details");
		return false;
	}

	if (!catalog_delete_s_table_chksum_all(sourceDB))
	{
		/* errors have already been logged */
		return false;
	}

	/* restore the target_pguri, we will need it later */
	copySpecs->connStrings.target_pguri = target_pguri;

	/* we start copySpecs->tableJobs workers to share the workload */
	if (!compare_start_workers(copySpecs, &compareQueue))
	{
		log_fatal("Failed to start %d compare data workers",
				  copySpecs->tableJobs);

		(void) queue_unlink(&compareQueue);
		return false;
	}

	/* now, add the tables to compare to the queue */
	if (!compare_queue_tables(copySpecs, &compareQueue))
	{
		log_fatal("Failed to queue tables to compare");

		(void) queue_unlink(&compareQueue);
		return false;
	}

	/* and wait until the compare data workers are done */
	if (!copydb_wait_for_subprocesses(copySpecs->failFast))
	{
		log_fatal("Some compare data worker process have failed, "
				  "see above for details");

		(void) queue_unlink(&compareQueue);
		return false;
	}

	if (!queue_unlink(&compareQueue))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_close(sourceDB))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * compare_queue_tables adds table to our queue.
 */
bool
compare_queue_tables(CopyDataSpec *copySpecs, Queue *queue)
{
	DatabaseCatalog *sourceDB = &(copySpecs->catalogs.source);

	/* now append the table OIDs to the queue */
	if (!catalog_iter_s_table(sourceDB, queue, &compare_queue_table_hook))
	{
		log_error("Failed to compare tables, see above for details");
		return false;
	}

	/* now append the STOP messages to the queue */
	for (int i = 0; i < copySpecs->tableJobs; i++)
	{
		QMessage stop = { .type = QMSG_TYPE_STOP, .data.oid = 0 };

		log_trace("Adding STOP message to compare queue %d", queue->qId);

		if (!queue_send(queue, &stop))
		{
			/* errors have already been logged */
			continue;
		}
	}

	return true;
}


/*
 * compare_queue_table_hook is an iterator callback function.
 */
static bool
compare_queue_table_hook(void *ctx, SourceTable *table)
{
	Queue *queue = (Queue *) ctx;

	if (asked_to_stop || asked_to_stop_fast || asked_to_quit)
	{
		log_error("Compare data has been interrupted");
		return false;
	}

	QMessage mesg = {
		.type = QMSG_TYPE_TABLEOID,
		.data.oid = table->oid
	};

	log_trace("compare_queue_tables(%d): %u", queue->qId, table->oid);

	if (!queue_send(queue, &mesg))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * compare_start_workers create as many sub-process as needed, per --table-jobs.
 */
bool
compare_start_workers(CopyDataSpec *copySpecs, Queue *queue)
{
	log_info("Starting %d table compare processes", copySpecs->tableJobs);

	for (int i = 0; i < copySpecs->tableJobs; i++)
	{
		/*
		 * Flush stdio channels just before fork, to avoid double-output
		 * problems.
		 */
		fflush(stdout);
		fflush(stderr);

		int fpid = fork();

		switch (fpid)
		{
			case -1:
			{
				log_error("Failed to fork a worker process: %m");
				return false;
			}

			case 0:
			{
				/* child process runs the command */
				(void) set_ps_title("pgcopydb: compare worker");

				if (!compare_data_worker(copySpecs, queue))
				{
					/* errors have already been logged */
					exit(EXIT_CODE_INTERNAL_ERROR);
				}

				exit(EXIT_CODE_QUIT);
			}

			default:
			{
				/* fork succeeded, in parent */
				break;
			}
		}
	}

	return true;
}


/*
 * compare_data__worker is a worker process that loops over messages received
 * from a queue, each message being the Oid of a table to compare.
 */
bool
compare_data_worker(CopyDataSpec *copySpecs, Queue *queue)
{
	pid_t pid = getpid();

	log_notice("Started table worker %d [%d]", pid, getppid());

	if (!catalog_init_from_specs(copySpecs))
	{
		log_error("Failed to open internal catalogs in COPY worker process, "
				  "see above for details");
		return false;
	}

	int errors = 0;
	bool stop = false;

	while (!stop)
	{
		QMessage mesg = { 0 };
		bool recv_ok = queue_receive(queue, &mesg);

		if (asked_to_stop || asked_to_stop_fast || asked_to_quit)
		{
			log_error("Compare data worker has been interrupted");
			return false;
		}

		if (!recv_ok)
		{
			/* errors have already been logged */
			return false;
		}

		switch (mesg.type)
		{
			case QMSG_TYPE_STOP:
			{
				stop = true;
				log_debug("Stop message received by compare data worker");
				break;
			}

			case QMSG_TYPE_TABLEOID:
			{
				if (!compare_data_by_table_oid(copySpecs, mesg.data.oid))
				{
					log_error("Failed to compare table with oid %u, "
							  "see above for details",
							  mesg.data.oid);
					return false;
				}
				break;
			}

			default:
			{
				log_error("Received unknown message type %ld on vacuum queue %d",
						  mesg.type,
						  queue->qId);
				break;
			}
		}
	}

	if (!catalog_close_from_specs(copySpecs))
	{
		/* errors have already been logged */
		return false;
	}

	bool success = (stop == true && errors == 0);

	if (errors > 0)
	{
		log_error("Compare data worker %d encountered %d errors, "
				  "see above for details",
				  pid,
				  errors);
	}

	return success;
}


/*
 * compare_data_by_table_oid reads the done file for the given table OID,
 * fetches the schemaname and relname from there, and then compare the table
 * contents on the souce and target databases.
 */
bool
compare_data_by_table_oid(CopyDataSpec *copySpecs, uint32_t oid)
{
	DatabaseCatalog *sourceDB = &(copySpecs->catalogs.source);

	SourceTable *table = (SourceTable *) calloc(1, sizeof(SourceTable));

	if (table == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	if (!catalog_lookup_s_table(sourceDB, oid, 0, table))
	{
		log_error("Failed to lookup for table %u in our internal catalogs",
				  oid);

		return false;
	}

	if (table->oid == 0)
	{
		log_error("Failed to find table with oid %u in our internal catalogs",
				  oid);

		return false;
	}

	if (!catalog_s_table_fetch_attrs(sourceDB, table))
	{
		log_error("Failed to fetch table %s attribute list, "
				  "see above for details",
				  table->qname);
		return false;
	}

	log_trace("compare_data_by_table_oid: %u %s", oid, table->qname);

	if (!compare_table(copySpecs, table))
	{
		log_error("Failed to compute rowcount and checksum for %s, "
				  "see above for details",
				  table->qname);

		return false;
	}

	return true;
}


/*
 * compare_table computes the rowcount and checksum of a table contents on the
 * source and on the target database instances and compare them.
 */
bool
compare_table(CopyDataSpec *copySpecs, SourceTable *source)
{
	ConnStrings *dsn = &(copySpecs->connStrings);

	PGSQL src = { 0 };
	PGSQL dst = { 0 };

	if (!pgsql_init(&src, dsn->source_pguri, PGSQL_CONN_SOURCE))
	{
		/* errors have already been logged */
		return false;
	}

	if (!pgsql_begin(&src))
	{
		/* errors have already been logged */
		return false;
	}

	if (!pgsql_init(&dst, dsn->target_pguri, PGSQL_CONN_TARGET))
	{
		/* errors have already been logged */
		(void) pgsql_finish(&src);
		return false;
	}

	if (!pgsql_begin(&dst))
	{
		/* errors have already been logged */
		(void) pgsql_finish(&src);
		return false;
	}

	/*
	 * First, send both the queries to the source and target databases,
	 * async.
	 */
	if (!schema_send_table_checksum(&src, source))
	{
		/* errors have already been logged */
		return false;
	}

	if (!schema_send_table_checksum(&dst, source))
	{
		/* errors have already been logged */
		return false;
	}

	/*
	 * Second, fetch the results from both the connections.
	 */
	TableChecksum *srcChk = &(source->sourceChecksum);
	TableChecksum *dstChk = &(source->targetChecksum);

	bool srcDone = false;
	bool dstDone = false;

	do {
		if (!srcDone)
		{
			if (!schema_fetch_table_checksum(&src, srcChk, &srcDone))
			{
				/* errors have already been logged */
				(void) pgsql_finish(&dst);
				return false;
			}
		}

		if (!dstDone)
		{
			if (!schema_fetch_table_checksum(&dst, dstChk, &dstDone))
			{
				/* errors have already been logged */
				(void) pgsql_finish(&src);
				return false;
			}
		}

		if (!srcDone || !dstDone)
		{
			pg_usleep(10 * 1000); /* 10 ms */
		}
	} while (!srcDone || !dstDone);


	if (!pgsql_commit(&src))
	{
		/* errors have already been logged */
		return false;
	}

	if (!pgsql_commit(&dst))
	{
		/* errors have already been logged */
		return false;
	}

	DatabaseCatalog *sourceDB = &(copySpecs->catalogs.source);

	log_notice("%s %u: %lld rows, checksum %s",
			   source->qname,
			   source->oid,
			   (long long) srcChk->rowcount,
			   srcChk->checksum);

	if (!catalog_add_s_table_chksum(sourceDB, source, srcChk, dstChk))
	{
		log_error("Failed to add checksum information to our internal catalogs, "
				  "see above for details");
		return false;
	}

	if (srcChk->rowcount != dstChk->rowcount)
	{
		log_error("Table %s has %lld rows on source, %lld rows on target",
				  source->qname,
				  (long long) srcChk->rowcount,
				  (long long) dstChk->rowcount);
	}

	/* if the rowcount is different, don't log the checksum mismatch */
	else if (!streq(srcChk->checksum, dstChk->checksum))
	{
		log_error("Table %s has checksum %s on source, %s on target",
				  source->qname,
				  srcChk->checksum,
				  dstChk->checksum);
	}

	log_notice("%s: %lld rows, checksum %s",
			   source->qname,
			   (long long) srcChk->rowcount,
			   srcChk->checksum);

	return true;
}


typedef struct CompareSchemaContext
{
	DatabaseCatalog *sourceDB;
	DatabaseCatalog *targetDB;
	uint64_t diffCount;
} CompareSchemaContext;


/*
 * compare_schemas compares the schemas between source and target instance, in
 * the context and scope of pgcopydb: conpare only the selected tables,
 * indexes, constraints and sequences from the source.
 */
bool
compare_schemas(CopyDataSpec *copySpecs)
{
	/*
	 * Now prepare two specifications with only the source uri.
	 *
	 * going to share pointers to memory allocated in the main copySpecs
	 * instance.
	 */
	CopyDataSpec sourceSpecs = { 0 };
	CopyDataSpec targetSpecs = { 0 };

	if (!compare_fetch_schemas(copySpecs, &sourceSpecs, &targetSpecs))
	{
		log_fatal("Failed to fetch source and target schemas, "
				  "see above for details");
		exit(EXIT_CODE_INTERNAL_ERROR);
	}

	CatalogCounts sCount = { 0 };
	CatalogCounts tCount = { 0 };

	DatabaseCatalog *sourceDB = &(sourceSpecs.catalogs.source);
	DatabaseCatalog *targetDB = &(targetSpecs.catalogs.source);

	if (!catalog_count_objects(sourceDB, &sCount) ||
		!catalog_count_objects(targetDB, &tCount))
	{
		log_error("Failed to count indexes and constraints in our catalogs");
		return false;
	}

	log_info("[SOURCE] table: %lld, index: %lld, constraint: %lld, sequence: %lld",
			 (long long) sCount.tables,
			 (long long) sCount.indexes,
			 (long long) sCount.constraints,
			 (long long) sCount.sequences);

	log_info("[TARGET] table: %lld, index: %lld, constraint: %lld, sequence: %lld",
			 (long long) tCount.tables,
			 (long long) tCount.indexes,
			 (long long) tCount.constraints,
			 (long long) tCount.sequences);

	CompareSchemaContext context = {
		.sourceDB = sourceDB,
		.targetDB = targetDB,
		.diffCount = 0
	};

	if (!catalog_iter_s_table(sourceDB, &context, &compare_schemas_table_hook))
	{
		log_error("Failed to compare tables, see above for details");
		return false;
	}

	if (!catalog_iter_s_index(sourceDB, &context, &compare_schemas_index_hook))
	{
		log_error("Failed to compare indexes, see above for details");
		return false;
	}

	if (!catalog_iter_s_seq(sourceDB, &context, &compare_schemas_seq_hook))
	{
		log_error("Failed to compare sequences, see above for details");
		return false;
	}

	if (context.diffCount > 0)
	{
		log_fatal("Schemas on source and target database differ");
		exit(EXIT_CODE_INTERNAL_ERROR);
	}

	log_info("pgcopydb schema inspection is successful");

	return true;
}


/*
 * compare_schemas_table_hook is an iterator callback function.
 */
static bool
compare_schemas_table_hook(void *ctx, SourceTable *sourceTable)
{
	CompareSchemaContext *context = (CompareSchemaContext *) ctx;

	SourceTable *targetTable = (SourceTable *) calloc(1, sizeof(SourceTable));

	if (targetTable == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	if (!catalog_lookup_s_table_by_name(context->targetDB,
										sourceTable->nspname,
										sourceTable->relname,
										targetTable))
	{
		log_error("Failed to lookup for table \"%s\".\"%s\" in our "
				  "internal target catalogs",
				  sourceTable->nspname,
				  sourceTable->relname);
		free(targetTable);
		return false;
	}

	if (targetTable->oid == 0)
	{
		++context->diffCount;
		log_error("Failed to find table %s in target database",
				  sourceTable->qname);
		free(targetTable);
		return true;
	}

	/* now fetch table attributes lists */
	if (!catalog_s_table_fetch_attrs(context->sourceDB, sourceTable) ||
		!catalog_s_table_fetch_attrs(context->targetDB, targetTable))
	{
		log_error("Failed to fetch table %s attribute list, "
				  "see above for details",
				  sourceTable->qname);
		free(targetTable);
		return false;
	}

	int diffsBefore = context->diffCount;

	/* check table columns */
	if (sourceTable->attributes.count != targetTable->attributes.count)
	{
		++context->diffCount;
		log_error("Table %s has %d columns on source, %d columns on target",
				  sourceTable->qname,
				  sourceTable->attributes.count,
				  targetTable->attributes.count);

		/*
		 * TODO: a future improvement could let the caller choose a
		 * reconciliation strategy — for example, allowing extra columns on the
		 * target (added post-migration) to be ignored.  For now we match
		 * columns by name so the caller still learns about individual missing
		 * columns even when the total counts differ.
		 */
	}

	/*
	 * Compare columns by name rather than by position.  We scan the target
	 * attribute list for each source column (N*M loop).  This is correct when
	 * column order or column sets differ, and is fast enough because tables
	 * have a small, bounded number of attributes in practice.
	 *
	 * A cross-database SQLite JOIN (via ATTACH) could do this in a single
	 * query, but it would require exposing the catalog file path and managing
	 * an ATTACH/DETACH around the lookup; the extra complexity is not worth it
	 * given the small N.
	 */
	for (int s = 0; s < sourceTable->attributes.count; s++)
	{
		SourceTableAttribute *srcAttr = &sourceTable->attributes.array[s];
		SourceTableAttribute *tgtAttr = NULL;

		for (int t = 0; t < targetTable->attributes.count; t++)
		{
			if (streq(srcAttr->attname, targetTable->attributes.array[t].attname))
			{
				tgtAttr = &targetTable->attributes.array[t];
				break;
			}
		}

		if (tgtAttr == NULL)
		{
			++context->diffCount;
			log_error("Table %s column \"%s\" exists on source but not on target",
					  sourceTable->qname,
					  srcAttr->attname);
		}
	}

	if (context->diffCount == diffsBefore)
	{
		log_notice("Matched table %s with %d columns ok",
				   sourceTable->qname,
				   sourceTable->attributes.count);
	}

	free(targetTable);

	return true;
}


/*
 * compare_schemas_index_hook is an iterator callback function.
 */
static bool
compare_schemas_index_hook(void *ctx, SourceIndex *sourceIndex)
{
	CompareSchemaContext *context = (CompareSchemaContext *) ctx;

	SourceIndex *targetIndex = (SourceIndex *) calloc(1, sizeof(SourceIndex));

	if (targetIndex == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	if (!catalog_lookup_s_index_by_name(context->targetDB,
										sourceIndex->indexNamespace,
										sourceIndex->indexRelname,
										targetIndex))
	{
		log_error("Failed to lookup for index \"%s\".\"%s\" in our "
				  "internal target catalogs",
				  sourceIndex->indexNamespace,
				  sourceIndex->indexRelname);
		free(targetIndex);
		return false;
	}

	if (targetIndex->indexOid == 0)
	{
		++context->diffCount;
		log_error("Failed to find index %s in target database",
				  sourceIndex->indexQname);
		free(targetIndex);
		return true;
	}

	int diffsBefore = context->diffCount;

	if (!streq(sourceIndex->indexNamespace, targetIndex->indexNamespace) ||
		!streq(sourceIndex->indexRelname, targetIndex->indexRelname))
	{
		++context->diffCount;
		log_error("Table %s index mismatch: %s on source, %s on target",
				  sourceIndex->indexQname,
				  sourceIndex->indexQname,
				  targetIndex->indexQname);
	}

	if (!streq(sourceIndex->indexDef, targetIndex->indexDef))
	{
		++context->diffCount;
		log_error("Table %s index %s mismatch on index definition",
				  sourceIndex->indexQname,
				  sourceIndex->indexQname);

		log_info("Source index %s: %s",
				 sourceIndex->indexQname,
				 sourceIndex->indexDef);

		log_info("Target index %s: %s",
				 targetIndex->indexQname,
				 targetIndex->indexDef);
	}

	if (sourceIndex->isPrimary != targetIndex->isPrimary)
	{
		++context->diffCount;
		log_error("Table %s index %s is %s on source "
				  "and %s on target",
				  sourceIndex->indexQname,
				  sourceIndex->indexQname,
				  sourceIndex->isPrimary ? "primary" : "not primary",
				  targetIndex->isPrimary ? "primary" : "not primary");
	}

	if (sourceIndex->isUnique != targetIndex->isUnique)
	{
		++context->diffCount;
		log_error("Table %s index %s is %s on source "
				  "and %s on target",
				  sourceIndex->indexQname,
				  sourceIndex->indexQname,
				  sourceIndex->isUnique ? "unique" : "not unique",
				  targetIndex->isUnique ? "unique" : "not unique");
	}

	if (!streq(sourceIndex->constraintName, targetIndex->constraintName))
	{
		++context->diffCount;
		log_error("Table %s index %s is supporting "
				  " constraint named %s on source "
				  "and %s on target",
				  sourceIndex->indexQname,
				  sourceIndex->indexQname,
				  sourceIndex->constraintName,
				  targetIndex->constraintName);
	}

	if (sourceIndex->constraintDef != NULL &&
		(targetIndex->constraintDef == NULL ||
		 !streq(sourceIndex->constraintDef, targetIndex->constraintDef)))
	{
		++context->diffCount;
		log_error("Table %s index %s constraint %s "
				  "definition mismatch.",
				  sourceIndex->indexQname,
				  sourceIndex->indexQname,
				  sourceIndex->constraintName);

		log_info("Source index %s constraint %s: %s",
				 sourceIndex->indexQname,
				 sourceIndex->constraintName,
				 sourceIndex->constraintDef);

		log_info("Target index %s constraint %s: %s",
				 targetIndex->indexQname,
				 targetIndex->constraintName,
				 targetIndex->constraintDef);
	}

	if (context->diffCount == diffsBefore)
	{
		log_notice("Matched index %s ok (%s, %s)",
				   sourceIndex->indexQname,
				   sourceIndex->isPrimary ? "primary" : "not primary",
				   sourceIndex->isUnique ? "unique" : "not unique");
	}

	free(targetIndex);

	return true;
}


/*
 * compare_schemas_seq_hook is an iterator callback function.
 */
static bool
compare_schemas_seq_hook(void *ctx, SourceSequence *sourceSeq)
{
	CompareSchemaContext *context = (CompareSchemaContext *) ctx;

	SourceSequence *targetSeq =
		(SourceSequence *) calloc(1, sizeof(SourceSequence));

	if (targetSeq == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	if (!catalog_lookup_s_seq_by_name(context->targetDB,
									  sourceSeq->nspname,
									  sourceSeq->relname,
									  targetSeq))
	{
		log_error("Failed to lookup for seq \"%s\".\"%s\" in our "
				  "internal target catalogs",
				  sourceSeq->nspname,
				  sourceSeq->relname);
		free(targetSeq);
		return false;
	}

	if (targetSeq->oid == 0)
	{
		++context->diffCount;
		log_error("Failed to find seq %s in target database",
				  sourceSeq->qname);
		free(targetSeq);
		return true;
	}

	int diffsBefore = context->diffCount;

	if (sourceSeq->lastValue != targetSeq->lastValue)
	{
		++context->diffCount;
		log_error("Sequence %s lastValue on source is %lld, on target %lld",
				  sourceSeq->qname,
				  (long long) sourceSeq->lastValue,
				  (long long) targetSeq->lastValue);
	}

	if (sourceSeq->isCalled != targetSeq->isCalled)
	{
		++context->diffCount;
		log_error("Sequence %s isCalled on source is %s, on target %s",
				  sourceSeq->qname,
				  sourceSeq->isCalled ? "yes" : "no",
				  targetSeq->isCalled ? "yes" : "no");
	}

	if (context->diffCount == diffsBefore)
	{
		log_notice("Matched sequence %s (last value %lld)",
				   sourceSeq->qname,
				   (long long) sourceSeq->lastValue);
	}

	free(targetSeq);

	return true;
}


/*
 * compare_fetch_schemas fetches the source and target schemas.
 */
bool
compare_fetch_schemas(CopyDataSpec *copySpecs,
					  CopyDataSpec *sourceSpecs,
					  CopyDataSpec *targetSpecs)
{
	/* copy the structure instances over */
	*sourceSpecs = *copySpecs;
	*targetSpecs = *copySpecs;

	/*
	 * Tweak the sourceSpecs so that we bypass retrieving catalog information
	 * about the target database entirely.
	 */
	ConnStrings *sourceConnStrings = &(sourceSpecs->connStrings);

	sourceConnStrings->target_pguri = NULL;

	CopyFilePaths *s_cfPaths = &(sourceSpecs->cfPaths);

	char sourceDir[MAXPGPATH] = { 0 };

	sformat(sourceDir, sizeof(sourceDir), "%s/source", s_cfPaths->schemadir);

	if (!copydb_rmdir_or_mkdir(sourceDir, true))
	{
		/* errors have already been logged */
		return false;
	}

	struct db
	{
		char *name;
		DatabaseCatalog *db;
	}
	dbs[] =
	{
		{ .name = "source", .db = &(sourceSpecs->catalogs.source) },
		{ .name = "filter", .db = &(sourceSpecs->catalogs.filter) },
		{ .name = "target", .db = &(sourceSpecs->catalogs.target) },
		{ NULL, NULL }
	};

	for (int i = 0; dbs[i].name != NULL; i++)
	{
		struct db *d = &(dbs[i]);
		sformat(d->db->dbfile, MAXPGPATH, "%s/%s.db", sourceDir, d->name);
	}

	/* ensure cache invalidation here, also refrain from filtering prep */
	sourceSpecs->fetchCatalogs = true;
	sourceSpecs->fetchFilteredOids = false;

	/*
	 * Retrieve our internal representation of the catalogs for both the source
	 * and the target database.
	 */
	log_info("SOURCE: Connecting to \"%s\"",
			 sourceConnStrings->safeSourcePGURI.pguri);

	if (!copydb_fetch_schema_and_prepare_specs(sourceSpecs))
	{
		log_fatal("Failed to retrieve source database schema, "
				  "see above for details.");
		return false;
	}

	/*
	 * Tweak the targetSpecs so that we fetch catalogs using the same code as
	 * for the source database, but target the target catalog database instead.
	 */
	ConnStrings *targetConnStrings = &(targetSpecs->connStrings);

	targetConnStrings->source_pguri = targetConnStrings->target_pguri;
	targetConnStrings->target_pguri = NULL;

	targetConnStrings->safeSourcePGURI = targetConnStrings->safeTargetPGURI;

	CopyFilePaths *t_cfPaths = &(targetSpecs->cfPaths);

	char targetDir[MAXPGPATH] = { 0 };

	sformat(targetDir, sizeof(targetDir), "%s/target", t_cfPaths->schemadir);

	if (!copydb_rmdir_or_mkdir(targetDir, true))
	{
		/* errors have already been logged */
		return false;
	}

	struct db dbt[] =
	{
		{ .name = "source", .db = &(targetSpecs->catalogs.source) },
		{ .name = "filter", .db = &(targetSpecs->catalogs.filter) },
		{ .name = "target", .db = &(targetSpecs->catalogs.target) },
		{ NULL, NULL }
	};

	for (int i = 0; dbt[i].name != NULL; i++)
	{
		struct db *d = &(dbt[i]);
		sformat(d->db->dbfile, MAXPGPATH, "%s/%s.db", targetDir, d->name);
	}

	/* ensure cache invalidation here, also refrain from filtering prep */
	targetSpecs->fetchCatalogs = true;
	targetSpecs->fetchFilteredOids = false;

	log_info("TARGET: Connecting to \"%s\"",
			 targetConnStrings->safeSourcePGURI.pguri);

	if (!copydb_fetch_schema_and_prepare_specs(targetSpecs))
	{
		log_fatal("Failed to retrieve source database schema, "
				  "see above for details.");
		return false;
	}

	return true;
}


/*
 * compare_wait_any_child waits for any child process to exit and returns
 * true iff it exited with EXIT_CODE_QUIT (success).
 */
static bool
compare_wait_any_child(void)
{
	int status = 0;
	pid_t pid = waitpid(-1, &status, 0);

	if (pid < 0)
	{
		log_error("waitpid: %m");
		return false;
	}

	if (!WIFEXITED(status))
	{
		log_error("Compare worker %d terminated by signal %d",
				  pid, WTERMSIG(status));
		return false;
	}

	return WEXITSTATUS(status) == EXIT_CODE_QUIT;
}


/*
 * compare_one_database_schema sets up per-database specs and runs
 * compare_schemas for a single named database.  Returns false on error;
 * compare_schemas itself may call exit() on schema differences.
 */
static bool
compare_one_database_schema(CopyDataSpec *parentSpecs, const char *datname)
{
	char *srcuri = NULL;
	char *tgturi = NULL;

	if (!multidb_build_uri_for_database(parentSpecs->connStrings.source_pguri,
										datname, &srcuri) ||
		!multidb_build_uri_for_database(parentSpecs->connStrings.target_pguri,
										datname, &tgturi))
	{
		log_error("Failed to build URIs for database \"%s\"", datname);
		free(srcuri);
		free(tgturi);
		return false;
	}

	char dbdir[MAXPGPATH] = { 0 };

	sformat(dbdir, sizeof(dbdir), "%s/db/%s",
			parentSpecs->cfPaths.topdir, datname);

	CopyDataSpec dbSpecs = *parentSpecs;

	dbSpecs.catalogs.source.db = NULL;
	dbSpecs.catalogs.filter.db = NULL;
	dbSpecs.catalogs.target.db = NULL;
	dbSpecs.catalogs.replay.db = NULL;

	dbSpecs.connStrings.source_pguri = srcuri;
	dbSpecs.connStrings.target_pguri = tgturi;

	if (!parse_and_scrub_connection_string(srcuri,
										   &dbSpecs.connStrings.safeSourcePGURI))
	{
		log_error("Failed to scrub source URI for database \"%s\"", datname);
		free(srcuri);
		free(tgturi);
		return false;
	}

	if (!parse_and_scrub_connection_string(tgturi,
										   &dbSpecs.connStrings.safeTargetPGURI))
	{
		log_error("Failed to scrub target URI for database \"%s\"", datname);
		free(srcuri);
		free(tgturi);
		if (dbSpecs.connStrings.safeSourcePGURI.pguri)
		{
			free(dbSpecs.connStrings.safeSourcePGURI.pguri);
		}
		return false;
	}

	if (!copydb_init_workdir(&dbSpecs, dbdir, false, NULL,
							 parentSpecs->restart, parentSpecs->resume, true))
	{
		log_error("Failed to init work dir for database \"%s\"", datname);
		free(srcuri);
		free(tgturi);
		if (dbSpecs.connStrings.safeSourcePGURI.pguri)
		{
			free(dbSpecs.connStrings.safeSourcePGURI.pguri);
		}
		if (dbSpecs.connStrings.safeTargetPGURI.pguri)
		{
			free(dbSpecs.connStrings.safeTargetPGURI.pguri);
		}
		return false;
	}

	bool ok = compare_schemas(&dbSpecs);

	free(srcuri);
	free(tgturi);
	if (dbSpecs.connStrings.safeSourcePGURI.pguri)
	{
		free(dbSpecs.connStrings.safeSourcePGURI.pguri);
	}
	if (dbSpecs.connStrings.safeTargetPGURI.pguri)
	{
		free(dbSpecs.connStrings.safeTargetPGURI.pguri);
	}

	return ok;
}


/*
 * compare_alldb_schema_summary prints a table showing per-database object
 * counts after all schema comparisons have completed.
 *
 * Format:
 *   Database         |   Tables    |   Indexes   | Constraints |  Sequences
 *                    | Src | Tgt   | Src | Tgt   | Src | Tgt   | Src | Tgt
 *   -----------------+-----+-------+-----+-------+-----+-------+-----+------
 *   chinook          |  11 |    11 |  22 |    22 |  11 |    11 |   4 |     4
 */
static void
compare_alldb_schema_summary(CopyDataSpec *parentSpecs,
							 const char (*datnames)[PG_NAMEDATALEN],
							 int dbCount)
{
	if (dbCount <= 0)
	{
		return;
	}

	/* Collect per-database counts */
	typedef struct
	{
		char datname[PG_NAMEDATALEN];
		CatalogCounts src;
		CatalogCounts tgt;
	} DbCounts;

	DbCounts *counts = (DbCounts *) calloc((size_t) dbCount, sizeof(DbCounts));

	if (counts == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return;
	}

	int maxNameLen = 8; /* "Database" */

	for (int i = 0; i < dbCount; i++)
	{
		strlcpy(counts[i].datname, datnames[i], PG_NAMEDATALEN);

		int nameLen = strlen(datnames[i]);

		if (nameLen > maxNameLen)
		{
			maxNameLen = nameLen;
		}

		/* source catalog: <topdir>/db/<datname>/schema/source/source.db */
		DatabaseCatalog srcCat = { .type = DATABASE_CATALOG_TYPE_SOURCE };

		sformat(srcCat.dbfile, sizeof(srcCat.dbfile),
				"%s/db/%s/schema/source/source.db",
				parentSpecs->cfPaths.topdir, datnames[i]);

		if (catalog_init(&srcCat))
		{
			(void) catalog_count_objects(&srcCat, &counts[i].src);
			(void) catalog_close(&srcCat);
		}

		/* target catalog: <topdir>/db/<datname>/schema/target/source.db */
		DatabaseCatalog tgtCat = { .type = DATABASE_CATALOG_TYPE_SOURCE };

		sformat(tgtCat.dbfile, sizeof(tgtCat.dbfile),
				"%s/db/%s/schema/target/source.db",
				parentSpecs->cfPaths.topdir, datnames[i]);

		if (catalog_init(&tgtCat))
		{
			(void) catalog_count_objects(&tgtCat, &counts[i].tgt);
			(void) catalog_close(&tgtCat);
		}
	}

	/* Build separator string for database name column */
	char dbSep[NAMEDATALEN] = { 0 };

	for (int j = 0; j < maxNameLen && j < (int) sizeof(dbSep) - 1; j++)
	{
		dbSep[j] = '-';
	}

	fformat(stdout, "\n");
	fformat(stdout, "%-*s |   Tables    |   Indexes   | Constraints |  Sequences\n",
			maxNameLen, "Database");
	fformat(stdout, "%-*s | Src | Tgt   | Src | Tgt   | Src | Tgt   | Src | Tgt\n",
			maxNameLen, "");
	fformat(stdout, "%s-+-----+-------+-----+-------+-----+-------+-----+------\n",
			dbSep);

	for (int i = 0; i < dbCount; i++)
	{
		fformat(stdout,
				"%-*s | %3lld | %5lld | %3lld | %5lld | %3lld | %5lld | %3lld | %5lld\n",
				maxNameLen, counts[i].datname,
				(long long) counts[i].src.tables,
				(long long) counts[i].tgt.tables,
				(long long) counts[i].src.indexes,
				(long long) counts[i].tgt.indexes,
				(long long) counts[i].src.constraints,
				(long long) counts[i].tgt.constraints,
				(long long) counts[i].src.sequences,
				(long long) counts[i].tgt.sequences);
	}

	fformat(stdout, "\n");

	free(counts);
}


/*
 * compare_all_databases_schema iterates all user databases on the source
 * instance and runs compare_schemas for each one in parallel (up to
 * tableJobs concurrent workers).  After all workers finish, it prints a
 * per-database object-count summary table.
 */
bool
compare_all_databases_schema(CopyDataSpec *parentSpecs)
{
	/* initialise the instance-level catalog to get the database list */
	if (!catalog_init_from_specs(parentSpecs))
	{
		log_error("Failed to initialise instance catalog for --all-databases compare");
		return false;
	}

	DatabaseCatalog *instanceCatalog = &(parentSpecs->catalogs.source);

	PGSQL src = { 0 };

	if (!pgsql_init(&src, parentSpecs->connStrings.source_pguri, PGSQL_CONN_SOURCE))
	{
		log_error("Failed to connect to source instance for --all-databases compare");
		(void) catalog_close_from_specs(parentSpecs);
		return false;
	}

	if (!schema_list_databases(&src, instanceCatalog))
	{
		log_error("Failed to list databases for --all-databases compare schema");
		pgsql_finish(&src);
		(void) catalog_close_from_specs(parentSpecs);
		return false;
	}

	pgsql_finish(&src);

	/* Collect all datnames before forking (SQLite iterators are not fork-safe) */
	int dbCount = 0;
	char (*datnames)[PG_NAMEDATALEN] = NULL;

	{
		SourceDatabaseIterator dbIter = {
			.catalog = instanceCatalog,
			.dat = NULL
		};

		if (!catalog_iter_s_database_init(&dbIter))
		{
			(void) catalog_close_from_specs(parentSpecs);
			return false;
		}

		for (;;)
		{
			if (!catalog_iter_s_database_next(&dbIter))
			{
				break;
			}

			SourceDatabase *db = dbIter.dat;

			if (db == NULL)
			{
				break;
			}

			dbCount++;
		}

		(void) catalog_iter_s_database_finish(&dbIter);
	}

	if (dbCount == 0)
	{
		(void) catalog_close_from_specs(parentSpecs);
		return true;
	}

	datnames = (char (*)[PG_NAMEDATALEN])calloc(dbCount, PG_NAMEDATALEN);

	if (datnames == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		(void) catalog_close_from_specs(parentSpecs);
		return false;
	}

	{
		SourceDatabaseIterator dbIter = {
			.catalog = instanceCatalog,
			.dat = NULL
		};
		int idx = 0;

		if (!catalog_iter_s_database_init(&dbIter))
		{
			free(datnames);
			(void) catalog_close_from_specs(parentSpecs);
			return false;
		}

		for (;;)
		{
			if (!catalog_iter_s_database_next(&dbIter))
			{
				break;
			}

			SourceDatabase *db = dbIter.dat;

			if (db == NULL)
			{
				break;
			}

			if (idx < dbCount)
			{
				strlcpy(datnames[idx++], db->datname, PG_NAMEDATALEN);
			}
		}

		(void) catalog_iter_s_database_finish(&dbIter);
	}

	(void) catalog_close_from_specs(parentSpecs);

	/* Fork up to tableJobs parallel compare workers */
	int maxWorkers = (parentSpecs->tableJobs < dbCount)
					 ? parentSpecs->tableJobs : dbCount;

	int activeCount = 0;
	bool ok = true;

	log_info("Comparing schema for %d databases with up to %d parallel workers",
			 dbCount, maxWorkers);

	for (int i = 0; i < dbCount; i++)
	{
		if (asked_to_stop || asked_to_stop_fast || asked_to_quit)
		{
			ok = false;
			break;
		}

		/* Wait for a slot if at capacity */
		if (activeCount >= maxWorkers)
		{
			if (!compare_wait_any_child())
			{
				ok = false;
			}

			activeCount--;
		}

		log_info("Comparing schema for database \"%s\"", datnames[i]);

		fflush(stdout);
		fflush(stderr);

		pid_t pid = fork();

		if (pid < 0)
		{
			log_error("Failed to fork schema-compare worker: %m");
			ok = false;
			break;
		}
		else if (pid == 0)
		{
			bool res = compare_one_database_schema(parentSpecs, datnames[i]);
			exit(res ? EXIT_CODE_QUIT : EXIT_CODE_INTERNAL_ERROR);
		}

		activeCount++;
	}

	/* Wait for remaining workers */
	while (activeCount > 0)
	{
		if (!compare_wait_any_child())
		{
			ok = false;
		}

		activeCount--;
	}

	/* Print per-database object count summary */
	if (ok || !parentSpecs->failFast)
	{
		compare_alldb_schema_summary(parentSpecs, datnames, dbCount);
	}

	free(datnames);

	return ok;
}


/*
 * compare_alldb_chksum_hook prints one table's checksum row in the unified
 * --all-databases data comparison summary.  The datname is prepended to qname
 * to produce a fully-qualified "datname.nspname.relname" identifier.
 */
static bool
compare_alldb_chksum_hook(void *ctx, SourceTable *table)
{
	TableChecksum *srcChk = &(table->sourceChecksum);
	TableChecksum *dstChk = &(table->targetChecksum);

	char fqname[2 * PG_NAMEDATALEN + 64] = { 0 };

	if (table->datname[0] != '\0')
	{
		sformat(fqname, sizeof(fqname), "%s.%s", table->datname, table->qname);
	}
	else
	{
		strlcpy(fqname, table->qname, sizeof(fqname));
	}

	fformat(stdout, "%50s | %s | %64s | %64s \n",
			fqname,
			streq(srcChk->checksum, dstChk->checksum) ? " " : "!",
			srcChk->checksum,
			dstChk->checksum);

	return true;
}


/*
 * compare_one_database_data sets up per-database specs and runs compare_data
 * for a single named database.  Returns false on error.
 */
static bool
compare_one_database_data(CopyDataSpec *parentSpecs, const char *datname)
{
	char *srcuri = NULL;
	char *tgturi = NULL;

	if (!multidb_build_uri_for_database(parentSpecs->connStrings.source_pguri,
										datname, &srcuri) ||
		!multidb_build_uri_for_database(parentSpecs->connStrings.target_pguri,
										datname, &tgturi))
	{
		log_error("Failed to build URIs for database \"%s\"", datname);
		free(srcuri);
		free(tgturi);
		return false;
	}

	char dbdir[MAXPGPATH] = { 0 };

	sformat(dbdir, sizeof(dbdir), "%s/db/%s",
			parentSpecs->cfPaths.topdir, datname);

	CopyDataSpec dbSpecs = *parentSpecs;

	dbSpecs.catalogs.source.db = NULL;
	dbSpecs.catalogs.filter.db = NULL;
	dbSpecs.catalogs.target.db = NULL;
	dbSpecs.catalogs.replay.db = NULL;

	dbSpecs.connStrings.source_pguri = srcuri;
	dbSpecs.connStrings.target_pguri = tgturi;
	strlcpy(dbSpecs.datname, datname, sizeof(dbSpecs.datname));

	if (!parse_and_scrub_connection_string(srcuri,
										   &dbSpecs.connStrings.safeSourcePGURI))
	{
		log_error("Failed to scrub source URI for database \"%s\"", datname);
		free(srcuri);
		free(tgturi);
		return false;
	}

	if (!parse_and_scrub_connection_string(tgturi,
										   &dbSpecs.connStrings.safeTargetPGURI))
	{
		log_error("Failed to scrub target URI for database \"%s\"", datname);
		free(srcuri);
		free(tgturi);
		if (dbSpecs.connStrings.safeSourcePGURI.pguri)
		{
			free(dbSpecs.connStrings.safeSourcePGURI.pguri);
		}
		return false;
	}

	if (!copydb_init_workdir(&dbSpecs, dbdir, false, NULL,
							 parentSpecs->restart, parentSpecs->resume, true))
	{
		log_error("Failed to init work dir for database \"%s\"", datname);
		free(srcuri);
		free(tgturi);
		if (dbSpecs.connStrings.safeSourcePGURI.pguri)
		{
			free(dbSpecs.connStrings.safeSourcePGURI.pguri);
		}
		if (dbSpecs.connStrings.safeTargetPGURI.pguri)
		{
			free(dbSpecs.connStrings.safeTargetPGURI.pguri);
		}
		return false;
	}

	strlcpy(dbSpecs.catalogs.source.dbfile, dbSpecs.cfPaths.sdbfile,
			sizeof(dbSpecs.catalogs.source.dbfile));
	strlcpy(dbSpecs.catalogs.filter.dbfile, dbSpecs.cfPaths.fdbfile,
			sizeof(dbSpecs.catalogs.filter.dbfile));
	strlcpy(dbSpecs.catalogs.target.dbfile, dbSpecs.cfPaths.tdbfile,
			sizeof(dbSpecs.catalogs.target.dbfile));

	bool ok = compare_data(&dbSpecs);

	free(srcuri);
	free(tgturi);
	if (dbSpecs.connStrings.safeSourcePGURI.pguri)
	{
		free(dbSpecs.connStrings.safeSourcePGURI.pguri);
	}
	if (dbSpecs.connStrings.safeTargetPGURI.pguri)
	{
		free(dbSpecs.connStrings.safeTargetPGURI.pguri);
	}

	return ok;
}


/*
 * compare_all_databases_data iterates all user databases on the source
 * instance, computes checksums for all tables in parallel (up to tableJobs
 * concurrent workers), and prints a single unified summary table.
 *
 * Table names in the text output use "datname.nspname.relname" format.
 */
bool
compare_all_databases_data(CopyDataSpec *parentSpecs)
{
	/* initialise the instance-level catalog to get the database list */
	if (!catalog_init_from_specs(parentSpecs))
	{
		log_error("Failed to initialise instance catalog for --all-databases compare");
		return false;
	}

	DatabaseCatalog *instanceCatalog = &(parentSpecs->catalogs.source);

	PGSQL src = { 0 };

	if (!pgsql_init(&src, parentSpecs->connStrings.source_pguri, PGSQL_CONN_SOURCE))
	{
		log_error("Failed to connect to source instance for --all-databases compare");
		(void) catalog_close_from_specs(parentSpecs);
		return false;
	}

	if (!schema_list_databases(&src, instanceCatalog))
	{
		log_error("Failed to list databases for --all-databases compare data");
		pgsql_finish(&src);
		(void) catalog_close_from_specs(parentSpecs);
		return false;
	}

	pgsql_finish(&src);

	/* Collect datnames before forking */
	int dbCount = 0;
	char (*datnames)[PG_NAMEDATALEN] = NULL;

	{
		SourceDatabaseIterator dbIter = {
			.catalog = instanceCatalog,
			.dat = NULL
		};

		if (!catalog_iter_s_database_init(&dbIter))
		{
			(void) catalog_close_from_specs(parentSpecs);
			return false;
		}

		for (;;)
		{
			if (!catalog_iter_s_database_next(&dbIter))
			{
				break;
			}

			if (dbIter.dat == NULL)
			{
				break;
			}

			dbCount++;
		}

		(void) catalog_iter_s_database_finish(&dbIter);
	}

	if (dbCount == 0)
	{
		(void) catalog_close_from_specs(parentSpecs);
		return true;
	}

	datnames = (char (*)[PG_NAMEDATALEN])calloc(dbCount, PG_NAMEDATALEN);

	if (datnames == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		(void) catalog_close_from_specs(parentSpecs);
		return false;
	}

	{
		SourceDatabaseIterator dbIter = {
			.catalog = instanceCatalog,
			.dat = NULL
		};
		int idx = 0;

		if (!catalog_iter_s_database_init(&dbIter))
		{
			free(datnames);
			(void) catalog_close_from_specs(parentSpecs);
			return false;
		}

		for (;;)
		{
			if (!catalog_iter_s_database_next(&dbIter))
			{
				break;
			}

			SourceDatabase *db = dbIter.dat;

			if (db == NULL)
			{
				break;
			}

			if (idx < dbCount)
			{
				strlcpy(datnames[idx++], db->datname, PG_NAMEDATALEN);
			}
		}

		(void) catalog_iter_s_database_finish(&dbIter);
	}

	(void) catalog_close_from_specs(parentSpecs);

	/* Fork up to tableJobs parallel workers */
	int maxWorkers = (parentSpecs->tableJobs < dbCount)
					 ? parentSpecs->tableJobs : dbCount;

	int activeCount = 0;
	bool ok = true;

	log_info("Comparing data for %d databases with up to %d parallel workers",
			 dbCount, maxWorkers);

	for (int i = 0; i < dbCount; i++)
	{
		if (asked_to_stop || asked_to_stop_fast || asked_to_quit)
		{
			ok = false;
			break;
		}

		/* Wait for a slot if at capacity */
		if (activeCount >= maxWorkers)
		{
			if (!compare_wait_any_child())
			{
				ok = false;
			}

			activeCount--;
		}

		log_info("Comparing data for database \"%s\"", datnames[i]);

		fflush(stdout);
		fflush(stderr);

		pid_t pid = fork();

		if (pid < 0)
		{
			log_error("Failed to fork data-compare worker: %m");
			ok = false;
			break;
		}
		else if (pid == 0)
		{
			bool res = compare_one_database_data(parentSpecs, datnames[i]);
			exit(res ? EXIT_CODE_QUIT : EXIT_CODE_INTERNAL_ERROR);
		}

		activeCount++;
	}

	/* Wait for remaining workers */
	while (activeCount > 0)
	{
		if (!compare_wait_any_child())
		{
			ok = false;
		}

		activeCount--;
	}

	/* Print unified checksum table (parent reads per-db catalogs) */
	fformat(stdout, "\n");
	fformat(stdout, "%50s | %s | %64s | %64s \n",
			"Table Name", "!", "Source Checksum", "Target Checksum");
	fformat(stdout, "%50s-+-%s-+-%64s-+-%64s \n",
			"--------------------------------------------------", "-",
			"----------------------------------------------------------------",
			"----------------------------------------------------------------");

	for (int i = 0; i < dbCount; i++)
	{
		/* Reopen per-db catalog to iterate checksum results */
		char sdbfile[MAXPGPATH] = { 0 };

		sformat(sdbfile, sizeof(sdbfile), "%s/db/%s/schema/source.db",
				parentSpecs->cfPaths.topdir, datnames[i]);

		DatabaseCatalog dbCat = { 0 };

		strlcpy(dbCat.dbfile, sdbfile, sizeof(dbCat.dbfile));

		if (catalog_init(&dbCat))
		{
			(void) catalog_iter_s_table(&dbCat, NULL, &compare_alldb_chksum_hook);
			(void) catalog_close(&dbCat);
		}
	}

	fformat(stdout, "\n");

	free(datnames);

	return ok;
}
