/*
 * src/bin/pgcopydb/catalog.c
 *	 Catalog management as a SQLite internal file
 */

#include <inttypes.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <time.h>
#include <unistd.h>

#include "sqlite3.h"

#include "catalog.h"
#include "cli_root.h"
#include "filtering.h"
#include "parson.h"
#include "pqexpbuffer.h"
#include "copydb.h"
#include "defaults.h"
#include "log.h"
#include "parsing_utils.h"
#include "schema.h"
#include "signals.h"
#include "string_utils.h"
#include "sql_queries.h"
#include "summary.h"


/*
 * pgcopydb catalog cache is a SQLite database with the following schema:
 */

/*
 * pgcopydb catalog cache is a SQLite database with the following schema.
 *
 * Note: indexes are split from tables for two reasons:
 *
 *  1. Two new non-unique indexes (s_i_tableoid, s_c_indexoid) are built after
 *     bulk data load so they land on a fully-populated table, which is 3-5×
 *     faster than maintaining them row-by-row.  They fix the O(N²) scan in
 *     catalog_s_table_count_indexes (called ~211K times on a 317K-row table).
 *
 *  2. catalog_create_indexes() is also called when resuming an operation on an
 *     existing catalog that pre-dates these indexes.  Using IF NOT EXISTS makes
 *     every call idempotent.
 *
 * All *other* indexes (including all UNIQUE indexes) are kept in the table DDL
 * arrays so that catalog_create_schema() installs them on the empty tables.
 * This is critical for INSERT OR IGNORE / INSERT OR REPLACE semantics: SQLite
 * needs the unique index present at insert time to suppress duplicates.
 */
static char *sourceDBcreateTableDDLs[] = {
	/*
	 * filters: JSON representation of the parsed SourceFilters struct,
	 * used to detect filter changes on resume.
	 */
	"create table setup("
	"  id integer primary key check (id = 1), "
	"  source_pg_uri text, "
	"  target_pg_uri text, "
	"  snapshot text, "
	"  split_tables_larger_than integer, "
	"  split_max_parts integer, "
	"  filters text"
	")",

	/*
	 * Dedicated table for replication slot metadata.  Replaces the slot file
	 * ($XDG_DATA_HOME/pgcopydb/slot) as the single authoritative store.
	 * One row (id = 1) per pgcopydb operation.
	 */
	"create table replication_slot("
	"  id integer primary key check (id = 1), "
	"  slot_name text not null, "
	"  lsn integer not null, "
	"  snapshot text not null, "
	"  plugin text not null, "
	"  wal2json_numeric_as_string integer not null default 0, "
	"  publication_name text not null default '', "
	"  publication_auto_managed integer not null default 0 "
	")",

	"create table section("
	"  name text primary key, fetched boolean, "
	"  start_time_epoch integer, done_time_epoch integer, duration integer"
	")",

	"create table s_database("
	"  oid integer primary key, datname text, bytes integer, bytes_pretty text,"
	"  snapshot text"
	")",

	"create table s_database_property("
	"  role_in_database boolean, rolname text, datname text, setconfig text"
	")",

	"create table s_namespace("
	"  oid integer primary key, nspname text not null, restore_list_name text"
	")",

	"create table s_table("
	"  oid integer primary key, "
	"  datname text, qname text, nspname text, relname text, amname text, "
	"  restore_list_name text, "
	"  relpages integer, reltuples integer, "
	"  exclude_data boolean, "
	"  part_key text"
	")",

	"create table s_matview("
	"  oid integer primary key, "
	"  qname text, nspname text, relname text, restore_list_name text, "
	"  exclude_data boolean"
	")",

	"create table s_table_size("
	"  oid integer primary key references s_table(oid), "
	"  bytes integer, bytes_pretty text "
	")",

	"create table s_attr("
	"  oid integer references s_table(oid), "
	"  attnum integer, attypid integer, attname text, "
	"  attisprimary bool, attisreplident bool, attisgenerated bool, "
	"  attidentity text default '', "
	"  attisbinarycompatible bool default true, "
	"  atttypsend text default '', "
	"  primary key(oid, attnum) "
	")",

	"create table s_table_part("
	"  oid integer references s_table(oid), "
	"  partnum integer, partcount integer, "
	"  min integer, max integer, count integer, "
	"  primary key(oid, partnum) "
	")",

	"create table s_table_chksum("
	"  oid integer primary key references s_table(oid), "
	"  srcrowcount integer, srcsum text, dstrowcount integer, dstsum text "
	")",

	"create table s_index("
	"  oid integer primary key, "
	"  qname text, nspname text, relname text, restore_list_name text, "
	"  tableoid references s_table(oid), "
	"  isprimary bool, isunique bool, columns text, sql text "
	")",

	"create table s_constraint("
	"  oid integer primary key, conname text, "
	"  indexoid references s_index(oid), "
	"  condeferrable bool, condeferred bool, sql text "
	")",

	"create table s_seq("
	"  oid integer, "
	"  ownedby integer, attrelid integer, attroid integer, "
	"  datname text, qname text, nspname text, relname text, "
	"  restore_list_name text, "
	"  last_value integer, isCalled bool, "
	"  primary key(oid, ownedby, attrelid, attroid)"
	")",

	/* internal activity tracking / completion / statistics */
	"create table process("
	"  pid integer primary key, "
	"  ps_type text, ps_title text, "
	"  tableoid integer references s_table(oid), "
	"  partnum integer, "
	"  indexoid integer references s_index(oid) "
	")",

	"create table timings("
	"  id integer primary key,"
	"  label text,"
	"  start_time_epoch integer, done_time_epoch integer, duration integer, "
	"  duration_pretty, "
	"  count integer, bytes integer, bytes_pretty text"
	")",

	"create table summary("
	"  pid integer, "
	"  tableoid integer references s_table(oid), "
	"  partnum integer, "
	"  indexoid integer references s_index(oid), "
	"  conoid integer references s_constraint(oid), "
	"  extoid integer, "
	"  start_time_epoch integer, done_time_epoch integer, duration integer, "
	"  bytes integer, "
	"  command text, "
	"  unique(tableoid, partnum)"
	")",

	"create table vacuum_summary("
	"  pid integer, "
	"  tableoid integer references s_table(oid), "
	"  start_time_epoch integer, done_time_epoch integer, duration integer, "
	"  unique(tableoid)"
	")",

	"create table s_table_parts_done("
	" tableoid integer primary key references s_table(oid), pid integer"
	")",

	"create table s_table_indexes_done("
	" tableoid integer primary key references s_table(oid), pid integer "
	")",

	/* use SQLite more general dynamic type system: pg_lsn is text */
	"create table sentinel("
	"  id integer primary key check (id = 1), "
	"  startpos pg_lsn, endpos pg_lsn, apply bool, "
	"  write_lsn pg_lsn, flush_lsn pg_lsn, "
	"  replay_lsn pg_lsn)",

	"create table cdc_files("
	"  id integer primary key, filename text unique, timeline integer, "
	"  startpos pg_lsn, endpos pg_lsn, "
	"  start_time_epoch integer, done_time_epoch integer)",

	"create table timeline_history("
	"  tli integer primary key, startpos pg_lsn, endpos pg_lsn)",

	/*
	 * pipeline_state — one row per CDC process (receive / apply).
	 *
	 * Written at the start and end of every run and kept current at every
	 * transaction boundary (piggybacking on sentinel_sync calls).  Used on
	 * restart to decide whether the last transaction was fully processed and
	 * whether an incomplete transaction must be skipped.
	 *
	 * LSN columns use pg_lsn (text) for consistency with the sentinel table.
	 * last_txn_end_lsn is NULL while the transaction is still open.
	 */
	"create table pipeline_state("
	"  process_name      text     primary key, "  /* 'receive'|'apply' */
	"  pid               integer, "
	"  run_state         text     not null, "     /* 'running'|'done'|'error' */
	"  run_start_lsn     pg_lsn   not null, "
	"  run_end_lsn       pg_lsn, "               /* NULL while running */
	"  started_at        integer  not null, "     /* Unix epoch */
	"  ended_at          integer, "              /* NULL while running */
	"  last_xid          integer, "
	"  last_txn_begin_lsn  pg_lsn, "
	"  last_txn_end_lsn    pg_lsn, "             /* NULL = transaction still open */
	"  last_txn_complete   integer, "            /* 1 = had COMMIT or ROLLBACK */
	"  last_txn_processed  integer"              /* 1 = fully written/applied */
	")",

	/*
	 * One row per pgcopydb invocation that opens this catalog.  Persists
	 * across filter invalidation (not cleared by catalog_delete_source_schema_data)
	 * so that operators can trace the exact command sequence that led to the
	 * current state.  Dropped and recreated only on --restart.
	 */
	"create table command_log("
	"  id integer primary key, "
	"  started_at integer not null default (strftime('%s', 'now')), "
	"  cmdline text not null"
	")",

	/* Filter entry tables — populated from SourceFilters to avoid C-side
	 * PQExpBuffer building; queried with group_concat / json1 functions. */
	"create table if not exists f_schema("
	"  section text not null, "
	"  nspname text, "
	"  nspname_re text"
	")",

	"create table if not exists f_table("
	"  section text not null, "
	"  nspname text, "
	"  nspname_re text, "
	"  relname text, "
	"  relname_re text"
	")",

	/* All indexes other than s_i_tableoid / s_c_indexoid stay here so they are
	 * created on empty tables and enforce uniqueness during data load. */
	"create index s_d_p_oid on s_database_property(datname)",
	"create unique index s_n_nspname on s_namespace(nspname)",
	"create index s_n_rlname on s_namespace(restore_list_name)",
	"create unique index s_t_qname on s_table(qname)",
	"create unique index s_t_rlname on s_table(restore_list_name)",
	"create unique index s_mv_rlname on s_matview(restore_list_name)",
	"create unique index s_mv_qname on s_matview(nspname, relname)",
	"create unique index s_ts_oid on s_table_size(oid)",
	"create index s_a_oid_attname on s_attr(oid, attname)",
	"create index s_a_attisgenerated on s_attr(attisgenerated) where attisgenerated",
	"create unique index s_i_rlname on s_index(restore_list_name)",
	"create index s_s_rlname on s_seq(restore_list_name)",
	"create unique index summary_extoid on summary(extoid) where extoid is not null"
};


/*
 * Two non-unique indexes deferred until after bulk data load.  Building them
 * on a fully-populated table is 3-5× faster and they fix the O(N²) scan in
 * catalog_s_table_count_indexes (called ~211K times against a 317K-row table):
 *
 *   s_i_tableoid — s_index rows looked up by tableoid for every table copy
 *   s_c_indexoid — s_constraint rows joined to s_index by indexoid
 *
 * IF NOT EXISTS makes every call idempotent (safe for --resume / multi-step).
 */
static char *sourceDBcreateIndexDDLs[] = {
	"create index if not exists s_i_tableoid on s_index(tableoid)",
	"create index if not exists s_c_indexoid on s_constraint(indexoid)"
};


/*
 * pgcopydb implements filtering which needs to be implement by editin the
 * `pg_restore --list` archive TOC. The TOC contains OIDs "restore list names",
 * and some TOC entries do not have an OID.
 *
 * pgcopydb catalog cache needs to enable matching TOC entries by either OID
 * for restore list names for the main SQL objects (tables, indexes,
 * constraints, dependencies).
 *
 * The schema definition used for those objects is the same as in the previous
 * section, but the data is different and the points in the code where the
 * filters are used are limited in scope, in such a way that it makes sense to
 * maintain a separate SQLite database for the filters catalog cache.
 *
 */
static char *filterDBcreateTableDDLs[] = {
	"create table section("
	"  name text primary key, fetched boolean, "
	"  start_time_epoch integer, done_time_epoch integer, duration integer"
	")",

	"create table s_coll("
	"  oid integer primary key, collname text, description text, "
	"  restore_list_name text"
	")",

	"create table s_extension("
	"  oid integer primary key, extname text, extnamespace text, "
	"  extrelocatable integer "
	")",

	"create table s_extension_config("
	"  extoid integer references s_extension(oid), "
	"  reloid integer, nspname text, relname text, condition text, "
	"  relkind integer "
	")",

	"create table s_extension_versions("
	"  oid integer, name text, default_version text, installed_version text, "
	"  versions_array text, "
	"  primary key (oid, name)"
	")",

	"create table s_namespace("
	"  oid integer primary key, nspname text, restore_list_name text "
	")",

	"create table s_table("
	"  oid integer primary key, "
	"  datname text, qname text, nspname text, relname text, amname text, "
	"  restore_list_name text, "
	"  relpages integer, reltuples integer, "
	"  exclude_data boolean, "
	"  srcrowcount integer, srcsum text, dstrowcount integer, dstsum text, "
	"  part_key text"
	")",

	"create table s_matview("
	"  oid integer primary key, "
	"  qname text, nspname text, relname text, restore_list_name text, "
	"  exclude_data boolean"
	")",

	"create table s_table_size("
	"  oid integer primary key references s_table(oid), "
	"  bytes integer, bytes_pretty text "
	")",

	"create table s_attr("
	"  oid integer references s_table(oid), "
	"  attnum integer, attypid integer, attname text, "
	"  attisprimary bool, attisreplident bool, attisgenerated bool, "
	"  attidentity text default '', "
	"  attisbinarycompatible bool default true, "
	"  atttypsend text default '', "
	"  primary key(oid, attnum) "
	")",

	"create table s_table_part("
	"  oid integer references s_table(oid), "
	"  partnum integer, partcount integer, "
	"  min integer, max integer, count integer, "
	"  primary key(oid, partnum) "
	")",

	"create table s_table_chksum("
	"  oid integer primary key references s_table(oid), "
	"  srcrowcount integer, srcsum text, dstrowcount integer, dstsum text "
	")",

	"create table s_index("
	"  oid integer primary key, "
	"  qname text, nspname text, relname text, restore_list_name text, "
	"  tableoid references s_table(oid), "
	"  isprimary bool, isunique bool, columns text, sql text "
	")",

	"create table s_constraint("
	"  oid integer primary key, conname text, "
	"  indexoid references s_index(oid), "
	"  condeferrable bool, condeferred bool, sql text "
	")",

	"create table s_seq("
	"  oid integer, "
	"  ownedby integer, attrelid integer, attroid integer, "
	"  datname text, qname text, nspname text, relname text, "
	"  restore_list_name text, "
	"  last_value integer, isCalled bool, "
	"  primary key(oid, ownedby, attrelid, attroid)"
	")",

	"create table s_depend("
	"  nspname text, relname text, "
	"  refclassid integer, refobjid integer, classid integer, objid integer, "
	"  deptype text, type text, identity text "
	")",

	/* catalog namespace lookup: OIDs of system catalog tables, fetched at runtime */
	"create table catnames(oid integer primary key, catname text unique)",

	/*
	 * The filter table is our hash-table.  catoid mirrors pg_depend.classid:
	 * it is the OID of the system catalog table that owns the object (e.g.
	 * pg_class for tables/indexes/sequences, pg_constraint for constraints).
	 * Using (catoid, oid) as the lookup key avoids false matches when OIDs
	 * from different catalog namespaces happen to share the same numeric value.
	 */
	"create table filter(catoid integer, oid integer, restore_list_name text, kind text)",

	/*
	 * extension_filter stores the user's [exclude-extension] and
	 * [include-only-extension] configuration so that catalog_prepare_filter
	 * can read it internally without needing it passed as C pointers.
	 */
	"create table extension_filter(extname text primary key, action text)",

	/*
	 * filter_*_pattern tables store the user's ~/regex/ filter rules verbatim
	 * (before expansion into exact OID lists) for debugging and change-detection.
	 *
	 * For schema patterns:  nspname_re holds the POSIX ERE pattern.
	 * For table patterns:   exactly one of (nspname, nspname_re) is non-NULL and
	 *                       exactly one of (relname, relname_re) is non-NULL.
	 *
	 * These tables are not used by the migration pipeline itself — expansion
	 * into exact (nspname, relname) pairs happens in filters_validate_and_normalize().
	 */
	"create table filter_incl_schema_pattern(nspname_re text not null)",
	"create table filter_excl_schema_pattern(nspname_re text not null)",

	"create table if not exists f_schema("
	"  section text not null, "
	"  nspname text, "
	"  nspname_re text"
	")",

	"create table if not exists f_table("
	"  section text not null, "
	"  nspname text, "
	"  nspname_re text, "
	"  relname text, "
	"  relname_re text"
	")",

	/*
	 * While we don't use a summary table in the filter database, some queries
	 * that are meant to work on both filters database and source database use
	 * LEFT JOIN summary.
	 */
	"create table summary("
	"  pid integer, "
	"  tableoid integer references s_table(oid), "
	"  partnum integer, "
	"  indexoid integer references s_index(oid), "
	"  conoid integer references s_constraint(oid), "
	"  extoid integer, "
	"  start_time_epoch integer, done_time_epoch integer, duration integer, "
	"  bytes integer, "
	"  command text, "
	"  unique(tableoid, partnum)"
	")",

	/* Indexes that must be present before data load (unique = INSERT OR IGNORE). */
	"create unique index s_coll_rlname on s_coll(restore_list_name)",
	"create index s_ec_oid on s_extension_config(extoid)",
	"create index s_n_rlname on s_namespace(restore_list_name)",
	"create unique index s_t_qname on s_table(qname)",
	"create unique index s_t_rlname on s_table(restore_list_name)",
	"create unique index s_mv_rlname on s_matview(restore_list_name)",
	"create unique index s_mv_qname on s_matview(nspname, relname)",
	"create unique index s_ts_oid on s_table_size(oid)",
	"create unique index s_i_rlname on s_index(restore_list_name)",
	"create index s_s_rlname on s_seq(restore_list_name)",
	"create index s_d_refobjid on s_depend(refobjid)",
	"create index s_d_objid on s_depend(objid)",

	/* filter_catoid_oid is critical: INSERT OR IGNORE into filter() relies on it. */
	"create unique index filter_catoid_oid on filter(catoid, oid) where oid > 0",
	"create index filter_rlname on filter(restore_list_name)"
};

/* Deferred indexes for filter DB — same two non-unique performance indexes. */
static char *filterDBcreateIndexDDLs[] = {
	"create index if not exists s_i_tableoid on s_index(tableoid)",
	"create index if not exists s_c_indexoid on s_constraint(indexoid)"
};


/*
 * Target schema objects, allowing to skip pre-existing entries.
 */
static char *targetDBcreateTableDDLs[] = {
	"create table section("
	"  name text primary key, fetched boolean, "
	"  start_time_epoch integer, done_time_epoch integer, duration integer"
	")",

	"create table s_role("
	"  oid integer primary key, rolname text"
	")",

	"create table s_namespace("
	"  nspname text primary key, restore_list_name text"
	")",

	"create table s_table("
	"  oid integer primary key, "
	"  datname text, qname text, nspname text, relname text, amname text, "
	"  restore_list_name text, "
	"  relpages integer, reltuples integer, "
	"  exclude_data boolean, "
	"  srcrowcount integer, srcsum text, dstrowcount integer, dstsum text, "
	"  part_key text"
	")",

	"create table s_attr("
	"  oid integer references s_table(oid), "
	"  attnum integer, attypid integer, attname text, "
	"  attisprimary bool, attisreplident bool, attisgenerated bool, "
	"  attidentity text default '', "
	"  attisbinarycompatible bool default true, "
	"  atttypsend text default '', "
	"  primary key(oid, attnum) "
	")",

	"create table s_index("
	"  oid integer primary key, "
	"  qname text, nspname text, relname text, restore_list_name text, "
	"  tableoid integer references s_table(oid), "
	"  isprimary bool, isunique bool, columns text, sql text "
	")",

	"create table s_constraint("
	"  oid integer primary key, conname text, "
	"  indexoid references s_index(oid), "
	"  condeferrable bool, condeferred bool, sql text "
	")",

	"create table s_rel("
	"  nspname text not null, relname text not null, "
	"  relkind char(1) not null, ispopulated bool, "
	"  primary key(nspname, relname)"
	")",

	"create table if not exists f_schema("
	"  section text not null, "
	"  nspname text, "
	"  nspname_re text"
	")",

	"create table if not exists f_table("
	"  section text not null, "
	"  nspname text, "
	"  nspname_re text, "
	"  relname text, "
	"  relname_re text"
	")",

	/* Indexes created before data load (unique indexes need to be inline). */
	"create index s_n_rlname on s_namespace(restore_list_name)",
	"create unique index s_t_qname on s_table(qname)",
	"create unique index s_t_rlname on s_table(restore_list_name)",
	"create unique index s_i_rlname on s_index(restore_list_name)"
};

/* Deferred indexes for target DB — same two non-unique performance indexes. */
static char *targetDBcreateIndexDDLs[] = {
	"create index if not exists s_i_tableoid on s_index(tableoid)",
	"create index if not exists s_c_indexoid on s_constraint(indexoid)"
};


/*
 * output.db schema — written exclusively by receive.
 * Apply opens this file read-only to iterate output rows (inline transform).
 */
static char *outputDBcreateDDLs[] = {
	"create table output("
	"  id integer primary key, "
	"  action text, xid integer, lsn integer, timestamp text, "
	"  message text, "
	"  nspname text, relname text, old_type text)",

	"create unique index o_a_lsn on output(action, lsn)",
	"create index o_a_xid on output(action, xid)",

	/*
	 * pgoutput stores structured column data here instead of a text blob.
	 * Other plugins never touch this table.
	 */
	"create table pgoutput_col("
	"  output_id integer not null, "
	"  section   text    not null, "
	"  pos       integer not null, "
	"  name      text    not null, "
	"  status    text    not null, "
	"  value     text"
	")",

	"create index pgoutput_col_idx on pgoutput_col(output_id, section, pos)",
};

/*
 * replay.db schema — written exclusively by apply.
 * stream catchup reads from this file.
 */
static char *replayDBcreateDDLs[] = {
	"create table stmt(hash text primary key, sql text)",
	"create unique index stmt_hash on stmt(hash)",

	"create table replay("
	"  id integer primary key, "
	"  action text, xid integer, lsn integer, endlsn integer, timestamp text, "
	"  nspname text, relname text, "
	"  stmt_hash text references stmt(hash), stmt_args jsonb)",

	"create index r_xid on replay(xid)",
	"create index r_lsn on replay(lsn)",
};


static char *sourceDBdropDDLs[] = {
	"drop table if exists setup",
	"drop table if exists section",

	"drop table if exists s_database",
	"drop table if exists s_database_property",
	"drop table if exists s_table",
	"drop table if exists s_matview",
	"drop table if exists s_attr",
	"drop table if exists s_table_part",
	"drop table if exists s_table_chksum",
	"drop table if exists s_table_size",
	"drop table if exists s_index",
	"drop table if exists s_constraint",
	"drop table if exists s_seq",
	"drop table if exists s_depend",

	"drop table if exists t_roles",
	"drop table if exists t_schema",
	"drop table if exists t_index",
	"drop table if exists t_constraint",

	"drop table if exists process",
	"drop table if exists summary",
	"drop table if exists s_table_parts_done",
	"drop table if exists s_table_indexes_done",

	"drop table if exists sentinel",
	"drop table if exists timeline_history",
	"drop table if exists command_log",

	"drop table if exists f_schema",
	"drop table if exists f_table"
};


static char *filterDBdropDDLs[] = {
	"drop table if exists section",

	"drop table if exists s_coll",
	"drop table if exists s_extension",
	"drop table if exists s_extension_config",
	"drop table if exists s_extension_versions",
	"drop table if exists s_namespace",
	"drop table if exists s_table",
	"drop table if exists s_matview",
	"drop table if exists s_attr",
	"drop table if exists s_table_part",
	"drop table if exists s_table_chksum",
	"drop table if exists s_table_size",
	"drop table if exists s_index",
	"drop table if exists s_constraint",
	"drop table if exists s_seq",
	"drop table if exists s_depend",
	"drop table if exists filter",
	"drop table if exists summary",

	"drop table if exists f_schema",
	"drop table if exists f_table"
};


static char *targetDBdropDDLs[] = {
	"drop table if exists section",

	"drop table if exists s_role",
	"drop table if exists s_namespace",
	"drop table if exists s_table",
	"drop table if exists s_attr",
	"drop table if exists s_index",
	"drop table if exists s_constraint",
	"drop table if exists s_rel",

	"drop table if exists f_schema",
	"drop table if exists f_table"
};


static char *outputDBdropDDLs[] = {
	"drop table if exists output"
};

static char *replayDBdropDDLs[] = {
	"drop table if exists replay",
	"drop table if exists stmt"
};


/*
 * catalog_init_from_specs initializes our internal catalog database file from
 * a specification.
 */
bool
catalog_init_from_specs(CopyDataSpec *copySpecs)
{
	DatabaseCatalog *sourceDB = &(copySpecs->catalogs.source);
	DatabaseCatalog *filtersDB = &(copySpecs->catalogs.filter);
	DatabaseCatalog *targetDB = &(copySpecs->catalogs.target);

	if (!catalog_init(sourceDB) ||
		!catalog_init(filtersDB) ||
		!catalog_init(targetDB))
	{
		/* errors have already been logged */
		return false;
	}

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

	return true;
}


/*
 * catalog_open_from_specs opens our SQLite databases for internal catalogs.
 */
bool
catalog_open_from_specs(CopyDataSpec *copySpecs)
{
	DatabaseCatalog *source = &(copySpecs->catalogs.source);
	DatabaseCatalog *filter = &(copySpecs->catalogs.filter);
	DatabaseCatalog *target = &(copySpecs->catalogs.target);

	return catalog_open(source) &&
		   catalog_open(filter) &&
		   catalog_open(target);
}


/*
 * catalog_close_from_specs closes our SQLite databases for internal catalogs.
 */
bool
catalog_close_from_specs(CopyDataSpec *copySpecs)
{
	DatabaseCatalog *source = &(copySpecs->catalogs.source);
	DatabaseCatalog *filter = &(copySpecs->catalogs.filter);
	DatabaseCatalog *target = &(copySpecs->catalogs.target);

	return catalog_close(source) &&
		   catalog_close(filter) &&
		   catalog_close(target);
}


/*
 * catalog_stored_filter_type parses the filter type from the JSON string
 * stored in the catalog setup, returning SOURCE_FILTER_TYPE_NONE on failure.
 */
static SourceFilterType
catalog_stored_filter_type(const char *filtersJson)
{
	if (filtersJson == NULL)
	{
		return SOURCE_FILTER_TYPE_NONE;
	}

	JSON_Value *root = json_parse_string(filtersJson);

	if (root == NULL)
	{
		return SOURCE_FILTER_TYPE_NONE;
	}

	JSON_Object *obj = json_value_get_object(root);
	const char *typeStr = json_object_get_string(obj, "type");

	SourceFilterType type = SOURCE_FILTER_TYPE_NONE;

	if (typeStr != NULL)
	{
		if (streq(typeStr, filterTypeToString(SOURCE_FILTER_TYPE_INCL)))
		{
			type = SOURCE_FILTER_TYPE_INCL;
		}
		else if (streq(typeStr, filterTypeToString(SOURCE_FILTER_TYPE_EXCL_INDEX)))
		{
			type = SOURCE_FILTER_TYPE_EXCL_INDEX;
		}
		else if (streq(typeStr, filterTypeToString(SOURCE_FILTER_TYPE_EXCL)))
		{
			type = SOURCE_FILTER_TYPE_EXCL;
		}
	}

	json_value_free(root);

	return type;
}


/*
 * catalog_register_setup_from_specs registers the current copySpecs setup.
 */
bool
catalog_register_setup_from_specs(CopyDataSpec *copySpecs)
{
	DatabaseCatalog *sourceDB = &(copySpecs->catalogs.source);

	/*
	 * Fetch and register the catalog setup.
	 *
	 * Because commands such as `pgcopydb list tables` and all might have
	 * fetched parts of the catalogs already, we need to make sure there is no
	 * mismatch between the on-disk catalog setup and the current catalog
	 * setup.
	 *
	 * In case of a mismatch:
	 *
	 *  - if we're running for DATA_SECTION_ALL we can implement cache
	 *    invalidation (drop everything, create everything again, register
	 *    current setup).
	 *
	 * - if we have specs->fetchCatalogs set to true (meaning --force was used)
	 *   we can also implement cache invalidation.
	 *
	 * - in all other cases, we error out with the mismatch information.
	 *
	 * So first prepare the setup information:
	 */
	SafeURI spguri = { 0 };
	SafeURI tpguri = { 0 };

	if (!bareConnectionString(copySpecs->connStrings.source_pguri, &spguri))
	{
		/* errors have already been logged */
		return false;
	}

	if (!bareConnectionString(copySpecs->connStrings.target_pguri, &tpguri))
	{
		/* errors have already been logged */
		return false;
	}

	SourceFilters *filters = &(copySpecs->filters);
	JSON_Value *jsFilters = json_value_init_object();

	if (!filters_as_json(filters, jsFilters))
	{
		/* errors have already been logged */
		return false;
	}

	char *json = json_serialize_to_string(jsFilters);

	/*
	 * Now see if the catalog already have been setup.
	 */

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

	if (sourceDB->setup.id == 0)
	{
		/* catalogs unregistered, register current setup */
		log_notice("Registering catalog setup for "
				   "source \"%s\", target \"%s\", snapshot \"%s\"",
				   spguri.pguri,
				   tpguri.pguri,
				   copySpecs->sourceSnapshot.snapshot);

		/*
		 * Filter-agnostic commands (snapshot, sentinel) register setup with
		 * filters = NULL so that a subsequent filter-owner command (clone
		 * --filters) can distinguish "no owner has ever claimed this catalog"
		 * from "an owner explicitly ran without filters".  The latter case
		 * (SOURCE_FILTER_TYPE_NONE stored by a real owner) requires catalog
		 * invalidation; the former does not.
		 *
		 * Snapshot-agnostic commands (list tables, list sequences, etc.)
		 * register setup with snapshot = NULL for the same reason: a
		 * subsequent clone command must be able to distinguish "no snapshot
		 * owner has claimed this catalog yet" (NULL) from "an owner
		 * explicitly set snapshot X" (non-empty string).
		 */
		const char *filterJson = copySpecs->skipFilterCheck ? NULL : json;

		const char *snapStr =
			(copySpecs->skipSnapshotCheck ||
			 IS_EMPTY_STRING_BUFFER(copySpecs->sourceSnapshot.snapshot))
			? NULL
			: copySpecs->sourceSnapshot.snapshot;

		if (!catalog_register_setup(sourceDB,
									spguri.pguri,
									tpguri.pguri,
									snapStr,
									copySpecs->splitTablesLargerThan.bytes,
									copySpecs->splitMaxParts,
									filterJson))
		{
			/* errors have already been logged */
			json_free_serialized_string(json);
			return false;
		}
	}
	else
	{
		CatalogSetup *setup = &(sourceDB->setup);

		log_debug("Catalog has been setup for "
				  "source \"%s\", target \"%s\", snapshot \"%s\"",
				  setup->source_pguri,
				  setup->target_pguri,
				  setup->snapshot);

		if (!streq(spguri.pguri, setup->source_pguri))
		{
			if (copySpecs->skipSourceURICheck)
			{
				log_debug("Catalogs at \"%s\" have been setup for "
						  "a different Postgres source \"%s\", "
						  "ignoring for this read-only command",
						  sourceDB->dbfile,
						  setup->source_pguri);
			}
			else
			{
				log_error("Catalogs at \"%s\" have been setup for "
						  "Postgres source \"%s\" and current source is \"%s\"",
						  sourceDB->dbfile,
						  setup->source_pguri,
						  spguri.pguri);
				return false;
			}
		}

		/*
		 * Not all commands need a target pguri, so we might have registered a
		 * previous setup for the same context but without a target pguri,
		 * which would be NULL in our catalogs at this point.
		 */
		if (setup->target_pguri != NULL && tpguri.pguri != NULL &&
			!streq(tpguri.pguri, setup->target_pguri))
		{
			log_error("Catalogs at \"%s\" have been setup for "
					  "Postgres target \"%s\" and current target is \"%s\"",
					  sourceDB->dbfile,
					  setup->target_pguri,
					  tpguri.pguri);
			return false;
		}

		/*
		 * Skip snapshot comparison for snapshot-agnostic commands (e.g. list
		 * tables, list sequences): they never export a snapshot and have no
		 * stake in snapshot consistency.
		 *
		 * For snapshot-owner commands (e.g. clone), apply the check only when
		 * --not-consistent is not in effect.
		 */
		if (copySpecs->skipSnapshotCheck)
		{
			log_debug("Skipping snapshot consistency check for "
					  "snapshot-agnostic command");
		}
		else if (copySpecs->consistent)
		{
			if (IS_EMPTY_STRING_BUFFER(setup->snapshot))
			{
				/*
				 * Case 0: catalog was first registered by a snapshot-agnostic
				 * command (e.g. pgcopydb list tables), which stored snapshot =
				 * NULL as a sentinel meaning "no snapshot owner has ever
				 * claimed this catalog yet".
				 *
				 * Accept the current snapshot and persist it so future
				 * commands can validate consistency against it.
				 */
				log_info("Catalog snapshot not yet set; "
						 "registering current snapshot \"%s\"",
						 copySpecs->sourceSnapshot.snapshot);

				if (!catalog_setup_replication(sourceDB,
											   copySpecs->sourceSnapshot.snapshot))
				{
					/* errors have already been logged */
					json_free_serialized_string(json);
					return false;
				}

				strlcpy(setup->snapshot,
						copySpecs->sourceSnapshot.snapshot,
						sizeof(setup->snapshot));
			}
			else if (!streq(copySpecs->sourceSnapshot.snapshot, setup->snapshot))
			{
				log_error("Catalogs at \"%s\" have been setup for "
						  "snapshot \"%s\" and current snapshot is \"%s\"",
						  sourceDB->dbfile,
						  setup->snapshot,
						  copySpecs->sourceSnapshot.snapshot);
				return false;
			}
		}

		/* skip comparing --split-tables-larger-than values unless needed */
		if (copySpecs->section == DATA_SECTION_ALL ||
			copySpecs->section == DATA_SECTION_TABLE_DATA_PARTS)
		{
			CatalogSection *tablePartsDataSection =
				&(sourceDB->sections[DATA_SECTION_TABLE_DATA_PARTS]);

			/* make sure the section has been initialized properly */
			tablePartsDataSection->section = DATA_SECTION_TABLE_DATA_PARTS;

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

			/*
			 * Difference in --split-at is only meaningful if table-data cache
			 * has already been populated.
			 */
			if (tablePartsDataSection->fetched &&
				copySpecs->splitTablesLargerThan.bytes != setup->
				splitTablesLargerThanBytes)
			{
				char bytesPretty[BUFSIZE] = { 0 };

				pretty_print_bytes(bytesPretty,
								   BUFSIZE,
								   setup->splitTablesLargerThanBytes);

				log_debug("setup: %lld (%s)",
						  (long long) setup->splitTablesLargerThanBytes,
						  bytesPretty);

				log_debug("specs: %lld (%s)",
						  (long long) copySpecs->splitTablesLargerThan.bytes,
						  copySpecs->splitTablesLargerThan.bytesPretty);

				log_error("Catalogs at \"%s\" have been setup for "
						  "--split-tables-larger-than \"%s\" "
						  "and current value is \"%s\"",
						  sourceDB->dbfile,
						  bytesPretty,
						  copySpecs->splitTablesLargerThan.bytesPretty);

				return false;
			}

			/*
			 * Difference in --split-max-parts is only meaningful if
			 * table-data cache has already been populated.
			 */
			if (tablePartsDataSection->fetched &&
				copySpecs->splitMaxParts != setup->splitMaxParts)
			{
				log_debug("setup: %d", setup->splitMaxParts);

				log_debug("specs: %d", copySpecs->splitMaxParts);

				log_error("Catalogs at \"%s\" have been setup for "
						  "--split-max-parts \"%d\" "
						  "and current value is \"%d\"",
						  sourceDB->dbfile,
						  setup->splitMaxParts,
						  copySpecs->splitMaxParts);

				return false;
			}
		}

		if (copySpecs->skipFilterCheck)
		{
			/*
			 * Filter-agnostic commands (snapshot, sentinel) open an existing
			 * catalog but have no stake in filter consistency — they neither
			 * read nor write filter-dependent data.  Skip the whole check.
			 */
			log_debug("Skipping filter consistency check for filter-agnostic command");
		}
		else if (setup->filters == NULL)
		{
			/*
			 * Case 0: catalog was first registered by a filter-agnostic
			 * command (e.g. pgcopydb snapshot with skipFilterCheck=true),
			 * which stored filters = NULL as a sentinel meaning "no filter
			 * owner has ever claimed this catalog yet".
			 *
			 * The source catalog sections are therefore empty (allDone=false),
			 * so no invalidation is needed — just write the current filter and
			 * proceed.
			 */
			log_info("Catalog was initialized by a filter-agnostic command; "
					 "registering current command filters");

			if (!catalog_update_filters(sourceDB, json))
			{
				log_error("Failed to register filters in catalog database");
				json_free_serialized_string(json);
				return false;
			}

			setup->filters = strdup(json);
		}
		else if (!streq(json, setup->filters))
		{
			if (strstr(setup->filters, "SOURCE_FILTER_TYPE_NONE") != NULL &&
				strstr(json, "SOURCE_FILTER_TYPE_NONE") == NULL)
			{
				/*
				 * Case 1: a filter-owner command previously ran without
				 * filters (SOURCE_FILTER_TYPE_NONE) and may have populated
				 * the source catalog; the current command introduces filters.
				 * Update the stored filter and — only when the catalog was
				 * actually populated — invalidate the stale unfiltered data so
				 * fetchCatalogs=true forces a fresh fetch with the new filter.
				 *
				 * catalog_iter_s_table_init queries s_table directly (no
				 * runtime filter join), so stale rows would cause all tables
				 * to be copied regardless of --filters.
				 *
				 * Also reset the filter catalog (filterDB) if the source
				 * catalog has data: it holds OID-based filter data built
				 * against the old unfiltered view and must be rebuilt.
				 */
				log_info("Current filtering setup is: %s", json);
				log_info("Catalog filtering setup is: %s", setup->filters);
				log_warn("Catalogs at \"%s\" have been setup without filters; "
						 "updating to current command filters",
						 sourceDB->dbfile);

				if (!catalog_update_filters(sourceDB, json))
				{
					log_error("Failed to update filters in catalog database");
					json_free_serialized_string(json);
					return false;
				}

				if (setup->filters != NULL)
				{
					free(setup->filters);
				}
				setup->filters = strdup(json);

				log_info("Catalog filters updated to: %s", setup->filters);

				/*
				 * Only invalidate when the source catalog was actually
				 * populated by the prior no-filter run.  If it is empty (e.g.
				 * the prior command was interrupted before fetch, or was
				 * itself a filter-agnostic command that somehow stored NONE),
				 * the DELETE statements and filterDB reset are unnecessary.
				 */
				int sourceHasData = 0;
				{
					char *existsSql =
						"select exists(select 1 from s_table limit 1)";
					SQLiteQuery existsQuery = { 0 };

					if (!catalog_sql_prepare(sourceDB->db,
											 existsSql,
											 &existsQuery))
					{
						/* errors have already been logged */
						json_free_serialized_string(json);
						return false;
					}

					if (catalog_sql_step(&existsQuery) == SQLITE_ROW)
					{
						sourceHasData =
							sqlite3_column_int(existsQuery.ppStmt, 0);
					}

					if (!catalog_sql_finalize(&existsQuery))
					{
						/* errors have already been logged */
						json_free_serialized_string(json);
						return false;
					}
				}

				if (sourceHasData)
				{
					if (!catalog_delete_source_schema_data(sourceDB))
					{
						log_error("Failed to invalidate source catalog data "
								  "after filter update");
						json_free_serialized_string(json);
						return false;
					}

					DatabaseCatalog *filterCatalog =
						&(copySpecs->catalogs.filter);

					if (filterCatalog->db != NULL)
					{
						if (!semaphore_lock(&(filterCatalog->sema)))
						{
							/* errors have already been logged */
							json_free_serialized_string(json);
							return false;
						}

						if (!catalog_drop_schema(filterCatalog) ||
							!catalog_create_schema(filterCatalog) ||
							!catalog_create_indexes(filterCatalog))
						{
							log_error("Failed to reset filter catalog "
									  "after filter update");
							(void) semaphore_unlock(&(filterCatalog->sema));
							json_free_serialized_string(json);
							return false;
						}

						(void) semaphore_unlock(&(filterCatalog->sema));
					}
				}
			}
			else if (strstr(setup->filters, "SOURCE_FILTER_TYPE_NONE") == NULL &&
					 strstr(json, "SOURCE_FILTER_TYPE_NONE") == NULL)
			{
				/* Case 2: both sides have filters but they differ — conflict. */
				log_info("Current filtering setup is: %s", json);
				log_info("Catalog filtering setup is: %s", setup->filters);
				log_error("Catalogs at \"%s\" have been setup for a different "
						  "filtering than the current command, "
						  "see above for details",
						  sourceDB->dbfile);

				json_free_serialized_string(json);
				return false;
			}
			else if (strstr(setup->filters, "SOURCE_FILTER_TYPE_NONE") == NULL &&
					 strstr(json, "SOURCE_FILTER_TYPE_NONE") != NULL)
			{
				/*
				 * Case 3: catalog was created with filters; the current
				 * command has none (e.g. pgcopydb clone without --filters,
				 * pgcopydb list table-parts, pgcopydb stream sentinel).
				 * These commands operate on the already-filtered catalog
				 * data and do not need --filters repeated.
				 *
				 * Adopt the stored filter TYPE so that
				 * copydb_fetch_filtered_oids uses the correct complement
				 * (e.g. EXCL to LIST_EXCL) and populates s_index for
				 * INDEX ATTACH filtering.  The filter patterns themselves
				 * are not needed: filtersDB already holds the pre-computed
				 * OIDs from the earlier list commands.
				 *
				 * Also update the stored JSON in source.db to match the
				 * current command's serialization (adopted type, empty
				 * patterns).  Worker processes independently call this
				 * function and must see the same stored JSON or they would
				 * trigger Case 2 (both sides non-NONE but mismatched).
				 */
				SourceFilterType storedType =
					catalog_stored_filter_type(setup->filters);

				if (storedType != SOURCE_FILTER_TYPE_NONE)
				{
					filters->type = storedType;

					json_free_serialized_string(json);

					JSON_Value *jsAdopted = json_value_init_object();

					if (!filters_as_json(filters, jsAdopted))
					{
						/* errors have already been logged */
						return false;
					}

					json = json_serialize_to_string(jsAdopted);

					if (!catalog_update_filters(sourceDB, json))
					{
						log_error("Failed to update filters in catalog database");
						json_free_serialized_string(json);
						return false;
					}

					if (setup->filters != NULL)
					{
						free(setup->filters);
					}
					setup->filters = strdup(json);
				}

				log_debug("Adopting catalog's stored filter type (%s) for "
						  "this command",
						  filterTypeToString(filters->type));
			}
			else
			{
				/* Unanticipated mismatch. */
				log_info("Current filtering setup is: %s", json);
				log_info("Catalog filtering setup is: %s", setup->filters);
				log_error("Catalogs at \"%s\" have been setup for a different "
						  "filtering than the current command, "
						  "see above for details",
						  sourceDB->dbfile);

				json_free_serialized_string(json);
				return false;
			}
		}
	}

	/*
	 * Populate extension_filter in the filter catalog from the in-memory
	 * filter configuration so that catalog_prepare_filter can read it via
	 * SQL without needing the C lists as parameters.
	 */
	DatabaseCatalog *filtersDB = &(copySpecs->catalogs.filter);

	for (int i = 0; i < filters->excludeExtensionList.count; i++)
	{
		if (!catalog_add_extension_filter(
				filtersDB,
				filters->excludeExtensionList.array[i].extname,
				"exclude"))
		{
			json_free_serialized_string(json);
			return false;
		}
	}

	for (int i = 0; i < filters->includeOnlyExtensionList.count; i++)
	{
		if (!catalog_add_extension_filter(
				filtersDB,
				filters->includeOnlyExtensionList.array[i].extname,
				"include-only"))
		{
			json_free_serialized_string(json);
			return false;
		}
	}

	json_free_serialized_string(json);

	/*
	 * Record this invocation in the command log so operators can trace the
	 * exact sequence of commands that led to the current catalog state.
	 * Best-effort: a failure here does not abort the command.
	 */
	if (!catalog_log_command(&(copySpecs->catalogs.source)))
	{
		log_warn("Failed to write command log entry to catalog \"%s\"",
				 copySpecs->catalogs.source.dbfile);
	}

	return true;
}


/*
 * catalog_open opens an already initialized catalog database file.
 */
bool
catalog_open(DatabaseCatalog *catalog)
{
	if (!file_exists(catalog->dbfile))
	{
		log_error("Failed to open catalog \"%s\", file does not exists",
				  catalog->dbfile);
		return false;
	}

	return catalog_init(catalog);
}


/*
 * catalog_open_readonly opens an existing catalog file in read-only mode
 * without creating or acquiring any semaphore.  Use this for catalogs that
 * are only ever read (never written) — SQLite WAL provides snapshot-
 * consistent reads with no contention between any number of readers.
 */
bool
catalog_open_readonly(DatabaseCatalog *catalog)
{
	if (catalog->db != NULL)
	{
		log_debug("Skipping opening read-only SQLite database \"%s\": "
				  "already opened", catalog->dbfile);
		return true;
	}

	if (!file_exists(catalog->dbfile))
	{
		log_error("Failed to open catalog \"%s\", file does not exist",
				  catalog->dbfile);
		return false;
	}

	log_debug("Opening read-only SQLite database \"%s\"", catalog->dbfile);

	int rc = sqlite3_open_v2(catalog->dbfile,
							 &(catalog->db),
							 SQLITE_OPEN_READONLY,
							 NULL);

	if (rc != SQLITE_OK)
	{
		const char *errmsg = sqlite3_errmsg(catalog->db);

		log_error("Failed to open \"%s\" read-only: %s",
				  catalog->dbfile, errmsg);

		if (catalog->db != NULL)
		{
			sqlite3_close(catalog->db);
		}
		catalog->db = NULL;
		return false;
	}

	return true;
}


/*
 * source_catalog_init initializes our internal catalog database file.
 */
bool
catalog_init(DatabaseCatalog *catalog)
{
	if (catalog->db != NULL)
	{
		log_debug("Skipping opening SQLite database \"%s\": already opened",
				  catalog->dbfile);
		return true;
	}

	log_debug("Opening SQLite database \"%s\" with lib version %s",
			  catalog->dbfile,
			  sqlite3_libversion());

	bool createSchema = !file_exists(catalog->dbfile);

	if (sqlite3_open(catalog->dbfile, &(catalog->db)) != SQLITE_OK)
	{
		/* capture errmsg before closing/null-ing handle */
		const char *errmsg = sqlite3_errmsg(catalog->db);

		log_error("Failed to open \"%s\": %s",
				  catalog->dbfile,
				  errmsg);

		if (catalog->db != NULL)
		{
			sqlite3_close(catalog->db);
		}
		catalog->db = NULL;
		return false;
	}

	/*
	 * The source catalog needs a semaphore to serialize concurrent write
	 * access to the SQLite database.
	 */
	if (!catalog_create_semaphore(catalog))
	{
		/* errors have already been logged */
		sqlite3_close(catalog->db);
		catalog->db = NULL;
		return false;
	}

	if (createSchema)
	{
		/*
		 * WAL journal_mode is significantly faster for writes and allows
		 * concurrency of readers not blocking writers and vice versa.
		 */
		if (!catalog_set_wal_mode(catalog))
		{
			/* errors have already been logged */
			sqlite3_close(catalog->db);
			catalog->db = NULL;
			return false;
		}

		if (!catalog_create_schema(catalog))
		{
			/* errors have already been logged */
			sqlite3_close(catalog->db);
			catalog->db = NULL;
			return false;
		}

		return true;
	}

	return true;
}


/*
 * catalog_create_semaphore creates a semaphore to protect concurrent access to
 * the SQLite database that hosts our internal catalogs, allowing sequential
 * access and enforce one-writer-at-a-time.
 */
bool
catalog_create_semaphore(DatabaseCatalog *catalog)
{
	catalog->sema.reentrant = true;

	/*
	 * When we don't have a semId yet (it's zero), create a semaphore. When the
	 * semaphore is non-zero, it's been created already and we can simply use
	 * it: all we need to know is the semId.
	 */
	if (catalog->sema.semId == 0)
	{
		catalog->sema.initValue = 1;

		if (!semaphore_create(&(catalog->sema)))
		{
			log_error("Failed to create the catalog concurrency semaphore");
			return false;
		}
	}

	return true;
}


/*
 * catalog_attach runs the ATTACH SQLite command to attach a catalog b in the
 * already open catalog a, in such a way that it's then possible to query e.g.
 * source.s_table from the filters database.
 */
bool
catalog_attach(DatabaseCatalog *a, DatabaseCatalog *b, const char *name)
{
	/* Check if the database is already attached with this name */
	char checkSql[BUFSIZE] = { 0 };
	sformat(checkSql, sizeof(checkSql),
			"SELECT name FROM pragma_database_list WHERE name = '%s'", name);

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(a->db, checkSql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* Check if the query returns a row (meaning the database is attached) */
	int checkResult = catalog_sql_step(&query);
	if (checkResult == SQLITE_ROW)
	{
		/* Database is already attached with this name */
		log_debug("Database '%s' is already attached as %s, skipping attach", b->dbfile,
				  name);
		if (!catalog_sql_finalize(&query))
		{
			/* errors have already been logged */
			return false;
		}
		return true;
	}
	else if (checkResult != SQLITE_DONE)
	{
		/* Error occurred */
		log_error("Failed to check if database is attached: %s", checkSql);
		if (!catalog_sql_finalize(&query))
		{
			/* errors have already been logged */
			return false;
		}
		return false;
	}

	/* No row returned, database is not attached */
	if (!catalog_sql_finalize(&query))
	{
		/* errors have already been logged */
		return false;
	}

	char *sqlTmpl = "attach '%s' as %s";
	char buf[BUFSIZE + MAXPGPATH] = { 0 };

	sformat(buf, sizeof(buf), sqlTmpl, b->dbfile, name);

	int rc = sqlite3_exec(a->db, buf, NULL, NULL, NULL);

	if (rc != SQLITE_OK)
	{
		log_error("Failed to attach '%s' as %s", b->dbfile, name);
		log_error("%s", sqlite3_errmsg(a->db));
		return false;
	}

	return true;
}


/*
 * catalog_close closes our internal catalog database file.
 */
bool
catalog_close(DatabaseCatalog *catalog)
{
	/* it's okay to try and close the same catalog twice */
	if (catalog->db == NULL)
	{
		return true;
	}

	if (!catalog_close_stmts(catalog))
	{
		/* logged inside */
		return false;
	}

	if (sqlite3_close(catalog->db) != SQLITE_OK)
	{
		log_error("Failed to close \"%s\":", catalog->dbfile);
		log_error("[SQLite]: %s", sqlite3_errmsg(catalog->db));
		return false;
	}

	catalog->db = NULL;

	return true;
}


/*
 * catalog_close_stmts finalizes all cached prepared statements and frees the
 * cache entries.
 */
bool
catalog_close_stmts(DatabaseCatalog *catalog)
{
	CachedStmt *entry = NULL;
	CachedStmt *tmp = NULL;

	HASH_ITER(hh, catalog->stmtCache, entry, tmp)
	{
		HASH_DEL(catalog->stmtCache, entry);
		sqlite3_finalize(entry->stmt);
		free(entry);
	}

	catalog->stmtCache = NULL;

	return true;
}


/*
 * catalog_create_schema creates the expected schema in the given catalog.
 * This includes all tables and all indexes *except* s_i_tableoid and
 * s_c_indexoid, which are deferred to catalog_create_indexes so they can
 * be built on a fully-populated table (3-5× faster than maintaining them
 * row-by-row during the bulk load).
 */
bool
catalog_create_schema(DatabaseCatalog *catalog)
{
	char **createDDLs = NULL;
	int count = 0;

	switch (catalog->type)
	{
		case DATABASE_CATALOG_TYPE_SOURCE:
		{
			createDDLs = sourceDBcreateTableDDLs;
			count = sizeof(sourceDBcreateTableDDLs) / sizeof(sourceDBcreateTableDDLs[0]);
			break;
		}

		case DATABASE_CATALOG_TYPE_FILTER:
		{
			createDDLs = filterDBcreateTableDDLs;
			count = sizeof(filterDBcreateTableDDLs) / sizeof(filterDBcreateTableDDLs[0]);
			break;
		}

		case DATABASE_CATALOG_TYPE_TARGET:
		{
			createDDLs = targetDBcreateTableDDLs;
			count = sizeof(targetDBcreateTableDDLs) / sizeof(targetDBcreateTableDDLs[0]);
			break;
		}

		case DATABASE_CATALOG_TYPE_REPLAY:
		{
			createDDLs = replayDBcreateDDLs;
			count = sizeof(replayDBcreateDDLs) / sizeof(replayDBcreateDDLs[0]);
			break;
		}

		case DATABASE_CATALOG_TYPE_OUTPUT:
		{
			createDDLs = outputDBcreateDDLs;
			count = sizeof(outputDBcreateDDLs) / sizeof(outputDBcreateDDLs[0]);
			break;
		}

		default:
		{
			log_error("BUG: called catalog_init for unknown type %d",
					  catalog->type);
			return false;
		}
	}

	for (int i = 0; i < count; i++)
	{
		char *ddl = createDDLs[i];

		log_sqlite("catalog_create_schema: %s", ddl);

		int rc = sqlite3_exec(catalog->db, ddl, NULL, NULL, NULL);

		if (rc != SQLITE_OK)
		{
			log_error("Failed to create catalog schema: %s", ddl);
			log_error("%s", sqlite3_errmsg(catalog->db));
			return false;
		}
	}

	return true;
}


/*
 * catalog_create_indexes builds SQLite indexes after bulk data has been
 * loaded.  Called once per catalog after the initial schema fetch completes.
 *
 * replay.db and output.db have their indexes inline in replayDBcreateDDLs /
 * outputDBcreateDDLs and are not bulk-loaded, so they are skipped here.
 */
bool
catalog_create_indexes(DatabaseCatalog *catalog)
{
	char **indexDDLs = NULL;
	int count = 0;

	switch (catalog->type)
	{
		case DATABASE_CATALOG_TYPE_SOURCE:
		{
			indexDDLs = sourceDBcreateIndexDDLs;
			count = sizeof(sourceDBcreateIndexDDLs) / sizeof(sourceDBcreateIndexDDLs[0]);
			break;
		}

		case DATABASE_CATALOG_TYPE_FILTER:
		{
			indexDDLs = filterDBcreateIndexDDLs;
			count = sizeof(filterDBcreateIndexDDLs) / sizeof(filterDBcreateIndexDDLs[0]);
			break;
		}

		case DATABASE_CATALOG_TYPE_TARGET:
		{
			indexDDLs = targetDBcreateIndexDDLs;
			count = sizeof(targetDBcreateIndexDDLs) / sizeof(targetDBcreateIndexDDLs[0]);
			break;
		}

		case DATABASE_CATALOG_TYPE_REPLAY:
		case DATABASE_CATALOG_TYPE_OUTPUT:
		{
			/* indexes already created inline with tables */
			return true;
		}

		default:
		{
			log_error("BUG: called catalog_create_indexes for unknown type %d",
					  catalog->type);
			return false;
		}
	}

	for (int i = 0; i < count; i++)
	{
		char *ddl = indexDDLs[i];

		log_sqlite("catalog_create_indexes: %s", ddl);

		int rc = sqlite3_exec(catalog->db, ddl, NULL, NULL, NULL);

		if (rc != SQLITE_OK)
		{
			log_error("Failed to create catalog index: %s", ddl);
			log_error("%s", sqlite3_errmsg(catalog->db));
			return false;
		}
	}

	return true;
}


/*
 * catalog_drop_schema drops all the catalog schema and data.
 */
bool
catalog_drop_schema(DatabaseCatalog *catalog)
{
	char **dropDDLs = NULL;
	int count = 0;

	switch (catalog->type)
	{
		case DATABASE_CATALOG_TYPE_SOURCE:
		{
			dropDDLs = sourceDBdropDDLs;
			count = sizeof(sourceDBdropDDLs) / sizeof(sourceDBdropDDLs[0]);
			break;
		}

		case DATABASE_CATALOG_TYPE_FILTER:
		{
			dropDDLs = filterDBdropDDLs;
			count = sizeof(filterDBdropDDLs) / sizeof(filterDBdropDDLs[0]);
			break;
		}

		case DATABASE_CATALOG_TYPE_TARGET:
		{
			dropDDLs = targetDBdropDDLs;
			count = sizeof(targetDBdropDDLs) / sizeof(targetDBdropDDLs[0]);
			break;
		}

		case DATABASE_CATALOG_TYPE_REPLAY:
		{
			dropDDLs = replayDBdropDDLs;
			count = sizeof(replayDBdropDDLs) / sizeof(replayDBdropDDLs[0]);
			break;
		}

		case DATABASE_CATALOG_TYPE_OUTPUT:
		{
			dropDDLs = outputDBdropDDLs;
			count = sizeof(outputDBdropDDLs) / sizeof(outputDBdropDDLs[0]);
			break;
		}

		default:
		{
			log_error("BUG: called catalog_drop_schema for unknown type %d",
					  catalog->type);
			return false;
		}
	}

	for (int i = 0; i < count; i++)
	{
		char *ddl = dropDDLs[i];

		log_sqlite("catalog_drop_schema: %s", ddl);

		int rc = sqlite3_exec(catalog->db, ddl, NULL, NULL, NULL);

		if (rc != SQLITE_OK)
		{
			log_error("Failed to init catalogs: %s", ddl);
			log_error("%s", sqlite3_errmsg(catalog->db));
			return false;
		}
	}

	return true;
}


/*
 * catalog_set_wal_mode convert given SQLite database to WAL mode
 * (https://www.sqlite.org/pragma.html#pragma_journal_mode).
 *
 * Note: It generates "additional quasi-persistent '-wal' file and '-shm'
 * shared memory file associated with each database"
 * (https://www.sqlite.org/wal.html).
 */
bool
catalog_set_wal_mode(DatabaseCatalog *catalog)
{
	return catalog_execute(catalog, "PRAGMA journal_mode = WAL");
}


/*
 * catalog_begin explicitely begins a SQLite transaction.
 */
bool
catalog_begin(DatabaseCatalog *catalog, bool immediate)
{
	char *sql = immediate ? "BEGIN IMMEDIATE" : "BEGIN";

	log_sqlite("[SQLite] %s", sql);

	int rc = sqlite3_exec(catalog->db, sql, NULL, NULL, NULL);

	if (rc == SQLITE_LOCKED || rc == SQLITE_BUSY)
	{
		ConnectionRetryPolicy retryPolicy = { 0 };

		int maxT = 5;            /* 5s */
		int maxSleepTime = 350;  /* 350ms */
		int baseSleepTime = 10;  /* 10ms */

		(void) pgsql_set_retry_policy(&retryPolicy,
									  maxT,
									  -1, /* unbounded number of attempts */
									  maxSleepTime,
									  baseSleepTime);

		while ((rc == SQLITE_LOCKED || rc == SQLITE_BUSY) &&
			   !pgsql_retry_policy_expired(&retryPolicy))
		{
			int sleepTimeMs =
				pgsql_compute_connection_retry_sleep_time(&retryPolicy);

			log_sqlite("[SQLite %d]: %s, try again in %dms",
					   rc,
					   sqlite3_errstr(rc),
					   sleepTimeMs);

			/* we have milliseconds, pg_usleep() wants microseconds */
			(void) pg_usleep(sleepTimeMs * 1000);

			rc = sqlite3_exec(catalog->db, "BEGIN", NULL, NULL, NULL);
		}
	}

	if (rc != SQLITE_OK)
	{
		log_error("[SQLite] Failed to %s", sql);
		return false;
	}

	return true;
}


/*
 * catalog_commit explicitely commits a SQLite transaction.
 */
bool
catalog_commit(DatabaseCatalog *catalog)
{
	return catalog_execute(catalog, "COMMIT");
}


/*
 * catalog_register_setup registers the setup metadata for this catalog.
 */
bool
catalog_register_setup(DatabaseCatalog *catalog,
					   const char *source_pg_uri,
					   const char *target_pg_uri,
					   const char *snapshot,
					   uint64_t splitTablesLargerThanBytes,
					   int splitMaxParts,
					   const char *filters)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_register_setup: db is NULL");
		return false;
	}

	char *sql =
		"insert into setup("
		"  id, source_pg_uri, target_pg_uri, snapshot, filters, "
		"  split_tables_larger_than, split_max_parts) "
		"values($1, $2, $3, $4, $5, $6, $7)";

	SQLiteQuery query = { 0 };

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "id", 1, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "source_pg_uri", 0, (char *) source_pg_uri },
		{ BIND_PARAMETER_TYPE_TEXT, "target_pg_uri", 0, (char *) target_pg_uri },
		{ BIND_PARAMETER_TYPE_TEXT, "snapshot", 0, (char *) snapshot },
		{ BIND_PARAMETER_TYPE_TEXT, "filters", 0, (char *) filters },

		{
			BIND_PARAMETER_TYPE_INT64, "split_tables_larger_than",
			splitTablesLargerThanBytes, NULL
		},
		{
			BIND_PARAMETER_TYPE_INT64, "split_max_parts",
			splitMaxParts, NULL
		}
	};

	int count = sizeof(params) / sizeof(params[0]);

	/*
	 * skip splitTableLargerThanBytes, and splitMaxParts when
	 * splitTableLargerThanBytes has not been set.
	 *
	 * skip only splitMaxParts when only splitMaxParts has not been
	 * set.
	 */
	if (splitTablesLargerThanBytes == 0)
	{
		sql =
			"insert into setup("
			"  id, source_pg_uri, target_pg_uri, snapshot, filters) "
			"values($1, $2, $3, $4, $5)";

		count -= 2;
	}
	else if (splitMaxParts == 0)
	{
		sql =
			"insert into setup("
			"  id, source_pg_uri, target_pg_uri, snapshot, filters, "
			"  split_tables_larger_than) "
			"values($1, $2, $3, $4, $5, $6)";

		--count;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_setup fetches the registered catalog setup metadata.
 */
bool
catalog_setup(DatabaseCatalog *catalog)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_setup: db is NULL");
		return false;
	}

	/*
	 * Reset the setup struct before querying so that stale values inherited
	 * from a parent CopyDataSpec (via struct copy) do not persist when the
	 * catalog is fresh and the setup table has no rows.
	 */
	catalog->setup.id = 0;
	catalog->setup.source_pguri = NULL;
	catalog->setup.target_pguri = NULL;
	catalog->setup.snapshot[0] = '\0';
	catalog->setup.filters = NULL;
	catalog->setup.splitTablesLargerThanBytes = 0;
	catalog->setup.splitMaxParts = 0;

	SQLiteQuery query = {
		.context = &(catalog->setup),
		.fetchFunction = &catalog_setup_fetch
	};

	char *sql =
		"select id, source_pg_uri, target_pg_uri, snapshot, "
		"       split_tables_larger_than, split_max_parts, filters "
		"from setup";

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_update_setup updates the registered catalog setup metadata.
 */
bool
catalog_update_setup(CopyDataSpec *copySpecs)
{
	DatabaseCatalog *catalog = &(copySpecs->catalogs.source);
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_setup: db is NULL");
		return false;
	}

	SafeURI tpguri = { 0 };

	if (!bareConnectionString(copySpecs->connStrings.target_pguri, &tpguri))
	{
		/* errors have already been logged */
		return false;
	}

	SQLiteQuery query = { 0 };

	char *sql =
		"update setup "
		"   set target_pg_uri = $1, "
		"       split_tables_larger_than = $2, "
		"       split_max_parts = $3 "
		" where id = 1";

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "target_pg_uri", 0, (char *) tpguri.pguri },
		{
			BIND_PARAMETER_TYPE_INT64, "split_tables_larger_than",
			copySpecs->splitTablesLargerThan.bytes, NULL
		},
		{
			BIND_PARAMETER_TYPE_INT, "split_max_parts",
			copySpecs->splitMaxParts, NULL
		}
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_update_filters updates just the filters field in the setup table.
 */
bool
catalog_update_filters(DatabaseCatalog *catalog, const char *filters)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_update_filters: db is NULL");
		return false;
	}

	SQLiteQuery query = { 0 };

	char *sql =
		"update setup "
		"   set filters = $1 "
		" where id = 1";

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "filters", 0, (char *) filters }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_section_fetch is a SQLiteQuery callback.
 */
bool
catalog_setup_fetch(SQLiteQuery *query)
{
	CatalogSetup *setup = (CatalogSetup *) query->context;

	/*
	 * id
	 */
	setup->id = sqlite3_column_int64(query->ppStmt, 0);

	/*
	 * source_pguri
	 */
	if (sqlite3_column_type(query->ppStmt, 1) != SQLITE_NULL)
	{
		int len = sqlite3_column_bytes(query->ppStmt, 1);
		int bytes = len + 1;

		setup->source_pguri = (char *) calloc(bytes, sizeof(char));

		if (setup->source_pguri == NULL)
		{
			log_fatal(ALLOCATION_FAILED_ERROR);
			return false;
		}

		strlcpy(setup->source_pguri,
				(char *) sqlite3_column_text(query->ppStmt, 1),
				bytes);
	}

	/*
	 * target_pguri
	 */
	if (sqlite3_column_type(query->ppStmt, 2) == SQLITE_NULL)
	{
		setup->target_pguri = NULL;
	}
	else
	{
		int len = sqlite3_column_bytes(query->ppStmt, 2);
		int bytes = len + 1;

		setup->target_pguri = (char *) calloc(bytes, sizeof(char));

		if (setup->target_pguri == NULL)
		{
			log_fatal(ALLOCATION_FAILED_ERROR);
			return false;
		}

		strlcpy(setup->target_pguri,
				(char *) sqlite3_column_text(query->ppStmt, 2),
				bytes);
	}

	/*
	 * snapshot (a string buffer)
	 */
	if (sqlite3_column_type(query->ppStmt, 3) != SQLITE_NULL)
	{
		strlcpy(setup->snapshot,
				(char *) sqlite3_column_text(query->ppStmt, 3),
				sizeof(setup->snapshot));
	}

	/*
	 * split-tables-larger-than
	 */
	setup->splitTablesLargerThanBytes = sqlite3_column_int64(query->ppStmt, 4);

	/*
	 * split-max-parts
	 */
	setup->splitMaxParts = sqlite3_column_int(query->ppStmt, 5);

	/*
	 * filters
	 */
	if (sqlite3_column_type(query->ppStmt, 6) == SQLITE_NULL)
	{
		setup->filters = NULL;
	}
	else
	{
		int len = sqlite3_column_bytes(query->ppStmt, 6);
		int bytes = len + 1;

		setup->filters = (char *) calloc(bytes, sizeof(char));

		if (setup->filters == NULL)
		{
			log_fatal(ALLOCATION_FAILED_ERROR);
			return false;
		}

		strlcpy(setup->filters,
				(char *) sqlite3_column_text(query->ppStmt, 6),
				bytes);
	}

	return true;
}


/*
 * catalog_setup_replication updates the catalog setup snapshot field.  It is
 * meant to be called after having initialized the catalog, once the replication
 * slot has been created and the snapshot exported.  Plugin and slot_name are
 * the sole authority of the replication_slot table; they are not duplicated
 * here.
 */
bool
catalog_setup_replication(DatabaseCatalog *catalog, const char *snapshot)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_setup_replication: db is NULL");
		return false;
	}

	char *sql =
		"update setup "
		"   set snapshot = $1 "
		" where id = 1";

	SQLiteQuery query = { .errorOnZeroRows = true };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "snapshot", 0, (char *) snapshot }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_write_replication_slot persists all fields of a ReplicationSlot
 * to the dedicated replication_slot table (INSERT OR REPLACE, id = 1).
 *
 * This replaces snapshot_write_slot() which wrote the same data to a flat
 * text file.  The SQLite table is the single authoritative store.
 */
bool
catalog_write_replication_slot(DatabaseCatalog *catalog,
							   const ReplicationSlot *slot)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_write_replication_slot: db is NULL");
		return false;
	}

	char *sql =
		"insert or replace into replication_slot"
		"  (id, slot_name, lsn, snapshot, plugin, wal2json_numeric_as_string,"
		"   publication_name, publication_auto_managed)"
		"  values(1, $1, $2, $3, $4, $5, $6, $7)";

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	char lsnStr[PG_LSN_MAXLENGTH] = { 0 };
	sformat(lsnStr, sizeof(lsnStr), "%X/%X", LSN_FORMAT_ARGS(slot->lsn));

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "slot_name", 0, (char *) slot->slotName },
		{ BIND_PARAMETER_TYPE_INT64, "lsn", slot->lsn, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "snapshot", 0, (char *) slot->snapshot },
		{
			BIND_PARAMETER_TYPE_TEXT, "plugin",
			0, (char *) OutputPluginToString(slot->plugin)
		},
		{
			BIND_PARAMETER_TYPE_INT64, "wal2json_numeric_as_string",
			slot->wal2jsonNumericAsString ? 1 : 0, NULL
		},
		{
			BIND_PARAMETER_TYPE_TEXT, "publication_name",
			0, (char *) slot->publicationName
		},
		{
			BIND_PARAMETER_TYPE_INT64, "publication_auto_managed",
			slot->publicationAutoManaged ? 1 : 0, NULL
		}
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	log_notice("Stored replication slot \"%s\" lsn %X/%X snapshot \"%s\" "
			   "plugin \"%s\" in catalog \"%s\"",
			   slot->slotName,
			   LSN_FORMAT_ARGS(slot->lsn),
			   slot->snapshot,
			   OutputPluginToString(slot->plugin),
			   catalog->dbfile);

	return true;
}


/*
 * catalog_replication_slot_fetch is a SQLiteQuery fetchFunction callback.
 */
static bool
catalog_replication_slot_fetch(SQLiteQuery *query)
{
	ReplicationSlot *slot = (ReplicationSlot *) query->context;

	/* slot_name */
	if (sqlite3_column_type(query->ppStmt, 0) != SQLITE_NULL)
	{
		strlcpy(slot->slotName,
				(char *) sqlite3_column_text(query->ppStmt, 0),
				sizeof(slot->slotName));
	}

	/* lsn */
	slot->lsn = (uint64_t) sqlite3_column_int64(query->ppStmt, 1);

	/* snapshot */
	if (sqlite3_column_type(query->ppStmt, 2) != SQLITE_NULL)
	{
		strlcpy(slot->snapshot,
				(char *) sqlite3_column_text(query->ppStmt, 2),
				sizeof(slot->snapshot));
	}

	/* plugin */
	if (sqlite3_column_type(query->ppStmt, 3) != SQLITE_NULL)
	{
		slot->plugin =
			OutputPluginFromString(
				(char *) sqlite3_column_text(query->ppStmt, 3));
	}

	/* wal2json_numeric_as_string */
	slot->wal2jsonNumericAsString = sqlite3_column_int(query->ppStmt, 4) != 0;

	/* publication_name (col 5) */
	if (sqlite3_column_type(query->ppStmt, 5) != SQLITE_NULL)
	{
		strlcpy(slot->publicationName,
				(char *) sqlite3_column_text(query->ppStmt, 5),
				sizeof(slot->publicationName));
	}

	/* publication_auto_managed (col 6) */
	slot->publicationAutoManaged = sqlite3_column_int(query->ppStmt, 6) != 0;

	return true;
}


/*
 * catalog_read_replication_slot reads the replication_slot row (id = 1) from
 * the given catalog and populates *slot.
 *
 * Returns true with slot->slotName empty when no row has been written yet
 * (first run).  Returns false only on a real database error.
 */
bool
catalog_read_replication_slot(DatabaseCatalog *catalog, ReplicationSlot *slot)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_read_replication_slot: db is NULL");
		return false;
	}

	char *sql =
		"select slot_name, lsn, snapshot, plugin, wal2json_numeric_as_string,"
		"       publication_name, publication_auto_managed "
		"  from replication_slot where id = 1";

	SQLiteQuery query = {
		.errorOnZeroRows = false,
		.context = slot,
		.fetchFunction = &catalog_replication_slot_fetch
	};

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_open_for_slot opens the source catalog at `dbfile` read-only, just
 * enough to call catalog_read_replication_slot.
 *
 * Returns false (not an error) when the file does not exist yet — the caller
 * treats that as "no prior slot, use defaults".  Returns false with an error
 * logged for actual open failures.
 */
bool
catalog_open_for_slot(const char *dbfile, DatabaseCatalog *catalog)
{
	if (!file_exists(dbfile))
	{
		return false;           /* no catalog yet, not an error */
	}

	strlcpy(catalog->dbfile, dbfile, sizeof(catalog->dbfile));
	catalog->type = DATABASE_CATALOG_TYPE_SOURCE;

	/*
	 * Open read-only: we only need to SELECT from replication_slot.
	 * We bypass the full catalog_init / catalog_create_schema path to avoid
	 * trying to CREATE TABLE on a read-only connection.
	 */
	int rc = sqlite3_open_v2(catalog->dbfile,
							 &(catalog->db),
							 SQLITE_OPEN_READONLY,
							 NULL);

	if (rc != SQLITE_OK)
	{
		log_error("Failed to open catalog \"%s\" read-only: %s",
				  catalog->dbfile,
				  sqlite3_errmsg(catalog->db));
		sqlite3_close(catalog->db);
		catalog->db = NULL;
		return false;
	}

	/*
	 * Create a private catalog semaphore so catalog_read_replication_slot
	 * can lock it.  We must NOT use semaphore_init() here: that function
	 * re-opens the shared log semaphore (PGCOPYDB_LOG_SEMAPHORE) when the
	 * environment variable is set.  Using the log semaphore as the catalog
	 * lock causes an immediate self-deadlock: catalog_read_replication_slot
	 * acquires it, then catalog_sql_prepare calls log_sqlite() which tries
	 * to acquire the same semaphore again (it is not reentrant).
	 */
	if (!catalog_create_semaphore(catalog))
	{
		/* errors have already been logged */
		sqlite3_close(catalog->db);
		catalog->db = NULL;
		return false;
	}

	return true;
}


/*
 * catalog_register_section registers that a section has been cached to the
 * internal catalogs.
 */
bool
catalog_register_section(DatabaseCatalog *catalog, TopLevelTiming *timing)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_register_section: db is NULL");
		return false;
	}

	char *sql =
		"insert or replace into section"
		"(name, fetched, start_time_epoch, done_time_epoch, duration) "
		"values($1, $2, $3, $4, $5)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "section", 0, (char *) timing->label },
		{ BIND_PARAMETER_TYPE_INT, "fetched", 1, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "start", timing->startTime, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "done", timing->doneTime, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "duration", timing->durationMs, NULL }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_section_state sets the fetched boolean to the catalog value.
 */
bool
catalog_section_state(DatabaseCatalog *catalog, CatalogSection *section)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_section_state: db is NULL");
		return false;
	}

	SQLiteQuery query = {
		.context = section,
		.fetchFunction = &catalog_section_fetch
	};

	char *sql = "select name, fetched, duration from section where name = $1";

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{
			BIND_PARAMETER_TYPE_TEXT, "name", 0,
			CopyDataSectionToString(section->section)
		}
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_section_fetch is a SQLiteQuery callback.
 */
bool
catalog_section_fetch(SQLiteQuery *query)
{
	CatalogSection *section = (CatalogSection *) query->context;

	strlcpy(section->name,
			(char *) sqlite3_column_text(query->ppStmt, 1),
			sizeof(section->name));

	section->fetched = sqlite3_column_int(query->ppStmt, 1) == 1;

	section->durationMs = sqlite3_column_int64(query->ppStmt, 2);

	return true;
}


/*
 * catalog_total_duration loops over a catalog section array and compute the
 * total duration in milliseconds.
 */
bool
catalog_total_duration(DatabaseCatalog *catalog)
{
	catalog->totalDurationMs = 0;

	for (int i = 1; i < DATA_SECTION_COUNT; i++)
	{
		CatalogSection *s = &(catalog->sections[i]);

		catalog->totalDurationMs += s->durationMs;
	}

	return true;
}


/*
 * CopyDataSectionToString returns a string representation of a section.
 */
char *
CopyDataSectionToString(CopyDataSection section)
{
	switch (section)
	{
		case DATA_SECTION_DATABASE_PROPERTIES:
		{
			return "database-properties";
		}

		case DATA_SECTION_COLLATIONS:
		{
			return "collations";
		}

		case DATA_SECTION_EXTENSIONS:
		{
			return "extension";
		}

		case DATA_SECTION_SCHEMA:
		{
			return "schema";
		}

		case DATA_SECTION_TABLE_DATA:
		{
			return "table-data";
		}

		case DATA_SECTION_TABLE_DATA_PARTS:
		{
			return "table-data-parts";
		}

		case DATA_SECTION_SET_SEQUENCES:
		{
			return "set-sequences";
		}

		case DATA_SECTION_INDEXES:
		{
			return "indexes";
		}

		case DATA_SECTION_CONSTRAINTS:
		{
			return "constraints";
		}

		case DATA_SECTION_DEPENDS:
		{
			return "pg_depend";
		}

		case DATA_SECTION_FILTERS:
		{
			return "filters";
		}

		case DATA_SECTION_BLOBS:
		{
			return "large-objects";
		}

		case DATA_SECTION_VACUUM:
		{
			return "vacuum";
		}

		case DATA_SECTION_ALL:
		{
			return "all";
		}

		case DATA_SECTION_NAMESPACES:
		{
			return "namespaces";
		}

		case DATA_SECTION_NONE:
		default:
		{
			log_error("BUG: CopyDataSectionToString unknown section %d", section);
			return NULL;
		}
	}

	/* keep compiler happy */
	return "unknown";
}


/*
 * catalog_add_s_matview INSERTs a SourceTable to our matview internal catalogs
 * database.
 */
bool
catalog_add_s_matview(DatabaseCatalog *catalog, SourceTable *table)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_matview: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_matview("
		"  oid, qname, nspname, relname, restore_list_name, "
		"  exclude_data) "
		"values($1, $2, $3, $4, $5, $6)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", table->oid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "qname", 0, table->qname },
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, table->nspname },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, table->relname },

		{
			BIND_PARAMETER_TYPE_TEXT, "restore_list_name", 0,
			table->restoreListName
		},

		{
			BIND_PARAMETER_TYPE_INT, "exclude_data",
			table->excludeData ? 1 : 0, NULL
		},
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_upsert_target_s_rel inserts or replaces a row in the target
 * catalog's s_rel table (nspname, relname, relkind, ispopulated).
 * Used to cache matviews ('m') and partitioned tables ('p') from the target.
 */
bool
catalog_upsert_target_s_rel(DatabaseCatalog *catalog,
							const char *nspname,
							const char *relname,
							char relkind,
							bool ispopulated)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_upsert_target_s_rel: db is NULL");
		return false;
	}

	char relkindStr[2] = { relkind, '\0' };

	char *sql =
		"insert into s_rel(nspname, relname, relkind, ispopulated)"
		" values($1, $2, $3, $4)"
		" on conflict(nspname, relname) do update"
		"   set relkind = excluded.relkind,"
		"       ispopulated = excluded.ispopulated";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, (char *) nspname },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, (char *) relname },
		{ BIND_PARAMETER_TYPE_TEXT, "relkind", 0, relkindStr },
		{ BIND_PARAMETER_TYPE_INT, "ispopulated", ispopulated ? 1 : 0, NULL },
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

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

	return true;
}


/*
 * catalog_lookup_s_matview_by_oid fetches a s_matview entry from catalog.
 */
bool
catalog_lookup_s_matview_by_oid(DatabaseCatalog *catalog,
								CatalogMatView *result,
								uint32_t oid)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_filter_by_oid: db is NULL");
		return false;
	}

	char *sql =
		"  select oid, nspname, relname, restore_list_name, exclude_data"
		"    from s_matview "
		"   where oid = $1 ";

	SQLiteQuery query = {
		.context = result,
		.fetchFunction = &catalog_s_matview_fetch
	};

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[1] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", oid, NULL },
	};

	if (!catalog_sql_bind(&query, params, 1))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_s_matview_fetch fetches a CatalogMatview entry from a SQLite ppStmt
 * result set.
 */
bool
catalog_s_matview_fetch(SQLiteQuery *query)
{
	CatalogMatView *entry = (CatalogMatView *) query->context;

	/* cleanup the memory area before re-use */
	bzero(entry, sizeof(CatalogMatView));

	entry->oid = sqlite3_column_int64(query->ppStmt, 0);

	if (sqlite3_column_type(query->ppStmt, 1) != SQLITE_NULL)
	{
		strlcpy(entry->nspname,
				(char *) sqlite3_column_text(query->ppStmt, 1),
				sizeof(entry->nspname));
	}

	if (sqlite3_column_type(query->ppStmt, 2) != SQLITE_NULL)
	{
		strlcpy(entry->relname,
				(char *) sqlite3_column_text(query->ppStmt, 2),
				sizeof(entry->relname));
	}

	if (sqlite3_column_type(query->ppStmt, 3) != SQLITE_NULL)
	{
		strlcpy(entry->restoreListName,
				(char *) sqlite3_column_text(query->ppStmt, 3),
				sizeof(entry->restoreListName));
	}

	entry->excludeData = sqlite3_column_int64(query->ppStmt, 4) == 1;

	return true;
}


/*
 * catalog_add_s_table INSERTs a SourceTable to our internal catalogs database.
 */
bool
catalog_add_s_table(DatabaseCatalog *catalog, SourceTable *table)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_table: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_table("
		"  oid, datname, qname, nspname, relname, amname, restore_list_name, "
		"  relpages, reltuples, exclude_data, part_key) "
		"values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", table->oid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "datname", 0, table->datname },
		{ BIND_PARAMETER_TYPE_TEXT, "qname", 0, table->qname },
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, table->nspname },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, table->relname },
		{ BIND_PARAMETER_TYPE_TEXT, "amname", 0, table->amname },

		{
			BIND_PARAMETER_TYPE_TEXT, "restore_list_name", 0,
			table->restoreListName
		},

		{ BIND_PARAMETER_TYPE_INT64, "relpages", table->relpages, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "reltuples", table->reltuples, NULL },

		{
			BIND_PARAMETER_TYPE_INT, "exclude_data",
			table->excludeData ? 1 : 0, NULL
		},

		{ BIND_PARAMETER_TYPE_TEXT, "part_key", 0, table->partKey }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_add_s_attr INSERTs a single SourceTableAttribute into s_attr.
 * Used when streaming attributes from the separate list_table_attributes query.
 */
bool
catalog_add_s_attr(DatabaseCatalog *catalog,
				   uint32_t tableoid,
				   SourceTableAttribute *attr)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_attr: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_attr("
		"oid, attnum, attypid, attname, "
		"attisprimary, attisreplident, attisgenerated, attidentity, "
		"attisbinarycompatible, atttypsend)"
		"values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)";

	/* store attidentity as a one-char string, or "" when not an identity col */
	char identityStr[2] = { attr->attidentity, '\0' };

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", tableoid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "attnum", attr->attnum, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "atttypid", attr->atttypid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "attname", 0, attr->attname },

		{
			BIND_PARAMETER_TYPE_INT, "attisprimary",
			attr->attisprimary ? 1 : 0, NULL
		},

		{
			BIND_PARAMETER_TYPE_INT, "attisreplident",
			attr->attisreplident ? 1 : 0, NULL
		},

		{
			BIND_PARAMETER_TYPE_INT, "attisgenerated",
			attr->attisgenerated ? 1 : 0, NULL
		},

		{ BIND_PARAMETER_TYPE_TEXT, "attidentity", 0, identityStr },

		{
			BIND_PARAMETER_TYPE_INT, "attisbinarycompatible",
			attr->attisbinarycompatible ? 1 : 0, NULL
		},

		{
			BIND_PARAMETER_TYPE_TEXT, "atttypsend", 0, attr->atttypsend
		}
	};

	int count = sizeof(params) / sizeof(params[0]);

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

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

	return true;
}


/*
 * catalog_oid_array_sql runs a single-row aggregation query of the form
 *
 *   SELECT coalesce('{' || group_concat(...) || '}', fallback), count(*)
 *
 * and returns the resulting text and count.  When null_if_empty is true,
 * *text is set to NULL (not '{}') when the table is empty; callers that
 * pass NULL to mean "no filter" require this behaviour.
 */
static bool
catalog_oid_array_sql(sqlite3 *db, const char *sql, bool null_if_empty,
					  char **text, int *count)
{
	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		return false;
	}

	int rc = catalog_sql_step(&query);

	if (rc != SQLITE_ROW)
	{
		log_error("[SQLite %d: %s]: %s", rc, query.sql, sqlite3_errmsg(db));
		(void) catalog_sql_finalize(&query);
		return false;
	}

	*count = sqlite3_column_int(query.ppStmt, 1);

	if (null_if_empty && *count == 0)
	{
		*text = NULL;
	}
	else
	{
		const char *str = (const char *) sqlite3_column_text(query.ppStmt, 0);

		*text = str ? strdup(str) : NULL;
	}

	(void) catalog_sql_finalize(&query);

	return *count == 0 || *text != NULL;
}


/*
 * catalog_s_table_oid_array builds a PostgreSQL array literal of all table
 * OIDs stored in s_table, formatted as "{oid1,oid2,...}" for use as a $1
 * text parameter with ::oid[] casting in SQL.  Returns "{}" when empty.
 * The caller must free *text.
 */
bool
catalog_s_table_oid_array(DatabaseCatalog *catalog, char **text, int *count)
{
	if (catalog->db == NULL)
	{
		log_error("BUG: catalog_s_table_oid_array: db is NULL");
		return false;
	}

	char *sql =
		"select coalesce('{' || group_concat(cast(oid as text), ',') || '}', '{}'),"
		"       count(*)"
		"  from s_table";

	return catalog_oid_array_sql(catalog->db, sql, false, text, count);
}


/*
 * catalog_s_class_oid_array is like catalog_s_table_oid_array but also
 * includes OIDs from s_matview.  Used by schema_list_pg_depend so that
 * dependencies on excluded materialized views are captured as well as
 * dependencies on excluded regular tables.  Returns "{}" when empty.
 */
bool
catalog_s_class_oid_array(DatabaseCatalog *catalog, char **text, int *count)
{
	if (catalog->db == NULL)
	{
		log_error("BUG: catalog_s_class_oid_array: db is NULL");
		return false;
	}

	char *sql =
		"select coalesce('{' || group_concat(cast(oid as text), ',') || '}', '{}'),"
		"       count(*)"
		"  from (select oid from s_table union all select oid from s_matview)";

	return catalog_oid_array_sql(catalog->db, sql, false, text, count);
}


/*
 * catalog_s_namespace_oid_array builds a PostgreSQL array literal of all
 * namespace OIDs stored in s_namespace, formatted as "{oid1,oid2,...}" for
 * use as a $1 text parameter with ::oid[] casting in SQL.  Returns NULL
 * (not "{}") when the catalog has no namespaces, because callers treat a
 * NULL parameter as "no filter".  The caller must free *text.
 */
bool
catalog_s_namespace_oid_array(DatabaseCatalog *catalog, char **text, int *count)
{
	if (catalog->db == NULL)
	{
		log_error("BUG: catalog_s_namespace_oid_array: db is NULL");
		return false;
	}

	char *sql =
		"select case when count(*) = 0 then null"
		"            else '{' || group_concat(cast(oid as text), ',') || '}'"
		"       end,"
		"       count(*)"
		"  from s_namespace";

	return catalog_oid_array_sql(catalog->db, sql, true, text, count);
}


/*
 * catalog_populate_filters writes all SourceFilters entries into the f_schema
 * and f_table SQLite tables.  The previous contents are replaced (DELETE +
 * INSERT) so this call is idempotent.  Callers use the tables through
 * catalog_filter_schema_array() and catalog_filter_table_arrays() instead of
 * building PostgreSQL array literals with PQExpBuffer loops in C.
 */
bool
catalog_populate_filters(DatabaseCatalog *catalog, SourceFilters *filters)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_populate_filters: db is NULL");
		return false;
	}

	if (!catalog_execute(catalog, "delete from f_schema; delete from f_table"))
	{
		log_error("Failed to clear filter tables");
		return false;
	}

	/* Schema sections: exact names and regex patterns share the f_schema table
	 * — exact entries have nspname IS NOT NULL, regex entries have nspname_re. */
	struct
	{
		const char *section;
		SourceFilterSchemaList *list;
		SourceFilterSchemaPatternList *plist;
	}
	sschemas[] = {
		{ "incl_schema", &(filters->includeOnlySchemaList),
		  &(filters->includeOnlySchemaPatternList) },
		{ "excl_schema", &(filters->excludeSchemaList),
		  &(filters->excludeSchemaPatternList) },
		{ NULL }
	};

	const char *ins_schema =
		"insert into f_schema(section, nspname, nspname_re) values($1, $2, $3)";

	for (int i = 0; sschemas[i].section != NULL; i++)
	{
		const char *sec = sschemas[i].section;
		SourceFilterSchemaList *list = sschemas[i].list;

		for (int j = 0; j < list->count; j++)
		{
			SQLiteQuery query = { 0 };

			if (!catalog_sql_prepare(db, ins_schema, &query))
			{
				return false;
			}

			BindParam params[] = {
				{ BIND_PARAMETER_TYPE_TEXT, "section", 0, (char *) sec },
				{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0,
				  list->array[j].nspname },
				{ BIND_PARAMETER_TYPE_NULL, "nspname_re", 0, NULL },
			};

			if (!catalog_sql_bind(&query, params, 3) ||
				!catalog_sql_execute_once(&query))
			{
				return false;
			}
		}

		SourceFilterSchemaPatternList *plist = sschemas[i].plist;

		for (int j = 0; j < plist->count; j++)
		{
			SQLiteQuery query = { 0 };

			if (!catalog_sql_prepare(db, ins_schema, &query))
			{
				return false;
			}

			BindParam params[] = {
				{ BIND_PARAMETER_TYPE_TEXT, "section", 0, (char *) sec },
				{ BIND_PARAMETER_TYPE_NULL, "nspname", 0, NULL },
				{ BIND_PARAMETER_TYPE_TEXT, "nspname_re", 0,
				  plist->array[j].nspname_re },
			};

			if (!catalog_sql_bind(&query, params, 3) ||
				!catalog_sql_execute_once(&query))
			{
				return false;
			}
		}
	}

	/* Table / index sections: EE entries come from the exact list (both
	 * nspname and relname non-NULL), ER/RE/RR entries come from the pattern
	 * list (at least one _re field non-empty). */
	struct
	{
		const char *section;
		SourceFilterTableList *list;
		SourceFilterTablePatternList *plist;
	}
	stables[] = {
		{ "incl_table", &(filters->includeOnlyTableList),
		  &(filters->includeOnlyTablePatternList) },
		{ "excl_table", &(filters->excludeTableList),
		  &(filters->excludeTablePatternList) },
		{ "xdat_table", &(filters->excludeTableDataList),
		  &(filters->excludeTableDataPatternList) },
		{ "excl_index", &(filters->excludeIndexList),
		  &(filters->excludeIndexPatternList) },
		{ NULL }
	};

	const char *ins_table =
		"insert into f_table(section, nspname, nspname_re, relname, relname_re)"
		" values($1, $2, $3, $4, $5)";

	for (int i = 0; stables[i].section != NULL; i++)
	{
		const char *sec = stables[i].section;
		SourceFilterTableList *list = stables[i].list;

		for (int j = 0; j < list->count; j++)
		{
			SQLiteQuery query = { 0 };

			if (!catalog_sql_prepare(db, ins_table, &query))
			{
				return false;
			}

			BindParam params[] = {
				{ BIND_PARAMETER_TYPE_TEXT, "section", 0, (char *) sec },
				{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0,
				  list->array[j].nspname },
				{ BIND_PARAMETER_TYPE_NULL, "nspname_re", 0, NULL },
				{ BIND_PARAMETER_TYPE_TEXT, "relname", 0,
				  list->array[j].relname },
				{ BIND_PARAMETER_TYPE_NULL, "relname_re", 0, NULL },
			};

			if (!catalog_sql_bind(&query, params, 5) ||
				!catalog_sql_execute_once(&query))
			{
				return false;
			}
		}

		SourceFilterTablePatternList *plist = stables[i].plist;

		for (int j = 0; j < plist->count; j++)
		{
			SQLiteQuery query = { 0 };

			if (!catalog_sql_prepare(db, ins_table, &query))
			{
				return false;
			}

			SourceFilterTablePattern *p = &(plist->array[j]);

			BindParam params[] = {
				{ BIND_PARAMETER_TYPE_TEXT, "section", 0, (char *) sec },
				{ p->nspname[0] != '\0' ? BIND_PARAMETER_TYPE_TEXT
				  : BIND_PARAMETER_TYPE_NULL,
				  "nspname", 0, p->nspname },
				{ p->nspname_re[0] != '\0' ? BIND_PARAMETER_TYPE_TEXT
				  : BIND_PARAMETER_TYPE_NULL,
				  "nspname_re", 0, p->nspname_re },
				{ p->relname[0] != '\0' ? BIND_PARAMETER_TYPE_TEXT
				  : BIND_PARAMETER_TYPE_NULL,
				  "relname", 0, p->relname },
				{ p->relname_re[0] != '\0' ? BIND_PARAMETER_TYPE_TEXT
				  : BIND_PARAMETER_TYPE_NULL,
				  "relname_re", 0, p->relname_re },
			};

			if (!catalog_sql_bind(&query, params, 5) ||
				!catalog_sql_execute_once(&query))
			{
				return false;
			}
		}
	}

	return true;
}


/*
 * catalog_filter_schema_array returns a PostgreSQL array literal
 * "{name1,name2,...}" (or NULL when there are no matching entries) built by
 * querying f_schema with SQLite's group_concat aggregation.  use_re=false
 * reads the exact nspname column; use_re=true reads nspname_re.
 */
bool
catalog_filter_schema_array(DatabaseCatalog *catalog, const char *section,
							bool use_re, char **out)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_filter_schema_array: db is NULL");
		return false;
	}

	/* One query for exact names, another for regex patterns. */
	const char *col = use_re ? "nspname_re" : "nspname";
	const char *where = use_re
						? "nspname_re is not null"
						: "nspname is not null";

	/* Build the SQL with the column/where baked in (not user input). */
	char sql[512];

	sformat(sql, sizeof(sql),
			"select '{' || group_concat(%s, ',') || '}'"
			"  from f_schema"
			" where section = $1 and %s"
			" having count(*) > 0",
			col, where);

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		return false;
	}

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "section", 0, (char *) section },
	};

	if (!catalog_sql_bind(&query, params, 1))
	{
		return false;
	}

	int rc = catalog_sql_step(&query);

	if (rc == SQLITE_DONE)
	{
		/* HAVING count(*) > 0 filtered out the zero-row case → NULL */
		*out = NULL;
		(void) catalog_sql_finalize(&query);
		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("[SQLite %d: %s]: %s", rc, query.sql, sqlite3_errmsg(db));
		(void) catalog_sql_finalize(&query);
		return false;
	}

	const char *str = (const char *) sqlite3_column_text(query.ppStmt, 0);

	*out = str ? strdup(str) : NULL;
	(void) catalog_sql_finalize(&query);

	return *out != NULL;
}


/*
 * catalog_filter_table_arrays builds all eight EE/ER/RE/RR paired array
 * literals for one filter section (e.g. "excl_table", "incl_table", ...) in
 * a single SQLite query (filter_table_arrays.sql).  One CTE scan of f_table
 * with conditional group_concat() FILTER clauses replaces eight correlated
 * subqueries.  NULL output pointers are skipped.  Each output is set to a
 * PostgreSQL array literal "{v1,v2,...}" or NULL when the category is empty.
 */
bool
catalog_filter_table_arrays(DatabaseCatalog *catalog, const char *section,
							char **ee_nsp, char **ee_rel,
							char **er_nsp, char **er_rel,
							char **re_nsp, char **re_rel,
							char **rr_nsp, char **rr_rel)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_filter_table_arrays: db is NULL");
		return false;
	}

	const char *sql = NULL;

	if (!pgcopydb_sql_filter_table_arrays(&sql))
	{
		return false;
	}

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		return false;
	}

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "section", 0, (char *) section },
	};

	if (!catalog_sql_bind(&query, params, 1))
	{
		return false;
	}

	int rc = catalog_sql_step(&query);

	if (rc != SQLITE_ROW)
	{
		log_error("[SQLite %d: %s]: %s", rc, query.sql, sqlite3_errmsg(db));
		(void) catalog_sql_finalize(&query);
		return false;
	}

	char **outputs[8] = {
		ee_nsp, ee_rel, er_nsp, er_rel,
		re_nsp, re_rel, rr_nsp, rr_rel
	};

	for (int col = 0; col < 8; col++)
	{
		if (outputs[col] == NULL)
		{
			continue;
		}

		if (sqlite3_column_type(query.ppStmt, col) == SQLITE_NULL)
		{
			*(outputs[col]) = NULL;
		}
		else
		{
			const char *str =
				(const char *) sqlite3_column_text(query.ppStmt, col);

			*(outputs[col]) = str ? strdup(str) : NULL;
		}
	}

	(void) catalog_sql_finalize(&query);

	return true;
}


/*
 * catalog_add_s_table_part INSERTs a SourceTableParts to our internal catalogs
 * database (s_table_parts).
 */
bool
catalog_add_s_table_part(DatabaseCatalog *catalog, SourceTable *table)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_table_part: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_table_part(oid, partnum, partcount, min, max, count)"
		"values($1, $2, $3, $4, $5, $6)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	SourceTableParts *part = &(table->partition);

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", table->oid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "partnum", part->partNumber, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "partcount", part->partCount, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "min", part->min, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "max", part->max, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "count", part->count, NULL },
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_add_s_table INSERTs a SourceTable to our internal catalogs database.
 */
bool
catalog_add_s_table_chksum(DatabaseCatalog *catalog,
						   SourceTable *table,
						   TableChecksum *srcChk,
						   TableChecksum *dstChk)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_table_chksum: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_table_chksum("
		"  oid, srcrowcount, srcsum, dstrowcount, dstsum)"
		"values($1, $2, $3, $4, $5)";

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", table->oid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "srcrowcount", srcChk->rowcount, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "srcsum", 0, srcChk->checksum },
		{ BIND_PARAMETER_TYPE_INT64, "dstrowcount", dstChk->rowcount, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "dstsum", 0, dstChk->checksum }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_add_s_table_size inserts a SourceTableSize to our internal catalogs database.
 */
bool
catalog_add_s_table_size(DatabaseCatalog *catalog,
						 SourceTableSize *tableSize)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_table_size: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_table_size("
		"  oid, bytes, bytes_pretty)"
		"values($1, $2, $3)";

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", tableSize->oid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "bytes", tableSize->bytes, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "bytes_pretty", 0, tableSize->bytesPretty },
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_delete_s_table_chksum_all implements cache invalidation for pgcopydb
 * compare data.
 */
bool
catalog_delete_s_table_chksum_all(DatabaseCatalog *catalog)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_delete_s_table_chksum_all: db is NULL");
		return false;
	}

	char *sql = "delete from s_table_chksum";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_stats fetches statistics about the objects we have in our catalog.
 */
bool
catalog_stats(DatabaseCatalog *catalog, CatalogStats *stats)
{
	if (!catalog_s_table_stats(catalog, &(stats->table)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_count_objects(catalog, &(stats->count)))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_s_table_stats fetches statistics about the SourceTable list we have
 * in our catalog.
 */
bool
catalog_s_table_stats(DatabaseCatalog *catalog, CatalogTableStats *stats)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_s_table_stats: db is NULL");
		return false;
	}

	char *sql =
		"select count(t.oid) as count, "
		"       count(p.oid) as countSplits, "
		"       sum(p.partcount) as countParts, "
		"       sum(ts.bytes) as totalBytes, "
		"       sum(reltuples) as totalTuples "
		"  from s_table t "
		"       left join "
		"         ("
		"             select oid, count(*) as partcount "
		"               from s_table_part "
		"           group by oid"
		"         ) p "
		"        on p.oid = t.oid"
		"       left join s_table_size ts on ts.oid = t.oid ";

	SQLiteQuery query = {
		.context = stats,
		.fetchFunction = &catalog_s_table_stats_fetch
	};

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_s_table_stats_fetch is a SQLiteQuery callback.
 */
bool
catalog_s_table_stats_fetch(SQLiteQuery *query)
{
	CatalogTableStats *stats = (CatalogTableStats *) query->context;

	stats->count = sqlite3_column_int64(query->ppStmt, 0);
	stats->countSplits = sqlite3_column_int64(query->ppStmt, 1);
	stats->countParts = sqlite3_column_int64(query->ppStmt, 2);
	stats->totalBytes = sqlite3_column_int64(query->ppStmt, 3);
	stats->totalTuples = sqlite3_column_int64(query->ppStmt, 4);

	(void) pretty_print_bytes(stats->bytesPretty, BUFSIZE, stats->totalBytes);
	(void) pretty_print_count(stats->relTuplesPretty, BUFSIZE, stats->totalTuples);

	return true;
}


/*
 * catalog_count_objects returns how many objects were added to the catalogs.
 */
bool
catalog_count_objects(DatabaseCatalog *catalog, CatalogCounts *count)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_count_objects: db is NULL");
		return false;
	}

	char *sql = NULL;

	switch (catalog->type)
	{
		case DATABASE_CATALOG_TYPE_SOURCE:
		{
			sql =
				"select (select count(1) as rel from s_table), "
				"       (select count(1) as idx from s_index), "
				"       (select count(1) as con from s_constraint),"
				"       (select count(1) as seq from s_seq),"
				"       0 as rol,"
				"       (select count(1) as dat from s_database),"
				"       0 as nsp,"
				"       0 as ext,"
				"       0 as colls,"
				"       0 as pg_depend";
			break;
		}

		case DATABASE_CATALOG_TYPE_FILTER:
		{
			sql =
				"select (select count(1) as rel from s_table), "
				"       (select count(1) as idx from s_index), "
				"       (select count(1) as con from s_constraint),"
				"       (select count(1) as seq from s_seq),"
				"       0 as rol,"
				"       0 as dat,"
				"       (select count(1) as nsp from s_namespace),"
				"       (select count(1) as ext from s_extension),"
				"       (select count(1) as col from s_coll),"
				"       (select count(1) as dep from s_depend)";
			break;
		}

		case DATABASE_CATALOG_TYPE_TARGET:
		{
			sql =
				"select (select count(1) as rel from s_table), "
				"       (select count(1) as idx from s_index), "
				"       (select count(1) as con from s_constraint),"
				"       0 as seq,"
				"       (select count(1) as rol from s_role),"
				"       0 as dat,"
				"       (select count(1) as nsp from s_namespace),"
				"       0 as ext,"
				"       0 as colls,"
				"       0 as pg_depend";
			break;
		}

		case DATABASE_CATALOG_TYPE_REPLAY:
		case DATABASE_CATALOG_TYPE_OUTPUT:
		{
			/* CDC databases don't have schema objects to count */
			sql =
				"select 0,0,0,0,0,0,0,0,0,0";
			break;
		}

		default:
		{
			log_error("BUG: called catalog_count_objects for unknown type %d",
					  catalog->type);
			return false;
		}
	}

	SQLiteQuery query = {
		.context = count,
		.fetchFunction = &catalog_count_fetch
	};

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_count_fetch fetches a CatalogIndexCount from a query ppStmt
 * result.
 */
bool
catalog_count_fetch(SQLiteQuery *query)
{
	CatalogCounts *count = (CatalogCounts *) query->context;

	count->tables = sqlite3_column_int64(query->ppStmt, 0);
	count->indexes = sqlite3_column_int64(query->ppStmt, 1);
	count->constraints = sqlite3_column_int64(query->ppStmt, 2);
	count->sequences = sqlite3_column_int64(query->ppStmt, 3);

	count->roles = sqlite3_column_int64(query->ppStmt, 4);
	count->databases = sqlite3_column_int64(query->ppStmt, 5);
	count->namespaces = sqlite3_column_int64(query->ppStmt, 6);
	count->extensions = sqlite3_column_int64(query->ppStmt, 7);
	count->colls = sqlite3_column_int64(query->ppStmt, 8);
	count->depends = sqlite3_column_int64(query->ppStmt, 9);

	return true;
}


/*
 * catalog_lookup_s_table fetches a SourceTable entry from our catalogs.
 */
bool
catalog_lookup_s_table(DatabaseCatalog *catalog,
					   uint32_t oid,
					   int partNumber,
					   SourceTable *table)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_s_table: db is NULL");
		return false;
	}

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	SQLiteQuery query = {
		.context = table,
		.fetchFunction = &catalog_s_table_fetch
	};

	if (partNumber > 0)
	{
		char *sql =
			"  select t.oid, t.datname, qname, nspname, relname, amname, restore_list_name, "
			"         relpages, reltuples, ts.bytes, ts.bytes_pretty, "
			"         exclude_data, part_key, "
			"         p.partcount as partcount, p.partnum, p.min, p.max "
			"    from s_table t "
			"         join s_table_part p "
			"           on t.oid = p.oid "
			"          and p.partnum = $1"
			"       left join s_table_size ts on ts.oid = t.oid "
			"   where t.oid = $2 ";

		if (!catalog_sql_prepare(db, sql, &query))
		{
			/* errors have already been logged */
			(void) semaphore_unlock(&(catalog->sema));
			return false;
		}

		/* bind our parameters now */
		BindParam params[] = {
			{ BIND_PARAMETER_TYPE_INT64, "partnum", partNumber, NULL },
			{ BIND_PARAMETER_TYPE_INT64, "oid", oid, NULL }
		};

		int count = sizeof(params) / sizeof(params[0]);

		if (!catalog_sql_bind(&query, params, count))
		{
			/* errors have already been logged */
			(void) semaphore_unlock(&(catalog->sema));
			return false;
		}
	}
	else
	{
		char *sql =
			"  select t.oid, t.datname, qname, nspname, relname, amname, restore_list_name, "
			"         relpages, reltuples, ts.bytes, ts.bytes_pretty, "
			"         exclude_data, part_key, "
			"         count(p.oid) as partcount "
			"    from s_table t left join s_table_part p on t.oid = p.oid"
			"       left join s_table_size ts on ts.oid = t.oid "
			"   where t.oid = $1 ";

		if (!catalog_sql_prepare(db, sql, &query))
		{
			/* errors have already been logged */
			(void) semaphore_unlock(&(catalog->sema));
			return false;
		}

		/* bind our parameters now */
		BindParam params[] = {
			{ BIND_PARAMETER_TYPE_INT64, "oid", oid, NULL }
		};

		int count = sizeof(params) / sizeof(params[0]);

		if (!catalog_sql_bind(&query, params, count))
		{
			/* errors have already been logged */
			(void) semaphore_unlock(&(catalog->sema));
			return false;
		}
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_lookup_s_table_by_name fetches a SourceTable from our catalogs.
 */
bool
catalog_lookup_s_table_by_name(DatabaseCatalog *catalog,
							   const char *nspname,
							   const char *relname,
							   SourceTable *table)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_s_table_stats: db is NULL");
		return false;
	}

	char *sql =
		"  select t.oid, t.datname, qname, nspname, relname, amname, restore_list_name, "
		"         relpages, reltuples, ts.bytes, ts.bytes_pretty, "
		"         exclude_data, part_key, "
		"         p.partcount, 0 as partnum, 0 as min, 0 as max "
		"    from s_table t "
		"         left join "
		"         ("
		"             select oid, count(*) as partcount "
		"               from s_table_part "
		"           group by oid"
		"         ) p "
		"        on p.oid = t.oid"
		"       left join s_table_size ts on ts.oid = t.oid "
		"   where nspname = $1 and relname = $2 ";

	SQLiteQuery query = {
		.context = table,
		.fetchFunction = &catalog_s_table_fetch
	};

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, (char *) nspname },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, (char *) relname },
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_lookup_s_attr_by_name fetches a SourceTable from our catalogs.
 */
bool
catalog_lookup_s_attr_by_name(DatabaseCatalog *catalog,
							  uint32_t reloid,
							  const char *attname,
							  SourceTableAttribute *attribute)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_s_attr_by_name: db is NULL");
		return false;
	}

	char *sql =
		"  select attnum, attypid, attname, "
		"         attisprimary, attisreplident, attisgenerated, attidentity "
		"    from s_attr "
		"   where oid = $1 and attname = $2";

	SQLiteQuery query = {
		.context = attribute,
		.fetchFunction = &catalog_s_attr_fetch
	};

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", reloid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "attname", 0, (char *) attname },
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_table iterates over the list of tables in our catalogs.
 */
