Skip to content

feat: parse PostgreSQL VACUUM, ANALYZE, CLUSTER and CHECKPOINT with shared utility options - #2890

Open
minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/postgresql-maintenance-20261103
Open

minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/postgresql-maintenance-20261103

Conversation

@minleejae

Copy link
Copy Markdown
Contributor

Purpose

PostgreSQL maintenance scripts run VACUUM, ANALYZE, CLUSTER and CHECKPOINT. On master (7123d25a1) these fail in every configuration, for example:

  • VACUUM (PARALLEL 2, INDEX_CLEANUP OFF) t (a), VACUUM FULL t, VACUUM (VERBOSE, ANALYZE) t
  • ANALYZE VERBOSE t (a), ANALYZE (SKIP_LOCKED) t, s, a bare ANALYZE
  • CLUSTER t USING t_pkey, CLUSTER VERBOSE, CLUSTER (VERBOSE) t, a bare CLUSTER, the pre-8.3 CLUSTER t_pkey ON t
  • ANALYSE t, PostgreSQL's other spelling of ANALYZE
  • CHECKPOINT

PostgreSQL 18 references: VACUUM, ANALYZE, CLUSTER, CHECKPOINT.

Current design (As-is)

  • The statement grammar has no alternative for VACUUM, CLUSTER or CHECKPOINT; CHECKPOINT is only known inside Oracle's ALTER SYSTEM CHECKPOINT.
  • Analyze models MySQL's ANALYZE [NO_WRITE_TO_BINLOG | LOCAL] [TABLE] t, … [histogram], where feat: parse MySQL CHECKSUM TABLE and histogram update options #2883 completed the MySQL 8.4 histogram clause: UPDATE HISTOGRAM ON c, … [WITH n BUCKETS] [{AUTO | MANUAL} UPDATE], UPDATE HISTOGRAM ON c USING DATA 'json' and DROP HISTOGRAM ON c, … (getHistogramBucketCount(), getHistogramUpdateMode(), getHistogramData()). It requires a table and has no options, so ANALYZE (SKIP_LOCKED) t, ANALYZE t (a) and ANALYZE fail, and ANALYZE VERBOSE t stops at t.
  • Analyze has small defects:
    • toString() always appends a space and the table list, so a statement without tables would render as ANALYZE .
    • StatementDeParser.visit(Analyze) lacks @Override.
    • Feature.analyze is in no capability list, so the validator rejects ANALYZE for MySQL, MariaDB and PostgreSQL alike.
  • PostgreSQL's utility_option_list (( name [value], … )) is shared by VACUUM, ANALYZE, CLUSTER, EXPLAIN and REINDEX. JSqlParser parses it for EXPLAIN, with a closed option enum and its own list of value tokens, and for REINDEX (Parse PostgreSQL event triggers, REINDEX, SECURITY LABEL and schema elements #2818) with another closed enum.

