| /*------------------------------------------------------------------------- |
| * |
| * pg_amcheck.c |
| * Detects corruption within database relations. |
| * |
| * Copyright (c) 2017-2021, PostgreSQL Global Development Group |
| * |
| * IDENTIFICATION |
| * src/bin/pg_amcheck/pg_amcheck.c |
| * |
| *------------------------------------------------------------------------- |
| */ |
| #include "postgres_fe.h" |
| |
| #include <time.h> |
| |
| #include "catalog/pg_am_d.h" |
| #include "catalog/pg_namespace_d.h" |
| #include "common/logging.h" |
| #include "common/username.h" |
| #include "fe_utils/cancel.h" |
| #include "fe_utils/option_utils.h" |
| #include "fe_utils/parallel_slot.h" |
| #include "fe_utils/query_utils.h" |
| #include "fe_utils/simple_list.h" |
| #include "fe_utils/string_utils.h" |
| #include "getopt_long.h" /* pgrminclude ignore */ |
| #include "pgtime.h" |
| #include "storage/block.h" |
| |
| typedef struct PatternInfo |
| { |
| const char *pattern; /* Unaltered pattern from the command line */ |
| char *db_regex; /* Database regexp parsed from pattern, or |
| * NULL */ |
| char *nsp_regex; /* Schema regexp parsed from pattern, or NULL */ |
| char *rel_regex; /* Relation regexp parsed from pattern, or |
| * NULL */ |
| bool heap_only; /* true if rel_regex should only match heap |
| * tables */ |
| bool btree_only; /* true if rel_regex should only match btree |
| * indexes */ |
| bool matched; /* true if the pattern matched in any database */ |
| } PatternInfo; |
| |
| typedef struct PatternInfoArray |
| { |
| PatternInfo *data; |
| size_t len; |
| } PatternInfoArray; |
| |
| /* pg_amcheck command line options controlled by user flags */ |
| typedef struct AmcheckOptions |
| { |
| bool dbpattern; |
| bool alldb; |
| bool echo; |
| bool verbose; |
| bool strict_names; |
| bool show_progress; |
| int jobs; |
| |
| /* |
| * Whether to install missing extensions, and optionally the name of the |
| * schema in which to install the extension's objects. |
| */ |
| bool install_missing; |
| char *install_schema; |
| |
| /* Objects to check or not to check, as lists of PatternInfo structs. */ |
| PatternInfoArray include; |
| PatternInfoArray exclude; |
| |
| /* |
| * As an optimization, if any pattern in the exclude list applies to heap |
| * tables, or similarly if any such pattern applies to btree indexes, or |
| * to schemas, then these will be true, otherwise false. These should |
| * always agree with what you'd conclude by grep'ing through the exclude |
| * list. |
| */ |
| bool excludetbl; |
| bool excludeidx; |
| bool excludensp; |
| |
| /* |
| * If any inclusion pattern exists, then we should only be checking |
| * matching relations rather than all relations, so this is true iff |
| * include is empty. |
| */ |
| bool allrel; |
| |
| /* heap table checking options */ |
| bool no_toast_expansion; |
| bool reconcile_toast; |
| bool on_error_stop; |
| int64 startblock; |
| int64 endblock; |
| const char *skip; |
| |
| /* btree index checking options */ |
| bool parent_check; |
| bool rootdescend; |
| bool heapallindexed; |
| |
| /* heap and btree hybrid option */ |
| bool no_btree_expansion; |
| } AmcheckOptions; |
| |
| static AmcheckOptions opts = { |
| .dbpattern = false, |
| .alldb = false, |
| .echo = false, |
| .verbose = false, |
| .strict_names = true, |
| .show_progress = false, |
| .jobs = 1, |
| .install_missing = false, |
| .install_schema = "pg_catalog", |
| .include = {NULL, 0}, |
| .exclude = {NULL, 0}, |
| .excludetbl = false, |
| .excludeidx = false, |
| .excludensp = false, |
| .allrel = true, |
| .no_toast_expansion = false, |
| .reconcile_toast = true, |
| .on_error_stop = false, |
| .startblock = -1, |
| .endblock = -1, |
| .skip = "none", |
| .parent_check = false, |
| .rootdescend = false, |
| .heapallindexed = false, |
| .no_btree_expansion = false |
| }; |
| |
| static const char *progname = NULL; |
| |
| /* Whether all relations have so far passed their corruption checks */ |
| static bool all_checks_pass = true; |
| |
| /* Time last progress report was displayed */ |
| static pg_time_t last_progress_report = 0; |
| static bool progress_since_last_stderr = false; |
| |
| typedef struct DatabaseInfo |
| { |
| char *datname; |
| char *amcheck_schema; /* escaped, quoted literal */ |
| } DatabaseInfo; |
| |
| typedef struct RelationInfo |
| { |
| const DatabaseInfo *datinfo; /* shared by other relinfos */ |
| Oid reloid; |
| bool is_heap; /* true if heap, false if btree */ |
| char *nspname; |
| char *relname; |
| int relpages; |
| int blocks_to_check; |
| char *sql; /* set during query run, pg_free'd after */ |
| } RelationInfo; |
| |
| /* |
| * Query for determining if contrib's amcheck is installed. If so, selects the |
| * namespace name where amcheck's functions can be found. |
| */ |
| static const char *amcheck_sql = |
| "SELECT n.nspname, x.extversion FROM pg_catalog.pg_extension x" |
| "\nJOIN pg_catalog.pg_namespace n ON x.extnamespace = n.oid" |
| "\nWHERE x.extname = 'amcheck'"; |
| |
| static void prepare_heap_command(PQExpBuffer sql, RelationInfo *rel, |
| PGconn *conn); |
| static void prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, |
| PGconn *conn); |
| static void run_command(ParallelSlot *slot, const char *sql); |
| static bool verify_heap_slot_handler(PGresult *res, PGconn *conn, |
| void *context); |
| static bool verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context); |
| static void help(const char *progname); |
| static void progress_report(uint64 relations_total, uint64 relations_checked, |
| uint64 relpages_total, uint64 relpages_checked, |
| const char *datname, bool force, bool finished); |
| |
| static void append_database_pattern(PatternInfoArray *pia, const char *pattern, |
| int encoding); |
| static void append_schema_pattern(PatternInfoArray *pia, const char *pattern, |
| int encoding); |
| static void append_relation_pattern(PatternInfoArray *pia, const char *pattern, |
| int encoding); |
| static void append_heap_pattern(PatternInfoArray *pia, const char *pattern, |
| int encoding); |
| static void append_btree_pattern(PatternInfoArray *pia, const char *pattern, |
| int encoding); |
| static void compile_database_list(PGconn *conn, SimplePtrList *databases, |
| const char *initial_dbname); |
| static void compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, |
| const DatabaseInfo *datinfo, |
| uint64 *pagecount); |
| |
| #define log_no_match(...) do { \ |
| if (opts.strict_names) \ |
| pg_log_generic(PG_LOG_ERROR, __VA_ARGS__); \ |
| else \ |
| pg_log_generic(PG_LOG_WARNING, __VA_ARGS__); \ |
| } while(0) |
| |
| #define FREE_AND_SET_NULL(x) do { \ |
| pg_free(x); \ |
| (x) = NULL; \ |
| } while (0) |
| |
| int |
| main(int argc, char *argv[]) |
| { |
| PGconn *conn = NULL; |
| SimplePtrListCell *cell; |
| SimplePtrList databases = {NULL, NULL}; |
| SimplePtrList relations = {NULL, NULL}; |
| bool failed = false; |
| const char *latest_datname; |
| int parallel_workers; |
| ParallelSlotArray *sa; |
| PQExpBufferData sql; |
| uint64 reltotal = 0; |
| uint64 pageschecked = 0; |
| uint64 pagestotal = 0; |
| uint64 relprogress = 0; |
| int pattern_id; |
| |
| static struct option long_options[] = { |
| /* Connection options */ |
| {"host", required_argument, NULL, 'h'}, |
| {"port", required_argument, NULL, 'p'}, |
| {"username", required_argument, NULL, 'U'}, |
| {"no-password", no_argument, NULL, 'w'}, |
| {"password", no_argument, NULL, 'W'}, |
| {"maintenance-db", required_argument, NULL, 1}, |
| |
| /* check options */ |
| {"all", no_argument, NULL, 'a'}, |
| {"database", required_argument, NULL, 'd'}, |
| {"exclude-database", required_argument, NULL, 'D'}, |
| {"echo", no_argument, NULL, 'e'}, |
| {"index", required_argument, NULL, 'i'}, |
| {"exclude-index", required_argument, NULL, 'I'}, |
| {"jobs", required_argument, NULL, 'j'}, |
| {"progress", no_argument, NULL, 'P'}, |
| {"relation", required_argument, NULL, 'r'}, |
| {"exclude-relation", required_argument, NULL, 'R'}, |
| {"schema", required_argument, NULL, 's'}, |
| {"exclude-schema", required_argument, NULL, 'S'}, |
| {"table", required_argument, NULL, 't'}, |
| {"exclude-table", required_argument, NULL, 'T'}, |
| {"verbose", no_argument, NULL, 'v'}, |
| {"no-dependent-indexes", no_argument, NULL, 2}, |
| {"no-dependent-toast", no_argument, NULL, 3}, |
| {"exclude-toast-pointers", no_argument, NULL, 4}, |
| {"on-error-stop", no_argument, NULL, 5}, |
| {"skip", required_argument, NULL, 6}, |
| {"startblock", required_argument, NULL, 7}, |
| {"endblock", required_argument, NULL, 8}, |
| {"rootdescend", no_argument, NULL, 9}, |
| {"no-strict-names", no_argument, NULL, 10}, |
| {"heapallindexed", no_argument, NULL, 11}, |
| {"parent-check", no_argument, NULL, 12}, |
| {"install-missing", optional_argument, NULL, 13}, |
| |
| {NULL, 0, NULL, 0} |
| }; |
| |
| int optindex; |
| int c; |
| |
| const char *db = NULL; |
| const char *maintenance_db = NULL; |
| |
| const char *host = NULL; |
| const char *port = NULL; |
| const char *username = NULL; |
| enum trivalue prompt_password = TRI_DEFAULT; |
| int encoding = pg_get_encoding_from_locale(NULL, false); |
| ConnParams cparams; |
| |
| pg_logging_init(argv[0]); |
| progname = get_progname(argv[0]); |
| set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_amcheck")); |
| |
| handle_help_version_opts(argc, argv, progname, help); |
| |
| /* process command-line options */ |
| while ((c = getopt_long(argc, argv, "ad:D:eh:Hi:I:j:p:Pr:R:s:S:t:T:U:wWv", |
| long_options, &optindex)) != -1) |
| { |
| char *endptr; |
| unsigned long optval; |
| |
| switch (c) |
| { |
| case 'a': |
| opts.alldb = true; |
| break; |
| case 'd': |
| opts.dbpattern = true; |
| append_database_pattern(&opts.include, optarg, encoding); |
| break; |
| case 'D': |
| opts.dbpattern = true; |
| append_database_pattern(&opts.exclude, optarg, encoding); |
| break; |
| case 'e': |
| opts.echo = true; |
| break; |
| case 'h': |
| host = pg_strdup(optarg); |
| break; |
| case 'i': |
| opts.allrel = false; |
| append_btree_pattern(&opts.include, optarg, encoding); |
| break; |
| case 'I': |
| opts.excludeidx = true; |
| append_btree_pattern(&opts.exclude, optarg, encoding); |
| break; |
| case 'j': |
| opts.jobs = atoi(optarg); |
| if (opts.jobs < 1) |
| { |
| pg_log_error("number of parallel jobs must be at least 1"); |
| exit(1); |
| } |
| break; |
| case 'p': |
| port = pg_strdup(optarg); |
| break; |
| case 'P': |
| opts.show_progress = true; |
| break; |
| case 'r': |
| opts.allrel = false; |
| append_relation_pattern(&opts.include, optarg, encoding); |
| break; |
| case 'R': |
| opts.excludeidx = true; |
| opts.excludetbl = true; |
| append_relation_pattern(&opts.exclude, optarg, encoding); |
| break; |
| case 's': |
| opts.allrel = false; |
| append_schema_pattern(&opts.include, optarg, encoding); |
| break; |
| case 'S': |
| opts.excludensp = true; |
| append_schema_pattern(&opts.exclude, optarg, encoding); |
| break; |
| case 't': |
| opts.allrel = false; |
| append_heap_pattern(&opts.include, optarg, encoding); |
| break; |
| case 'T': |
| opts.excludetbl = true; |
| append_heap_pattern(&opts.exclude, optarg, encoding); |
| break; |
| case 'U': |
| username = pg_strdup(optarg); |
| break; |
| case 'w': |
| prompt_password = TRI_NO; |
| break; |
| case 'W': |
| prompt_password = TRI_YES; |
| break; |
| case 'v': |
| opts.verbose = true; |
| pg_logging_increase_verbosity(); |
| break; |
| case 1: |
| maintenance_db = pg_strdup(optarg); |
| break; |
| case 2: |
| opts.no_btree_expansion = true; |
| break; |
| case 3: |
| opts.no_toast_expansion = true; |
| break; |
| case 4: |
| opts.reconcile_toast = false; |
| break; |
| case 5: |
| opts.on_error_stop = true; |
| break; |
| case 6: |
| if (pg_strcasecmp(optarg, "all-visible") == 0) |
| opts.skip = "all-visible"; |
| else if (pg_strcasecmp(optarg, "all-frozen") == 0) |
| opts.skip = "all-frozen"; |
| else if (pg_strcasecmp(optarg, "none") == 0) |
| opts.skip = "none"; |
| else |
| { |
| pg_log_error("invalid argument for option %s", "--skip"); |
| exit(1); |
| } |
| break; |
| case 7: |
| errno = 0; |
| optval = strtoul(optarg, &endptr, 10); |
| if (endptr == optarg || *endptr != '\0' || errno != 0) |
| { |
| pg_log_error("invalid start block"); |
| exit(1); |
| } |
| if (optval > MaxBlockNumber) |
| { |
| pg_log_error("start block out of bounds"); |
| exit(1); |
| } |
| opts.startblock = optval; |
| break; |
| case 8: |
| errno = 0; |
| optval = strtoul(optarg, &endptr, 10); |
| if (endptr == optarg || *endptr != '\0' || errno != 0) |
| { |
| pg_log_error("invalid end block"); |
| exit(1); |
| } |
| if (optval > MaxBlockNumber) |
| { |
| pg_log_error("end block out of bounds"); |
| exit(1); |
| } |
| opts.endblock = optval; |
| break; |
| case 9: |
| opts.rootdescend = true; |
| opts.parent_check = true; |
| break; |
| case 10: |
| opts.strict_names = false; |
| break; |
| case 11: |
| opts.heapallindexed = true; |
| break; |
| case 12: |
| opts.parent_check = true; |
| break; |
| case 13: |
| opts.install_missing = true; |
| if (optarg) |
| opts.install_schema = pg_strdup(optarg); |
| break; |
| default: |
| fprintf(stderr, |
| _("Try \"%s --help\" for more information.\n"), |
| progname); |
| exit(1); |
| } |
| } |
| |
| if (opts.endblock >= 0 && opts.endblock < opts.startblock) |
| { |
| pg_log_error("end block precedes start block"); |
| exit(1); |
| } |
| |
| /* |
| * A single non-option arguments specifies a database name or connection |
| * string. |
| */ |
| if (optind < argc) |
| { |
| db = argv[optind]; |
| optind++; |
| } |
| |
| if (optind < argc) |
| { |
| pg_log_error("too many command-line arguments (first is \"%s\")", |
| argv[optind]); |
| fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname); |
| exit(1); |
| } |
| |
| /* fill cparams except for dbname, which is set below */ |
| cparams.pghost = host; |
| cparams.pgport = port; |
| cparams.pguser = username; |
| cparams.prompt_password = prompt_password; |
| cparams.dbname = NULL; |
| cparams.override_dbname = NULL; |
| |
| setup_cancel_handler(NULL); |
| |
| /* choose the database for our initial connection */ |
| if (opts.alldb) |
| { |
| if (db != NULL) |
| { |
| pg_log_error("cannot specify a database name with --all"); |
| exit(1); |
| } |
| cparams.dbname = maintenance_db; |
| } |
| else if (db != NULL) |
| { |
| if (opts.dbpattern) |
| { |
| pg_log_error("cannot specify both a database name and database patterns"); |
| exit(1); |
| } |
| cparams.dbname = db; |
| } |
| |
| if (opts.alldb || opts.dbpattern) |
| { |
| conn = connectMaintenanceDatabase(&cparams, progname, opts.echo); |
| compile_database_list(conn, &databases, NULL); |
| } |
| else |
| { |
| if (cparams.dbname == NULL) |
| { |
| if (getenv("PGDATABASE")) |
| cparams.dbname = getenv("PGDATABASE"); |
| else if (getenv("PGUSER")) |
| cparams.dbname = getenv("PGUSER"); |
| else |
| cparams.dbname = get_user_name_or_exit(progname); |
| } |
| conn = connectDatabase(&cparams, progname, opts.echo, false, true); |
| compile_database_list(conn, &databases, PQdb(conn)); |
| } |
| |
| if (databases.head == NULL) |
| { |
| if (conn != NULL) |
| disconnectDatabase(conn); |
| pg_log_error("no databases to check"); |
| exit(0); |
| } |
| |
| /* |
| * Compile a list of all relations spanning all databases to be checked. |
| */ |
| for (cell = databases.head; cell; cell = cell->next) |
| { |
| PGresult *result; |
| int ntups; |
| const char *amcheck_schema = NULL; |
| DatabaseInfo *dat = (DatabaseInfo *) cell->ptr; |
| |
| cparams.override_dbname = dat->datname; |
| if (conn == NULL || strcmp(PQdb(conn), dat->datname) != 0) |
| { |
| if (conn != NULL) |
| disconnectDatabase(conn); |
| conn = connectDatabase(&cparams, progname, opts.echo, false, true); |
| } |
| |
| /* |
| * Optionally install amcheck if not already installed in this |
| * database. |
| */ |
| if (opts.install_missing) |
| { |
| char *schema; |
| char *install_sql; |
| |
| /* |
| * Must re-escape the schema name for each database, as the |
| * escaping rules may change. |
| */ |
| schema = PQescapeIdentifier(conn, opts.install_schema, |
| strlen(opts.install_schema)); |
| install_sql = psprintf("CREATE EXTENSION IF NOT EXISTS amcheck WITH SCHEMA %s", |
| schema); |
| |
| executeCommand(conn, install_sql, opts.echo); |
| pfree(install_sql); |
| pfree(schema); |
| } |
| |
| /* |
| * Verify that amcheck is installed for this next database. User |
| * error could result in a database not having amcheck that should |
| * have it, but we also could be iterating over multiple databases |
| * where not all of them have amcheck installed (for example, |
| * 'template1'). |
| */ |
| result = executeQuery(conn, amcheck_sql, opts.echo); |
| if (PQresultStatus(result) != PGRES_TUPLES_OK) |
| { |
| /* Querying the catalog failed. */ |
| pg_log_error("database \"%s\": %s", |
| PQdb(conn), PQerrorMessage(conn)); |
| pg_log_info("query was: %s", amcheck_sql); |
| PQclear(result); |
| disconnectDatabase(conn); |
| exit(1); |
| } |
| ntups = PQntuples(result); |
| if (ntups == 0) |
| { |
| /* Querying the catalog succeeded, but amcheck is missing. */ |
| pg_log_warning("skipping database \"%s\": amcheck is not installed", |
| PQdb(conn)); |
| disconnectDatabase(conn); |
| conn = NULL; |
| continue; |
| } |
| amcheck_schema = PQgetvalue(result, 0, 0); |
| if (opts.verbose) |
| pg_log_info("in database \"%s\": using amcheck version \"%s\" in schema \"%s\"", |
| PQdb(conn), PQgetvalue(result, 0, 1), amcheck_schema); |
| dat->amcheck_schema = PQescapeIdentifier(conn, amcheck_schema, |
| strlen(amcheck_schema)); |
| PQclear(result); |
| |
| compile_relation_list_one_db(conn, &relations, dat, &pagestotal); |
| } |
| |
| /* |
| * Check that all inclusion patterns matched at least one schema or |
| * relation that we can check. |
| */ |
| for (pattern_id = 0; pattern_id < opts.include.len; pattern_id++) |
| { |
| PatternInfo *pat = &opts.include.data[pattern_id]; |
| |
| if (!pat->matched && (pat->nsp_regex != NULL || pat->rel_regex != NULL)) |
| { |
| failed = opts.strict_names; |
| |
| if (pat->heap_only) |
| log_no_match("no heap tables to check matching \"%s\"", |
| pat->pattern); |
| else if (pat->btree_only) |
| log_no_match("no btree indexes to check matching \"%s\"", |
| pat->pattern); |
| else if (pat->rel_regex == NULL) |
| log_no_match("no relations to check in schemas matching \"%s\"", |
| pat->pattern); |
| else |
| log_no_match("no relations to check matching \"%s\"", |
| pat->pattern); |
| } |
| } |
| |
| if (failed) |
| { |
| if (conn != NULL) |
| disconnectDatabase(conn); |
| exit(1); |
| } |
| |
| /* |
| * Set parallel_workers to the lesser of opts.jobs and the number of |
| * relations. |
| */ |
| parallel_workers = 0; |
| for (cell = relations.head; cell; cell = cell->next) |
| { |
| reltotal++; |
| if (parallel_workers < opts.jobs) |
| parallel_workers++; |
| } |
| |
| if (reltotal == 0) |
| { |
| if (conn != NULL) |
| disconnectDatabase(conn); |
| pg_log_error("no relations to check"); |
| exit(1); |
| } |
| progress_report(reltotal, relprogress, pagestotal, pageschecked, |
| NULL, true, false); |
| |
| /* |
| * Main event loop. |
| * |
| * We use server-side parallelism to check up to parallel_workers |
| * relations in parallel. The list of relations was computed in database |
| * order, which minimizes the number of connects and disconnects as we |
| * process the list. |
| */ |
| latest_datname = NULL; |
| sa = ParallelSlotsSetup(parallel_workers, &cparams, progname, opts.echo, |
| NULL); |
| if (conn != NULL) |
| { |
| ParallelSlotsAdoptConn(sa, conn); |
| conn = NULL; |
| } |
| |
| initPQExpBuffer(&sql); |
| for (relprogress = 0, cell = relations.head; cell; cell = cell->next) |
| { |
| ParallelSlot *free_slot; |
| RelationInfo *rel; |
| |
| rel = (RelationInfo *) cell->ptr; |
| |
| if (CancelRequested) |
| { |
| failed = true; |
| break; |
| } |
| |
| /* |
| * The list of relations is in database sorted order. If this next |
| * relation is in a different database than the last one seen, we are |
| * about to start checking this database. Note that other slots may |
| * still be working on relations from prior databases. |
| */ |
| latest_datname = rel->datinfo->datname; |
| |
| progress_report(reltotal, relprogress, pagestotal, pageschecked, |
| latest_datname, false, false); |
| |
| relprogress++; |
| pageschecked += rel->blocks_to_check; |
| |
| /* |
| * Get a parallel slot for the next amcheck command, blocking if |
| * necessary until one is available, or until a previously issued slot |
| * command fails, indicating that we should abort checking the |
| * remaining objects. |
| */ |
| free_slot = ParallelSlotsGetIdle(sa, rel->datinfo->datname); |
| if (!free_slot) |
| { |
| /* |
| * Something failed. We don't need to know what it was, because |
| * the handler should already have emitted the necessary error |
| * messages. |
| */ |
| failed = true; |
| break; |
| } |
| |
| if (opts.verbose) |
| PQsetErrorVerbosity(free_slot->connection, PQERRORS_VERBOSE); |
| |
| /* |
| * Execute the appropriate amcheck command for this relation using our |
| * slot's database connection. We do not wait for the command to |
| * complete, nor do we perform any error checking, as that is done by |
| * the parallel slots and our handler callback functions. |
| */ |
| if (rel->is_heap) |
| { |
| if (opts.verbose) |
| { |
| if (opts.show_progress && progress_since_last_stderr) |
| fprintf(stderr, "\n"); |
| pg_log_info("checking heap table \"%s.%s.%s\"", |
| rel->datinfo->datname, rel->nspname, rel->relname); |
| progress_since_last_stderr = false; |
| } |
| prepare_heap_command(&sql, rel, free_slot->connection); |
| rel->sql = pstrdup(sql.data); /* pg_free'd after command */ |
| ParallelSlotSetHandler(free_slot, verify_heap_slot_handler, rel); |
| run_command(free_slot, rel->sql); |
| } |
| else |
| { |
| if (opts.verbose) |
| { |
| if (opts.show_progress && progress_since_last_stderr) |
| fprintf(stderr, "\n"); |
| |
| pg_log_info("checking btree index \"%s.%s.%s\"", |
| rel->datinfo->datname, rel->nspname, rel->relname); |
| progress_since_last_stderr = false; |
| } |
| prepare_btree_command(&sql, rel, free_slot->connection); |
| rel->sql = pstrdup(sql.data); /* pg_free'd after command */ |
| ParallelSlotSetHandler(free_slot, verify_btree_slot_handler, rel); |
| run_command(free_slot, rel->sql); |
| } |
| } |
| termPQExpBuffer(&sql); |
| |
| if (!failed) |
| { |
| |
| /* |
| * Wait for all slots to complete, or for one to indicate that an |
| * error occurred. Like above, we rely on the handler emitting the |
| * necessary error messages. |
| */ |
| if (sa && !ParallelSlotsWaitCompletion(sa)) |
| failed = true; |
| |
| progress_report(reltotal, relprogress, pagestotal, pageschecked, NULL, true, true); |
| } |
| |
| if (sa) |
| { |
| ParallelSlotsTerminate(sa); |
| FREE_AND_SET_NULL(sa); |
| } |
| |
| if (failed) |
| exit(1); |
| |
| if (!all_checks_pass) |
| exit(2); |
| } |
| |
| /* |
| * prepare_heap_command |
| * |
| * Creates a SQL command for running amcheck checking on the given heap |
| * relation. The command is phrased as a SQL query, with column order and |
| * names matching the expectations of verify_heap_slot_handler, which will |
| * receive and handle each row returned from the verify_heapam() function. |
| * |
| * The constructed SQL command will silently skip temporary tables, as checking |
| * them would needlessly draw errors from the underlying amcheck function. |
| * |
| * sql: buffer into which the heap table checking command will be written |
| * rel: relation information for the heap table to be checked |
| * conn: the connection to be used, for string escaping purposes |
| */ |
| static void |
| prepare_heap_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn) |
| { |
| resetPQExpBuffer(sql); |
| appendPQExpBuffer(sql, |
| "SELECT v.blkno, v.offnum, v.attnum, v.msg " |
| "FROM pg_catalog.pg_class c, %s.verify_heapam(" |
| "\nrelation := c.oid, on_error_stop := %s, check_toast := %s, skip := '%s'", |
| rel->datinfo->amcheck_schema, |
| opts.on_error_stop ? "true" : "false", |
| opts.reconcile_toast ? "true" : "false", |
| opts.skip); |
| |
| if (opts.startblock >= 0) |
| appendPQExpBuffer(sql, ", startblock := " INT64_FORMAT, opts.startblock); |
| if (opts.endblock >= 0) |
| appendPQExpBuffer(sql, ", endblock := " INT64_FORMAT, opts.endblock); |
| |
| appendPQExpBuffer(sql, |
| "\n) v WHERE c.oid = %u " |
| "AND c.relpersistence != 't'", |
| rel->reloid); |
| } |
| |
| /* |
| * prepare_btree_command |
| * |
| * Creates a SQL command for running amcheck checking on the given btree index |
| * relation. The command does not select any columns, as btree checking |
| * functions do not return any, but rather return corruption information by |
| * raising errors, which verify_btree_slot_handler expects. |
| * |
| * The constructed SQL command will silently skip temporary indexes, and |
| * indexes being reindexed concurrently, as checking them would needlessly draw |
| * errors from the underlying amcheck functions. |
| * |
| * sql: buffer into which the heap table checking command will be written |
| * rel: relation information for the index to be checked |
| * conn: the connection to be used, for string escaping purposes |
| */ |
| static void |
| prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn) |
| { |
| resetPQExpBuffer(sql); |
| |
| if (opts.parent_check) |
| appendPQExpBuffer(sql, |
| "SELECT %s.bt_index_parent_check(" |
| "index := c.oid, heapallindexed := %s, rootdescend := %s)" |
| "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i " |
| "WHERE c.oid = %u " |
| "AND c.oid = i.indexrelid " |
| "AND c.relpersistence != 't' " |
| "AND i.indisready AND i.indisvalid AND i.indislive", |
| rel->datinfo->amcheck_schema, |
| (opts.heapallindexed ? "true" : "false"), |
| (opts.rootdescend ? "true" : "false"), |
| rel->reloid); |
| else |
| appendPQExpBuffer(sql, |
| "SELECT %s.bt_index_check(" |
| "index := c.oid, heapallindexed := %s)" |
| "\nFROM pg_catalog.pg_class c, pg_catalog.pg_index i " |
| "WHERE c.oid = %u " |
| "AND c.oid = i.indexrelid " |
| "AND c.relpersistence != 't' " |
| "AND i.indisready AND i.indisvalid AND i.indislive", |
| rel->datinfo->amcheck_schema, |
| (opts.heapallindexed ? "true" : "false"), |
| rel->reloid); |
| } |
| |
| /* |
| * run_command |
| * |
| * Sends a command to the server without waiting for the command to complete. |
| * Logs an error if the command cannot be sent, but otherwise any errors are |
| * expected to be handled by a ParallelSlotHandler. |
| * |
| * If reconnecting to the database is necessary, the cparams argument may be |
| * modified. |
| * |
| * slot: slot with connection to the server we should use for the command |
| * sql: query to send |
| */ |
| static void |
| run_command(ParallelSlot *slot, const char *sql) |
| { |
| if (opts.echo) |
| printf("%s\n", sql); |
| |
| if (PQsendQuery(slot->connection, sql) == 0) |
| { |
| pg_log_error("error sending command to database \"%s\": %s", |
| PQdb(slot->connection), |
| PQerrorMessage(slot->connection)); |
| pg_log_error("command was: %s", sql); |
| exit(1); |
| } |
| } |
| |
| /* |
| * should_processing_continue |
| * |
| * Checks a query result returned from a query (presumably issued on a slot's |
| * connection) to determine if parallel slots should continue issuing further |
| * commands. |
| * |
| * Note: Heap relation corruption is reported by verify_heapam() via the result |
| * set, rather than an ERROR, but running verify_heapam() on a corrupted heap |
| * table may still result in an error being returned from the server due to |
| * missing relation files, bad checksums, etc. The btree corruption checking |
| * functions always use errors to communicate corruption messages. We can't |
| * just abort processing because we got a mere ERROR. |
| * |
| * res: result from an executed sql query |
| */ |
| static bool |
| should_processing_continue(PGresult *res) |
| { |
| const char *severity; |
| |
| switch (PQresultStatus(res)) |
| { |
| /* These are expected and ok */ |
| case PGRES_COMMAND_OK: |
| case PGRES_TUPLES_OK: |
| case PGRES_NONFATAL_ERROR: |
| break; |
| |
| /* This is expected but requires closer scrutiny */ |
| case PGRES_FATAL_ERROR: |
| severity = PQresultErrorField(res, PG_DIAG_SEVERITY_NONLOCALIZED); |
| if (severity == NULL) |
| return false; /* libpq failure, probably lost connection */ |
| if (strcmp(severity, "FATAL") == 0) |
| return false; |
| if (strcmp(severity, "PANIC") == 0) |
| return false; |
| break; |
| |
| /* These are unexpected */ |
| case PGRES_BAD_RESPONSE: |
| case PGRES_EMPTY_QUERY: |
| case PGRES_COPY_OUT: |
| case PGRES_COPY_IN: |
| case PGRES_COPY_BOTH: |
| case PGRES_SINGLE_TUPLE: |
| case PGRES_PIPELINE_SYNC: |
| case PGRES_PIPELINE_ABORTED: |
| return false; |
| } |
| return true; |
| } |
| |
| /* |
| * Returns a copy of the argument string with all lines indented four spaces. |
| * |
| * The caller should pg_free the result when finished with it. |
| */ |
| static char * |
| indent_lines(const char *str) |
| { |
| PQExpBufferData buf; |
| const char *c; |
| char *result; |
| |
| initPQExpBuffer(&buf); |
| appendPQExpBufferStr(&buf, " "); |
| for (c = str; *c; c++) |
| { |
| appendPQExpBufferChar(&buf, *c); |
| if (c[0] == '\n' && c[1] != '\0') |
| appendPQExpBufferStr(&buf, " "); |
| } |
| result = pstrdup(buf.data); |
| termPQExpBuffer(&buf); |
| |
| return result; |
| } |
| |
| /* |
| * verify_heap_slot_handler |
| * |
| * ParallelSlotHandler that receives results from a heap table checking command |
| * created by prepare_heap_command and outputs the results for the user. |
| * |
| * res: result from an executed sql query |
| * conn: connection on which the sql query was executed |
| * context: the sql query being handled, as a cstring |
| */ |
| static bool |
| verify_heap_slot_handler(PGresult *res, PGconn *conn, void *context) |
| { |
| RelationInfo *rel = (RelationInfo *) context; |
| |
| if (PQresultStatus(res) == PGRES_TUPLES_OK) |
| { |
| int i; |
| int ntups = PQntuples(res); |
| |
| if (ntups > 0) |
| all_checks_pass = false; |
| |
| for (i = 0; i < ntups; i++) |
| { |
| const char *msg; |
| |
| /* The message string should never be null, but check */ |
| if (PQgetisnull(res, i, 3)) |
| msg = "NO MESSAGE"; |
| else |
| msg = PQgetvalue(res, i, 3); |
| |
| if (!PQgetisnull(res, i, 2)) |
| printf(_("heap table \"%s.%s.%s\", block %s, offset %s, attribute %s:\n"), |
| rel->datinfo->datname, rel->nspname, rel->relname, |
| PQgetvalue(res, i, 0), /* blkno */ |
| PQgetvalue(res, i, 1), /* offnum */ |
| PQgetvalue(res, i, 2)); /* attnum */ |
| |
| else if (!PQgetisnull(res, i, 1)) |
| printf(_("heap table \"%s.%s.%s\", block %s, offset %s:\n"), |
| rel->datinfo->datname, rel->nspname, rel->relname, |
| PQgetvalue(res, i, 0), /* blkno */ |
| PQgetvalue(res, i, 1)); /* offnum */ |
| |
| else if (!PQgetisnull(res, i, 0)) |
| printf(_("heap table \"%s.%s.%s\", block %s:\n"), |
| rel->datinfo->datname, rel->nspname, rel->relname, |
| PQgetvalue(res, i, 0)); /* blkno */ |
| |
| else |
| printf(_("heap table \"%s.%s.%s\":\n"), |
| rel->datinfo->datname, rel->nspname, rel->relname); |
| |
| printf(" %s\n", msg); |
| } |
| } |
| else if (PQresultStatus(res) != PGRES_TUPLES_OK) |
| { |
| char *msg = indent_lines(PQerrorMessage(conn)); |
| |
| all_checks_pass = false; |
| printf(_("heap table \"%s.%s.%s\":\n"), |
| rel->datinfo->datname, rel->nspname, rel->relname); |
| printf("%s", msg); |
| if (opts.verbose) |
| printf(_("query was: %s\n"), rel->sql); |
| FREE_AND_SET_NULL(msg); |
| } |
| |
| FREE_AND_SET_NULL(rel->sql); |
| FREE_AND_SET_NULL(rel->nspname); |
| FREE_AND_SET_NULL(rel->relname); |
| |
| return should_processing_continue(res); |
| } |
| |
| /* |
| * verify_btree_slot_handler |
| * |
| * ParallelSlotHandler that receives results from a btree checking command |
| * created by prepare_btree_command and outputs them for the user. The results |
| * from the btree checking command is assumed to be empty, but when the results |
| * are an error code, the useful information about the corruption is expected |
| * in the connection's error message. |
| * |
| * res: result from an executed sql query |
| * conn: connection on which the sql query was executed |
| * context: unused |
| */ |
| static bool |
| verify_btree_slot_handler(PGresult *res, PGconn *conn, void *context) |
| { |
| RelationInfo *rel = (RelationInfo *) context; |
| |
| if (PQresultStatus(res) == PGRES_TUPLES_OK) |
| { |
| int ntups = PQntuples(res); |
| |
| if (ntups > 1) |
| { |
| /* |
| * We expect the btree checking functions to return one void row |
| * each, or zero rows if the check was skipped due to the object |
| * being in the wrong state to be checked, so we should output some |
| * sort of warning if we get anything more, not because it |
| * indicates corruption, but because it suggests a mismatch between |
| * amcheck and pg_amcheck versions. |
| * |
| * In conjunction with --progress, anything written to stderr at |
| * this time would present strangely to the user without an extra |
| * newline, so we print one. If we were multithreaded, we'd have |
| * to avoid splitting this across multiple calls, but we're in an |
| * event loop, so it doesn't matter. |
| */ |
| if (opts.show_progress && progress_since_last_stderr) |
| fprintf(stderr, "\n"); |
| pg_log_warning("btree index \"%s.%s.%s\": btree checking function returned unexpected number of rows: %d", |
| rel->datinfo->datname, rel->nspname, rel->relname, ntups); |
| if (opts.verbose) |
| pg_log_info("query was: %s", rel->sql); |
| pg_log_warning("Are %s's and amcheck's versions compatible?", |
| progname); |
| progress_since_last_stderr = false; |
| } |
| } |
| else |
| { |
| char *msg = indent_lines(PQerrorMessage(conn)); |
| |
| all_checks_pass = false; |
| printf(_("btree index \"%s.%s.%s\":\n"), |
| rel->datinfo->datname, rel->nspname, rel->relname); |
| printf("%s", msg); |
| if (opts.verbose) |
| printf(_("query was: %s\n"), rel->sql); |
| FREE_AND_SET_NULL(msg); |
| } |
| |
| FREE_AND_SET_NULL(rel->sql); |
| FREE_AND_SET_NULL(rel->nspname); |
| FREE_AND_SET_NULL(rel->relname); |
| |
| return should_processing_continue(res); |
| } |
| |
| /* |
| * help |
| * |
| * Prints help page for the program |
| * |
| * progname: the name of the executed program, such as "pg_amcheck" |
| */ |
| static void |
| help(const char *progname) |
| { |
| printf(_("%s checks objects in a PostgreSQL database for corruption.\n\n"), progname); |
| printf(_("Usage:\n")); |
| printf(_(" %s [OPTION]... [DBNAME]\n"), progname); |
| printf(_("\nTarget options:\n")); |
| printf(_(" -a, --all check all databases\n")); |
| printf(_(" -d, --database=PATTERN check matching database(s)\n")); |
| printf(_(" -D, --exclude-database=PATTERN do NOT check matching database(s)\n")); |
| printf(_(" -i, --index=PATTERN check matching index(es)\n")); |
| printf(_(" -I, --exclude-index=PATTERN do NOT check matching index(es)\n")); |
| printf(_(" -r, --relation=PATTERN check matching relation(s)\n")); |
| printf(_(" -R, --exclude-relation=PATTERN do NOT check matching relation(s)\n")); |
| printf(_(" -s, --schema=PATTERN check matching schema(s)\n")); |
| printf(_(" -S, --exclude-schema=PATTERN do NOT check matching schema(s)\n")); |
| printf(_(" -t, --table=PATTERN check matching table(s)\n")); |
| printf(_(" -T, --exclude-table=PATTERN do NOT check matching table(s)\n")); |
| printf(_(" --no-dependent-indexes do NOT expand list of relations to include indexes\n")); |
| printf(_(" --no-dependent-toast do NOT expand list of relations to include TOAST tables\n")); |
| printf(_(" --no-strict-names do NOT require patterns to match objects\n")); |
| printf(_("\nTable checking options:\n")); |
| printf(_(" --exclude-toast-pointers do NOT follow relation TOAST pointers\n")); |
| printf(_(" --on-error-stop stop checking at end of first corrupt page\n")); |
| printf(_(" --skip=OPTION do NOT check \"all-frozen\" or \"all-visible\" blocks\n")); |
| printf(_(" --startblock=BLOCK begin checking table(s) at the given block number\n")); |
| printf(_(" --endblock=BLOCK check table(s) only up to the given block number\n")); |
| printf(_("\nB-tree index checking options:\n")); |
| printf(_(" --heapallindexed check that all heap tuples are found within indexes\n")); |
| printf(_(" --parent-check check index parent/child relationships\n")); |
| printf(_(" --rootdescend search from root page to refind tuples\n")); |
| printf(_("\nConnection options:\n")); |
| printf(_(" -h, --host=HOSTNAME database server host or socket directory\n")); |
| printf(_(" -p, --port=PORT database server port\n")); |
| printf(_(" -U, --username=USERNAME user name to connect as\n")); |
| printf(_(" -w, --no-password never prompt for password\n")); |
| printf(_(" -W, --password force password prompt\n")); |
| printf(_(" --maintenance-db=DBNAME alternate maintenance database\n")); |
| printf(_("\nOther options:\n")); |
| printf(_(" -e, --echo show the commands being sent to the server\n")); |
| printf(_(" -j, --jobs=NUM use this many concurrent connections to the server\n")); |
| printf(_(" -P, --progress show progress information\n")); |
| printf(_(" -v, --verbose write a lot of output\n")); |
| printf(_(" -V, --version output version information, then exit\n")); |
| printf(_(" --install-missing install missing extensions\n")); |
| printf(_(" -?, --help show this help, then exit\n")); |
| |
| printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT); |
| printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL); |
| } |
| |
| /* |
| * Print a progress report based on the global variables. |
| * |
| * Progress report is written at maximum once per second, unless the force |
| * parameter is set to true. |
| * |
| * If finished is set to true, this is the last progress report. The cursor |
| * is moved to the next line. |
| */ |
| static void |
| progress_report(uint64 relations_total, uint64 relations_checked, |
| uint64 relpages_total, uint64 relpages_checked, |
| const char *datname, bool force, bool finished) |
| { |
| int percent_rel = 0; |
| int percent_pages = 0; |
| char checked_rel[32]; |
| char total_rel[32]; |
| char checked_pages[32]; |
| char total_pages[32]; |
| pg_time_t now; |
| |
| if (!opts.show_progress) |
| return; |
| |
| now = time(NULL); |
| if (now == last_progress_report && !force && !finished) |
| return; /* Max once per second */ |
| |
| last_progress_report = now; |
| if (relations_total) |
| percent_rel = (int) (relations_checked * 100 / relations_total); |
| if (relpages_total) |
| percent_pages = (int) (relpages_checked * 100 / relpages_total); |
| |
| /* |
| * Separate step to keep platform-dependent format code out of fprintf |
| * calls. We only test for INT64_FORMAT availability in snprintf, not |
| * fprintf. |
| */ |
| snprintf(checked_rel, sizeof(checked_rel), INT64_FORMAT, relations_checked); |
| snprintf(total_rel, sizeof(total_rel), INT64_FORMAT, relations_total); |
| snprintf(checked_pages, sizeof(checked_pages), INT64_FORMAT, relpages_checked); |
| snprintf(total_pages, sizeof(total_pages), INT64_FORMAT, relpages_total); |
| |
| #define VERBOSE_DATNAME_LENGTH 35 |
| if (opts.verbose) |
| { |
| if (!datname) |
| |
| /* |
| * No datname given, so clear the status line (used for first and |
| * last call) |
| */ |
| fprintf(stderr, |
| _("%*s/%s relations (%d%%), %*s/%s pages (%d%%) %*s"), |
| (int) strlen(total_rel), |
| checked_rel, total_rel, percent_rel, |
| (int) strlen(total_pages), |
| checked_pages, total_pages, percent_pages, |
| VERBOSE_DATNAME_LENGTH + 2, ""); |
| else |
| { |
| bool truncate = (strlen(datname) > VERBOSE_DATNAME_LENGTH); |
| |
| fprintf(stderr, |
| _("%*s/%s relations (%d%%), %*s/%s pages (%d%%) (%s%-*.*s)"), |
| (int) strlen(total_rel), |
| checked_rel, total_rel, percent_rel, |
| (int) strlen(total_pages), |
| checked_pages, total_pages, percent_pages, |
| /* Prefix with "..." if we do leading truncation */ |
| truncate ? "..." : "", |
| truncate ? VERBOSE_DATNAME_LENGTH - 3 : VERBOSE_DATNAME_LENGTH, |
| truncate ? VERBOSE_DATNAME_LENGTH - 3 : VERBOSE_DATNAME_LENGTH, |
| /* Truncate datname at beginning if it's too long */ |
| truncate ? datname + strlen(datname) - VERBOSE_DATNAME_LENGTH + 3 : datname); |
| } |
| } |
| else |
| fprintf(stderr, |
| _("%*s/%s relations (%d%%), %*s/%s pages (%d%%)"), |
| (int) strlen(total_rel), |
| checked_rel, total_rel, percent_rel, |
| (int) strlen(total_pages), |
| checked_pages, total_pages, percent_pages); |
| |
| /* |
| * Stay on the same line if reporting to a terminal and we're not done |
| * yet. |
| */ |
| if (!finished && isatty(fileno(stderr))) |
| { |
| fputc('\r', stderr); |
| progress_since_last_stderr = true; |
| } |
| else |
| fputc('\n', stderr); |
| } |
| |
| /* |
| * Extend the pattern info array to hold one additional initialized pattern |
| * info entry. |
| * |
| * Returns a pointer to the new entry. |
| */ |
| static PatternInfo * |
| extend_pattern_info_array(PatternInfoArray *pia) |
| { |
| PatternInfo *result; |
| |
| pia->len++; |
| pia->data = (PatternInfo *) pg_realloc(pia->data, pia->len * sizeof(PatternInfo)); |
| result = &pia->data[pia->len - 1]; |
| memset(result, 0, sizeof(*result)); |
| |
| return result; |
| } |
| |
| /* |
| * append_database_pattern |
| * |
| * Adds the given pattern interpreted as a database name pattern. |
| * |
| * pia: the pattern info array to be appended |
| * pattern: the database name pattern |
| * encoding: client encoding for parsing the pattern |
| */ |
| static void |
| append_database_pattern(PatternInfoArray *pia, const char *pattern, int encoding) |
| { |
| PQExpBufferData buf; |
| int dotcnt; |
| PatternInfo *info = extend_pattern_info_array(pia); |
| |
| initPQExpBuffer(&buf); |
| patternToSQLRegex(encoding, NULL, NULL, &buf, pattern, false, false, |
| &dotcnt); |
| if (dotcnt > 0) |
| { |
| pg_log_error("improper qualified name (too many dotted names): %s", pattern); |
| exit(2); |
| } |
| info->pattern = pattern; |
| info->db_regex = pstrdup(buf.data); |
| |
| termPQExpBuffer(&buf); |
| } |
| |
| /* |
| * append_schema_pattern |
| * |
| * Adds the given pattern interpreted as a schema name pattern. |
| * |
| * pia: the pattern info array to be appended |
| * pattern: the schema name pattern |
| * encoding: client encoding for parsing the pattern |
| */ |
| static void |
| append_schema_pattern(PatternInfoArray *pia, const char *pattern, int encoding) |
| { |
| PQExpBufferData dbbuf; |
| PQExpBufferData nspbuf; |
| int dotcnt; |
| PatternInfo *info = extend_pattern_info_array(pia); |
| |
| initPQExpBuffer(&dbbuf); |
| initPQExpBuffer(&nspbuf); |
| |
| patternToSQLRegex(encoding, NULL, &dbbuf, &nspbuf, pattern, false, false, |
| &dotcnt); |
| if (dotcnt > 1) |
| { |
| pg_log_error("improper qualified name (too many dotted names): %s", pattern); |
| exit(2); |
| } |
| info->pattern = pattern; |
| if (dbbuf.data[0]) |
| { |
| opts.dbpattern = true; |
| info->db_regex = pstrdup(dbbuf.data); |
| } |
| if (nspbuf.data[0]) |
| info->nsp_regex = pstrdup(nspbuf.data); |
| |
| termPQExpBuffer(&dbbuf); |
| termPQExpBuffer(&nspbuf); |
| } |
| |
| /* |
| * append_relation_pattern_helper |
| * |
| * Adds to a list the given pattern interpreted as a relation pattern. |
| * |
| * pia: the pattern info array to be appended |
| * pattern: the relation name pattern |
| * encoding: client encoding for parsing the pattern |
| * heap_only: whether the pattern should only be matched against heap tables |
| * btree_only: whether the pattern should only be matched against btree indexes |
| */ |
| static void |
| append_relation_pattern_helper(PatternInfoArray *pia, const char *pattern, |
| int encoding, bool heap_only, bool btree_only) |
| { |
| PQExpBufferData dbbuf; |
| PQExpBufferData nspbuf; |
| PQExpBufferData relbuf; |
| int dotcnt; |
| PatternInfo *info = extend_pattern_info_array(pia); |
| |
| initPQExpBuffer(&dbbuf); |
| initPQExpBuffer(&nspbuf); |
| initPQExpBuffer(&relbuf); |
| |
| patternToSQLRegex(encoding, &dbbuf, &nspbuf, &relbuf, pattern, false, |
| false, &dotcnt); |
| if (dotcnt > 2) |
| { |
| pg_log_error("improper relation name (too many dotted names): %s", pattern); |
| exit(2); |
| } |
| info->pattern = pattern; |
| if (dbbuf.data[0]) |
| { |
| opts.dbpattern = true; |
| info->db_regex = pstrdup(dbbuf.data); |
| } |
| if (nspbuf.data[0]) |
| info->nsp_regex = pstrdup(nspbuf.data); |
| if (relbuf.data[0]) |
| info->rel_regex = pstrdup(relbuf.data); |
| |
| termPQExpBuffer(&dbbuf); |
| termPQExpBuffer(&nspbuf); |
| termPQExpBuffer(&relbuf); |
| |
| info->heap_only = heap_only; |
| info->btree_only = btree_only; |
| } |
| |
| /* |
| * append_relation_pattern |
| * |
| * Adds the given pattern interpreted as a relation pattern, to be matched |
| * against both heap tables and btree indexes. |
| * |
| * pia: the pattern info array to be appended |
| * pattern: the relation name pattern |
| * encoding: client encoding for parsing the pattern |
| */ |
| static void |
| append_relation_pattern(PatternInfoArray *pia, const char *pattern, int encoding) |
| { |
| append_relation_pattern_helper(pia, pattern, encoding, false, false); |
| } |
| |
| /* |
| * append_heap_pattern |
| * |
| * Adds the given pattern interpreted as a relation pattern, to be matched only |
| * against heap tables. |
| * |
| * pia: the pattern info array to be appended |
| * pattern: the relation name pattern |
| * encoding: client encoding for parsing the pattern |
| */ |
| static void |
| append_heap_pattern(PatternInfoArray *pia, const char *pattern, int encoding) |
| { |
| append_relation_pattern_helper(pia, pattern, encoding, true, false); |
| } |
| |
| /* |
| * append_btree_pattern |
| * |
| * Adds the given pattern interpreted as a relation pattern, to be matched only |
| * against btree indexes. |
| * |
| * pia: the pattern info array to be appended |
| * pattern: the relation name pattern |
| * encoding: client encoding for parsing the pattern |
| */ |
| static void |
| append_btree_pattern(PatternInfoArray *pia, const char *pattern, int encoding) |
| { |
| append_relation_pattern_helper(pia, pattern, encoding, false, true); |
| } |
| |
| /* |
| * append_db_pattern_cte |
| * |
| * Appends to the buffer the body of a Common Table Expression (CTE) containing |
| * the database portions filtered from the list of patterns expressed as two |
| * columns: |
| * |
| * pattern_id: the index of this pattern in pia->data[] |
| * rgx: the database regular expression parsed from the pattern |
| * |
| * Patterns without a database portion are skipped. Patterns with more than |
| * just a database portion are optionally skipped, depending on argument |
| * 'inclusive'. |
| * |
| * buf: the buffer to be appended |
| * pia: the array of patterns to be inserted into the CTE |
| * conn: the database connection |
| * inclusive: whether to include patterns with schema and/or relation parts |
| * |
| * Returns whether any database patterns were appended. |
| */ |
| static bool |
| append_db_pattern_cte(PQExpBuffer buf, const PatternInfoArray *pia, |
| PGconn *conn, bool inclusive) |
| { |
| int pattern_id; |
| const char *comma; |
| bool have_values; |
| |
| comma = ""; |
| have_values = false; |
| for (pattern_id = 0; pattern_id < pia->len; pattern_id++) |
| { |
| PatternInfo *info = &pia->data[pattern_id]; |
| |
| if (info->db_regex != NULL && |
| (inclusive || (info->nsp_regex == NULL && info->rel_regex == NULL))) |
| { |
| if (!have_values) |
| appendPQExpBufferStr(buf, "\nVALUES"); |
| have_values = true; |
| appendPQExpBuffer(buf, "%s\n(%d, ", comma, pattern_id); |
| appendStringLiteralConn(buf, info->db_regex, conn); |
| appendPQExpBufferStr(buf, ")"); |
| comma = ","; |
| } |
| } |
| |
| if (!have_values) |
| appendPQExpBufferStr(buf, "\nSELECT NULL, NULL, NULL WHERE false"); |
| |
| return have_values; |
| } |
| |
| /* |
| * compile_database_list |
| * |
| * If any database patterns exist, or if --all was given, compiles a distinct |
| * list of databases to check using a SQL query based on the patterns plus the |
| * literal initial database name, if given. If no database patterns exist and |
| * --all was not given, the query is not necessary, and only the initial |
| * database name (if any) is added to the list. |
| * |
| * conn: connection to the initial database |
| * databases: the list onto which databases should be appended |
| * initial_dbname: an optional extra database name to include in the list |
| */ |
| static void |
| compile_database_list(PGconn *conn, SimplePtrList *databases, |
| const char *initial_dbname) |
| { |
| PGresult *res; |
| PQExpBufferData sql; |
| int ntups; |
| int i; |
| bool fatal; |
| |
| if (initial_dbname) |
| { |
| DatabaseInfo *dat = (DatabaseInfo *) pg_malloc0(sizeof(DatabaseInfo)); |
| |
| /* This database is included. Add to list */ |
| if (opts.verbose) |
| pg_log_info("including database \"%s\"", initial_dbname); |
| |
| dat->datname = pstrdup(initial_dbname); |
| simple_ptr_list_append(databases, dat); |
| } |
| |
| initPQExpBuffer(&sql); |
| |
| /* Append the include patterns CTE. */ |
| appendPQExpBufferStr(&sql, "WITH include_raw (pattern_id, rgx) AS ("); |
| if (!append_db_pattern_cte(&sql, &opts.include, conn, true) && |
| !opts.alldb) |
| { |
| /* |
| * None of the inclusion patterns (if any) contain database portions, |
| * so there is no need to query the database to resolve database |
| * patterns. |
| * |
| * Since we're also not operating under --all, we don't need to query |
| * the exhaustive list of connectable databases, either. |
| */ |
| termPQExpBuffer(&sql); |
| return; |
| } |
| |
| /* Append the exclude patterns CTE. */ |
| appendPQExpBufferStr(&sql, "),\nexclude_raw (pattern_id, rgx) AS ("); |
| append_db_pattern_cte(&sql, &opts.exclude, conn, false); |
| appendPQExpBufferStr(&sql, "),"); |
| |
| /* |
| * Append the database CTE, which includes whether each database is |
| * connectable and also joins against exclude_raw to determine whether |
| * each database is excluded. |
| */ |
| appendPQExpBufferStr(&sql, |
| "\ndatabase (datname) AS (" |
| "\nSELECT d.datname " |
| "FROM pg_catalog.pg_database d " |
| "LEFT OUTER JOIN exclude_raw e " |
| "ON d.datname ~ e.rgx " |
| "\nWHERE d.datallowconn " |
| "AND e.pattern_id IS NULL" |
| ")," |
| |
| /* |
| * Append the include_pat CTE, which joins the include_raw CTE against the |
| * databases CTE to determine if all the inclusion patterns had matches, |
| * and whether each matched pattern had the misfortune of only matching |
| * excluded or unconnectable databases. |
| */ |
| "\ninclude_pat (pattern_id, checkable) AS (" |
| "\nSELECT i.pattern_id, " |
| "COUNT(*) FILTER (" |
| "WHERE d IS NOT NULL" |
| ") AS checkable" |
| "\nFROM include_raw i " |
| "LEFT OUTER JOIN database d " |
| "ON d.datname ~ i.rgx" |
| "\nGROUP BY i.pattern_id" |
| ")," |
| |
| /* |
| * Append the filtered_databases CTE, which selects from the database CTE |
| * optionally joined against the include_raw CTE to only select databases |
| * that match an inclusion pattern. This appears to duplicate what the |
| * include_pat CTE already did above, but here we want only databases, and |
| * there we wanted patterns. |
| */ |
| "\nfiltered_databases (datname) AS (" |
| "\nSELECT DISTINCT d.datname " |
| "FROM database d"); |
| if (!opts.alldb) |
| appendPQExpBufferStr(&sql, |
| " INNER JOIN include_raw i " |
| "ON d.datname ~ i.rgx"); |
| appendPQExpBufferStr(&sql, |
| ")" |
| |
| /* |
| * Select the checkable databases and the unmatched inclusion patterns. |
| */ |
| "\nSELECT pattern_id, datname FROM (" |
| "\nSELECT pattern_id, NULL::TEXT AS datname " |
| "FROM include_pat " |
| "WHERE checkable = 0 " |
| "UNION ALL" |
| "\nSELECT NULL, datname " |
| "FROM filtered_databases" |
| ") AS combined_records" |
| "\nORDER BY pattern_id NULLS LAST, datname"); |
| |
| res = executeQuery(conn, sql.data, opts.echo); |
| if (PQresultStatus(res) != PGRES_TUPLES_OK) |
| { |
| pg_log_error("query failed: %s", PQerrorMessage(conn)); |
| pg_log_info("query was: %s", sql.data); |
| disconnectDatabase(conn); |
| exit(1); |
| } |
| termPQExpBuffer(&sql); |
| |
| ntups = PQntuples(res); |
| for (fatal = false, i = 0; i < ntups; i++) |
| { |
| int pattern_id = -1; |
| const char *datname = NULL; |
| |
| if (!PQgetisnull(res, i, 0)) |
| pattern_id = atoi(PQgetvalue(res, i, 0)); |
| if (!PQgetisnull(res, i, 1)) |
| datname = PQgetvalue(res, i, 1); |
| |
| if (pattern_id >= 0) |
| { |
| /* |
| * Current record pertains to an inclusion pattern that matched no |
| * checkable databases. |
| */ |
| fatal = opts.strict_names; |
| if (pattern_id >= opts.include.len) |
| { |
| pg_log_error("internal error: received unexpected database pattern_id %d", |
| pattern_id); |
| exit(1); |
| } |
| log_no_match("no connectable databases to check matching \"%s\"", |
| opts.include.data[pattern_id].pattern); |
| } |
| else |
| { |
| DatabaseInfo *dat; |
| |
| /* Current record pertains to a database */ |
| Assert(datname != NULL); |
| |
| /* Avoid entering a duplicate entry matching the initial_dbname */ |
| if (initial_dbname != NULL && strcmp(initial_dbname, datname) == 0) |
| continue; |
| |
| /* This database is included. Add to list */ |
| if (opts.verbose) |
| pg_log_info("including database \"%s\"", datname); |
| |
| dat = (DatabaseInfo *) pg_malloc0(sizeof(DatabaseInfo)); |
| dat->datname = pstrdup(datname); |
| simple_ptr_list_append(databases, dat); |
| } |
| } |
| PQclear(res); |
| |
| if (fatal) |
| { |
| if (conn != NULL) |
| disconnectDatabase(conn); |
| exit(1); |
| } |
| } |
| |
| /* |
| * append_rel_pattern_raw_cte |
| * |
| * Appends to the buffer the body of a Common Table Expression (CTE) containing |
| * the given patterns as six columns: |
| * |
| * pattern_id: the index of this pattern in pia->data[] |
| * db_regex: the database regexp parsed from the pattern, or NULL if the |
| * pattern had no database part |
| * nsp_regex: the namespace regexp parsed from the pattern, or NULL if the |
| * pattern had no namespace part |
| * rel_regex: the relname regexp parsed from the pattern, or NULL if the |
| * pattern had no relname part |
| * heap_only: true if the pattern applies only to heap tables (not indexes) |
| * btree_only: true if the pattern applies only to btree indexes (not tables) |
| * |
| * buf: the buffer to be appended |
| * patterns: the array of patterns to be inserted into the CTE |
| * conn: the database connection |
| */ |
| static void |
| append_rel_pattern_raw_cte(PQExpBuffer buf, const PatternInfoArray *pia, |
| PGconn *conn) |
| { |
| int pattern_id; |
| const char *comma; |
| bool have_values; |
| |
| comma = ""; |
| have_values = false; |
| for (pattern_id = 0; pattern_id < pia->len; pattern_id++) |
| { |
| PatternInfo *info = &pia->data[pattern_id]; |
| |
| if (!have_values) |
| appendPQExpBufferStr(buf, "\nVALUES"); |
| have_values = true; |
| appendPQExpBuffer(buf, "%s\n(%d::INTEGER, ", comma, pattern_id); |
| if (info->db_regex == NULL) |
| appendPQExpBufferStr(buf, "NULL"); |
| else |
| appendStringLiteralConn(buf, info->db_regex, conn); |
| appendPQExpBufferStr(buf, "::TEXT, "); |
| if (info->nsp_regex == NULL) |
| appendPQExpBufferStr(buf, "NULL"); |
| else |
| appendStringLiteralConn(buf, info->nsp_regex, conn); |
| appendPQExpBufferStr(buf, "::TEXT, "); |
| if (info->rel_regex == NULL) |
| appendPQExpBufferStr(buf, "NULL"); |
| else |
| appendStringLiteralConn(buf, info->rel_regex, conn); |
| if (info->heap_only) |
| appendPQExpBufferStr(buf, "::TEXT, true::BOOLEAN"); |
| else |
| appendPQExpBufferStr(buf, "::TEXT, false::BOOLEAN"); |
| if (info->btree_only) |
| appendPQExpBufferStr(buf, ", true::BOOLEAN"); |
| else |
| appendPQExpBufferStr(buf, ", false::BOOLEAN"); |
| appendPQExpBufferStr(buf, ")"); |
| comma = ","; |
| } |
| |
| if (!have_values) |
| appendPQExpBufferStr(buf, |
| "\nSELECT NULL::INTEGER, NULL::TEXT, NULL::TEXT, " |
| "NULL::TEXT, NULL::BOOLEAN, NULL::BOOLEAN " |
| "WHERE false"); |
| } |
| |
| /* |
| * append_rel_pattern_filtered_cte |
| * |
| * Appends to the buffer a Common Table Expression (CTE) which selects |
| * all patterns from the named raw CTE, filtered by database. All patterns |
| * which have no database portion or whose database portion matches our |
| * connection's database name are selected, with other patterns excluded. |
| * |
| * The basic idea here is that if we're connected to database "foo" and we have |
| * patterns "foo.bar.baz", "alpha.beta" and "one.two.three", we only want to |
| * use the first two while processing relations in this database, as the third |
| * one is not relevant. |
| * |
| * buf: the buffer to be appended |
| * raw: the name of the CTE to select from |
| * filtered: the name of the CTE to create |
| * conn: the database connection |
| */ |
| static void |
| append_rel_pattern_filtered_cte(PQExpBuffer buf, const char *raw, |
| const char *filtered, PGconn *conn) |
| { |
| appendPQExpBuffer(buf, |
| "\n%s (pattern_id, nsp_regex, rel_regex, heap_only, btree_only) AS (" |
| "\nSELECT pattern_id, nsp_regex, rel_regex, heap_only, btree_only " |
| "FROM %s r" |
| "\nWHERE (r.db_regex IS NULL " |
| "OR ", |
| filtered, raw); |
| appendStringLiteralConn(buf, PQdb(conn), conn); |
| appendPQExpBufferStr(buf, " ~ r.db_regex)"); |
| appendPQExpBufferStr(buf, |
| " AND (r.nsp_regex IS NOT NULL" |
| " OR r.rel_regex IS NOT NULL)" |
| "),"); |
| } |
| |
| /* |
| * compile_relation_list_one_db |
| * |
| * Compiles a list of relations to check within the currently connected |
| * database based on the user supplied options, sorted by descending size, |
| * and appends them to the given list of relations. |
| * |
| * The cells of the constructed list contain all information about the relation |
| * necessary to connect to the database and check the object, including which |
| * database to connect to, where contrib/amcheck is installed, and the Oid and |
| * type of object (heap table vs. btree index). Rather than duplicating the |
| * database details per relation, the relation structs use references to the |
| * same database object, provided by the caller. |
| * |
| * conn: connection to this next database, which should be the same as in 'dat' |
| * relations: list onto which the relations information should be appended |
| * dat: the database info struct for use by each relation |
| * pagecount: gets incremented by the number of blocks to check in all |
| * relations added |
| */ |
| static void |
| compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, |
| const DatabaseInfo *dat, |
| uint64 *pagecount) |
| { |
| PGresult *res; |
| PQExpBufferData sql; |
| int ntups; |
| int i; |
| |
| initPQExpBuffer(&sql); |
| appendPQExpBufferStr(&sql, "WITH"); |
| |
| /* Append CTEs for the relation inclusion patterns, if any */ |
| if (!opts.allrel) |
| { |
| appendPQExpBufferStr(&sql, |
| " include_raw (pattern_id, db_regex, nsp_regex, rel_regex, heap_only, btree_only) AS ("); |
| append_rel_pattern_raw_cte(&sql, &opts.include, conn); |
| appendPQExpBufferStr(&sql, "\n),"); |
| append_rel_pattern_filtered_cte(&sql, "include_raw", "include_pat", conn); |
| } |
| |
| /* Append CTEs for the relation exclusion patterns, if any */ |
| if (opts.excludetbl || opts.excludeidx || opts.excludensp) |
| { |
| appendPQExpBufferStr(&sql, |
| " exclude_raw (pattern_id, db_regex, nsp_regex, rel_regex, heap_only, btree_only) AS ("); |
| append_rel_pattern_raw_cte(&sql, &opts.exclude, conn); |
| appendPQExpBufferStr(&sql, "\n),"); |
| append_rel_pattern_filtered_cte(&sql, "exclude_raw", "exclude_pat", conn); |
| } |
| |
| /* Append the relation CTE. */ |
| appendPQExpBufferStr(&sql, |
| " relation (pattern_id, oid, nspname, relname, reltoastrelid, relpages, is_heap, is_btree) AS (" |
| "\nSELECT DISTINCT ON (c.oid"); |
| if (!opts.allrel) |
| appendPQExpBufferStr(&sql, ", ip.pattern_id) ip.pattern_id,"); |
| else |
| appendPQExpBufferStr(&sql, ") NULL::INTEGER AS pattern_id,"); |
| appendPQExpBuffer(&sql, |
| "\nc.oid, n.nspname, c.relname, c.reltoastrelid, c.relpages, " |
| "c.relam = %u AS is_heap, " |
| "c.relam = %u AS is_btree" |
| "\nFROM pg_catalog.pg_class c " |
| "INNER JOIN pg_catalog.pg_namespace n " |
| "ON c.relnamespace = n.oid", |
| HEAP_TABLE_AM_OID, BTREE_AM_OID); |
| if (!opts.allrel) |
| appendPQExpBuffer(&sql, |
| "\nINNER JOIN include_pat ip" |
| "\nON (n.nspname ~ ip.nsp_regex OR ip.nsp_regex IS NULL)" |
| "\nAND (c.relname ~ ip.rel_regex OR ip.rel_regex IS NULL)" |
| "\nAND (c.relam = %u OR NOT ip.heap_only)" |
| "\nAND (c.relam = %u OR NOT ip.btree_only)", |
| HEAP_TABLE_AM_OID, BTREE_AM_OID); |
| if (opts.excludetbl || opts.excludeidx || opts.excludensp) |
| appendPQExpBuffer(&sql, |
| "\nLEFT OUTER JOIN exclude_pat ep" |
| "\nON (n.nspname ~ ep.nsp_regex OR ep.nsp_regex IS NULL)" |
| "\nAND (c.relname ~ ep.rel_regex OR ep.rel_regex IS NULL)" |
| "\nAND (c.relam = %u OR NOT ep.heap_only OR ep.rel_regex IS NULL)" |
| "\nAND (c.relam = %u OR NOT ep.btree_only OR ep.rel_regex IS NULL)", |
| HEAP_TABLE_AM_OID, BTREE_AM_OID); |
| |
| /* |
| * Exclude temporary tables and indexes, which must necessarily belong to |
| * other sessions. (We don't create any ourselves.) We must ultimately |
| * exclude indexes marked invalid or not ready, but we delay that decision |
| * until firing off the amcheck command, as the state of an index may |
| * change by then. |
| */ |
| appendPQExpBufferStr(&sql, "\nWHERE c.relpersistence != 't'"); |
| if (opts.excludetbl || opts.excludeidx || opts.excludensp) |
| appendPQExpBufferStr(&sql, "\nAND ep.pattern_id IS NULL"); |
| |
| /* |
| * We need to be careful not to break the --no-dependent-toast and |
| * --no-dependent-indexes options. By default, the btree indexes, toast |
| * tables, and toast table btree indexes associated with primary heap |
| * tables are included, using their own CTEs below. We implement the |
| * --exclude-* options by not creating those CTEs, but that's no use if |
| * we've already selected the toast and indexes here. On the other hand, |
| * we want inclusion patterns that match indexes or toast tables to be |
| * honored. So, if inclusion patterns were given, we want to select all |
| * tables, toast tables, or indexes that match the patterns. But if no |
| * inclusion patterns were given, and we're simply matching all relations, |
| * then we only want to match the primary tables here. |
| */ |
| if (opts.allrel) |
| appendPQExpBuffer(&sql, |
| " AND c.relam = %u " |
| "AND c.relkind IN ('r', 'm', 't') " |
| "AND c.relnamespace != %u", |
| HEAP_TABLE_AM_OID, PG_TOAST_NAMESPACE); |
| else |
| appendPQExpBuffer(&sql, |
| " AND c.relam IN (%u, %u)" |
| "AND c.relkind IN ('r', 'm', 't', 'i') " |
| "AND ((c.relam = %u AND c.relkind IN ('r', 'm', 't')) OR " |
| "(c.relam = %u AND c.relkind = 'i'))", |
| HEAP_TABLE_AM_OID, BTREE_AM_OID, |
| HEAP_TABLE_AM_OID, BTREE_AM_OID); |
| |
| appendPQExpBufferStr(&sql, |
| "\nORDER BY c.oid)"); |
| |
| if (!opts.no_toast_expansion) |
| { |
| /* |
| * Include a CTE for toast tables associated with primary heap tables |
| * selected above, filtering by exclusion patterns (if any) that match |
| * toast table names. |
| */ |
| appendPQExpBufferStr(&sql, |
| ", toast (oid, nspname, relname, relpages) AS (" |
| "\nSELECT t.oid, 'pg_toast', t.relname, t.relpages" |
| "\nFROM pg_catalog.pg_class t " |
| "INNER JOIN relation r " |
| "ON r.reltoastrelid = t.oid"); |
| if (opts.excludetbl || opts.excludensp) |
| appendPQExpBufferStr(&sql, |
| "\nLEFT OUTER JOIN exclude_pat ep" |
| "\nON ('pg_toast' ~ ep.nsp_regex OR ep.nsp_regex IS NULL)" |
| "\nAND (t.relname ~ ep.rel_regex OR ep.rel_regex IS NULL)" |
| "\nAND ep.heap_only" |
| "\nWHERE ep.pattern_id IS NULL" |
| "\nAND t.relpersistence != 't'"); |
| appendPQExpBufferStr(&sql, |
| "\n)"); |
| } |
| if (!opts.no_btree_expansion) |
| { |
| /* |
| * Include a CTE for btree indexes associated with primary heap tables |
| * selected above, filtering by exclusion patterns (if any) that match |
| * btree index names. |
| */ |
| appendPQExpBuffer(&sql, |
| ", index (oid, nspname, relname, relpages) AS (" |
| "\nSELECT c.oid, r.nspname, c.relname, c.relpages " |
| "FROM relation r" |
| "\nINNER JOIN pg_catalog.pg_index i " |
| "ON r.oid = i.indrelid " |
| "INNER JOIN pg_catalog.pg_class c " |
| "ON i.indexrelid = c.oid " |
| "AND c.relpersistence != 't'"); |
| if (opts.excludeidx || opts.excludensp) |
| appendPQExpBufferStr(&sql, |
| "\nINNER JOIN pg_catalog.pg_namespace n " |
| "ON c.relnamespace = n.oid" |
| "\nLEFT OUTER JOIN exclude_pat ep " |
| "ON (n.nspname ~ ep.nsp_regex OR ep.nsp_regex IS NULL) " |
| "AND (c.relname ~ ep.rel_regex OR ep.rel_regex IS NULL) " |
| "AND ep.btree_only" |
| "\nWHERE ep.pattern_id IS NULL"); |
| else |
| appendPQExpBufferStr(&sql, |
| "\nWHERE true"); |
| appendPQExpBuffer(&sql, |
| " AND c.relam = %u " |
| "AND c.relkind = 'i'", |
| BTREE_AM_OID); |
| if (opts.no_toast_expansion) |
| appendPQExpBuffer(&sql, |
| " AND c.relnamespace != %u", |
| PG_TOAST_NAMESPACE); |
| appendPQExpBufferStr(&sql, "\n)"); |
| } |
| |
| if (!opts.no_toast_expansion && !opts.no_btree_expansion) |
| { |
| /* |
| * Include a CTE for btree indexes associated with toast tables of |
| * primary heap tables selected above, filtering by exclusion patterns |
| * (if any) that match the toast index names. |
| */ |
| appendPQExpBuffer(&sql, |
| ", toast_index (oid, nspname, relname, relpages) AS (" |
| "\nSELECT c.oid, 'pg_toast', c.relname, c.relpages " |
| "FROM toast t " |
| "INNER JOIN pg_catalog.pg_index i " |
| "ON t.oid = i.indrelid" |
| "\nINNER JOIN pg_catalog.pg_class c " |
| "ON i.indexrelid = c.oid " |
| "AND c.relpersistence != 't'"); |
| if (opts.excludeidx) |
| appendPQExpBufferStr(&sql, |
| "\nLEFT OUTER JOIN exclude_pat ep " |
| "ON ('pg_toast' ~ ep.nsp_regex OR ep.nsp_regex IS NULL) " |
| "AND (c.relname ~ ep.rel_regex OR ep.rel_regex IS NULL) " |
| "AND ep.btree_only " |
| "WHERE ep.pattern_id IS NULL"); |
| else |
| appendPQExpBufferStr(&sql, |
| "\nWHERE true"); |
| appendPQExpBuffer(&sql, |
| " AND c.relam = %u" |
| " AND c.relkind = 'i')", |
| BTREE_AM_OID); |
| } |
| |
| /* |
| * Roll-up distinct rows from CTEs. |
| * |
| * Relations that match more than one pattern may occur more than once in |
| * the list, and indexes and toast for primary relations may also have |
| * matched in their own right, so we rely on UNION to deduplicate the |
| * list. |
| */ |
| appendPQExpBuffer(&sql, |
| "\nSELECT pattern_id, is_heap, is_btree, oid, nspname, relname, relpages " |
| "FROM ("); |
| appendPQExpBufferStr(&sql, |
| /* Inclusion patterns that failed to match */ |
| "\nSELECT pattern_id, is_heap, is_btree, " |
| "NULL::OID AS oid, " |
| "NULL::TEXT AS nspname, " |
| "NULL::TEXT AS relname, " |
| "NULL::INTEGER AS relpages" |
| "\nFROM relation " |
| "WHERE pattern_id IS NOT NULL " |
| "UNION" |
| /* Primary relations */ |
| "\nSELECT NULL::INTEGER AS pattern_id, " |
| "is_heap, is_btree, oid, nspname, relname, relpages " |
| "FROM relation"); |
| if (!opts.no_toast_expansion) |
| appendPQExpBufferStr(&sql, |
| " UNION" |
| /* Toast tables for primary relations */ |
| "\nSELECT NULL::INTEGER AS pattern_id, TRUE AS is_heap, " |
| "FALSE AS is_btree, oid, nspname, relname, relpages " |
| "FROM toast"); |
| if (!opts.no_btree_expansion) |
| appendPQExpBufferStr(&sql, |
| " UNION" |
| /* Indexes for primary relations */ |
| "\nSELECT NULL::INTEGER AS pattern_id, FALSE AS is_heap, " |
| "TRUE AS is_btree, oid, nspname, relname, relpages " |
| "FROM index"); |
| if (!opts.no_toast_expansion && !opts.no_btree_expansion) |
| appendPQExpBufferStr(&sql, |
| " UNION" |
| /* Indexes for toast relations */ |
| "\nSELECT NULL::INTEGER AS pattern_id, FALSE AS is_heap, " |
| "TRUE AS is_btree, oid, nspname, relname, relpages " |
| "FROM toast_index"); |
| appendPQExpBufferStr(&sql, |
| "\n) AS combined_records " |
| "ORDER BY relpages DESC NULLS FIRST, oid"); |
| |
| res = executeQuery(conn, sql.data, opts.echo); |
| if (PQresultStatus(res) != PGRES_TUPLES_OK) |
| { |
| pg_log_error("query failed: %s", PQerrorMessage(conn)); |
| pg_log_info("query was: %s", sql.data); |
| disconnectDatabase(conn); |
| exit(1); |
| } |
| termPQExpBuffer(&sql); |
| |
| ntups = PQntuples(res); |
| for (i = 0; i < ntups; i++) |
| { |
| int pattern_id = -1; |
| bool is_heap = false; |
| bool is_btree PG_USED_FOR_ASSERTS_ONLY = false; |
| Oid oid = InvalidOid; |
| const char *nspname = NULL; |
| const char *relname = NULL; |
| int relpages = 0; |
| |
| if (!PQgetisnull(res, i, 0)) |
| pattern_id = atoi(PQgetvalue(res, i, 0)); |
| if (!PQgetisnull(res, i, 1)) |
| is_heap = (PQgetvalue(res, i, 1)[0] == 't'); |
| if (!PQgetisnull(res, i, 2)) |
| is_btree = (PQgetvalue(res, i, 2)[0] == 't'); |
| if (!PQgetisnull(res, i, 3)) |
| oid = atooid(PQgetvalue(res, i, 3)); |
| if (!PQgetisnull(res, i, 4)) |
| nspname = PQgetvalue(res, i, 4); |
| if (!PQgetisnull(res, i, 5)) |
| relname = PQgetvalue(res, i, 5); |
| if (!PQgetisnull(res, i, 6)) |
| relpages = atoi(PQgetvalue(res, i, 6)); |
| |
| if (pattern_id >= 0) |
| { |
| /* |
| * Current record pertains to an inclusion pattern. Record that |
| * it matched. |
| */ |
| |
| if (pattern_id >= opts.include.len) |
| { |
| pg_log_error("internal error: received unexpected relation pattern_id %d", |
| pattern_id); |
| exit(1); |
| } |
| |
| opts.include.data[pattern_id].matched = true; |
| } |
| else |
| { |
| /* Current record pertains to a relation */ |
| |
| RelationInfo *rel = (RelationInfo *) pg_malloc0(sizeof(RelationInfo)); |
| |
| Assert(OidIsValid(oid)); |
| Assert((is_heap && !is_btree) || (is_btree && !is_heap)); |
| |
| rel->datinfo = dat; |
| rel->reloid = oid; |
| rel->is_heap = is_heap; |
| rel->nspname = pstrdup(nspname); |
| rel->relname = pstrdup(relname); |
| rel->relpages = relpages; |
| rel->blocks_to_check = relpages; |
| if (is_heap && (opts.startblock >= 0 || opts.endblock >= 0)) |
| { |
| /* |
| * We apply --startblock and --endblock to heap tables, but |
| * not btree indexes, and for progress purposes we need to |
| * track how many blocks we expect to check. |
| */ |
| if (opts.endblock >= 0 && rel->blocks_to_check > opts.endblock) |
| rel->blocks_to_check = opts.endblock + 1; |
| if (opts.startblock >= 0) |
| { |
| if (rel->blocks_to_check > opts.startblock) |
| rel->blocks_to_check -= opts.startblock; |
| else |
| rel->blocks_to_check = 0; |
| } |
| } |
| *pagecount += rel->blocks_to_check; |
| |
| simple_ptr_list_append(relations, rel); |
| } |
| } |
| PQclear(res); |
| } |