bool
catalog_iter_s_table(DatabaseCatalog *catalog,
					 void *context,
					 SourceTableIterFun *callback)
{
	SourceTableIterator *iter =
		(SourceTableIterator *) calloc(1, sizeof(SourceTableIterator));

	iter->catalog = catalog;

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

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

		SourceTable *table = iter->table;

		if (table == NULL)
		{
			if (!catalog_iter_s_table_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, table))
		{
			log_error("Failed to iterate over list of tables, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_table_nopk iterates over the list of tables that don't have a
 * Primary Key in our catalogs.
 */
bool
catalog_iter_s_table_nopk(DatabaseCatalog *catalog,
						  void *context,
						  SourceTableIterFun *callback)
{
	SourceTableIterator *iter =
		(SourceTableIterator *) calloc(1, sizeof(SourceTableIterator));

	iter->catalog = catalog;

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

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

		SourceTable *table = iter->table;

		if (table == NULL)
		{
			if (!catalog_iter_s_table_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, table))
		{
			log_error("Failed to iterate over list of tables, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_table_init initializes an Interator over our catalog of
 * SourceTable entries.
 */
bool
catalog_iter_s_table_init(SourceTableIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_table iterator: db is NULL");
		return false;
	}

	iter->table = (SourceTable *) calloc(1, sizeof(SourceTable));

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

	char *sql =
		"  select t.oid, t.datname, qname, nspname, relname, amname, restore_list_name, "
		"         relpages, reltuples, ts.bytes, ts.bytes_pretty, "
		"         exclude_data, part_key, "
		"         coalesce(p.partcount, 0) as partcount, "
		"         coalesce(p.partnum, 0) as partnum, "
		"         coalesce(p.min, 0) as min, coalesce(p.max, 0) as max, "
		"         c.srcrowcount, c.srcsum, c.dstrowcount, c.dstsum, "
		"         sum(s.duration), sum(s.bytes) "
		"    from s_table t "
		"         left join s_table_part p on p.oid = t.oid "
		"         left join s_table_chksum c on c.oid = t.oid "
		"         left join summary s on s.tableoid = t.oid "
		"         left join s_table_size ts on ts.oid = t.oid "
		"group by t.oid "
		"order by ts.bytes desc";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->table;
	query->fetchFunction = &catalog_s_table_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_table_init initializes an Interator over our catalog of
 * SourceTable entries.
 */
bool
catalog_iter_s_table_nopk_init(SourceTableIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_table iterator: db is NULL");
		return false;
	}

	iter->table = (SourceTable *) calloc(1, sizeof(SourceTable));

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

	char *sql =
		"  select t.oid, t.datname, qname, nspname, relname, amname, restore_list_name, "
		"         relpages, reltuples, ts.bytes, ts.bytes_pretty, "
		"         exclude_data, part_key, "
		"         (select count(1) from s_table_part p where p.oid = t.oid) "
		"    from s_table t join join s_attr a on a.oid = t.oid "
		"       left join s_table_size ts on ts.oid = t.oid "
		"group by t.oid "
		"  having sum(a.attisprimary) = 0 "
		"order by bytes desc";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->table;
	query->fetchFunction = &catalog_s_table_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_table_next fetches the next SourceTable entry in our catalogs.
 */
bool
catalog_iter_s_table_next(SourceTableIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	int rc = catalog_sql_step(query);

	if (rc == SQLITE_DONE)
	{
		iter->table = NULL;

		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("Failed to step through statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return catalog_s_table_fetch(query);
}


/*
 * catalog_s_table_fetch fetches a SourceTable entry from a SQLite ppStmt
 * result set.
 */
bool
catalog_s_table_fetch(SQLiteQuery *query)
{
	SourceTable *table = (SourceTable *) query->context;

	/* cleanup the memory area before re-use */
	bzero(table, sizeof(SourceTable));

	table->oid = sqlite3_column_int64(query->ppStmt, 0);

	if (sqlite3_column_type(query->ppStmt, 1) != SQLITE_NULL)
	{
		strlcpy(table->datname,
				(char *) sqlite3_column_text(query->ppStmt, 1),
				sizeof(table->datname));
	}

	if (sqlite3_column_type(query->ppStmt, 2) != SQLITE_NULL)
	{
		strlcpy(table->qname,
				(char *) sqlite3_column_text(query->ppStmt, 2),
				sizeof(table->qname));
	}

	if (sqlite3_column_type(query->ppStmt, 3) != SQLITE_NULL)
	{
		strlcpy(table->nspname,
				(char *) sqlite3_column_text(query->ppStmt, 3),
				sizeof(table->nspname));
	}

	if (sqlite3_column_type(query->ppStmt, 4) != SQLITE_NULL)
	{
		strlcpy(table->relname,
				(char *) sqlite3_column_text(query->ppStmt, 4),
				sizeof(table->relname));
	}

	if (sqlite3_column_type(query->ppStmt, 5) != SQLITE_NULL)
	{
		strlcpy(table->amname,
				(char *) sqlite3_column_text(query->ppStmt, 5),
				sizeof(table->amname));
	}

	if (sqlite3_column_type(query->ppStmt, 6) != SQLITE_NULL)
	{
		strlcpy(table->restoreListName,
				(char *) sqlite3_column_text(query->ppStmt, 6),
				sizeof(table->restoreListName));
	}

	table->relpages = sqlite3_column_int64(query->ppStmt, 7);
	table->reltuples = sqlite3_column_int64(query->ppStmt, 8);
	table->bytes = sqlite3_column_int64(query->ppStmt, 9);

	if (sqlite3_column_type(query->ppStmt, 10) != SQLITE_NULL)
	{
		strlcpy(table->bytesPretty,
				(char *) sqlite3_column_text(query->ppStmt, 10),
				sizeof(table->bytesPretty));
	}

	table->excludeData = sqlite3_column_int64(query->ppStmt, 11) == 1;

	if (sqlite3_column_type(query->ppStmt, 12) != SQLITE_NULL)
	{
		strlcpy(table->partKey,
				(char *) sqlite3_column_text(query->ppStmt, 12),
				sizeof(table->partKey));
	}

	table->partition.partCount = sqlite3_column_int64(query->ppStmt, 13);

	/*
	 * The main iterator query returns partition count, whereas the catalog
	 * fetch query, which is given a table oid, then returns partNumber, min,
	 * max, and count values.
	 */
	int cols = sqlite3_column_count(query->ppStmt);

	/* partition information from s_table_part */
	if (cols >= 17)
	{
		table->partition.partNumber = sqlite3_column_int64(query->ppStmt, 14);
		table->partition.min = sqlite3_column_int64(query->ppStmt, 15);
		table->partition.max = sqlite3_column_int64(query->ppStmt, 16);
	}

	/* checksum information from s_table_chksum */
	if (cols >= 21)
	{
		table->sourceChecksum.rowcount =
			sqlite3_column_int64(query->ppStmt, 17);

		if (sqlite3_column_type(query->ppStmt, 18) != SQLITE_NULL)
		{
			strlcpy(table->sourceChecksum.checksum,
					(char *) sqlite3_column_text(query->ppStmt, 18),
					sizeof(table->sourceChecksum.checksum));
		}

		table->targetChecksum.rowcount =
			sqlite3_column_int64(query->ppStmt, 19);

		if (sqlite3_column_type(query->ppStmt, 20) != SQLITE_NULL)
		{
			strlcpy(table->targetChecksum.checksum,
					(char *) sqlite3_column_text(query->ppStmt, 20),
					sizeof(table->targetChecksum.checksum));
		}
	}

	/* summary information from s_table_parts_done */
	if (cols == 23)
	{
		table->durationMs = sqlite3_column_int64(query->ppStmt, 21);
		table->bytesTransmitted = sqlite3_column_int64(query->ppStmt, 22);
	}

	return true;
}


/*
 * catalog_iter_s_table_finish cleans-up the internal memory used for the
 * iteration.
 */
bool
catalog_iter_s_table_finish(SourceTableIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

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

	return true;
}


/*
 * catalog_iter_s_table_part iterates over the list of a table partitions in
 * our catalogs.
 */
bool
catalog_iter_s_table_parts(DatabaseCatalog *catalog,
						   uint32_t oid,
						   void *context,
						   SourceTablePartsIterFun *callback)
{
	SourceTablePartsIterator *iter =
		(SourceTablePartsIterator *) calloc(1, sizeof(SourceTablePartsIterator));

	iter->catalog = catalog;
	iter->oid = oid;

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

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

		SourceTableParts *part = iter->part;

		if (part == NULL)
		{
			if (!catalog_iter_s_table_part_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, part))
		{
			log_error("Failed to iterate over list of tables, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_table_part_init initializes an Interator over our catalog of
 * SourceTable entries.
 */
bool
catalog_iter_s_table_part_init(SourceTablePartsIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_table iterator: db is NULL");
		return false;
	}

	iter->part = (SourceTableParts *) calloc(1, sizeof(SourceTableParts));

	if (iter->part == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	char *sql =
		"  select partnum, partcount, min, max, count "
		"    from s_table_part "
		"   where oid = $1 "
		"order by partnum";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->part;
	query->fetchFunction = &catalog_s_table_part_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	BindParam params[1] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", iter->oid, NULL }
	};

	if (!catalog_sql_bind(query, params, 1))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_table_part_next fetches the next SourceTable entry in our catalogs.
 */
bool
catalog_iter_s_table_part_next(SourceTablePartsIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	int rc = catalog_sql_step(query);

	if (rc == SQLITE_DONE)
	{
		iter->part = NULL;

		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("Failed to step through statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return catalog_s_table_part_fetch(query);
}


/*
 * catalog_s_table_part_fetch fetches a SourceTableParts entry from a SQLite
 * ppStmt result set.
 */
bool
catalog_s_table_part_fetch(SQLiteQuery *query)
{
	SourceTableParts *part = (SourceTableParts *) query->context;

	/* cleanup the memory area before re-use */
	bzero(part, sizeof(SourceTableParts));

	part->partNumber = sqlite3_column_int64(query->ppStmt, 0);
	part->partCount = sqlite3_column_int64(query->ppStmt, 1);
	part->min = sqlite3_column_int64(query->ppStmt, 2);
	part->max = sqlite3_column_int64(query->ppStmt, 3);
	part->count = sqlite3_column_int64(query->ppStmt, 4);

	return true;
}


/*
 * catalog_iter_s_table_part_finish cleans-up the internal memory used for the
 * iteration.
 */
bool
catalog_iter_s_table_part_finish(SourceTablePartsIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

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

	return true;
}


/*
 * catalog_s_table_attrlist fetches the attributes of a table as a single
 * C-string, using ', ' as a separator.
 */
bool
catalog_s_table_attrlist(DatabaseCatalog *catalog, SourceTable *table)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_s_table_attrlist: db is NULL");
		return false;
	}

	char *sql =
		" select group_concat(attname order by attnum, ', ') "
		"       filter (where not attisgenerated) "
		"  from s_attr "
		" where oid = $1";

	SQLiteQuery query = {
		.context = table,
		.fetchFunction = &catalog_s_table_fetch_attrlist
	};

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[1] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", table->oid, NULL }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_s_table_fetch_attrlist fetches a SourceTable attrlist from a SQLite
 * query result.
 */
bool
catalog_s_table_fetch_attrlist(SQLiteQuery *query)
{
	SourceTable *table = (SourceTable *) query->context;

	/* the default empty attribute list is an empty string */
	table->attrList = "";

	if (sqlite3_column_type(query->ppStmt, 0) != SQLITE_NULL)
	{
		int len = sqlite3_column_bytes(query->ppStmt, 0);
		int bytes = len + 1;

		table->attrList = (char *) calloc(bytes, sizeof(char));

		if (table->attrList == NULL)
		{
			log_fatal(ALLOCATION_FAILED_ERROR);
			return false;
		}

		strlcpy(table->attrList,
				(char *) sqlite3_column_text(query->ppStmt, 0),
				bytes);
	}

	return true;
}


/*
 * catalog_s_table_all_binary_compatible returns true in *allCompatible when
 * every column of the given table has attisbinarycompatible = 1 in s_attr.
 * Returns false when any column's type has a send function known to produce
 * alignment-unsafe binary output (i.e., the column is on the blocklist).
 */
bool
catalog_s_table_all_binary_compatible(DatabaseCatalog *catalog,
									  SourceTable *table,
									  bool *allCompatible)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_s_table_all_binary_compatible: db is NULL");
		return false;
	}

	char *sql =
		"select count(*) from s_attr "
		" where oid = $1 and attisbinarycompatible = 0";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	BindParam params[1] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", table->oid, NULL }
	};

	if (!catalog_sql_bind(&query, params, 1))
	{
		/* errors have already been logged */
		return false;
	}

	int rc = catalog_sql_step(&query);

	if (rc != SQLITE_ROW)
	{
		log_error("[SQLite %d: %s]: %s", rc, query.sql, sqlite3_errmsg(db));
		(void) catalog_sql_finalize(&query);
		return false;
	}

	int incompatCount = sqlite3_column_int(query.ppStmt, 0);

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

	*allCompatible = (incompatCount == 0);
	return true;
}


/*
 * catalog_s_table_fetch_attrs fetches the table SourceTableAttribute array
 * from our s_attr catalog.
 */
bool
catalog_s_table_fetch_attrs(DatabaseCatalog *catalog, SourceTable *table)
{
	SourceTableAttrsIterator *iter =
		(SourceTableAttrsIterator *) calloc(1,
											sizeof(SourceTableAttrsIterator));

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

	iter->catalog = catalog;
	iter->table = table;

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

	while (!iter->done)
	{
		if (!catalog_iter_s_table_attrs_next(iter))
		{
			/* errors have already been logged */
			return false;
		}
	}

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


	return true;
}


/*
 * catalog_iter_s_table_attrs_init initializes an Interator over our catalog of
 * SourceTableAttributes entries.
 */
bool
catalog_iter_s_table_attrs_init(SourceTableAttrsIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_table iterator: db is NULL");
		return false;
	}

	SourceTable *table = iter->table;

	if (iter->table == NULL)
	{
		log_error("BUG: Failed to initialize s_table iterator: table is NULL");
		return false;
	}

	char *sql =
		"  select count(*) over(order by attnum) as num, "
		"         count(*) over() as count, "
		"         attnum, attypid, attname, "
		"         attisprimary, attisreplident, attisgenerated, attidentity "
		"    from s_attr "
		"   where oid = $1 "
		"order by attnum";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->table;
	query->fetchFunction = &catalog_s_table_attrs_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[1] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", table->oid, NULL }
	};

	if (!catalog_sql_bind(query, params, 1))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_table_attrs_next fetches the next SourceTable entry in our
 * catalogs.
 */
bool
catalog_iter_s_table_attrs_next(SourceTableAttrsIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	int rc = catalog_sql_step(query);

	if (rc == SQLITE_DONE)
	{
		iter->done = true;
		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("Failed to step through statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return catalog_s_table_attrs_fetch(query);
}


/*
 * catalog_iter_s_table_attrs_finish cleans-up the internal memory used for the
 * iteration.
 */
bool
catalog_iter_s_table_attrs_finish(SourceTableAttrsIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

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

	return true;
}


/*
 * catalog_s_table_attrs_fetch  is a SQLiteQuery callback.
 */
bool
catalog_s_table_attrs_fetch(SQLiteQuery *query)
{
	SourceTable *table = (SourceTable *) query->context;

	int num = sqlite3_column_int(query->ppStmt, 0);
	int count = sqlite3_column_int(query->ppStmt, 1);

	if (num == 1)
	{
		table->attributes.count = count;
		table->attributes.array =
			(SourceTableAttribute *) calloc(count,
											sizeof(SourceTableAttribute));

		if (table->attributes.array == NULL)
		{
			log_error(ALLOCATION_FAILED_ERROR);
			return false;
		}
	}

	SourceTableAttribute *attr = &(table->attributes.array[num - 1]);

	attr->attnum = sqlite3_column_int(query->ppStmt, 2);
	attr->atttypid = sqlite3_column_int64(query->ppStmt, 3);

	strlcpy(attr->attname,
			(char *) sqlite3_column_text(query->ppStmt, 4),
			sizeof(attr->attname));

	attr->attisprimary = sqlite3_column_int(query->ppStmt, 5) == 1;
	attr->attisreplident = sqlite3_column_int(query->ppStmt, 6) == 1;
	attr->attisgenerated = sqlite3_column_int(query->ppStmt, 7) == 1;

	const char *identity = (const char *) sqlite3_column_text(query->ppStmt, 8);
	attr->attidentity = (identity != NULL && identity[0] != '\0') ? identity[0] : '\0';

	return true;
}


/*
 * catalog_s_attr_fetch  is a SQLiteQuery callback.
 */
bool
catalog_s_attr_fetch(SQLiteQuery *query)
{
	SourceTableAttribute *attr = (SourceTableAttribute *) query->context;

	attr->attnum = sqlite3_column_int64(query->ppStmt, 0);
	attr->atttypid = sqlite3_column_int64(query->ppStmt, 1);

	strlcpy(attr->attname,
			(char *) sqlite3_column_text(query->ppStmt, 2),
			sizeof(attr->attname));

	attr->attisprimary = sqlite3_column_int(query->ppStmt, 3) == 1;
	attr->attisreplident = sqlite3_column_int(query->ppStmt, 4) == 1;
	attr->attisgenerated = sqlite3_column_int(query->ppStmt, 5) == 1;

	const char *identity = (const char *) sqlite3_column_text(query->ppStmt, 6);
	attr->attidentity = (identity != NULL && identity[0] != '\0') ? identity[0] : '\0';

	return true;
}


/*
 * catalog_add_s_index INSERTs a SourceIndex to our internal catalogs database.
 */
bool
catalog_add_s_index(DatabaseCatalog *catalog, SourceIndex *index)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_index: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_index("
		"  oid, qname, nspname, relname, restore_list_name, tableoid, "
		"  isprimary, isunique, columns, sql) "
		"values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", index->indexOid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "qname", 0, index->indexQname },
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, index->indexNamespace },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, index->indexRelname },

		{
			BIND_PARAMETER_TYPE_TEXT, "restore_list_name", 0,
			index->indexRestoreListName
		},

		{ BIND_PARAMETER_TYPE_INT64, "tableoid", index->tableOid, NULL },

		{ BIND_PARAMETER_TYPE_INT, "isprimary", index->isPrimary ? 1 : 0, NULL },
		{ BIND_PARAMETER_TYPE_INT, "isunique", index->isUnique ? 1 : 0, NULL },

		{ BIND_PARAMETER_TYPE_TEXT, "columns", 0, index->indexColumns },
		{ BIND_PARAMETER_TYPE_TEXT, "sql", 0, index->indexDef }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_add_s_constraint INSERTs a SourceIndex constraint to our internal
 * catalogs database.
 */
bool
catalog_add_s_constraint(DatabaseCatalog *catalog, SourceIndex *index)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_index: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_constraint("
		"  oid, conname, indexoid, condeferrable, condeferred, sql)"
		"values($1, $2, $3, $4, $5, $6)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", index->constraintOid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "conname", 0, index->constraintName },
		{ BIND_PARAMETER_TYPE_INT64, "indexoid", index->indexOid, NULL },

		{
			BIND_PARAMETER_TYPE_INT, "condeferable",
			index->condeferrable ? 1 : 0, NULL
		},
		{
			BIND_PARAMETER_TYPE_INT, "condeffered",
			index->condeferred ? 1 : 0, NULL
		},

		{ BIND_PARAMETER_TYPE_TEXT, "sql", 0, index->constraintDef }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_add_s_table_batch INSERTs multiple SourceTable rows in one
 * multi-row VALUES statement.  Batch size is bounded by SQLite's
 * SQLITE_LIMIT_VARIABLE_NUMBER so that the total parameter count never
 * exceeds the compile-time limit.
 */
bool
catalog_add_s_table_batch(DatabaseCatalog *catalog, SourceTable *tables, int count)
{
	if (count <= 0)
	{
		return true;
	}

	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_table_batch: db is NULL");
		return false;
	}

	int maxVars = sqlite3_limit(db, SQLITE_LIMIT_VARIABLE_NUMBER, -1);
	int batchSize = maxVars / CATALOG_INSERT_NCOLS_S_TABLE;

	for (int offset = 0; offset < count; offset += batchSize)
	{
		int rows = count - offset;

		if (rows > batchSize)
		{
			rows = batchSize;
		}

		PQExpBufferData buf;
		initPQExpBuffer(&buf);

		appendPQExpBufferStr(&buf,
							 "insert into s_table("
							 "oid, datname, qname, nspname, relname, amname, "
							 "restore_list_name, relpages, reltuples, "
							 "exclude_data, part_key) values");

		int paramIdx = 1;

		for (int r = 0; r < rows; r++)
		{
			appendPQExpBuffer(&buf,
							  "%s(?%d,?%d,?%d,?%d,?%d,?%d,?%d,?%d,?%d,?%d,?%d)",
							  r == 0 ? "" : ",",
							  paramIdx, paramIdx + 1, paramIdx + 2,
							  paramIdx + 3, paramIdx + 4, paramIdx + 5,
							  paramIdx + 6, paramIdx + 7, paramIdx + 8,
							  paramIdx + 9, paramIdx + 10);
			paramIdx += CATALOG_INSERT_NCOLS_S_TABLE;
		}

		if (PQExpBufferBroken(&buf))
		{
			log_error("Failed to build batch INSERT for s_table: out of memory");
			termPQExpBuffer(&buf);
			return false;
		}

		sqlite3_stmt *stmt = NULL;
		int rc = sqlite3_prepare_v2(db, buf.data, -1, &stmt, NULL);

		termPQExpBuffer(&buf);

		if (rc != SQLITE_OK)
		{
			log_error("Failed to prepare batch INSERT for s_table: %s",
					  sqlite3_errmsg(db));
			return false;
		}

		for (int r = 0; r < rows; r++)
		{
			SourceTable *t = &tables[offset + r];
			int base = r * CATALOG_INSERT_NCOLS_S_TABLE + 1;

			sqlite3_bind_int64(stmt, base + 0, t->oid);
			sqlite3_bind_text(stmt, base + 1, t->datname, -1, SQLITE_STATIC);
			sqlite3_bind_text(stmt, base + 2, t->qname, -1, SQLITE_STATIC);
			sqlite3_bind_text(stmt, base + 3, t->nspname, -1, SQLITE_STATIC);
			sqlite3_bind_text(stmt, base + 4, t->relname, -1, SQLITE_STATIC);
			sqlite3_bind_text(stmt, base + 5, t->amname, -1, SQLITE_STATIC);
			sqlite3_bind_text(stmt, base + 6, t->restoreListName, -1, SQLITE_STATIC);
			sqlite3_bind_int64(stmt, base + 7, t->relpages);
			sqlite3_bind_int64(stmt, base + 8, t->reltuples);
			sqlite3_bind_int(stmt, base + 9, t->excludeData ? 1 : 0);
			sqlite3_bind_text(stmt, base + 10, t->partKey, -1, SQLITE_STATIC);
		}

		rc = sqlite3_step(stmt);
		sqlite3_finalize(stmt);

		if (rc != SQLITE_DONE)
		{
			log_error("Failed to execute batch INSERT for s_table: %s",
					  sqlite3_errmsg(db));
			return false;
		}
	}

	return true;
}


/*
 * catalog_add_s_attr_batch INSERTs multiple SourceTableAttribute rows in one
 * multi-row VALUES statement.
 */
bool
catalog_add_s_attr_batch(DatabaseCatalog *catalog,
						 uint32_t *tableoids,
						 SourceTableAttribute *attrs,
						 int count)
{
	if (count <= 0)
	{
		return true;
	}

	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_attr_batch: db is NULL");
		return false;
	}

	int maxVars = sqlite3_limit(db, SQLITE_LIMIT_VARIABLE_NUMBER, -1);
	int batchSize = maxVars / CATALOG_INSERT_NCOLS_S_ATTR;

	for (int offset = 0; offset < count; offset += batchSize)
	{
		int rows = count - offset;

		if (rows > batchSize)
		{
			rows = batchSize;
		}

		PQExpBufferData buf;
		initPQExpBuffer(&buf);

		appendPQExpBufferStr(&buf,
							 "insert into s_attr("
							 "oid, attnum, attypid, attname, "
							 "attisprimary, attisreplident, attisgenerated, "
							 "attidentity, attisbinarycompatible, atttypsend) values");

		int paramIdx = 1;

		for (int r = 0; r < rows; r++)
		{
			appendPQExpBuffer(&buf,
							  "%s(?%d,?%d,?%d,?%d,?%d,?%d,?%d,?%d,?%d,?%d)",
							  r == 0 ? "" : ",",
							  paramIdx, paramIdx + 1, paramIdx + 2,
							  paramIdx + 3, paramIdx + 4, paramIdx + 5,
							  paramIdx + 6, paramIdx + 7, paramIdx + 8,
							  paramIdx + 9);
			paramIdx += CATALOG_INSERT_NCOLS_S_ATTR;
		}

		if (PQExpBufferBroken(&buf))
		{
			log_error("Failed to build batch INSERT for s_attr: out of memory");
			termPQExpBuffer(&buf);
			return false;
		}

		sqlite3_stmt *stmt = NULL;
		int rc = sqlite3_prepare_v2(db, buf.data, -1, &stmt, NULL);

		termPQExpBuffer(&buf);

		if (rc != SQLITE_OK)
		{
			log_error("Failed to prepare batch INSERT for s_attr: %s",
					  sqlite3_errmsg(db));
			return false;
		}

		/*
		 * attidentity is stored as a one-char string.  We need a stable
		 * char[2] buffer for each row; use a local VLA-style stack array.
		 * The maximum batch size is bounded by SQLITE_LIMIT_VARIABLE_NUMBER
		 * (32766) / 10 ≈ 3276 rows per batch — a reasonable stack footprint.
		 */
		char (*identBufs)[2] = malloc(rows * sizeof(char[2]));

		if (identBufs == NULL)
		{
			log_error("Failed to allocate identity buffer in catalog_add_s_attr_batch");
			sqlite3_finalize(stmt);
			return false;
		}

		for (int r = 0; r < rows; r++)
		{
			SourceTableAttribute *a = &attrs[offset + r];
			int base = r * CATALOG_INSERT_NCOLS_S_ATTR + 1;

			identBufs[r][0] = a->attidentity;
			identBufs[r][1] = '\0';

			sqlite3_bind_int64(stmt, base + 0, tableoids[offset + r]);
			sqlite3_bind_int64(stmt, base + 1, a->attnum);
			sqlite3_bind_int64(stmt, base + 2, a->atttypid);
			sqlite3_bind_text(stmt, base + 3, a->attname, -1, SQLITE_STATIC);
			sqlite3_bind_int(stmt, base + 4, a->attisprimary ? 1 : 0);
			sqlite3_bind_int(stmt, base + 5, a->attisreplident ? 1 : 0);
			sqlite3_bind_int(stmt, base + 6, a->attisgenerated ? 1 : 0);
			sqlite3_bind_text(stmt, base + 7, identBufs[r], -1, SQLITE_STATIC);
			sqlite3_bind_int(stmt, base + 8, a->attisbinarycompatible ? 1 : 0);
			sqlite3_bind_text(stmt, base + 9, a->atttypsend, -1, SQLITE_STATIC);
		}

		rc = sqlite3_step(stmt);
		sqlite3_finalize(stmt);
		free(identBufs);

		if (rc != SQLITE_DONE)
		{
			log_error("Failed to execute batch INSERT for s_attr: %s",
					  sqlite3_errmsg(db));
			return false;
		}
	}

	return true;
}


/*
 * catalog_add_s_index_batch INSERTs multiple SourceIndex rows into s_index in
 * one multi-row VALUES statement.
 */
bool
catalog_add_s_index_batch(DatabaseCatalog *catalog,
						  SourceIndex *indexes,
						  int count)
{
	if (count <= 0)
	{
		return true;
	}

	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_index_batch: db is NULL");
		return false;
	}

	int maxVars = sqlite3_limit(db, SQLITE_LIMIT_VARIABLE_NUMBER, -1);
	int batchSize = maxVars / CATALOG_INSERT_NCOLS_S_INDEX;

	for (int offset = 0; offset < count; offset += batchSize)
	{
		int rows = count - offset;

		if (rows > batchSize)
		{
			rows = batchSize;
		}

		PQExpBufferData buf;
		initPQExpBuffer(&buf);

		appendPQExpBufferStr(&buf,
							 "insert into s_index("
							 "oid, qname, nspname, relname, restore_list_name, "
							 "tableoid, isprimary, isunique, columns, sql) values");

		int paramIdx = 1;

		for (int r = 0; r < rows; r++)
		{
			appendPQExpBuffer(&buf,
							  "%s(?%d,?%d,?%d,?%d,?%d,?%d,?%d,?%d,?%d,?%d)",
							  r == 0 ? "" : ",",
							  paramIdx, paramIdx + 1, paramIdx + 2,
							  paramIdx + 3, paramIdx + 4, paramIdx + 5,
							  paramIdx + 6, paramIdx + 7, paramIdx + 8,
							  paramIdx + 9);
			paramIdx += CATALOG_INSERT_NCOLS_S_INDEX;
		}

		if (PQExpBufferBroken(&buf))
		{
			log_error("Failed to build batch INSERT for s_index: out of memory");
			termPQExpBuffer(&buf);
			return false;
		}

		sqlite3_stmt *stmt = NULL;
		int rc = sqlite3_prepare_v2(db, buf.data, -1, &stmt, NULL);

		termPQExpBuffer(&buf);

		if (rc != SQLITE_OK)
		{
			log_error("Failed to prepare batch INSERT for s_index: %s",
					  sqlite3_errmsg(db));
			return false;
		}

		for (int r = 0; r < rows; r++)
		{
			SourceIndex *idx = &indexes[offset + r];
			int base = r * CATALOG_INSERT_NCOLS_S_INDEX + 1;

			sqlite3_bind_int64(stmt, base + 0, idx->indexOid);
			sqlite3_bind_text(stmt, base + 1, idx->indexQname, -1, SQLITE_STATIC);
			sqlite3_bind_text(stmt, base + 2, idx->indexNamespace, -1, SQLITE_STATIC);
			sqlite3_bind_text(stmt, base + 3, idx->indexRelname, -1, SQLITE_STATIC);
			sqlite3_bind_text(stmt, base + 4,
							  idx->indexRestoreListName, -1, SQLITE_STATIC);
			sqlite3_bind_int64(stmt, base + 5, idx->tableOid);
			sqlite3_bind_int(stmt, base + 6, idx->isPrimary ? 1 : 0);
			sqlite3_bind_int(stmt, base + 7, idx->isUnique ? 1 : 0);
			sqlite3_bind_text(stmt, base + 8, idx->indexColumns, -1, SQLITE_STATIC);
			sqlite3_bind_text(stmt, base + 9, idx->indexDef, -1, SQLITE_STATIC);
		}

		rc = sqlite3_step(stmt);
		sqlite3_finalize(stmt);

		if (rc != SQLITE_DONE)
		{
			log_error("Failed to execute batch INSERT for s_index: %s",
					  sqlite3_errmsg(db));
			return false;
		}
	}

	return true;
}


/*
 * catalog_add_s_constraint_batch INSERTs multiple SourceIndex constraint rows
 * into s_constraint in one multi-row VALUES statement.  Only rows where
 * constraintOid != 0 are inserted (not all indexes have a constraint).
 */
bool
catalog_add_s_constraint_batch(DatabaseCatalog *catalog,
							   SourceIndex *indexes,
							   int count)
{
	if (count <= 0)
	{
		return true;
	}

	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_constraint_batch: db is NULL");
		return false;
	}

	/* count how many have a constraint */
	int conCount = 0;

	for (int i = 0; i < count; i++)
	{
		if (indexes[i].constraintOid != 0)
		{
			conCount++;
		}
	}

	if (conCount == 0)
	{
		return true;
	}

	int maxVars = sqlite3_limit(db, SQLITE_LIMIT_VARIABLE_NUMBER, -1);
	int batchSize = maxVars / CATALOG_INSERT_NCOLS_S_CONSTRAINT;

	/* iterate over the original array but skip non-constraint entries */
	int conOffset = 0;

	while (conOffset < conCount)
	{
		int rows = conCount - conOffset;

		if (rows > batchSize)
		{
			rows = batchSize;
		}

		PQExpBufferData buf;
		initPQExpBuffer(&buf);

		appendPQExpBufferStr(&buf,
							 "insert into s_constraint("
							 "oid, conname, indexoid, "
							 "condeferrable, condeferred, sql) values");

		int paramIdx = 1;

		for (int r = 0; r < rows; r++)
		{
			appendPQExpBuffer(&buf,
							  "%s(?%d,?%d,?%d,?%d,?%d,?%d)",
							  r == 0 ? "" : ",",
							  paramIdx, paramIdx + 1, paramIdx + 2,
							  paramIdx + 3, paramIdx + 4, paramIdx + 5);
			paramIdx += CATALOG_INSERT_NCOLS_S_CONSTRAINT;
		}

		if (PQExpBufferBroken(&buf))
		{
			log_error("Failed to build batch INSERT for s_constraint: out of memory");
			termPQExpBuffer(&buf);
			return false;
		}

		sqlite3_stmt *stmt = NULL;
		int rc = sqlite3_prepare_v2(db, buf.data, -1, &stmt, NULL);

		termPQExpBuffer(&buf);

		if (rc != SQLITE_OK)
		{
			log_error("Failed to prepare batch INSERT for s_constraint: %s",
					  sqlite3_errmsg(db));
			return false;
		}

		/* walk the original array, binding only constraint-bearing entries */
		int r = 0;
		int srcIdx = 0;
		int skipped = 0;  /* how many constraint rows we've already emitted */

		while (r < rows)
		{
			if (srcIdx >= count)
			{
				break;
			}

			SourceIndex *idx = &indexes[srcIdx++];

			if (idx->constraintOid == 0)
			{
				continue;
			}

			/* skip rows already emitted in prior batches */
			if (skipped < conOffset)
			{
				skipped++;
				continue;
			}

			int base = r * CATALOG_INSERT_NCOLS_S_CONSTRAINT + 1;

			sqlite3_bind_int64(stmt, base + 0, idx->constraintOid);
			sqlite3_bind_text(stmt, base + 1,
							  idx->constraintName, -1, SQLITE_STATIC);
			sqlite3_bind_int64(stmt, base + 2, idx->indexOid);
			sqlite3_bind_int(stmt, base + 3, idx->condeferrable ? 1 : 0);
			sqlite3_bind_int(stmt, base + 4, idx->condeferred ? 1 : 0);
			sqlite3_bind_text(stmt, base + 5,
							  idx->constraintDef, -1, SQLITE_STATIC);

			r++;
		}

		rc = sqlite3_step(stmt);
		sqlite3_finalize(stmt);

		if (rc != SQLITE_DONE)
		{
			log_error("Failed to execute batch INSERT for s_constraint: %s",
					  sqlite3_errmsg(db));
			return false;
		}

		conOffset += rows;
	}

	return true;
}


/*
 * catalog_lookup_s_index fetches a SourceIndex entry from our catalogs.
 */
bool
catalog_lookup_s_index(DatabaseCatalog *catalog, uint32_t oid, SourceIndex *index)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_s_index: db is NULL");
		return false;
	}

	char *sql =
		"  select i.oid, i.qname, i.nspname, i.relname, i.restore_list_name, "
		"         i.tableoid, t.qname, t.nspname, t.relname, "
		"         isprimary, isunique, columns, i.sql, "
		"         c.oid as constraintoid, conname, "
		"         condeferrable, condeferred, c.sql as condef"
		"    from s_index i "
		"         join s_table t on t.oid = i.tableoid "
		"         left join s_constraint c on c.indexoid = i.oid"
		"   where i.oid = $1 ";

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	SQLiteQuery query = {
		.context = index,
		.fetchFunction = &catalog_s_index_fetch
	};

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[1] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", oid, NULL },
	};

	if (!catalog_sql_bind(&query, params, 1))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_lookup_s_index_by_name fetches a SourceIndex entry from our catalogs.
 */
bool
catalog_lookup_s_index_by_name(DatabaseCatalog *catalog,
							   const char *nspname,
							   const char *relname,
							   SourceIndex *index)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_s_index_by_name: db is NULL");
		return false;
	}

	char *sql =
		"  select i.oid, i.qname, i.nspname, i.relname, i.restore_list_name, "
		"         i.tableoid, t.qname, t.nspname, t.relname, "
		"         isprimary, isunique, columns, i.sql, "
		"         c.oid as constraintoid, conname, "
		"         condeferrable, condeferred, c.sql as condef"
		"    from s_index i "
		"         join s_table t on t.oid = i.tableoid "
		"         left join s_constraint c on c.indexoid = i.oid"
		"   where i.nspname = $1 and i.relname = $2 ";

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	SQLiteQuery query = {
		.context = index,
		.fetchFunction = &catalog_s_index_fetch
	};

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, (char *) nspname },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, (char *) relname }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_s_index_fetch fetches a SourceIndex entry from a SQLite ppStmt
 * result set.
 */
bool
catalog_s_index_fetch(SQLiteQuery *query)
{
	SourceIndex *index = (SourceIndex *) query->context;

	/* cleanup the memory area before re-use */
	bzero(index, sizeof(SourceIndex));

	index->indexOid = sqlite3_column_int64(query->ppStmt, 0);

	strlcpy(index->indexQname,
			(char *) sqlite3_column_text(query->ppStmt, 1),
			sizeof(index->indexQname));

	strlcpy(index->indexNamespace,
			(char *) sqlite3_column_text(query->ppStmt, 2),
			sizeof(index->indexNamespace));

	strlcpy(index->indexRelname,
			(char *) sqlite3_column_text(query->ppStmt, 3),
			sizeof(index->indexRelname));

	strlcpy(index->indexRestoreListName,
			(char *) sqlite3_column_text(query->ppStmt, 4),
			sizeof(index->indexRestoreListName));

	index->tableOid = sqlite3_column_int64(query->ppStmt, 5);

	strlcpy(index->tableQname,
			(char *) sqlite3_column_text(query->ppStmt, 6),
			sizeof(index->tableQname));

	strlcpy(index->tableNamespace,
			(char *) sqlite3_column_text(query->ppStmt, 7),
			sizeof(index->tableNamespace));

	strlcpy(index->tableRelname,
			(char *) sqlite3_column_text(query->ppStmt, 8),
			sizeof(index->tableRelname));

	index->isPrimary = sqlite3_column_int(query->ppStmt, 9) == 1;
	index->isUnique = sqlite3_column_int(query->ppStmt, 10) == 1;

	if (sqlite3_column_type(query->ppStmt, 11) != SQLITE_NULL)
	{
		int len = sqlite3_column_bytes(query->ppStmt, 11);
		int bytes = len + 1;

		index->indexColumns = (char *) calloc(bytes, sizeof(char));

		if (index->indexColumns == NULL)
		{
			log_fatal(ALLOCATION_FAILED_ERROR);
			return false;
		}

		strlcpy(index->indexColumns,
				(char *) sqlite3_column_text(query->ppStmt, 11),
				bytes);
	}

	if (sqlite3_column_type(query->ppStmt, 12) != SQLITE_NULL)
	{
		int len = sqlite3_column_bytes(query->ppStmt, 12);
		int bytes = len + 1;

		index->indexDef = (char *) calloc(bytes, sizeof(char));

		if (index->indexDef == NULL)
		{
			log_fatal(ALLOCATION_FAILED_ERROR);
			return false;
		}

		strlcpy(index->indexDef,
				(char *) sqlite3_column_text(query->ppStmt, 12),
				bytes);
	}

	/* constraint */
	if (sqlite3_column_type(query->ppStmt, 13) != SQLITE_NULL)
	{
		index->constraintOid = sqlite3_column_int64(query->ppStmt, 13);

		strlcpy(index->constraintName,
				(char *) sqlite3_column_text(query->ppStmt, 14),
				sizeof(index->constraintName));

		index->condeferrable = sqlite3_column_int(query->ppStmt, 15) == 1;
		index->condeferred = sqlite3_column_int(query->ppStmt, 16) == 1;


		if (sqlite3_column_type(query->ppStmt, 17) != SQLITE_NULL)
		{
			int len = sqlite3_column_bytes(query->ppStmt, 17);
			int bytes = len + 1;

			index->constraintDef = (char *) calloc(bytes, sizeof(char));

			if (index->constraintDef == NULL)
			{
				log_fatal(ALLOCATION_FAILED_ERROR);
				return false;
			}

			strlcpy(index->constraintDef,
					(char *) sqlite3_column_text(query->ppStmt, 17),
					bytes);
		}
	}

	return true;
}


/*
 * catalog_iter_s_index iterates over the list of indexes in our catalogs.
 */
bool
catalog_iter_s_index(DatabaseCatalog *catalog,
					 void *context,
					 SourceIndexIterFun *callback)
{
	SourceIndexIterator *iter =
		(SourceIndexIterator *) calloc(1, sizeof(SourceIndexIterator));

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

	iter->catalog = catalog;

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

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

		SourceIndex *index = iter->index;

		if (index == NULL)
		{
			if (!catalog_iter_s_index_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, index))
		{
			log_error("Failed to iterate over list of indexes, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_index iterates over the list of indexes in our catalogs.
 */
bool
catalog_iter_s_index_table(DatabaseCatalog *catalog,
						   const char *nspname,
						   const char *relname,
						   void *context,
						   SourceIndexIterFun *callback)
{
	SourceIndexIterator *iter =
		(SourceIndexIterator *) calloc(1, sizeof(SourceIndexIterator));

	iter->catalog = catalog;
	iter->nspname = nspname;
	iter->relname = relname;

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_iter_s_index_table_init(iter))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	for (;;)
	{
		if (!catalog_iter_s_index_next(iter))
		{
			/* errors have already been logged */
			(void) semaphore_unlock(&(catalog->sema));
			return false;
		}

		SourceIndex *index = iter->index;

		if (index == NULL)
		{
			if (!catalog_iter_s_index_finish(iter))
			{
				/* errors have already been logged */
				(void) semaphore_unlock(&(catalog->sema));
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, index))
		{
			log_error("Failed to iterate over list of indexes, "
					  "see above for details");
			(void) semaphore_unlock(&(catalog->sema));
			return false;
		}
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_iter_s_index_init initializes an Interator over our catalog of
 * SourceIndex entries.
 */
bool
catalog_iter_s_index_init(SourceIndexIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_index iterator: db is NULL");
		return false;
	}

	iter->index = (SourceIndex *) calloc(1, sizeof(SourceIndex));

	if (iter->index == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	char *sql =
		"  select i.oid, i.qname, i.nspname, i.relname, i.restore_list_name, "
		"         i.tableoid, t.qname, t.nspname, t.relname, "
		"         isprimary, isunique, columns, i.sql, "
		"         c.oid as constraintoid, conname, "
		"         condeferrable, condeferred, c.sql as condef"
		"    from s_index i "
		"         join s_table t on t.oid = i.tableoid "
		"		  left join s_table_size ts on ts.oid = i.tableoid"
		"         left join s_constraint c on c.indexoid = i.oid "
		"order by ts.bytes desc, t.oid";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->index;
	query->fetchFunction = &catalog_s_index_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_index_init initializes an Interator over our catalog of
 * SourceIndex entries.
 */
bool
catalog_iter_s_index_table_init(SourceIndexIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_index iterator: db is NULL");
		return false;
	}

	iter->index = (SourceIndex *) calloc(1, sizeof(SourceIndex));

	if (iter->index == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	char *sql =
		"  select i.oid, i.qname, i.nspname, i.relname, i.restore_list_name, "
		"         i.tableoid, t.qname, t.nspname, t.relname, "
		"         isprimary, isunique, columns, i.sql, "
		"         c.oid as constraintoid, conname, "
		"         condeferrable, condeferred, c.sql as condef"
		"    from s_index i "
		"         join s_table t on t.oid = i.tableoid "
		"         left join s_constraint c on c.indexoid = i.oid "
		"   where t.nspname = $1 and t.relname = $2";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->index;
	query->fetchFunction = &catalog_s_index_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, (char *) iter->nspname },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, (char *) iter->relname }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_index_next fetches the next SourceIndex entry in our catalogs.
 */
bool
catalog_iter_s_index_next(SourceIndexIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	int rc = catalog_sql_step(query);

	if (rc == SQLITE_DONE)
	{
		iter->index = NULL;

		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("Failed to step through statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return catalog_s_index_fetch(query);
}


/*
 * catalog_iter_s_index_finish cleans-up the internal memory used for the
 * iteration.
 */
bool
catalog_iter_s_index_finish(SourceIndexIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	/* in case we finish before reaching the DONE step */
	if (iter->index != NULL)
	{
		iter->index = NULL;
	}

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

	return true;
}


/*
 * catalog_s_table_fetch_attrs fetches the table SourceTableAttribute array
 * from our s_attr catalog.
 */
bool
catalog_s_table_count_indexes(DatabaseCatalog *catalog, SourceTable *table)
{
	if (catalog->db == NULL)
	{
		log_error("BUG: catalog_s_table_count_indexes: db is NULL");
		return false;
	}

	static const char *sql =
		"select count(1) as indexes, "
		"       count(c.oid) as constraints "
		"  from s_index i "
		"       left join s_constraint c on c.indexoid = i.oid "
		" where tableoid = $1";

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	SQLiteQuery query = {
		.context = table,
		.fetchFunction = &catalog_s_table_count_indexes_fetch
	};

	if (!catalog_sql_prepare_cached(catalog, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[1] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", table->oid, NULL }
	};

	if (!catalog_sql_bind(&query, params, 1))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_s_table_count_indexes_fetch  is a SQLiteQuery callback.
 */
bool
catalog_s_table_count_indexes_fetch(SQLiteQuery *query)
{
	SourceTable *table = (SourceTable *) query->context;

	table->indexCount = sqlite3_column_int64(query->ppStmt, 0);
	table->constraintCount = sqlite3_column_int64(query->ppStmt, 1);

	return true;
}


/*
 * catalog_delete_s_index_all DELETE all the indexes registered in the given
 * database catalog.
 */
bool
catalog_delete_s_index_all(DatabaseCatalog *catalog)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_delete_s_index_all: db is NULL");
		return false;
	}

	char *sql = "delete from s_index";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_add_s_seq INSERTs a SourceSequence to our internal catalogs database.
 */
bool
catalog_add_s_seq(DatabaseCatalog *catalog, SourceSequence *seq)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_seq: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_seq("
		"  oid, ownedby, attrelid, attroid, "
		"  datname, qname, nspname, relname, restore_list_name)"
		"values($1, $2, $3, $4, $5, $6, $7, $8, $9)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", seq->oid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "ownedby", seq->ownedby, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "attrelid", seq->attrelid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "attroid", seq->attroid, NULL },

		{ BIND_PARAMETER_TYPE_TEXT, "datname", 0, seq->datname },
		{ BIND_PARAMETER_TYPE_TEXT, "qname", 0, seq->qname },
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, seq->nspname },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, seq->relname },

		{
			BIND_PARAMETER_TYPE_TEXT, "restore_list_name", 0,
			seq->restoreListName
		}
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_update_sequence_values UPDATEs a SourceSequence lastValue and
 * isCalled parameters in our catalogs.
 */
bool
catalog_update_sequence_values(DatabaseCatalog *catalog, SourceSequence *seq)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_update_sequence_values: db is NULL");
		return false;
	}

	char *sql =
		"update s_seq "
		"   set last_value = $1, isCalled = $2 "
		" where nspname = $3 and relname = $4";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "last_alue", seq->lastValue, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "isCalled", seq->isCalled ? 1 : 0, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, seq->nspname },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, seq->relname }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * Updates the 'relpages' field of the 's_table' table in the database catalog with the given values.
 */
bool
catalog_update_s_table_relpages(DatabaseCatalog *catalog, SourceTable *sourceTable)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_update_s_table_relpages: db is NULL");
		return false;
	}

	char *sql =
		"update s_table "
		"   set relpages = $1 "
		" where oid = $2";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "relpages", sourceTable->relpages, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "oid", sourceTable->oid, NULL },
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_lookup_s_seq_by_name fetches a SourceSeq from our catalogs.
 */
bool
catalog_lookup_s_seq_by_name(DatabaseCatalog *catalog,
							 const char *nspname,
							 const char *relname,
							 SourceSequence *seq)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_s_seq_stats: db is NULL");
		return false;
	}

	char *sql =
		"  select oid, ownedby, attrelid, attroid, "
		"         datname, qname, nspname, relname, restore_list_name, "
		"         last_value, isCalled "
		"    from s_seq "
		"   where nspname = $1 and relname = $2 ";

	SQLiteQuery query = {
		.context = seq,
		.fetchFunction = &catalog_s_seq_fetch
	};

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, (char *) nspname },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, (char *) relname },
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_seq iterates over the list of sequences in our catalogs.
 */
bool
catalog_iter_s_seq(DatabaseCatalog *catalog,
				   void *context,
				   SourceSequenceIterFun *callback)
{
	SourceSeqIterator *iter =
		(SourceSeqIterator *) calloc(1, sizeof(SourceSeqIterator));

	iter->catalog = catalog;

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

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

		SourceSequence *seq = iter->seq;

		if (seq == NULL)
		{
			if (!catalog_iter_s_seq_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, seq))
		{
			log_error("Failed to iterate over list of seqs, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_seq_init initializes an Interator over our catalog of
 * SourceSequence entries.
 */
bool
catalog_iter_s_seq_init(SourceSeqIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_seq iterator: db is NULL");
		return false;
	}

	iter->seq = (SourceSequence *) calloc(1, sizeof(SourceSequence));

	if (iter->seq == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	char *sql =
		"  select oid, ownedby, attrelid, attroid, "
		"         datname, qname, nspname, relname, restore_list_name, "
		"         last_value, isCalled "
		"    from s_seq "
		"order by nspname, relname";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->seq;
	query->fetchFunction = &catalog_s_seq_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_seq_next fetches the next SourceSequence entry in our catalogs.
 */
bool
catalog_iter_s_seq_next(SourceSeqIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	int rc = catalog_sql_step(query);

	if (rc == SQLITE_DONE)
	{
		iter->seq = NULL;

		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("Failed to step through statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return catalog_s_seq_fetch(query);
}


/*
 * catalog_s_seq_fetch fetches a SourceSequence entry from a SQLite ppStmt
 * result set.
 */
bool
catalog_s_seq_fetch(SQLiteQuery *query)
{
	SourceSequence *seq = (SourceSequence *) query->context;

	/* cleanup the memory area before re-use */
	bzero(seq, sizeof(SourceSequence));

	seq->oid = sqlite3_column_int64(query->ppStmt, 0);
	seq->ownedby = sqlite3_column_int64(query->ppStmt, 1);
	seq->attrelid = sqlite3_column_int64(query->ppStmt, 2);
	seq->attroid = sqlite3_column_int64(query->ppStmt, 3);

	if (sqlite3_column_type(query->ppStmt, 4) != SQLITE_NULL)
	{
		strlcpy(seq->datname,
				(char *) sqlite3_column_text(query->ppStmt, 4),
				sizeof(seq->datname));
	}

	strlcpy(seq->qname,
			(char *) sqlite3_column_text(query->ppStmt, 5),
			sizeof(seq->qname));

	strlcpy(seq->nspname,
			(char *) sqlite3_column_text(query->ppStmt, 6),
			sizeof(seq->nspname));

	strlcpy(seq->relname,
			(char *) sqlite3_column_text(query->ppStmt, 7),
			sizeof(seq->relname));

	strlcpy(seq->restoreListName,
			(char *) sqlite3_column_text(query->ppStmt, 8),
			sizeof(seq->restoreListName));

	seq->lastValue = sqlite3_column_int64(query->ppStmt, 9);
	seq->isCalled = sqlite3_column_int(query->ppStmt, 10);

	return true;
}


/*
 * catalog_iter_s_seq_finish cleans-up the internal memory used for the
 * iteration.
 */
bool
catalog_iter_s_seq_finish(SourceSeqIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	/* in case we finish before reaching the DONE step */
	if (iter->seq != NULL)
	{
		iter->seq = NULL;
	}

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

	return true;
}


/*
 * catalog_add_catname INSERTs a (oid, catname) row into the catnames table.
 */
bool
catalog_add_catname(DatabaseCatalog *catalog, uint32_t oid, const char *catname)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_catname: db is NULL");
		return false;
	}

	char *sql = "insert or ignore into catnames(oid, catname) values($1, $2)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		return false;
	}

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", oid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "catname", 0, (char *) catname },
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		return false;
	}

	if (!catalog_sql_execute_once(&query))
	{
		return false;
	}

	return true;
}


typedef struct CatNamesContext
{
	DatabaseCatalog *catalog;
	bool parsedOk;
} CatNamesContext;


static void
getCatNamesList(void *ctx, PGresult *result)
{
	CatNamesContext *context = (CatNamesContext *) ctx;
	int nTuples = PQntuples(result);

	if (PQnfields(result) != 2)
	{
		log_error("Query returned %d columns, expected 2", PQnfields(result));
		context->parsedOk = false;
		return;
	}

	int errors = 0;

	for (int rowNumber = 0; rowNumber < nTuples; rowNumber++)
	{
		uint32_t oid = 0;
		char *value = PQgetvalue(result, rowNumber, 0);

		if (!stringToUInt32(value, &oid) || oid == 0)
		{
			log_error("Invalid catalog OID \"%s\"", value);
			++errors;
			continue;
		}

		char *catname = PQgetvalue(result, rowNumber, 1);

		if (!catalog_add_catname(context->catalog, oid, catname))
		{
			++errors;
		}
	}

	context->parsedOk = (errors == 0);
}


/*
 * catalog_fetch_catnames queries PostgreSQL for the OIDs of the system catalog
 * tables that pgcopydb uses as filter namespace identifiers (catoid), then
 * stores the (oid, catname) pairs in the catnames table of filterDB.
 *
 * This must be called before catalog_prepare_filter() so that the catnames
 * subqueries inside the filter INSERT SQL resolve correctly.
 */
bool
catalog_fetch_catnames(DatabaseCatalog *filterDB, PGSQL *pgsql)
{
	CatNamesContext context = { filterDB, false };

	char *sql =
		"  select c.oid, c.relname "
		"    from pg_catalog.pg_class c "
		"    join pg_catalog.pg_namespace n on n.oid = c.relnamespace "
		"   where n.nspname = 'pg_catalog' "
		"     and c.relname in ('pg_class', 'pg_constraint', 'pg_attrdef', "
		"                       'pg_extension', 'pg_collation')";

	if (!pgsql_execute_with_params(pgsql, sql, 0, NULL, NULL,
								   &context, &getCatNamesList))
	{
		log_error("Failed to fetch catalog namespace OIDs for filter catnames table");
		return false;
	}

	if (!context.parsedOk)
	{
		log_error("Failed to fetch catalog namespace OIDs for filter catnames table");
		return false;
	}

	return true;
}


/*
 * catalog_add_extension_filter stores one entry from [exclude-extension] or
 * [include-only-extension] in the filter catalog's extension_filter table.
 * catalog_prepare_filter reads this table instead of accepting the C lists
 * as parameters, keeping its signature stable.
 */
bool
catalog_add_extension_filter(DatabaseCatalog *catalog,
							 const char *extname,
							 const char *action)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_extension_filter: db is NULL");
		return false;
	}

	char *sql =
		"insert or replace into extension_filter(extname, action) "
		"values ($1, $2)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	BindParam params[2] = {
		{ BIND_PARAMETER_TYPE_TEXT, "extname", 0, (char *) extname },
		{ BIND_PARAMETER_TYPE_TEXT, "action", 0, (char *) action }
	};

	if (!catalog_sql_bind(&query, params, 2))
	{
		/* errors have already been logged */
		return false;
	}

	return catalog_sql_execute_once(&query);
}


/*
 * catalog_prepare_filter prepares our filter Hash-Table, that used to be an
 * in-memory only thing, and now is a SQLite table with indexes, so that it can
 * spill to disk when we have giant database catalogs to take care of.
 */
bool
catalog_prepare_filter(DatabaseCatalog *catalog,
					   bool skipExtensions,
					   bool skipCollations)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_prepare_filter: db is NULL");
		return false;
	}

	char *sql =
		"insert into filter(catoid, oid, restore_list_name, kind) "

		"     select (select oid from catnames where catname = 'pg_class'),"
		"            oid, restore_list_name, 'table' "
		"       from s_table "

		/*
		 * This is only for materialized views. Materialized view refresh
		 * filtering is done with the help of s_matview table on source
		 * catalog.
		 */
		"  union all "

		"	 select (select oid from catnames where catname = 'pg_class'),"
		"            oid, restore_list_name, 'matview' "
		"	   from s_matview"

		"  union all "

		"     select (select oid from catnames where catname = 'pg_class'),"
		"            oid, restore_list_name, 'index' "
		"       from s_index "

		"  union all "

		/* at the moment we lack restore names for constraints */
		"     select (select oid from catnames where catname = 'pg_constraint'),"
		"            oid, NULL as restore_list_name, 'constraint' "
		"       from s_constraint "

		/*
		 * Filtering-out sequences works with the following 3 Archive Catalog
		 * entry kinds:
		 *
		 *  - SEQUENCE, matched by sequence oid (pg_class namespace)
		 *  - SEQUENCE OWNED BY, matched by sequence restore name
		 *  - DEFAULT, matched by attroid (pg_attrdef namespace)
		 *
		 * In some cases we want to create the sequence, but we might want to
		 * skip the SEQUENCE OWNED BY statement, because we didn't actually
		 * create the owner table.
		 *
		 * In those cases we will find the sequence both in the catalogs of
		 * objects we want to migrate, and also in the list of objects we want
		 * to skip. The catalog entry typically has seq->ownedby !=
		 * seq->attrelid, where the ownedby table is skipped from the migration
		 * because of the filtering.
		 */
		"  union all "

		/*
		 * When we find the sequence in our source catalog selection, then we
		 * still create it and refrain to add the sequence Oid to our hash
		 * table here.
		 */
		"     select distinct "
		"            (select oid from catnames where catname = 'pg_class'),"
		"            s.oid, NULL as restore_list_name, 'sequence' "
		"       from s_seq s "
		"      where not exists"
		"            (select 1 from source.s_seq ss where ss.oid = s.oid)"

		/*
		 * Filter out SEQUENCE OWNED BY when the owning table is not being
		 * cloned.  This covers the case where a sequence is shared between an
		 * excluded table and an included one (e.g. via LIKE … INCLUDING ALL):
		 * the sequence itself must still be created for the included table, but
		 * the OWNED BY link to the excluded table must be suppressed so that
		 * pg_restore does not fail trying to attach it to a non-existent table.
		 */
		"  union all "

		"     select (select oid from catnames where catname = 'pg_class'),"
		"            NULL as oid, restore_list_name, 'sequence owned by' "
		"       from ( "
		"              select distinct s.restore_list_name "
		"                from s_seq s "
		"               where not exists"
		"                    (select 1 "
		"                       from source.s_table st "
		"                      where st.oid = s.ownedby) "
		"            ) as seqownedby "

		/*
		 * Also add attroid when it's not null (non-zero here). This takes care
		 * of the DEFAULT entries in the pg_dump Archive Catalog; pg_dump emits
		 * the pg_attrdef OID as the catalogId for these entries, so we store
		 * attroid under the pg_attrdef catalog namespace.
		 */
		"  union all "

		"     select distinct "
		"            (select oid from catnames where catname = 'pg_attrdef'),"
		"            s.attroid, s.restore_list_name, 'default' "
		"       from s_seq s "
		"      where s.attroid > 0";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	/*
	 * In some cases with sequences we might want to skip adding a dependency
	 * in our hash table here. See the previous discussion for details.
	 */
	char *s_depend_sql =
		"insert or ignore into filter(catoid, oid, restore_list_name, kind) "
		"     select distinct classid, objid, identity as restore_list_name, 'pg_depend' "
		"       from s_depend d "
		"      where not exists"
		"            (select 1 from source.s_seq ss where ss.oid = d.objid) ";

	if (!catalog_sql_prepare(db, s_depend_sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	/*
	 * Implement --skip-extensions
	 */
	if (skipExtensions)
	{
		char *s_extension_sql =
			"insert or ignore into filter(catoid, oid, restore_list_name, kind) "
			"     select (select oid from catnames where catname = 'pg_extension'),"
			"            oid, extname, 'extension' "
			"       from s_extension ";

		if (!catalog_sql_prepare(db, s_extension_sql, &query))
		{
			/* errors have already been logged */
			return false;
		}

		/* now execute the query, which does not return any row */
		if (!catalog_sql_execute_once(&query))
		{
			/* errors have already been logged */
			return false;
		}
	}

	/*
	 * Implement [exclude-extension]: join s_extension against
	 * extension_filter (action = 'exclude') to insert named extensions into
	 * the filter table so that copydb_objectid_is_filtered_out skips their
	 * TOC entries during the pg_restore list pass.
	 */
	char *s_excl_ext_sql =
		"insert or ignore into filter(catoid, oid, restore_list_name, kind) "
		"     select (select oid from catnames where catname = 'pg_extension'),"
		"            e.oid, e.extname, 'extension' "
		"       from s_extension e "
		"      where exists "
		"            (select 1 from extension_filter ef "
		"              where ef.extname = e.extname "
		"                and ef.action = 'exclude')";

	if (!catalog_sql_prepare(db, s_excl_ext_sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

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

	/*
	 * Implement [include-only-extension]: insert ALL extensions into the
	 * filter table, then remove the ones listed in extension_filter (action =
	 * 'include-only'), so that only the non-listed extensions remain in the
	 * filter and are therefore excluded from the copy.
	 */
	char *s_all_ext_sql =
		"insert or ignore into filter(catoid, oid, restore_list_name, kind) "
		"     select (select oid from catnames where catname = 'pg_extension'),"
		"            e.oid, e.extname, 'extension' "
		"       from s_extension e "
		"      where (select count(*) from extension_filter "
		"              where action = 'include-only') > 0";

	if (!catalog_sql_prepare(db, s_all_ext_sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

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

	char *s_keep_ext_sql =
		"delete from filter "
		" where catoid = (select oid from catnames "
		"                  where catname = 'pg_extension') "
		"   and kind = 'extension' "
		"   and exists "
		"       (select 1 from extension_filter ef "
		"         where ef.extname = filter.restore_list_name "
		"           and ef.action = 'include-only')";

	if (!catalog_sql_prepare(db, s_keep_ext_sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

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

	/*
	 * Implement --skip-collations
	 */
	if (skipCollations)
	{
		char *s_coll_sql =
			"insert or ignore into filter(catoid, oid, restore_list_name, kind) "
			"    select (select oid from catnames where catname = 'pg_collation'),"
			"           oid, restore_list_name, 'coll' "
			"      from s_coll ";

		if (!catalog_sql_prepare(db, s_coll_sql, &query))
		{
			/* errors have already been logged */
			return false;
		}

		/* now execute the query, which does not return any row */
		if (!catalog_sql_execute_once(&query))
		{
			/* errors have already been logged */
			return false;
		}
	}

	return true;
}


/*
 * catalog_lookup_filter_by_oid fetches a filter entry from our catalogs using
 * the (catoid, objectOid) key that mirrors PostgreSQL's own (classid, objid)
 * object identity from pg_depend.
 */
bool
catalog_lookup_filter_by_oid(DatabaseCatalog *catalog,
							 CatalogFilter *result,
							 uint32_t catalogOid,
							 uint32_t objectOid)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_filter_by_oid: db is NULL");
		return false;
	}

	char *sql =
		"  select catoid, oid, restore_list_name, kind "
		"    from filter "
		"   where catoid = $1 and oid = $2 ";

	SQLiteQuery query = {
		.context = result,
		.fetchFunction = &catalog_filter_fetch
	};

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[2] = {
		{ BIND_PARAMETER_TYPE_INT64, "catoid", catalogOid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "oid", objectOid, NULL },
	};

	if (!catalog_sql_bind(&query, params, 2))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_lookup_filter_by_oid_only fetches a filter entry using the objectOid
 * alone, without a catoid constraint. Used for pg_dump TOC entries that have
 * catalogId.tableoid == 0 (such as REFRESH MATERIALIZED VIEW), where the
 * (catoid, oid) pair is not available. PostgreSQL draws OIDs from a single
 * counter per database, so any objectOid is unique across all system catalogs
 * within that database, making an OID-only lookup safe in this case.
 */
bool
catalog_lookup_filter_by_oid_only(DatabaseCatalog *catalog,
								  CatalogFilter *result,
								  uint32_t objectOid)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_filter_by_oid_only: db is NULL");
		return false;
	}

	char *sql =
		"  select catoid, oid, restore_list_name, kind "
		"    from filter "
		"   where oid = $1 ";

	SQLiteQuery query = {
		.context = result,
		.fetchFunction = &catalog_filter_fetch
	};

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[1] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", objectOid, NULL },
	};

	if (!catalog_sql_bind(&query, params, 1))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_lookup_filter_by_rlname fetches a filter entry from our catalogs.
 */
bool
catalog_lookup_filter_by_rlname(DatabaseCatalog *catalog,
								CatalogFilter *result,
								const char *restoreListName)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_filter_by_oid: db is NULL");
		return false;
	}

	/*
	 * In the case of archive entries for SEQUENCE, SEQUENCE OWNED BY, and
	 * DEFAULT values that depend on sequences, we might find the same sequence
	 * restore_list_name more than once with different values for the OID (the
	 * sequence oid, NUL, or the attroid oid).
	 *
	 * Because of that, add a LIMIT 1 to our query here to avoid throwing an
	 * SQLite error condition about "another row available".
	 */
	char *sql =
		"  select catoid, oid, restore_list_name, kind "
		"    from filter "
		"   where restore_list_name = $1 "
		"   limit 1";

	SQLiteQuery query = {
		.context = result,
		.fetchFunction = &catalog_filter_fetch
	};

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[1] = {
		{
			BIND_PARAMETER_TYPE_TEXT, "restore_list_name", 0,
			(char *) restoreListName
		},
	};

	if (!catalog_sql_bind(&query, params, 1))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_filter_fetch fetches a CatalogFilter entry from a SQLite ppStmt
 * result set.  Column layout: 0=catoid, 1=oid, 2=restore_list_name, 3=kind.
 */
bool
catalog_filter_fetch(SQLiteQuery *query)
{
	CatalogFilter *entry = (CatalogFilter *) query->context;

	/* cleanup the memory area before re-use */
	bzero(entry, sizeof(CatalogFilter));

	entry->catoid = sqlite3_column_int64(query->ppStmt, 0);
	entry->oid = sqlite3_column_int64(query->ppStmt, 1);

	if (sqlite3_column_type(query->ppStmt, 2) != SQLITE_NULL)
	{
		strlcpy(entry->restoreListName,
				(char *) sqlite3_column_text(query->ppStmt, 2),
				sizeof(entry->restoreListName));
	}

	strlcpy(entry->kind,
			(char *) sqlite3_column_text(query->ppStmt, 3),
			sizeof(entry->kind));

	return true;
}


/*
 * catalog_add_s_database INSERTs a SourceDatabase to our internal catalogs
 * database.
 */
bool
catalog_add_s_database(DatabaseCatalog *catalog, SourceDatabase *dat)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_database: db is NULL");
		return false;
	}

	char *sql =
		"insert or replace into s_database(oid, datname, bytes, bytes_pretty)"
		"values($1, $2, $3, $4)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", dat->oid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "datname", 0, dat->datname },
		{ BIND_PARAMETER_TYPE_INT64, "bytes", dat->bytes, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "bytes_pretty", 0, dat->bytesPretty }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_add_s_database_properties INSERTs a SourceProperty to our internal
 * catalogs database.
 */
bool
catalog_add_s_database_properties(DatabaseCatalog *catalog, SourceProperty *guc)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_database_properties: db is NULL");
		return false;
	}

	char *sql =
		"insert or replace into s_database_property("
		"  role_in_database, rolname, datname, setconfig)"
		"values($1, $2, $3, $4)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{
			BIND_PARAMETER_TYPE_INT, "role_in_database",
			guc->roleInDatabase ? 1 : 0, NULL
		},

		{ BIND_PARAMETER_TYPE_TEXT, "rolname", 0, guc->rolname },
		{ BIND_PARAMETER_TYPE_TEXT, "datname", 0, guc->datname },
		{ BIND_PARAMETER_TYPE_TEXT, "setconfig", 0, guc->setconfig }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_database iterates over the list of databases in our catalogs.
 */
bool
catalog_iter_s_database(DatabaseCatalog *catalog,
						void *context,
						SourceDatabaseIterFun *callback)
{
	SourceDatabaseIterator *iter =
		(SourceDatabaseIterator *) calloc(1, sizeof(SourceDatabaseIterator));

	iter->catalog = catalog;

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

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

		SourceDatabase *dat = iter->dat;

		if (dat == NULL)
		{
			if (!catalog_iter_s_database_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, dat))
		{
			log_error("Failed to iterate over list of dats, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_database_init initializes an Interator over our catalog of
 * SourceDatabase entries.
 */
bool
catalog_iter_s_database_init(SourceDatabaseIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_dat iterator: db is NULL");
		return false;
	}

	iter->dat = (SourceDatabase *) calloc(1, sizeof(SourceDatabase));

	if (iter->dat == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	char *sql =
		"  select oid, datname, bytes, bytes_pretty, coalesce(snapshot, '')"
		"    from s_database "
		"order by datname";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->dat;
	query->fetchFunction = &catalog_s_database_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_database_next fetches the next SourceDatabase entry in our
 * catalogs.
 */
bool
catalog_iter_s_database_next(SourceDatabaseIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	int rc = catalog_sql_step(query);

	if (rc == SQLITE_DONE)
	{
		iter->dat = NULL;

		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("Failed to step through statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return catalog_s_database_fetch(query);
}


/*
 * catalog_s_dat_fetch fetches a SourceDatabase entry from a SQLite ppStmt
 * result set.
 */
bool
catalog_s_database_fetch(SQLiteQuery *query)
{
	SourceDatabase *dat = (SourceDatabase *) query->context;

	/* cleanup the memory area before re-use */
	bzero(dat, sizeof(SourceDatabase));

	dat->oid = sqlite3_column_int64(query->ppStmt, 0);

	strlcpy(dat->datname,
			(char *) sqlite3_column_text(query->ppStmt, 1),
			sizeof(dat->datname));

	dat->bytes = sqlite3_column_int64(query->ppStmt, 2);

	strlcpy(dat->bytesPretty,
			(char *) sqlite3_column_text(query->ppStmt, 3),
			sizeof(dat->bytesPretty));

	strlcpy(dat->snapshot,
			(char *) sqlite3_column_text(query->ppStmt, 4),
			sizeof(dat->snapshot));

	return true;
}


/*
 * catalog_update_s_database_snapshot stores the exported snapshot identifier
 * for a given database in the instance catalog.  Called by the snapshot holder
 * subprocess after it has exported a per-database REPEATABLE READ snapshot.
 */
bool
catalog_update_s_database_snapshot(DatabaseCatalog *catalog,
								   const char *datname,
								   const char *snapshot)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_update_s_database_snapshot: db is NULL");
		return false;
	}

	char *sql =
		"update s_database set snapshot = $1 where datname = $2";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "snapshot", 0, (char *) snapshot },
		{ BIND_PARAMETER_TYPE_TEXT, "datname", 0, (char *) datname }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

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

	return true;
}


/*
 * catalog_iter_s_database_finish cleans-up the internal memory used for the
 * iteration.
 */
bool
catalog_iter_s_database_finish(SourceDatabaseIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	/* in case we finish before reaching the DONE step */
	if (iter->dat != NULL)
	{
		iter->dat = NULL;
	}

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

	return true;
}


/*
 * catalog_iter_s_database_guc iterates over the list of database properties in
 * our catalogs.
 */
bool
catalog_iter_s_database_guc(DatabaseCatalog *catalog,
							const char *dbname,
							void *context,
							SourcePropertyIterFun *callback)
{
	SourcePropertyIterator *iter =
		(SourcePropertyIterator *) calloc(1, sizeof(SourcePropertyIterator));

	iter->catalog = catalog;
	iter->dbname = dbname;

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

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

		SourceProperty *property = iter->property;

		if (property == NULL)
		{
			if (!catalog_iter_s_database_guc_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, property))
		{
			log_error("Failed to iterate over list of dats, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_database_guc_init initializes an Interator over our catalog of
 * SourceProperty entries.
 */
bool
catalog_iter_s_database_guc_init(SourcePropertyIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_database_guc iterator: db is NULL");
		return false;
	}

	iter->property = (SourceProperty *) calloc(1, sizeof(SourceProperty));

	if (iter->property == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	char *sql =
		"  select role_in_database, rolname, datname, setconfig"
		"    from s_database_property "
		"   where datname = $1 ";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->property;
	query->fetchFunction = &catalog_s_database_guc_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "datname", 0, (char *) iter->dbname }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_database_guc_next fetches the next SourceProperty entry in our
 * catalogs.
 */
bool
catalog_iter_s_database_guc_next(SourcePropertyIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	int rc = catalog_sql_step(query);

	if (rc == SQLITE_DONE)
	{
		iter->property = NULL;

		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("Failed to step through statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return catalog_s_database_guc_fetch(query);
}


/*
 * catalog_s_dat_fetch fetches a SourceProperty entry from a SQLite ppStmt
 * result set.
 */
bool
catalog_s_database_guc_fetch(SQLiteQuery *query)
{
	SourceProperty *property = (SourceProperty *) query->context;

	/* cleanup the memory area before re-use */
	bzero(property, sizeof(SourceProperty));

	property->roleInDatabase = sqlite3_column_int(query->ppStmt, 0) == 1;

	if (sqlite3_column_type(query->ppStmt, 1) != SQLITE_NULL)
	{
		strlcpy(property->rolname,
				(char *) sqlite3_column_text(query->ppStmt, 1),
				sizeof(property->rolname));
	}

	if (sqlite3_column_type(query->ppStmt, 2) != SQLITE_NULL)
	{
		strlcpy(property->datname,
				(char *) sqlite3_column_text(query->ppStmt, 2),
				sizeof(property->datname));
	}

	if (sqlite3_column_type(query->ppStmt, 3) != SQLITE_NULL)
	{
		int len = sqlite3_column_bytes(query->ppStmt, 3);
		int bytes = len + 1;

		property->setconfig = (char *) calloc(bytes, sizeof(char));

		if (property->setconfig == NULL)
		{
			log_fatal(ALLOCATION_FAILED_ERROR);
			return false;
		}

		strlcpy(property->setconfig,
				(char *) sqlite3_column_text(query->ppStmt, 3),
				bytes);
	}

	return true;
}


/*
 * catalog_iter_s_database_guc_finish cleans-up the internal memory used for the
 * iteration.
 */
bool
catalog_iter_s_database_guc_finish(SourcePropertyIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	/* in case we finish before reaching the DONE step */
	if (iter->property != NULL)
	{
		iter->property = NULL;
	}

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

	return true;
}


/*
 * catalog_add_s_coll INSERTs a SourceSchema to our internal catalogs
 * database.
 */
bool
catalog_add_s_coll(DatabaseCatalog *catalog, SourceCollation *coll)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_coll: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_coll(oid, collname, description, restore_list_name) "
		"values($1, $2, $3, $4)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", coll->oid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, coll->collname },
		{ BIND_PARAMETER_TYPE_TEXT, "description", 0, coll->desc },

		{
			BIND_PARAMETER_TYPE_TEXT, "restore_list_name", 0,
			coll->restoreListName
		}
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_coll iterates over the list of datuences in our catalogs.
 */
bool
catalog_iter_s_coll(DatabaseCatalog *catalog,
					void *context,
					SourceCollationIterFun *callback)
{
	SourceCollationIterator *iter =
		(SourceCollationIterator *) calloc(1, sizeof(SourceCollationIterator));

	iter->catalog = catalog;

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

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

		SourceCollation *coll = iter->coll;

		if (coll == NULL)
		{
			if (!catalog_iter_s_coll_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, coll))
		{
			log_error("Failed to iterate over list of colls, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_coll_init initializes an Interator over our catalog of
 * SourceCollation entries.
 */
bool
catalog_iter_s_coll_init(SourceCollationIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_coll iterator: db is NULL");
		return false;
	}

	iter->coll = (SourceCollation *) calloc(1, sizeof(SourceCollation));

	if (iter->coll == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	char *sql =
		"  select oid, collname, description, restore_list_name"
		"    from s_coll "
		"order by oid";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->coll;
	query->fetchFunction = &catalog_s_coll_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_coll_next fetches the next SourceCollation entry in our
 * catalogs.
 */
bool
catalog_iter_s_coll_next(SourceCollationIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	int rc = catalog_sql_step(query);

	if (rc == SQLITE_DONE)
	{
		iter->coll = NULL;

		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("Failed to step through statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return catalog_s_coll_fetch(query);
}


/*
 * catalog_s_coll_fetch fetches a SourceCollation entry from a SQLite ppStmt
 * result set.
 */
bool
catalog_s_coll_fetch(SQLiteQuery *query)
{
	SourceCollation *coll = (SourceCollation *) query->context;

	/* cleanup the memory area before re-use */
	bzero(coll, sizeof(SourceCollation));

	coll->oid = sqlite3_column_int64(query->ppStmt, 0);

	strlcpy(coll->collname,
			(char *) sqlite3_column_text(query->ppStmt, 1),
			sizeof(coll->collname));

	/* coll->desc is a malloc'ed area */
	if (sqlite3_column_type(query->ppStmt, 2) != SQLITE_NULL)
	{
		int len = sqlite3_column_bytes(query->ppStmt, 2);
		int bytes = len + 1;

		coll->desc = (char *) calloc(bytes, sizeof(char));

		if (coll->desc == NULL)
		{
			log_fatal(ALLOCATION_FAILED_ERROR);
			return false;
		}

		strlcpy(coll->desc,
				(char *) sqlite3_column_text(query->ppStmt, 2),
				bytes);
	}

	strlcpy(coll->restoreListName,
			(char *) sqlite3_column_text(query->ppStmt, 3),
			sizeof(coll->restoreListName));

	return true;
}


/*
 * catalog_iter_s_coll_finish cleans-up the internal memory used for the
 * iteration.
 */
bool
catalog_iter_s_coll_finish(SourceCollationIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	/* in case we finish before reaching the DONE step */
	if (iter->coll != NULL)
	{
		iter->coll = NULL;
	}

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

	return true;
}


/*
 * catalog_add_s_namespace INSERTs a SourceSchema to our internal catalogs
 * database.
 */
bool
catalog_add_s_namespace(DatabaseCatalog *catalog, SourceSchema *nsp)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_namespace: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_namespace(oid, nspname, restore_list_name) "
		"values($1, $2, $3)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", nsp->oid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, nsp->nspname },

		{
			BIND_PARAMETER_TYPE_TEXT, "restore_list_name", 0,
			nsp->restoreListName
		}
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_lookup_s_namespace_by_nspname fetches a s_namespace entry from our
 * catalogs.
 */
bool
catalog_lookup_s_namespace_by_nspname(DatabaseCatalog *catalog,
									  const char *nspname,
									  SourceSchema *result)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_s_namespace_by_nspname: db is NULL");
		return false;
	}

	char *sql =
		"  select oid, nspname, restore_list_name "
		"    from s_namespace "
		"   where nspname = $1 ";

	SQLiteQuery query = {
		.context = result,
		.fetchFunction = &catalog_s_namespace_fetch
	};

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[1] = {
		{
			BIND_PARAMETER_TYPE_TEXT, "nspname", 0,
			(char *) nspname
		},
	};

	if (!catalog_sql_bind(&query, params, 1))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_lookup_s_namespace_by_oid fetches a s_namespace entry from our
 * catalogs using the oid.
 */
bool
catalog_lookup_s_namespace_by_oid(DatabaseCatalog *catalog,
								  uint32_t oid,
								  SourceSchema *result)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_s_namespace_by_oid: db is NULL");
		return false;
	}

	char *sql =
		"  select oid, nspname, restore_list_name "
		"    from s_namespace "
		"   where oid = $1 ";

	SQLiteQuery query = {
		.context = result,
		.fetchFunction = &catalog_s_namespace_fetch
	};

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[1] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", oid, NULL },
	};

	if (!catalog_sql_bind(&query, params, 1))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_s_extension_fetch fetches a SourceExtension entry from a SQLite
 * ppStmt result set.
 */
bool
catalog_s_namespace_fetch(SQLiteQuery *query)
{
	SourceSchema *schema = (SourceSchema *) query->context;

	/* cleanup the memory area before re-use */
	bzero(schema, sizeof(SourceSchema));

	schema->oid = sqlite3_column_int64(query->ppStmt, 0);

	strlcpy(schema->nspname,
			(char *) sqlite3_column_text(query->ppStmt, 1),
			sizeof(schema->nspname));

	strlcpy(schema->restoreListName,
			(char *) sqlite3_column_text(query->ppStmt, 2),
			sizeof(schema->restoreListName));

	return true;
}


/*
 * catalog_extension_fetch fetches a SourceExtension entry from a SQLite
 * ppStmt result set.
 */
bool
catalog_extension_fetch(SQLiteQuery *query)
{
	SourceExtension *extension = (SourceExtension *) query->context;

	/* cleanup the memory area before re-use */
	bzero(extension, sizeof(SourceExtension));

	extension->oid = sqlite3_column_int64(query->ppStmt, 0);

	strlcpy(extension->extname,
			(char *) sqlite3_column_text(query->ppStmt, 1),
			sizeof(extension->extname));

	strlcpy(extension->extnamespace,
			(char *) sqlite3_column_text(query->ppStmt, 2),
			sizeof(extension->extnamespace));

	extension->extrelocatable = sqlite3_column_int(query->ppStmt, 3) == 1;

	return true;
}


/*
 * catalog_lookup_ext_namespace_by_extname fetches a s_extension entry from our
 * catalogs.
 */
bool
catalog_lookup_s_extension_by_extname(DatabaseCatalog *catalog,
									  const char *extname,
									  SourceExtension *result)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_s_extension_by_extname: db is NULL");
		return false;
	}

	char *sql =
		"  select oid, extname, extnamespace, extrelocatable "
		"    from s_extension "
		"   where extname = $1 ";

	SQLiteQuery query = {
		.context = result,
		.fetchFunction = &catalog_extension_fetch
	};

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[1] = {
		{
			BIND_PARAMETER_TYPE_TEXT, "extname", 0,
			(char *) extname
		},
	};

	if (!catalog_sql_bind(&query, params, 1))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_add_s_extension INSERTs a SourceExtension to our internal catalogs
 * database.
 */
bool
catalog_add_s_extension(DatabaseCatalog *catalog, SourceExtension *extension)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_extension: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_extension(oid, extname, extnamespace, extrelocatable) "
		"values($1, $2, $3, $4)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", extension->oid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "extname", 0, extension->extname },
		{ BIND_PARAMETER_TYPE_TEXT, "extnamespace", 0, extension->extnamespace },

		{
			BIND_PARAMETER_TYPE_INT, "extrelocatable",
			extension->extrelocatable ? 1 : 0, NULL
		}
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_add_s_extension_config INSERTs a SourceExtensionConfig to our internal
 * catalogs database.
 */
bool
catalog_add_s_extension_config(DatabaseCatalog *catalog,
							   SourceExtensionConfig *config)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_extension_config: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_extension_config"
		"  (extoid, reloid, nspname, relname, condition, relkind) "
		"values($1, $2, $3, $4, $5, $6)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "extoid", config->extoid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "reloid", config->reloid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, config->nspname },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, config->relname },
		{ BIND_PARAMETER_TYPE_TEXT, "condition", 0, config->condition },
		{ BIND_PARAMETER_TYPE_INT, "relkind", (int) config->relkind, NULL }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_extension iterates over the list of extensions in our
 * catalogs.
 */
bool
catalog_iter_s_extension(DatabaseCatalog *catalog,
						 void *context,
						 SourceExtensionIterFun *callback)
{
	SourceExtensionIterator *iter =
		(SourceExtensionIterator *) calloc(1, sizeof(SourceExtensionIterator));

	iter->catalog = catalog;

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

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

		SourceExtension *ext = iter->ext;

		if (ext == NULL)
		{
			if (!catalog_iter_s_extension_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, ext))
		{
			log_error("Failed to iterate over list of extensions, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_extension_init initializes an Interator over our catalog of
 * SourceExtension entries.
 */
bool
catalog_iter_s_extension_init(SourceExtensionIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_extension iterator: db is NULL");
		return false;
	}

	iter->ext = (SourceExtension *) calloc(1, sizeof(SourceExtension));

	if (iter->ext == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	char *sql =
		"  select oid, extname, extnamespace, extrelocatable "
		"    from s_extension "
		"order by extname";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->ext;
	query->fetchFunction = &catalog_s_extension_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_extension_next fetches the next SourceExtension entry in our
 * catalogs.
 */
bool
catalog_iter_s_extension_next(SourceExtensionIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	int rc = catalog_sql_step(query);

	if (rc == SQLITE_DONE)
	{
		iter->ext = NULL;

		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("Failed to step through statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return catalog_s_extension_fetch(query);
}


/*
 * catalog_s_extension_fetch fetches a SourceExtension entry from a SQLite
 * ppStmt result set.
 */
bool
catalog_s_extension_fetch(SQLiteQuery *query)
{
	SourceExtension *ext = (SourceExtension *) query->context;

	/* cleanup the memory area before re-use */
	bzero(ext, sizeof(SourceExtension));

	ext->oid = sqlite3_column_int64(query->ppStmt, 0);

	strlcpy(ext->extname,
			(char *) sqlite3_column_text(query->ppStmt, 1),
			sizeof(ext->extname));

	strlcpy(ext->extnamespace,
			(char *) sqlite3_column_text(query->ppStmt, 2),
			sizeof(ext->extnamespace));

	ext->extrelocatable = sqlite3_column_int(query->ppStmt, 3) == 1;

	return true;
}


/*
 * catalog_iter_s_extension_finish cleans-up the internal memory used for the
 * iteration.
 */
bool
catalog_iter_s_extension_finish(SourceExtensionIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	/* in case we finish before reaching the DONE step */
	if (iter->ext != NULL)
	{
		iter->ext = NULL;
	}

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

	return true;
}


/*
 * catalog_s_ext_fetch_extconfig fetches the ext SourceExtensionConfig array
 * from our s_extension_config catalog.
 */
bool
catalog_s_ext_fetch_extconfig(DatabaseCatalog *catalog, SourceExtension *ext)
{
	SourceExtConfigIterator *iter =
		(SourceExtConfigIterator *) calloc(1, sizeof(SourceExtConfigIterator));

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

	iter->catalog = catalog;
	iter->ext = ext;

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

	while (!iter->done)
	{
		if (!catalog_iter_s_ext_extconfig_next(iter))
		{
			/* errors have already been logged */
			return false;
		}
	}

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


	return true;
}


/*
 * catalog_iter_s_ext_extconfig_init initializes an Interator over our catalog
 * of SourceExtensionConfig entries.
 */
bool
catalog_iter_s_ext_extconfig_init(SourceExtConfigIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_ext iterator: db is NULL");
		return false;
	}

	SourceExtension *ext = iter->ext;

	if (iter->ext == NULL)
	{
		log_error("BUG: Failed to initialize s_ext iterator: ext is NULL");
		return false;
	}

	/*
	 * Query extension config table based on the order at which it is
	 * inserted using sqlite's inbuilt "rowid". The insertion order ensures
	 * that the config tables are inserted according to it's foreign key
	 * dependency.
	 */
	char *sql =
		"  select count(*) over(order by rowid) as num,  "
		"         count(*) over() as count, "
		"         oid, reloid, nspname, relname, condition, relkind "
		"    from s_extension_config "
		"   where extoid = $1 "
		"order by rowid";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->ext;
	query->fetchFunction = &catalog_s_ext_extconfig_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[1] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", ext->oid, NULL }
	};

	if (!catalog_sql_bind(query, params, 1))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_ext_extconfig_next fetches the next SourceExtensionConfig
 * entry in our catalogs.
 */
bool
catalog_iter_s_ext_extconfig_next(SourceExtConfigIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	int rc = catalog_sql_step(query);

	if (rc == SQLITE_DONE)
	{
		iter->done = true;
		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("Failed to step through statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return catalog_s_ext_extconfig_fetch(query);
}


/*
 * catalog_iter_s_ext_extconfig_finish cleans-up the internal memory used for the
 * iteration.
 */
bool
catalog_iter_s_ext_extconfig_finish(SourceExtConfigIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

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

	return true;
}


/*
 * catalog_s_ext_extconfig_fetch is a SQLiteQuery callback.
 */
bool
catalog_s_ext_extconfig_fetch(SQLiteQuery *query)
{
	SourceExtension *ext = (SourceExtension *) query->context;

	int num = sqlite3_column_int(query->ppStmt, 0);
	int count = sqlite3_column_int(query->ppStmt, 1);

	if (num == 1)
	{
		ext->config.count = count;
		ext->config.array =
			(SourceExtensionConfig *) calloc(count,
											 sizeof(SourceExtensionConfig));

		if (ext->config.array == NULL)
		{
			log_error(ALLOCATION_FAILED_ERROR);
			return false;
		}
	}

	SourceExtensionConfig *conf = &(ext->config.array[num - 1]);

	conf->extoid = sqlite3_column_int64(query->ppStmt, 2);
	conf->reloid = sqlite3_column_int64(query->ppStmt, 3);

	strlcpy(conf->nspname,
			(char *) sqlite3_column_text(query->ppStmt, 4),
			sizeof(conf->nspname));

	strlcpy(conf->relname,
			(char *) sqlite3_column_text(query->ppStmt, 5),
			sizeof(conf->relname));

	/* config->condition is a malloc'ed area */
	if (sqlite3_column_type(query->ppStmt, 6) != SQLITE_NULL)
	{
		int len = sqlite3_column_bytes(query->ppStmt, 6);
		int bytes = len + 1;

		conf->condition = (char *) calloc(bytes, sizeof(char));

		if (conf->condition == NULL)
		{
			log_fatal(ALLOCATION_FAILED_ERROR);
			return false;
		}

		strlcpy(conf->condition,
				(char *) sqlite3_column_text(query->ppStmt, 6),
				bytes);
	}

	conf->relkind = sqlite3_column_int(query->ppStmt, 7);

	return true;
}


/*
 * catalog_add_s_role INSERTs a SourceRole to our internal catalogs
 * database.
 */
bool
catalog_add_s_role(DatabaseCatalog *catalog, SourceRole *role)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_role: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_role(oid, rolname) values($1, $2)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "oid", role->oid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "rolname", 0, role->rolname }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_lookup_s_role fetches a SourceRole entry from our catalogs.
 */
bool
catalog_lookup_s_role_by_name(DatabaseCatalog *catalog,
							  const char *rolname,
							  SourceRole *role)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_s_role_by_name: db is NULL");
		return false;
	}

	SQLiteQuery query = {
		.context = role,
		.fetchFunction = &catalog_s_role_fetch
	};

	char *sql =
		"  select oid, rolname "
		"    from s_role"
		"   where rolname = $1 ";

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "rolname", 0, (char *) rolname }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_s_role_stats_fetch is a SQLiteQuery callback.
 */
bool
catalog_s_role_fetch(SQLiteQuery *query)
{
	SourceRole *role = (SourceRole *) query->context;

	role->oid = sqlite3_column_int64(query->ppStmt, 0);

	strlcpy(role->rolname,
			(char *) sqlite3_column_text(query->ppStmt, 1),
			sizeof(role->rolname));

	return true;
}


/*
 * catalog_add_s_depend INSERTs a SourceDepend to our internal catalogs
 * database.
 */
bool
catalog_add_s_depend(DatabaseCatalog *catalog, SourceDepend *depend)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_s_depend: db is NULL");
		return false;
	}

	char *sql =
		"insert into s_depend("
		"  nspname, relname, refclassid, refobjid, classid, objid, "
		"  deptype, type, identity)"
		"values($1, $2, $3, $4, $5, $6, $7, $8, $9)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* depend->deptype is a single char, we want a C-string */
	char deptype[2] = " ";
	deptype[0] = depend->deptype;

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "nspname", 0, depend->nspname },
		{ BIND_PARAMETER_TYPE_TEXT, "relname", 0, depend->relname },
		{ BIND_PARAMETER_TYPE_INT64, "refclassid", depend->refclassid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "refobjid", depend->refobjid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "classid", depend->classid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "objid", depend->objid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "deptype", 0, deptype },
		{ BIND_PARAMETER_TYPE_TEXT, "type", 0, depend->type },
		{ BIND_PARAMETER_TYPE_TEXT, "identity", 0, depend->identity }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_depend iterates over the list of datuences in our catalogs.
 */
bool
catalog_iter_s_depend(DatabaseCatalog *catalog,
					  void *context,
					  SourceDependIterFun *callback)
{
	SourceDependIterator *iter =
		(SourceDependIterator *) calloc(1, sizeof(SourceDependIterator));

	iter->catalog = catalog;

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

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

		SourceDepend *dep = iter->dep;

		if (dep == NULL)
		{
			if (!catalog_iter_s_depend_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, dep))
		{
			log_error("Failed to iterate over list of deps, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_depend_init initializes an Interator over our catalog of
 * SourceDepend entries.
 */
bool
catalog_iter_s_depend_init(SourceDependIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_depend iterator: db is NULL");
		return false;
	}

	iter->dep = (SourceDepend *) calloc(1, sizeof(SourceDepend));

	if (iter->dep == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	char *sql =
		"  select nspname, relname, refclassid, refobjid, classid, objid, "
		"         deptype, type, identity "
		"    from s_depend "
		"order by nspname, relname, refclassid";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->dep;
	query->fetchFunction = &catalog_s_depend_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_depend_next fetches the next SourceDepend entry in our
 * catalogs.
 */
bool
catalog_iter_s_depend_next(SourceDependIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	int rc = catalog_sql_step(query);

	if (rc == SQLITE_DONE)
	{
		iter->dep = NULL;

		return true;
	}

	if (rc != SQLITE_ROW)
	{
		log_error("Failed to step through statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return catalog_s_depend_fetch(query);
}


/*
 * catalog_s_depend_fetch fetches a SourceDepend entry from a SQLite ppStmt
 * result set.
 */
bool
catalog_s_depend_fetch(SQLiteQuery *query)
{
	SourceDepend *dep = (SourceDepend *) query->context;

	/* cleanup the memory area before re-use */
	bzero(dep, sizeof(SourceDepend));

	strlcpy(dep->nspname,
			(char *) sqlite3_column_text(query->ppStmt, 0),
			sizeof(dep->nspname));

	strlcpy(dep->relname,
			(char *) sqlite3_column_text(query->ppStmt, 1),
			sizeof(dep->relname));

	dep->refclassid = sqlite3_column_int64(query->ppStmt, 2);
	dep->refobjid = sqlite3_column_int64(query->ppStmt, 3);
	dep->classid = sqlite3_column_int64(query->ppStmt, 4);
	dep->objid = sqlite3_column_int64(query->ppStmt, 5);

	char *deptype = (char *) sqlite3_column_text(query->ppStmt, 6);

	/* we have a single char deptype */
	dep->deptype = deptype[0];

	if (sqlite3_column_type(query->ppStmt, 7) != SQLITE_NULL)
	{
		strlcpy(dep->type,
				(char *) sqlite3_column_text(query->ppStmt, 7),
				sizeof(dep->type));
	}

	if (sqlite3_column_type(query->ppStmt, 8) != SQLITE_NULL)
	{
		strlcpy(dep->identity,
				(char *) sqlite3_column_text(query->ppStmt, 8),
				sizeof(dep->identity));
	}

	return true;
}


/*
 * catalog_iter_s_depend_finish cleans-up the internal memory used for the
 * iteration.
 */
bool
catalog_iter_s_depend_finish(SourceDependIterator *iter)
{
	SQLiteQuery *query = &(iter->query);

	/* in case we finish before reaching the DONE step */
	if (iter->dep != NULL)
	{
		iter->dep = NULL;
	}

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

	return true;
}


/*
 * catalog_upsert_process_info INSERTs or UPDATEs a process information entry
 * in our catalogs, allowing to keep track of what's happening.
 */
bool
catalog_upsert_process_info(DatabaseCatalog *catalog, ProcessInfo *ps)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_upsert_process_info: db is NULL");
		return false;
	}

	char *sql =
		"insert or replace into process("
		"  pid, ps_type, ps_title, tableoid, partnum, indexoid)"
		"values($1, $2, $3, $4, $5, $6)";

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "pid", (long long) ps->pid, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "ps_type", 0, ps->psType },
		{ BIND_PARAMETER_TYPE_TEXT, "ps_title", 0, ps->psTitle },
		{ BIND_PARAMETER_TYPE_INT64, "tableoid", ps->tableOid, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "partnum", ps->partNumber, NULL },
		{ BIND_PARAMETER_TYPE_INT64, "indexoid", ps->indexOid, NULL }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_delete_s_table deletes an s_table entry for the given oid.
 */
bool
catalog_delete_process(DatabaseCatalog *catalog, pid_t pid)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_delete_process: db is NULL");
		return false;
	}

	char *sql = "delete from process where pid = $1";

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT64, "pid", (long long) pid, NULL }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_delete_all_process_entries removes all rows from the process table.
 * Called by each supervisor before forking workers so that stale entries from
 * a previous run (including entries left by crashed workers or by a prior
 * Docker container whose PID namespace has been reset) cannot interfere with
 * resume logic or SQLite write contention checks.
 */
bool
catalog_delete_all_process_entries(DatabaseCatalog *catalog)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_delete_all_process_entries: db is NULL");
		return false;
	}

	char *sql = "delete from process";

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_delete_source_schema_data clears all source catalog DATA rows
 * while preserving rows that must survive a filter change: setup,
 * replication_slot, process, timings, sentinel, cdc_files,
 * timeline_history, and pipeline_state.
 *
 * Called when the stored filter is updated from SOURCE_FILTER_TYPE_NONE to
 * a real filter (Case 1 in catalog_register_setup_from_specs).  Clearing
 * the data forces fetchCatalogs=true on the next call so the catalog is
 * re-populated from PostgreSQL using the new filter.
 */
bool
catalog_delete_source_schema_data(DatabaseCatalog *catalog)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_delete_source_schema_data: db is NULL");
		return false;
	}

	static char *sqls[] = {
		/* delete in FK-safe order: dependents first, then referenced tables */
		"delete from summary",
		"delete from s_table_parts_done",
		"delete from s_table_indexes_done",
		"delete from vacuum_summary",
		"delete from s_table_chksum",
		"delete from s_table_size",
		"delete from s_table_part",
		"delete from s_attr",
		"delete from s_constraint",
		"delete from s_index",
		"delete from s_depend",
		"delete from s_seq",
		"delete from s_matview",
		"delete from s_table",
		"delete from s_namespace",
		"delete from s_database_property",
		"delete from s_database",
		"delete from catnames",

		/* clear fetched flags last so allDone=false forces a re-fetch */
		"delete from section"
	};

	int count = sizeof(sqls) / sizeof(sqls[0]);

	if (!semaphore_lock(&(catalog->sema)))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_begin(catalog, false))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	for (int i = 0; i < count; i++)
	{
		if (!catalog_execute(catalog, sqls[i]))
		{
			/* errors have already been logged */
			(void) catalog_execute(catalog, "ROLLBACK");
			(void) semaphore_unlock(&(catalog->sema));
			return false;
		}
	}

	if (!catalog_commit(catalog))
	{
		/* errors have already been logged */
		(void) semaphore_unlock(&(catalog->sema));
		return false;
	}

	(void) semaphore_unlock(&(catalog->sema));

	return true;
}


/*
 * catalog_log_command records the current pgcopydb invocation in the
 * command_log table of the source catalog.  This provides an audit trail for
 * diagnosing filter-related issues and other operational problems: operators
 * can inspect `sqlite3 source.db "select * from command_log"` to see exactly
 * which commands ran against a catalog directory and in what order.
 *
 * The full command line is taken from the pgcopydb_cmdline global, which is
 * populated in main() from argc/argv before any other code runs.
 *
 * Failure is non-fatal: errors are logged at WARN level and the caller
 * continues.  Never call this inside a held semaphore or open transaction.
 */
bool
catalog_log_command(DatabaseCatalog *catalog)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		return true;
	}

	/*
	 * Only one row per user-invoked command.  Two guards:
	 *
	 * 1. already_logged: catalog_init_from_specs can be called more than once
	 *    in a single command (e.g. restore pre-data calls it explicitly, then
	 *    copydb_fetch_source_catalog_setup calls it again).  The static flag
	 *    ensures only the first call logs; subsequent calls in the same process
	 *    are no-ops.  Fork'd children inherit already_logged=true once the
	 *    parent has logged, so they are also silenced by this guard.
	 *
	 * 2. getpid() != getpgid(0): guards the race where a child is forked
	 *    before the parent's first catalog_log_command call.  main() calls
	 *    setpgrp() so the top-level process has PID==PGID; fork-only children
	 *    inherit the parent's PGID but have a different PID.
	 */
	static bool already_logged = false;

	if (already_logged || getpid() != getpgid(0))
	{
		return true;
	}

	char *sql = "insert into command_log(cmdline) values($1)";

	SQLiteQuery query = { 0 };

	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_TEXT, "cmdline", 0, pgcopydb_cmdline },
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

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

	already_logged = true;

	return true;
}


/*
 * catalog_iter_s_table_in_copy iterates over the list of tables with a COPY
 * process in our catalogs.
 */
bool
catalog_iter_s_table_in_copy(DatabaseCatalog *catalog,
							 void *context,
							 SourceTableIterFun *callback)
{
	SourceTableIterator *iter =
		(SourceTableIterator *) calloc(1, sizeof(SourceTableIterator));

	iter->catalog = catalog;

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

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

		SourceTable *table = iter->table;

		if (table == NULL)
		{
			if (!catalog_iter_s_table_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, table))
		{
			log_error("Failed to iterate over list of tables, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_table_in_copy_init initializes an Interator over our catalog
 * of SourceTable entries.
 */
bool
catalog_iter_s_table_in_copy_init(SourceTableIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_table iterator: db is NULL");
		return false;
	}

	iter->table = (SourceTable *) calloc(1, sizeof(SourceTable));

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

	char *sql =
		"  select t.oid, t.datname, qname, nspname, relname, amname, restore_list_name, "
		"         relpages, reltuples, ts.bytes, ts.bytes_pretty, "
		"         exclude_data, part_key, "
		"         part.partcount, s.partnum, part.min, part.max "

		"    from process p "
		"         join s_table t on p.tableoid = t.oid "
		"         join summary s on s.pid = p.pid "
		"                       and s.tableoid = p.tableoid "

		"         left join s_table_part part "
		"                on part.oid = p.tableoid "
		"               and part.partnum = s.partnum "

		"         left join s_table_chksum c on c.oid = p.tableoid "
		"		  left join s_table_size ts on ts.oid = p.tableoid "
		"   where p.ps_type = 'COPY' "
		"order by p.pid";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->table;
	query->fetchFunction = &catalog_s_table_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_iter_s_index_in_progress iterates over the list of indexs with a
 * CREATE INDEX process in our catalogs.
 */
bool
catalog_iter_s_index_in_progress(DatabaseCatalog *catalog,
								 void *context,
								 SourceIndexIterFun *callback)
{
	SourceIndexIterator *iter =
		(SourceIndexIterator *) calloc(1, sizeof(SourceIndexIterator));

	iter->catalog = catalog;

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

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

		SourceIndex *index = iter->index;

		if (index == NULL)
		{
			if (!catalog_iter_s_index_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, index))
		{
			log_error("Failed to iterate over list of indexs, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_index_in_progress_init initializes an Interator over our
 * catalog of SourceIndex entries.
 */
bool
catalog_iter_s_index_in_progress_init(SourceIndexIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error("BUG: Failed to initialize s_index iterator: db is NULL");
		return false;
	}

	iter->index = (SourceIndex *) calloc(1, sizeof(SourceIndex));

	if (iter->index == NULL)
	{
		log_error(ALLOCATION_FAILED_ERROR);
		return false;
	}

	char *sql =
		"  select i.oid, i.qname, i.nspname, i.relname, i.restore_list_name, "
		"         i.tableoid, t.qname, t.nspname, t.relname, "
		"         isprimary, isunique, columns, i.sql, "
		"         c.oid as constraintoid, conname, "
		"         condeferrable, condeferred, c.sql as condef"
		"    from process p "
		"         join s_index i on p.indexoid = i.oid "
		"         join s_table t on t.oid = i.tableoid "
		"         left join s_constraint c on c.indexoid = i.oid"
		"   where p.ps_type = 'CREATE INDEX'";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->index;
	query->fetchFunction = &catalog_s_index_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_count_summary_done counts the number of tables and indexes that have
 * already been processed from the summary table.
 */
bool
catalog_count_summary_done(DatabaseCatalog *catalog,
						   CatalogProgressCount *count)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_count_summary_done: db is NULL");
		return false;
	}

	char *sql =
		"select "
		"  ("
		"    with pdone as "
		"    ("
		"     select tableoid, "
		"            count(s.partnum) as partdone, "
		"            coalesce(p.partcount, 1) as partcount "
		"       from summary s "
		"            join s_table t on t.oid = s.tableoid "
		"            left join s_table_part p on p.oid = t.oid and p.partnum = s.partnum "
		"      where tableoid is not null "
		"        and done_time_epoch is not null "
		"   group by tableoid"
		"    ) "
		"    select count(tableoid) from pdone where partdone = partcount"
		"  ) as tblcount,"
		"  ("
		"   select count(indexoid) "
		"     from summary "
		"    where indexoid is not null and done_time_epoch is not null"
		"  ) as idxcount";

	SQLiteQuery query = {
		.context = count,
		.fetchFunction = &catalog_count_summary_done_fetch
	};

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_count_summary_done_fetch fetches a CatalogProgressCount from a query
 * ppStmt result.
 */
bool
catalog_count_summary_done_fetch(SQLiteQuery *query)
{
	CatalogProgressCount *count = (CatalogProgressCount *) query->context;

	/* cleanup the memory area before re-use */
	bzero(count, sizeof(CatalogProgressCount));

	count->table = sqlite3_column_int64(query->ppStmt, 0);
	count->index = sqlite3_column_int64(query->ppStmt, 1);

	return true;
}


/*
 * catalog_count_bytes returns byte totals used by list progress to show
 * transfer volume.  total is the sum of source catalog sizes (s_table.bytes);
 * transferred is the sum of summary.bytes for all table rows — done rows
 * hold their final bytesTransmitted, in-progress rows hold the last value
 * flushed by the periodic stats hook (every ~5 s with PID-based jitter).
 */
bool
catalog_count_bytes(DatabaseCatalog *catalog, CatalogBytesCounts *count)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_count_bytes: db is NULL");
		return false;
	}

	char *sql =
		"select "
		"  coalesce((select sum(bytes) from s_table), 0) as total, "
		"  coalesce((select sum(bytes) from summary"
		"             where tableoid is not null and done_time_epoch is not null), 0)"
		"    as done, "
		"  coalesce((select sum(bytes) from summary"
		"             where tableoid is not null and done_time_epoch is null), 0)"
		"    as in_progress";

	SQLiteQuery query = {
		.context = count,
		.fetchFunction = &catalog_count_bytes_fetch
	};

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which returns exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_count_bytes_fetch fetches a CatalogBytesCounts from a query result.
 */
bool
catalog_count_bytes_fetch(SQLiteQuery *query)
{
	CatalogBytesCounts *count = (CatalogBytesCounts *) query->context;

	bzero(count, sizeof(CatalogBytesCounts));

	count->total = sqlite3_column_int64(query->ppStmt, 0);
	count->done = sqlite3_column_int64(query->ppStmt, 1);
	count->inProgress = sqlite3_column_int64(query->ppStmt, 2);

	return true;
}


/*
 * catalog_add_timeline_history inserts a timeline history entry to our
 * internal catalogs database.
 */
bool
catalog_add_timeline_history(DatabaseCatalog *catalog, TimelineHistoryEntry *entry)
{
	if (catalog == NULL)
	{
		log_error("BUG: catalog_add_timeline_history: catalog is NULL");
		return false;
	}

	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_add_timeline_history: db is NULL");
		return false;
	}

	char *sql =
		"insert or replace into timeline_history(tli, startpos, endpos)"
		"values($1, $2, $3)";

	SQLiteQuery query = { 0 };

	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	char slsn[PG_LSN_MAXLENGTH] = { 0 };
	char elsn[PG_LSN_MAXLENGTH] = { 0 };

	sformat(slsn, sizeof(slsn), "%X/%X", LSN_FORMAT_ARGS(entry->begin));
	sformat(elsn, sizeof(elsn), "%X/%X", LSN_FORMAT_ARGS(entry->end));

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT, "tli", entry->tli, NULL },
		{ BIND_PARAMETER_TYPE_TEXT, "startpos", 0, slsn },
		{ BIND_PARAMETER_TYPE_TEXT, "endpos", 0, elsn }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which does not return any row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_lookup_timeline_history fetches the current TimelineHistoryEntry
 * from our catalogs.
 */
bool
catalog_lookup_timeline_history(DatabaseCatalog *catalog,
								int tli,
								TimelineHistoryEntry *entry)
{
	sqlite3 *db = catalog->db;

	if (db == NULL)
	{
		log_error("BUG: catalog_lookup_timeline_history: db is NULL");
		return false;
	}

	SQLiteQuery query = {
		.context = entry,
		.fetchFunction = &catalog_timeline_history_fetch
	};

	char *sql =
		"  select tli, startpos, endpos"
		"    from timeline_history"
		"   where tli = $1";


	if (!catalog_sql_prepare(db, sql, &query))
	{
		/* errors have already been logged */
		return false;
	}

	/* bind our parameters now */
	BindParam params[] = {
		{ BIND_PARAMETER_TYPE_INT, "tli", tli, NULL }
	};

	int count = sizeof(params) / sizeof(params[0]);

	if (!catalog_sql_bind(&query, params, count))
	{
		/* errors have already been logged */
		return false;
	}

	/* now execute the query, which return exactly one row */
	if (!catalog_sql_execute_once(&query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}


/*
 * catalog_timeline_history_fetch fetches a TimelineHistoryEntry from a query
 * ppStmt result.
 */
bool
catalog_timeline_history_fetch(SQLiteQuery *query)
{
	TimelineHistoryEntry *entry = (TimelineHistoryEntry *) query->context;

	bzero(entry, sizeof(TimelineHistoryEntry));

	/* tli */
	entry->tli = sqlite3_column_int(query->ppStmt, 0);

	/* begin LSN */
	if (sqlite3_column_type(query->ppStmt, 1) != SQLITE_NULL)
	{
		const char *startpos = (const char *) sqlite3_column_text(query->ppStmt, 1);

		if (!parseLSN(startpos, &entry->begin))
		{
			log_error("Failed to parse LSN from \"%s\"", startpos);
			return false;
		}
	}

	/* end LSN */
	if (sqlite3_column_type(query->ppStmt, 2) != SQLITE_NULL)
	{
		const char *endpos = (const char *) sqlite3_column_text(query->ppStmt, 2);

		if (!parseLSN(endpos, &entry->end))
		{
			log_error("Failed to parse LSN from \"%s\"", endpos);
			return false;
		}
	}

	return true;
}


/*
 * catalog_execute executes sqlite query
 */
bool
catalog_execute(DatabaseCatalog *catalog, char *sql)
{
	log_sqlite("[SQLite] %s", sql);

	int rc = sqlite3_exec(catalog->db, sql, NULL, NULL, NULL);

	if (rc != SQLITE_OK)
	{
		log_error("[SQLite]: %s failed: %s", sql, sqlite3_errstr(rc));
		return false;
	}

	return true;
}


/*
 * catalog_sql_prepare prepares a SQLite query for our internal catalogs.
 */
bool
catalog_sql_prepare(sqlite3 *db, const char *sql, SQLiteQuery *query)
{
	query->db = db;
	query->sql = sql;

	log_sqlite("[SQLite] %s", sql);

	int rc = sqlite3_prepare_v2(db, sql, -1, &(query->ppStmt), NULL);

	if (rc == SQLITE_LOCKED || rc == SQLITE_BUSY)
	{
		ConnectionRetryPolicy retryPolicy = { 0 };

		int maxT = 5;            /* 5s */
		int maxSleepTime = 150;  /* 150ms */
		int baseSleepTime = 10;  /* 10ms */

		(void) pgsql_set_retry_policy(&retryPolicy,
									  maxT,
									  -1, /* unbounded number of attempts */
									  maxSleepTime,
									  baseSleepTime);

		while ((rc == SQLITE_LOCKED || rc == SQLITE_BUSY) &&
			   !pgsql_retry_policy_expired(&retryPolicy))
		{
			int sleepTimeMs =
				pgsql_compute_connection_retry_sleep_time(&retryPolicy);

			log_sqlite("[SQLite %d]: %s, try again in %dms",
					   rc,
					   sqlite3_errstr(rc),
					   sleepTimeMs);

			/* we have milliseconds, pg_usleep() wants microseconds */
			(void) pg_usleep(sleepTimeMs * 1000);

			rc = sqlite3_prepare_v2(db, sql, -1, &(query->ppStmt), NULL);
		}
	}

	if (rc != SQLITE_OK || query->ppStmt == NULL)
	{
		log_error("Failed to prepare SQLite statement: %s", query->sql);
		log_error("[SQLite] %s", sqlite3_errmsg(query->db));
		return false;
	}

	return true;
}


/*
 * catalog_sql_prepare_cached prepares a SQLite statement and caches it by
 * SQL pointer identity.  On a cache hit the statement is reset and ready for
 * re-use; on a miss it is compiled and added to the cache.
 *
 * Callers must pass a string-literal address (not a stack buffer) as sql so
 * pointer identity is stable across calls.
 */
bool
catalog_sql_prepare_cached(DatabaseCatalog *catalog,
						   const char *sql,
						   SQLiteQuery *query)
{
	CachedStmt *entry = NULL;

	HASH_FIND_PTR(catalog->stmtCache, &sql, entry);

	if (entry != NULL)
	{
		/* cache hit: reset to initial state for re-use */
		sqlite3_reset(entry->stmt);
		sqlite3_clear_bindings(entry->stmt);

		query->db = catalog->db;
		query->sql = sql;
		query->ppStmt = entry->stmt;
		query->fromCache = true;

		return true;
	}

	/* cache miss: compile and add to cache */
	if (!catalog_sql_prepare(catalog->db, sql, query))
	{
		return false;
	}

	entry = (CachedStmt *) malloc(sizeof(CachedStmt));

	if (entry == NULL)
	{
		log_error("Failed to allocate CachedStmt: out of memory");
		return false;
	}

	entry->sql = sql;
	entry->stmt = query->ppStmt;

	HASH_ADD_PTR(catalog->stmtCache, sql, entry);

	query->fromCache = true;

	return true;
}


/*
 * catalog_sql_bind binds parameters to our SQL query before execution.
 */
bool
catalog_sql_bind(SQLiteQuery *query, BindParam *params, int count)
{
	if (!catalog_bind_parameters(query->db, query->ppStmt, params, count))
	{
		log_error("[SQLite] Failed to bind parameters in query: %s",
				  query->sql);

		(void) sqlite3_clear_bindings(query->ppStmt);
		(void) sqlite3_finalize(query->ppStmt);
		return false;
	}

	return true;
}


/*
 * catalog_sql_execute_once executes a query once and fetches its results.
 * Cached statements (fromCache=true) are reset instead of finalized so they
 * can be reused on the next call to catalog_sql_prepare_cached.
 */
bool
catalog_sql_execute_once(SQLiteQuery *query)
{
	if (!catalog_sql_execute(query))
	{
		log_error("Failed to execute SQLite query, see above for details");
		return false;
	}

	if (query->fromCache)
	{
		/* reset is handled by catalog_sql_prepare_cached on next use */
		return true;
	}

	if (!catalog_sql_finalize(query))
	{
		log_error("Failed to finalize SQLite query, see above for details");
		return false;
	}

	return true;
}


/*
 * catalog_sql_execute executes a query and fetches its results.
 */
bool
catalog_sql_execute(SQLiteQuery *query)
{
	/* we expect SQLITE_DONE when we don't have a fetchFunction callback */
	if (query->fetchFunction == NULL)
	{
		int rc = catalog_sql_step(query);

		if (rc != SQLITE_DONE)
		{
			log_error("Failed to execute statement: %s", query->sql);

			int offset = sqlite3_error_offset(query->db);

			if (offset != -1)
			{
				/* "Failed to step through statement: %s" is 34 chars of prefix */
				log_error("%34s%*s^", " ", offset, " ");
			}

			log_error("[SQLite %d: %s]: %s",
					  rc,
					  sqlite3_errstr(rc),
					  sqlite3_errmsg(query->db));

			(void) sqlite3_clear_bindings(query->ppStmt);
			(void) sqlite3_finalize(query->ppStmt);

			return false;
		}
	}

	/* when we have a fetchFunction we expect only one row, and exactly one */
	else
	{
		int rc = catalog_sql_step(query);

		if (rc == SQLITE_DONE)
		{
			if (query->errorOnZeroRows)
			{
				log_error("SQLite query returned 0 row: %s", query->sql);
				return false;
			}
		}
		else
		{
			if (rc != SQLITE_ROW)
			{
				log_error("Failed to step through statement: %s", query->sql);

				int offset = sqlite3_error_offset(query->db);

				if (offset != -1)
				{
					/* "Failed to step through statement: %s" is 34 chars of prefix */
					log_error("%34s%*s^", " ", offset, " ");
				}

				log_error("[SQLite %d: %s]: %s",
						  rc,
						  sqlite3_errstr(rc),
						  sqlite3_errmsg(query->db));

				(void) sqlite3_clear_bindings(query->ppStmt);
				(void) sqlite3_finalize(query->ppStmt);

				return false;
			}

			/* callback */
			if (!query->fetchFunction(query))
			{
				log_error("Failed to fetch current row, "
						  "see above for details");
				(void) sqlite3_clear_bindings(query->ppStmt);
				(void) sqlite3_finalize(query->ppStmt);
				return false;
			}

			if (catalog_sql_step(query) != SQLITE_DONE)
			{
				log_error("Failed to execute statement: %s", query->sql);

				int offset = sqlite3_error_offset(query->db);

				if (offset != -1)
				{
					/* "Failed to step through statement: %s" is 34 chars of prefix */
					log_error("%34s%*s^", " ", offset, " ");
				}

				log_error("[SQLite %d: %s]: %s",
						  rc,
						  sqlite3_errstr(rc),
						  sqlite3_errmsg(query->db));

				(void) sqlite3_clear_bindings(query->ppStmt);
				(void) sqlite3_finalize(query->ppStmt);

				return false;
			}
		}
	}

	/* clean-up after execute */
	int rc = sqlite3_clear_bindings(query->ppStmt);

	if (rc != SQLITE_OK)
	{
		log_error("Failed to clear SQLite bindings: %s", sqlite3_errstr(rc));
		return false;
	}

	/* reset the prepared Statement too */
	rc = sqlite3_reset(query->ppStmt);

	if (rc != SQLITE_OK)
	{
		log_error("Failed to reset SQLite statement: %s", sqlite3_errstr(rc));
		return false;
	}

	return true;
}


/*
 * catalog_sql_step is a wrapper around sqlite3_step() that implements a retry
 * policy when the return code is SQLITE_LOCKED or SQLITE_BUSY, allowing for
 * hanlding concurrent accesses between our sub-processes.
 */
int
catalog_sql_step(SQLiteQuery *query)
{
	int rc = sqlite3_step(query->ppStmt);

	if (rc == SQLITE_LOCKED || rc == SQLITE_BUSY)
	{
		ConnectionRetryPolicy retryPolicy = { 0 };

		int maxT = 5;            /* 5s */
		int maxSleepTime = 350;  /* 350ms */
		int baseSleepTime = 10;  /* 10ms */

		(void) pgsql_set_retry_policy(&retryPolicy,
									  maxT,
									  -1, /* unbounded number of attempts */
									  maxSleepTime,
									  baseSleepTime);

		while ((rc == SQLITE_LOCKED || rc == SQLITE_BUSY) &&
			   !pgsql_retry_policy_expired(&retryPolicy))
		{
			int sleepTimeMs =
				pgsql_compute_connection_retry_sleep_time(&retryPolicy);

			log_notice("[SQLite %d]: %s, try again in %dms",
					   rc,
					   sqlite3_errmsg(query->db),
					   sleepTimeMs);

			/* we have milliseconds, pg_usleep() wants microseconds */
			(void) pg_usleep(sleepTimeMs * 1000);

			rc = sqlite3_step(query->ppStmt);
		}
	}

	return rc;
}


/*
 * catalog_sql_finalize finalizes a SQL query.
 */
bool
catalog_sql_finalize(SQLiteQuery *query)
{
	if (sqlite3_finalize(query->ppStmt) != SQLITE_OK)
	{
		log_error("Failed to finalize SQLite statement: %s",
				  sqlite3_errmsg(query->db));
		return false;
	}

	return true;
}


/*
 * catalog_bind_parameters binds parameters to a SQLite prepared statement.
 */
bool
catalog_bind_parameters(sqlite3 *db,
						sqlite3_stmt *ppStmt,
						BindParam *params,
						int count)
{
	PQExpBuffer debugParameters = NULL;

	bool logSQL = log_get_level() <= LOG_SQLITE;

	if (logSQL)
	{
		debugParameters = createPQExpBuffer();
	}

	for (int i = 0; i < count; i++)
	{
		int n = i + 1;
		BindParam *p = &(params[i]);

		if (logSQL && i > 0)
		{
			appendPQExpBufferStr(debugParameters, ", ");
		}

		switch (p->type)
		{
			case BIND_PARAMETER_TYPE_NULL:
			{
				int rc = sqlite3_bind_null(ppStmt, n);

				if (rc != SQLITE_OK)
				{
					log_error("[SQLite %d] Failed to bind \"%s\" to NULL: %s",
							  rc,
							  p->name,
							  sqlite3_errstr(rc));
					return false;
				}

				if (logSQL)
				{
					appendPQExpBuffer(debugParameters, "%s", "null");
				}

				break;
			}

			case BIND_PARAMETER_TYPE_INT:
			{
				int rc = sqlite3_bind_int(ppStmt, n, p->intVal);

				if (rc != SQLITE_OK)
				{
					log_error("[SQLite %d] Failed to bind \"%s\" value %lld: %s",
							  rc,
							  p->name,
							  (long long) p->intVal,
							  sqlite3_errstr(rc));
					return false;
				}

				if (logSQL)
				{
					appendPQExpBuffer(debugParameters, "%lld", (long long) p->intVal);
				}

				break;
			}

			case BIND_PARAMETER_TYPE_INT64:
			{
				int rc = sqlite3_bind_int64(ppStmt, n, p->intVal);

				if (rc != SQLITE_OK)
				{
					log_error("[SQLite %d] Failed to bind \"%s\" value %lld: %s",
							  rc,
							  p->name,
							  (long long) p->intVal,
							  sqlite3_errstr(rc));
					return false;
				}

				if (logSQL)
				{
					appendPQExpBuffer(debugParameters, "%lld", (long long) p->intVal);
				}

				break;
			}

			case BIND_PARAMETER_TYPE_TEXT:
			{
				int rc;

				/* deal with empty string[] buffers same as NULL pointers */
				if (p->strVal == NULL ||
					IS_EMPTY_STRING_BUFFER(p->strVal))
				{
					rc = sqlite3_bind_null(ppStmt, n);

					if (logSQL)
					{
						appendPQExpBuffer(debugParameters, "%s", "null");
					}
				}
				else
				{
					rc = sqlite3_bind_text(ppStmt,
										   n,
										   p->strVal,
										   strlen(p->strVal),
										   SQLITE_STATIC);
					if (logSQL)
					{
						appendPQExpBuffer(debugParameters, "%s", p->strVal);
					}
				}

				if (rc != SQLITE_OK)
				{
					log_error("[SQLite %d] Failed to bind \"%s\" value \"%s\": %s",
							  rc,
							  p->name,
							  p->strVal,
							  sqlite3_errstr(rc));
					return false;
				}

				break;
			}

			default:
			{
				log_error("BUG: catalog_bind_parameters called with unknown "
						  "parameter type %d",
						  p->type);
				return false;
			}
		}
	}


	if (logSQL)
	{
		if (PQExpBufferBroken(debugParameters))
		{
			log_error("Failed to create log message for SQL query parameters: "
					  "out of memory");
			destroyPQExpBuffer(debugParameters);
			return false;
		}

		log_sqlite("[SQLite] %s", debugParameters->data);

		destroyPQExpBuffer(debugParameters);
	}

	return true;
}


/*
 * catalog_start_timing starts our timing.
 */
void
catalog_start_timing(TopLevelTiming *timing)
{
	timing->startTime = time(NULL);
	INSTR_TIME_SET_CURRENT(timing->startTimeInstr);
}


/*
 * catalog_start_timing stops our timing and compute the duration in
 * milliseconds.
 */
void
catalog_stop_timing(TopLevelTiming *timing)
{
	timing->doneTime = time(NULL);

	/* cumulative timings increment their duration separately */
	if (!timing->cumulative)
	{
		INSTR_TIME_SET_CURRENT(timing->durationInstr);
		INSTR_TIME_SUBTRACT(timing->durationInstr, timing->startTimeInstr);

		timing->durationMs = INSTR_TIME_GET_MILLISEC(timing->durationInstr);

		IntervalToString(timing->durationMs,
						 timing->ppDuration,
						 INTSTRING_MAX_DIGITS);
	}
}


/*
 * catalog_iter_s_table_generated_columns iterates over the list of tables that
 * have a generated columns in our catalogs.
 */
bool
catalog_iter_s_table_generated_columns(DatabaseCatalog *catalog,
									   void *context,
									   SourceTableIterFun *callback)
{
	SourceTableIterator *iter =
		(SourceTableIterator *) calloc(1, sizeof(SourceTableIterator));

	iter->catalog = catalog;

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

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

		SourceTable *table = iter->table;

		if (table == NULL)
		{
			if (!catalog_iter_s_table_finish(iter))
			{
				/* errors have already been logged */
				return false;
			}

			break;
		}

		/* now call the provided callback */
		if (!(*callback)(context, table))
		{
			log_error("Failed to iterate over list of tables, "
					  "see above for details");
			return false;
		}
	}


	return true;
}


/*
 * catalog_iter_s_table_generated_columns_init initializes an Interator over our
 * catalog of SourceTable entries which has generated columns.
 */
bool
catalog_iter_s_table_generated_columns_init(SourceTableIterator *iter)
{
	sqlite3 *db = iter->catalog->db;

	if (db == NULL)
	{
		log_error(
			"BUG: Failed to initialize catalog_iter_s_table_generated_columns_init iterator: db is NULL");
		return false;
	}

	iter->table = (SourceTable *) calloc(1, sizeof(SourceTable));

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

	char *sql =
		"  select t.oid, t.datname, qname, nspname, relname, amname, restore_list_name, "
		"         relpages, reltuples, ts.bytes, ts.bytes_pretty, "
		"         exclude_data, part_key, "
		"         (select count(1) from s_table_part p where p.oid = t.oid) "
		"    from s_table t join s_attr a "

		/* match tables that have generated or identity-always columns */
		"       on (a.oid = t.oid "
		"           and (a.attisgenerated = 1 or a.attidentity = 'a')) "
		"       left join s_table_size ts on ts.oid = t.oid "
		"group by t.oid "
		"  having sum(case when a.attisgenerated = 1 or a.attidentity = 'a'"
		"                  then 1 else 0 end) > 0 "
		"order by bytes desc";

	SQLiteQuery *query = &(iter->query);

	query->context = iter->table;
	query->fetchFunction = &catalog_s_table_fetch;

	if (!catalog_sql_prepare(db, sql, query))
	{
		/* errors have already been logged */
		return false;
	}

	return true;
}