Target design (To-be)

  • Shared option model: UtilityOption (net.sf.jsqlparser.statement) is one name [value] element.

    • getName() keeps the name as written. getValueType() is BOOLEAN (TRUE/FALSE/ON/OFF), NUMBER (signed integer or decimal), STRING or WORD (such as AUTO); getValue() is the SQL text.
    • Typed readers follow ReplicationOption: getBooleanValue() (as PostgreSQL's defGetBoolean: also 0/1 and 'on'), getLongValue(), getStringValue() (unquoted and unescaped) and getEnumValue(Class).
    • The name is not a closed enum field. Each statement has a small OptionKind enum (VacuumStatement.OptionKind, Analyze.OptionKind, ClusterStatement.OptionKind), and getKind(Class) maps a name to it, or null. A new PostgreSQL option therefore needs one enum constant, not a model change. As in PostgreSQL, ANALYSE names the ANALYZE option.
    • Factories UtilityOption.of(name, boolean), of(name, long), ofString(name, text) and ofWord(name, word).
  • Shared grammar that follows PostgreSQL's: UtilityOptionList(), UtilityOption() and UtilityOptionArgument() follow utility_option_list/utility_option_elem/utility_option_arg of PostgreSQL 18's gram.y.

    • VACUUM, ANALYZE and CLUSTER use them and accept what PostgreSQL's grammar accepts: any option name, and as value a Boolean word, a non-reserved word, a string (also E'…' and $$…$$) or a signed integer or decimal (NumericOnly). A name or word can be a double-quoted identifier, such as "verbose".
    • The semantic checks are left to the server, as PostgreSQL does them only when it runs the statement: VACUUM (FOO) t and VACUUM (PARALLEL -1) t parse, and PostgreSQL then reports unrecognized VACUUM option "foo" or parallel workers for vacuum must be between 0 and 1024. The model still tells them apart: getKind(Class) is null for an unknown name, getValueType() records the value as written, and the typed readers return null for a value of another type (getLongValue() of PARALLEL 1.5).
    • Names and words are identifiers and JSqlParser's non-reserved words, the closest match to PostgreSQL's NonReservedWord. PostgreSQL's reserved keywords are rejected as unquoted names and words (VACUUM (TABLE) t, VACUUM (VERBOSE analyze) t), except ANALYZE and ANALYSE as names (analyze_keyword), as in gram.y.
    • A double-quoted name keeps its case, as in PostgreSQL: getKind reads "verbose" as VERBOSE and "VERBOSE" as unknown, and the unquoted ANALYSE is the option ANALYZE.
  • EXPLAIN (…) is unchanged: it reads its elements through UtilityOption() and maps them to its released ExplainStatement.Option, whose closed OptionType names and existing value checks still decide what is accepted. On a grid of 1,092 option combinations it accepts and renders exactly what master does. Moving ExplainStatement.Option, and the Reindex.Option of REINDEX (…), to UtilityOption would let them follow the grammar too; that is left for a follow-up because it changes their released APIs.

  • Unparenthesized keywords are options: VACUUM FULL t holds the option FULL like VACUUM (FULL) t, and isParenthesizedOptions() tells the forms apart, as ExplainStatement does for EXPLAIN ANALYZE. Consumers read one list for both spellings. The keyword form is rendered in PostgreSQL's required order (FULL FREEZE VERBOSE ANALYZE); options it cannot express are rendered in parentheses.

  • MaintenanceTarget (statement.maintenance) is PostgreSQL's table_and_columns: [ONLY] table [*] [(column, …)].

    • The relation reuses TableReference and the RelationTarget() production of LOCK and TRUNCATE; PostgreSQL 18's vacuum_relation is a relation_expr, so ONLY and * are accepted. Columns are Columns.
    • VACUUM and ANALYZE share it and the MaintenanceTargetList() production.
  • One Analyze for both products: Analyze is extended in place, and the MySQL and PostgreSQL forms are two mutually exclusive shapes of the same statement.

    MySQL form PostgreSQL form
    Example ANALYZE NO_WRITE_TO_BINLOG TABLE t1, t2 UPDATE HISTOGRAM ON c WITH 32 BUCKETS AUTO UPDATE ANALYZE (VERBOSE, SKIP_LOCKED) ONLY t (a, b), u
    Own state getModifier(), isTableKeyword(), getHistogramAction(), getHistogramColumns(), getHistogramBucketCount(), getHistogramUpdateMode(), getHistogramData() getOptions(), isParenthesizedOptions(), targets with ONLY, * or columns
    Shared state getTargets() (plain tables) getTargets()
    • The tables of both forms are MaintenanceTargets in getTargets(), the single source of truth. A MySQL table is a plain target (isPlainTable()). getTable(), getTables(), setTable, setTables, addTables and the builders stay and are derived from the targets: getTables() is now a read-only snapshot, and the setters create plain targets.
    • The parser enforces the exclusion in one place (requireAnalyzeForm): the modifier, the TABLE keyword and the histogram clause, which carries the bucket count, the update mode and the JSON data, cannot be combined with options or with targets that use ONLY, * or columns, and they require a table. Every feat: parse MySQL CHECKSUM TABLE and histogram update options #2883 rule stays as it is: USING DATA excludes the bucket count and the update mode, and DROP HISTOGRAM takes none of them.
    • PostgreSQL's analyze_keyword is ANALYZE | ANALYSE. Analyze.Command (ANALYZE, ANALYSE) keeps the spelling, like StartTransaction.Command, so ANALYSE VERBOSE t (a) round-trips. ANALYSE takes only the PostgreSQL form; ANALYSE TABLE t is rejected, as in both databases.
    • toString() and StatementDeParser share appendTo, which renders {ANALYZE | ANALYSE} [modifier] [options] [TABLE] [targets] [histogram], so each form prints as written and a programmatic Analyze without targets prints ANALYZE.
    • New: getOptions(), isParenthesizedOptions(), getTargets(), getCommand(), Analyze.OptionKind and Analyze.Command; Modifier, HistogramAction, HistogramUpdateMode and all histogram accessors keep their signatures and behaviour.
  • Other statements:

    • VacuumStatement: options, isParenthesizedOptions(), targets. VACUUM [FULL] [FREEZE] [VERBOSE] [ANALYZE | ANALYSE] accepts the words in that order; as in PostgreSQL they are never table names here.
    • ClusterStatement: options, table and index name. Without a table the index name is discarded, and setIndexName requires a table.
    • PostgreSQL 18 still accepts the pre-8.3 CLUSTER [VERBOSE] index ON table (CLUSTER opt_verbose name ON qualified_name). It parses to the same table and index, and ClusterStatement.IndexSyntax (USING, ON) keeps the spelling. The ON form takes no option list, so a programmatic statement that has parenthesized options is rendered in the USING form.
    • CheckpointStatement. PostgreSQL 18 accepts no options; CHECKPOINT (…) comes in a later release and can be added to this class the same way.
  • AST nodes (fix: give every parsed object a node spanning its source #2870): the three statements extend ASTNodeAccessImpl and get the node SingleStatement links. UtilityOption and MaintenanceTarget extend ASTNodeAccessImpl too and are linked to their source range (PARALLEL 2, VERBOSE, ONLY s.t (a, b), u *), and the columns of a target get the node of their name. ASTNodeCoverageTest walks VACUUM, ANALYZE (both forms and the ANALYSE spelling), CLUSTER (with ON) and CHECKPOINT statements.

  • Dialects:

    • The PostgreSQL forms are read with Dialect.POSTGRESQL and without a dialect. Other dialects do not change: VACUUM, CLUSTER and CHECKPOINT are not statements there, and ANALYZE keeps the MySQL form.
    • Without a dialect, VACUUM, CLUSTER, CHECKPOINT and ANALYSE start a statement only when the next tokens take the PostgreSQL form. Other products' variants such as VACUUM INTO 'backup.db', VACUUM t RETAIN 168 HOURS or CHECKPOINT 10 stay UnsupportedStatements. Other dialects never read ANALYSE as a statement.
    • Behavior change: with Dialect.POSTGRESQL, ANALYZE verbose now means the VERBOSE option, as in PostgreSQL. Without a dialect it still analyzes a table named verbose unless a table name follows (ANALYZE VERBOSE t); other dialects keep the table reading.
  • Grammar:

    • VACUUM and CLUSTER become non-reserved tokens, next to feat: parse MySQL CHECKSUM TABLE and histogram update options #2883's CHECKSUM. A statement that starts with a plain identifier adds choice conflicts at trailing optional identifiers, as for NOTIFY and REINDEX. Both remain identifiers (KeywordsTest, keyword sweeps). The contextual CLUSTER ON/SET WITHOUT CLUSTER of ALTER TABLE and BigQuery's CLUSTER BY accept the new token. FREEZE stays a contextual keyword; ANALYZE, CHECKPOINT and VERBOSE were already non-reserved tokens.
    • FULL after VACUUM is read with an explicit lookahead, because FULL can now also be a name (reserved keyword usable as an identifier).
    • ANALYSE was already a non-reserved token (for MySQL's PROCEDURE ANALYSE()). SingleStatement takes it as a statement start only through isPostgreSqlMaintenanceAhead(), so analyse stays an identifier (SELECT analyse FROM analyse) and PROCEDURE ANALYSE() is unchanged.
    • isStatementStartAhead() recognizes VACUUM, CLUSTER, CHECKPOINT and ANALYSE, so with unsupported statements enabled they are no longer captured as opaque, and they end a preceding statement.
  • Wiring:

    • StatementVisitor defaults, StatementVisitorAdapter, StatementDeParser (now also @Override and appendTo for Analyze).
    • TablesNamesFinder reports the target tables and the CLUSTER table, not the columns or the index.
    • Validation: new features vacuum, cluster and checkpoint. They and analyze are in PostgresqlVersion; analyze is also added to MySqlVersion and MariaDbVersion.
    • Feature analysis: VACUUM and CLUSTER report MODIFIES_SCHEMA and READS_DATA, like ANALYZE; CHECKPOINT reports MODIFIES_SESSION, like ALTER SYSTEM CHECKPOINT.
    • The usage guide, README and index list the statements.

Validation

  • 123 new tests:
    • VacuumStatementTest 36, PostgreSqlAnalyzeTest 42, ClusterStatementTest 25, CheckpointStatementTest 8, UtilityOptionTest 5, MaintenanceValidatorTest 4 and MaintenanceNodeTest 3.
    • Every form: parse, toString(), StatementDeParser and reparse with Dialect.POSTGRESQL and without a dialect, plus the AST fields (option kinds, typed values, ONLY/*, columns, index, IndexSyntax, Command).
    • Every PostgreSQL 18 VACUUM option, and options the grammar accepts but the server rejects when it runs (VACUUM (FOO) t, VACUUM (PARALLEL -1) t, VERBOSE maybe, PARALLEL 1.5, (t)), with a null getKind or the value type as parsed; double-quoted names and values, $$…$$ strings, and the syntax errors (VACUUM (), VACUUM (VERBOSE,) t, VACUUM (TABLE) t, VACUUM (VERBOSE analyze) t, VACUUM ANALYZE FULL t, VACUUM t USING x, CLUSTER t USING, CLUSTER (VERBOSE) i ON t, CLUSTER s.i ON t, CHECKPOINT (FLUSH_UNLOGGED)).
    • Mixed MySQL/PostgreSQL ANALYZE forms, including the feat: parse MySQL CHECKSUM TABLE and histogram update options #2883 histogram options (ANALYZE (VERBOSE) t UPDATE HISTOGRAM ON a WITH 8 BUCKETS AUTO UPDATE, ANALYZE t (a) UPDATE HISTOGRAM ON a, ANALYZE UPDATE HISTOGRAM ON a) and ANALYSE TABLE t.
    • The MySQL forms, with AUTO UPDATE, MANUAL UPDATE and USING DATA, unchanged in the MySQL, MariaDB and PostgreSQL dialects and held as plain targets; the existing AnalyzeTest of feat: parse MySQL CHECKSUM TABLE and histogram update options #2883 passes unchanged.
    • Other dialects, other products' variants as unsupported statements, statement boundaries (including after SHOW and after an unsupported statement), builders and rendering of programmatic options and of the ON form, table discovery, validation capabilities and statement features.
    • The words as identifiers (vacuum, cluster, checkpoint, freeze, analyse), MySQL's PROCEDURE ANALYSE(), ALTER TABLE … CLUSTER ON, SET WITHOUT CLUSTER, BigQuery CLUSTER BY, and EXPLAIN (…) through the shared production, including the option names and values it still rejects; KeywordsTest covers VACUUM and CLUSTER.
    • Source ranges of the statements, options, targets and target columns (MaintenanceNodeTest); ASTNodeCoverageTest walks nine VACUUM, ANALYZE, ANALYSE, CLUSTER and CHECKPOINT statements, the MySQL histogram form among them.
    • ReindexTest and SecurityLabelTest used VACUUM t as their example of an unsupported statement before REINDEX and SECURITY LABEL; they now use LISTEN ch.
  • Every form was run against PostgreSQL 18: each statement the parser accepts is accepted by the server's grammar, and each one it rejects is a server syntax error.
  • Java 17 offline Gradle spotlessApply check jar: 12,565 tests, 12,540 passed, 25 skipped, zero failures/errors. The grammar choice conflict check passes.
  • Compared with master (7123d25a1) on a corpus of 188 MySQL 8.4 and 166 PostgreSQL 18 statements other than DDL/DCL, each accepted by the server, with the MySQL or PostgreSQL dialect and without a dialect:
    • Fixed with Dialect.POSTGRESQL and without a dialect: ANALYZE VERBOSE t (a), ANALYZE (SKIP_LOCKED) t, s, VACUUM (VERBOSE, ANALYZE) t, VACUUM FULL t, VACUUM (PARALLEL 2, INDEX_CLEANUP OFF) t (a), CLUSTER t USING t_pkey, CLUSTER VERBOSE and CHECKPOINT. Each renders identically to the source.
    • 0 regressed and 0 changed in status, statement class or rendering. AST change: the MySQL statements ANALYZE TABLE t UPDATE HISTOGRAM ON a WITH 10 BUCKETS and ANALYZE NO_WRITE_TO_BINLOG TABLE t, s hold their tables as targets[i].reference.table instead of tables[i] and gain command = ANALYZE; getTables() returns the same tables.
  • Compared with master (7123d25a1) on a corpus of 635 MySQL 8.4 and PostgreSQL 18 DDL/DCL statements, each accepted by the server, plus variants, scripts and the keyword sweeps (which use vacuum, cluster and checkpoint as table and column names): 0 regressed, 0 changed. ANALYZE TABLE t1, t2 has the same AST change.
  • EXPLAIN (…) compared with master (7123d25a1) on 1,092 option combinations (21 names, 26 values, with Dialect.POSTGRESQL and without a dialect): identical acceptance and rendering.

AI assistance: prepared with Claude Code.

…hared utility options

PostgreSQL maintenance scripts failed to parse: VACUUM, CLUSTER and
CHECKPOINT had no statement, and ANALYZE accepted only the MySQL form,
so option lists, ANALYZE VERBOSE, column lists and a bare ANALYZE
failed.

Add UtilityOption, one name [value] element of PostgreSQL's
utility_option_list with a typed value, and the shared
UtilityOptionList()/UtilityOptionArgument() productions. VACUUM,
ANALYZE and CLUSTER use them and, like PostgreSQL's grammar, accept any
option name and value but PostgreSQL's reserved words; the server
checks them when it runs the statement, and getKind() maps known names
to each statement's OptionKind. EXPLAIN reads its options through the same production and
accepts no more than before. The unparenthesized keywords (VACUUM FULL,
ANALYZE VERBOSE, CLUSTER VERBOSE) are kept as options with
isParenthesizedOptions() false.

Add VacuumStatement, ClusterStatement (also the pre-8.3 CLUSTER index
ON table) and CheckpointStatement, and extend Analyze with options,
the ANALYSE spelling and MaintenanceTarget tables
([ONLY] t [*] [(columns)]). Analyze keeps its MySQL form with the whole
histogram clause (bucket count, AUTO/MANUAL UPDATE, USING DATA); the
MySQL clauses and the PostgreSQL options and targets exclude each
other, and getTables() is derived from the targets. The statements,
options and targets carry AST nodes spanning their source.

VACUUM and CLUSTER become non-reserved keywords. The PostgreSQL forms
are read with Dialect.POSTGRESQL and without a dialect; other dialects
are unchanged. Wire the statements into the deparser, visitors, table
finder, validation (vacuum, cluster, checkpoint; analyze is now in the
MySQL, MariaDB and PostgreSQL capabilities) and feature analysis.

Signed-off-by: minleejae <mmj9808@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant