From b6643d4640e3d397b4aaf7bf7aea2a83e5b2453f Mon Sep 17 00:00:00 2001 From: david Date: Thu, 9 Jul 2026 15:59:58 +0200 Subject: [PATCH 01/18] added vertical-align option --- doc/pg_format.conf.sample | 2 ++ lib/pgFormatter/Beautify.pm | 4 +++- lib/pgFormatter/CLI.pm | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/pg_format.conf.sample b/doc/pg_format.conf.sample index 549d0c9..6782e95 100644 --- a/doc/pg_format.conf.sample +++ b/doc/pg_format.conf.sample @@ -86,3 +86,5 @@ wrap-comment=0 # Remove the space character between a function call and the open parenthesis that follow. no-space-function=0 +# Vertically align CREATE TABLE column definitions. +vertical-align=0 diff --git a/lib/pgFormatter/Beautify.pm b/lib/pgFormatter/Beautify.pm index 83cc358..f8f35a9 100755 --- a/lib/pgFormatter/Beautify.pm +++ b/lib/pgFormatter/Beautify.pm @@ -166,6 +166,8 @@ Takes options as hash. Following options are recognized: =item * redundant_parenthesis - do not eliminate redundant parenthesis in DML queries +=item * vertical_align - vertically align CREATE TABLE column definitions + =back For defaults, please check function L. @@ -180,7 +182,7 @@ sub new { $self->set_defaults(); for my $key ( - qw( query spaces space break wrap keywords functions rules uc_keywords uc_functions uc_types no_comments no_grouping placeholder multiline separator comma comma_break format colorize format_type wrap_limit wrap_after wrap_comment numbering redshift no_extra_line keep_newline no_space_function redundant_parenthesis) + qw( query spaces space break wrap keywords functions rules uc_keywords uc_functions uc_types no_comments no_grouping placeholder multiline separator comma comma_break format colorize format_type wrap_limit wrap_after wrap_comment numbering redshift no_extra_line keep_newline no_space_function redundant_parenthesis vertical_align) ) { $self->{$key} = $options{$key} if defined $options{$key}; diff --git a/lib/pgFormatter/CLI.pm b/lib/pgFormatter/CLI.pm index f3c8ae1..7277ef8 100755 --- a/lib/pgFormatter/CLI.pm +++ b/lib/pgFormatter/CLI.pm @@ -130,6 +130,7 @@ sub beautify { $args{'extra_keyword'} = $self->{'cfg'}->{'extra-keyword'}; $args{'no_space_function'} = $self->{'cfg'}->{'no-space-function'}; $args{'redundant_parenthesis'} = $self->{'cfg'}->{'redundant-parenthesis'}; + $args{'vertical_align'} = $self->{'cfg'}->{'vertical-align'}; # Backward compatibility $args{'extra_keyword'} = 'redshift' @@ -390,7 +391,7 @@ sub get_command_line_args { 'wrap-limit|w=i', 'wrap-after|W=i', 'inplace|i!', 'extra-function=s', 'extra-keyword=s', 'no-space-function!', - 'redundant-parenthesis!', + 'redundant-parenthesis!', 'vertical-align!', ); $self->show_help_and_die(1) unless GetOptions( \%cfg, @options ); @@ -461,7 +462,8 @@ sub get_command_line_args { $cfg{'no-extra-line'} //= 0; $cfg{'inplace'} //= 0; $cfg{'extra-keyword'} //= ''; - $cfg{'extra-keyword'} = 'redshift' if ( $cfg{'redshift'} ); + $cfg{'vertical-align'} //= 0; + $cfg{'extra-keyword'} = 'redshift' if ( $cfg{'redshift'} ); if ( $cfg{'tabs'} ) { $cfg{'spaces'} = 1; From 8120e60df505cd8f2e7c16fbf9b5ffb385b7f69c Mon Sep 17 00:00:00 2001 From: david Date: Thu, 9 Jul 2026 16:03:22 +0200 Subject: [PATCH 02/18] fixed spacing --- lib/pgFormatter/Beautify.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pgFormatter/Beautify.pm b/lib/pgFormatter/Beautify.pm index f8f35a9..e8737dd 100755 --- a/lib/pgFormatter/Beautify.pm +++ b/lib/pgFormatter/Beautify.pm @@ -4920,6 +4920,7 @@ sub set_defaults { $self->{'keep_newline'} = 0; $self->{'no_space_function'} = 0; $self->{'redundant_parenthesis'} = 0; + $self->{'vertical_align'} = 0; return; } From c717eda9c105bbae377d00c55a0876eb8b3b1362 Mon Sep 17 00:00:00 2001 From: David Aukes <90037434+david-auk@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:03:53 +0200 Subject: [PATCH 03/18] Fix alignment of vertical_align assignment --- lib/pgFormatter/CLI.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pgFormatter/CLI.pm b/lib/pgFormatter/CLI.pm index 7277ef8..f8a7de9 100755 --- a/lib/pgFormatter/CLI.pm +++ b/lib/pgFormatter/CLI.pm @@ -130,7 +130,7 @@ sub beautify { $args{'extra_keyword'} = $self->{'cfg'}->{'extra-keyword'}; $args{'no_space_function'} = $self->{'cfg'}->{'no-space-function'}; $args{'redundant_parenthesis'} = $self->{'cfg'}->{'redundant-parenthesis'}; - $args{'vertical_align'} = $self->{'cfg'}->{'vertical-align'}; + $args{'vertical_align'} = $self->{'cfg'}->{'vertical-align'}; # Backward compatibility $args{'extra_keyword'} = 'redshift' From 2f157f6c63ed3e8dbf71d6b9e5f9e49eac3cbed8 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 9 Jul 2026 16:04:44 +0200 Subject: [PATCH 04/18] Replaced space with tab --- lib/pgFormatter/Beautify.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pgFormatter/Beautify.pm b/lib/pgFormatter/Beautify.pm index e8737dd..ecd57ac 100755 --- a/lib/pgFormatter/Beautify.pm +++ b/lib/pgFormatter/Beautify.pm @@ -4920,7 +4920,7 @@ sub set_defaults { $self->{'keep_newline'} = 0; $self->{'no_space_function'} = 0; $self->{'redundant_parenthesis'} = 0; - $self->{'vertical_align'} = 0; + $self->{'vertical_align'} = 0; return; } From 7503daef77bb6074cd0451531f55f8557bd8decd Mon Sep 17 00:00:00 2001 From: david Date: Thu, 9 Jul 2026 16:20:04 +0200 Subject: [PATCH 05/18] Added help option --- lib/pgFormatter/CLI.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pgFormatter/CLI.pm b/lib/pgFormatter/CLI.pm index f8a7de9..434c668 100755 --- a/lib/pgFormatter/CLI.pm +++ b/lib/pgFormatter/CLI.pm @@ -324,7 +324,7 @@ Options: --no-space-function : remove space between function call and the open parenthesis. --redundant-parenthesis: do not remove redundant parenthesis in DML. - + --vertical-align : vertically align CREATE TABLE column definitions. Examples: cat samples/ex1.sql | $0 - From 28fab0df7a5ca71d2f63b16365797f88c49c912d Mon Sep 17 00:00:00 2001 From: david Date: Thu, 9 Jul 2026 16:32:43 +0200 Subject: [PATCH 06/18] Added splitting logic --- lib/pgFormatter/Beautify.pm | 96 +++++++++++++++++++++++++++++++ t/03_vertical_align.t | 110 ++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 t/03_vertical_align.t diff --git a/lib/pgFormatter/Beautify.pm b/lib/pgFormatter/Beautify.pm index ecd57ac..1ce81e8 100755 --- a/lib/pgFormatter/Beautify.pm +++ b/lib/pgFormatter/Beautify.pm @@ -780,6 +780,102 @@ sub tokenize_sql { $self->{'_tokens'} = \@query; } +=head2 _parse_create_table_column + +Parse one single-line CREATE TABLE column definition into the parts needed by +vertical alignment. Returns undef for table constraints, comments, multiline +input, and definitions that do not contain both a column name and a data type. + +This method only analyzes input. It does not change the formatted SQL. + +=cut + +sub _parse_create_table_column { + my ( $self, $line ) = @_; + + return if ( !defined $line || $line !~ /\S/ || $line =~ /[\r\n]/ ); + + my ($indent) = $line =~ /^(\s*)/; + my $definition = $line; + $definition =~ s/^\s+|\s+$//g; + + my $comma = ''; + $comma = ',' if ( $definition =~ s/,\s*$// ); + + my @tokens = $self->tokenize_sql($definition); + return if ( @tokens < 2 ); + return if ( grep { $self->_is_comment($_) } @tokens ); + + my $name = shift @tokens; + + # These tokens start table-level definitions rather than column definitions. + my %table_constraint = map { $_ => 1 } qw( + CHECK CONSTRAINT EXCLUDE FOREIGN LIKE PRIMARY UNIQUE + ); + return if ( $name !~ /^"/ && $table_constraint{ uc($name) } ); + return if ( $name =~ /^(?:[\[\](),;])$/ ); + + # A column's data type ends at the first top-level column constraint. Tokens + # inside type modifiers, such as numeric(10, 2), must remain part of the type. + my %column_constraint = map { $_ => 1 } qw( + CHECK COLLATE COMPRESSION CONSTRAINT DEFAULT GENERATED IDENTITY NOT NULL + PRIMARY REFERENCES STORAGE UNIQUE + ); + + my $parenthesis_depth = 0; + my $bracket_depth = 0; + my $constraint_index = scalar @tokens; + + for my $index ( 0 .. $#tokens ) { + my $token = $tokens[$index]; + + if ( + $parenthesis_depth == 0 + && $bracket_depth == 0 + && $column_constraint{ uc($token) } + ) + { + $constraint_index = $index; + last; + } + + $parenthesis_depth++ if ( $token eq '(' ); + $parenthesis_depth-- + if ( $token eq ')' && $parenthesis_depth > 0 ); + $bracket_depth++ if ( $token eq '[' ); + $bracket_depth-- if ( $token eq ']' && $bracket_depth > 0 ); + } + + my @declaration_tokens = + $constraint_index > 0 ? @tokens[ 0 .. $constraint_index - 1 ] : (); + my @remainder_tokens = + $constraint_index <= $#tokens ? @tokens[ $constraint_index .. $#tokens ] : (); + + return if ( !@declaration_tokens ); + + # Keep an immediately following PRIMARY KEY or UNIQUE with the declaration. + # This supports the intended visual grouping without reordering SQL tokens. + if ( + @remainder_tokens >= 2 + && uc( $remainder_tokens[0] ) eq 'PRIMARY' + && uc( $remainder_tokens[1] ) eq 'KEY' + ) + { + push @declaration_tokens, splice( @remainder_tokens, 0, 2 ); + } + elsif ( @remainder_tokens && uc( $remainder_tokens[0] ) eq 'UNIQUE' ) { + push @declaration_tokens, shift @remainder_tokens; + } + + return { + indent => $indent, + name => $name, + declaration_tokens => \@declaration_tokens, + remainder_tokens => \@remainder_tokens, + comma => $comma, + }; +} + sub _pop_level { my ( $self, $token, $last_token ) = @_; diff --git a/t/03_vertical_align.t b/t/03_vertical_align.t new file mode 100644 index 0000000..fa012f0 --- /dev/null +++ b/t/03_vertical_align.t @@ -0,0 +1,110 @@ +use strict; +use warnings; + +use Test::More; +use lib 'lib'; +use pgFormatter::Beautify; + +my $beautifier = pgFormatter::Beautify->new(); + +is_deeply( + $beautifier->_parse_create_table_column( + ' id uuid primary key default gen_random_uuid(),' + ), + { + indent => ' ', + name => 'id', + declaration_tokens => [qw(uuid primary key)], + remainder_tokens => [ 'default', 'gen_random_uuid', '(', ')' ], + comma => ',', + }, + 'parses PRIMARY KEY and DEFAULT separately' +); + +is_deeply( + $beautifier->_parse_create_table_column( + q{ payload jsonb not null default '{}'::jsonb,} + ), + { + indent => ' ', + name => 'payload', + declaration_tokens => ['jsonb'], + remainder_tokens => [ 'not', 'null', 'default', q{'{}'}, '::jsonb' ], + comma => ',', + }, + 'parses multiple column constraints' +); + +is_deeply( + $beautifier->_parse_create_table_column( + ' amount numeric(10, 2),' + ), + { + indent => ' ', + name => 'amount', + declaration_tokens => [ 'numeric', '(', '10', ',', '2', ')' ], + remainder_tokens => [], + comma => ',', + }, + 'keeps type modifiers inside the declaration' +); + +is_deeply( + $beautifier->_parse_create_table_column( + ' "quoted column" public.custom_type[] unique' + ), + { + indent => ' ', + name => '"quoted column"', + declaration_tokens => + [ 'public.custom_type', '[', ']', 'unique' ], + remainder_tokens => [], + comma => '', + }, + 'parses quoted names, qualified custom types, arrays, and UNIQUE' +); + +is_deeply( + $beautifier->_parse_create_table_column( + ' owner_id uuid references app_user (id) on delete set null,' + ), + { + indent => ' ', + name => 'owner_id', + declaration_tokens => ['uuid'], + remainder_tokens => + [ 'references', 'app_user', '(', 'id', ')', 'on', 'delete', 'set', 'null' ], + comma => ',', + }, + 'parses a REFERENCES constraint' +); + +for my $table_constraint ( + 'constraint demo_pk primary key (id)', + 'primary key (id)', + 'foreign key (owner_id) references app_user (id)', + 'unique (correlation_key)', + 'check (id is not null)', + ) +{ + ok( + !defined $beautifier->_parse_create_table_column($table_constraint), + "skips table constraint: $table_constraint" + ); +} + +ok( + !defined $beautifier->_parse_create_table_column( + " score integer check (\n score >= 0\n )" + ), + 'skips multiline definitions in the first implementation' +); + +ok( + !defined $beautifier->_parse_create_table_column( + ' username text -- explanatory comment' + ), + 'skips definitions containing comments in the first implementation' +); + +done_testing(); From 4223c330ae6c04f1435e2fdadb164b0bdd583da2 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 9 Jul 2026 16:43:51 +0200 Subject: [PATCH 07/18] Added rendering tokens --- lib/pgFormatter/Beautify.pm | 62 +++++++++++++++++++++++++++++++++ t/03_vertical_align.t | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/lib/pgFormatter/Beautify.pm b/lib/pgFormatter/Beautify.pm index 1ce81e8..d219a33 100755 --- a/lib/pgFormatter/Beautify.pm +++ b/lib/pgFormatter/Beautify.pm @@ -876,6 +876,68 @@ sub _parse_create_table_column { }; } +=head2 _render_sql_tokens + +Render a list of SQL tokens as one compact, single-line SQL fragment. + +This helper is intentionally limited to the fragments produced by +_parse_create_table_column(). It preserves token order and only decides where +spaces belong. It does not perform alignment or change keyword casing. + +=cut + +sub _render_sql_tokens { + my ( $self, $tokens ) = @_; + + return '' if ( !$tokens || !@{$tokens} ); + + my %space_before_parenthesis = map { $_ => 1 } qw( + AS CHECK DEFAULT IN REFERENCES + ); + + my $rendered = ''; + + for my $index ( 0 .. $#{$tokens} ) { + my $token = $tokens->[$index]; + my $previous_token = $index > 0 ? $tokens->[ $index - 1 ] : undef; + my $before_previous = $index > 1 ? $tokens->[ $index - 2 ] : undef; + + if ( !defined $previous_token ) { + $rendered = $token; + next; + } + + # Closing punctuation, casts, and array suffixes attach directly to the + # preceding token. Tokens immediately inside parentheses and brackets do + # not receive a leading space either. + if ( + $token =~ /^(?:\)|\]|,|;)$/ + || $token =~ /^::/ + || $previous_token eq '(' + || $previous_token eq '[' + || $token eq '[' + ) + { + $rendered .= $token; + next; + } + + if ( $token eq '(' ) { + my $needs_space = + $space_before_parenthesis{ uc($previous_token) } + || ( defined $before_previous + && uc($before_previous) eq 'REFERENCES' ); + + $rendered .= ( $needs_space ? ' ' : '' ) . $token; + next; + } + + $rendered .= ' ' . $token; + } + + return $rendered; +} + sub _pop_level { my ( $self, $token, $last_token ) = @_; diff --git a/t/03_vertical_align.t b/t/03_vertical_align.t index fa012f0..02ada60 100644 --- a/t/03_vertical_align.t +++ b/t/03_vertical_align.t @@ -107,4 +107,72 @@ ok( 'skips definitions containing comments in the first implementation' ); +is( + $beautifier->_render_sql_tokens( [qw(uuid primary key)] ), + 'uuid primary key', + 'renders a simple declaration' +); + +is( + $beautifier->_render_sql_tokens( + [ 'numeric', '(', '10', ',', '2', ')' ] + ), + 'numeric(10, 2)', + 'renders a parameterized type' +); + +is( + $beautifier->_render_sql_tokens( + [ 'public.custom_type', '[', ']' ] + ), + 'public.custom_type[]', + 'renders an array type' +); + +is( + $beautifier->_render_sql_tokens( + [ 'default', 'gen_random_uuid', '(', ')' ] + ), + 'default gen_random_uuid()', + 'renders an extension function without a space before its parenthesis' +); + +is( + $beautifier->_render_sql_tokens( + [ 'references', 'app_user', '(', 'id', ')', 'on', 'delete', 'set', 'null' ] + ), + 'references app_user (id) on delete set null', + 'keeps a space before a referenced column list' +); + +is( + $beautifier->_render_sql_tokens( + [ 'not', 'null', 'default', q{'{}'}, '::jsonb' ] + ), + q{not null default '{}'::jsonb}, + 'renders a cast without surrounding spaces' +); + +is( + $beautifier->_render_sql_tokens( + [ 'check', '(', 'score', '>=', '0', ')' ] + ), + 'check (score >= 0)', + 'keeps a space after CHECK before a parenthesis' +); + +is( + $beautifier->_render_sql_tokens( + [ 'generated', 'always', 'as', '(', 'lower', '(', 'name', ')', ')', 'stored' ] + ), + 'generated always as (lower(name)) stored', + 'renders nested generated-column expressions' +); + +is( + $beautifier->_render_sql_tokens([]), + '', + 'renders an empty token list as an empty string' +); + done_testing(); From ae226036b28d206315ffccb6948296b8dc5c0bfd Mon Sep 17 00:00:00 2001 From: david Date: Thu, 9 Jul 2026 16:52:57 +0200 Subject: [PATCH 08/18] Added aligning of grouped comulms --- lib/pgFormatter/Beautify.pm | 101 ++++++++++++++++++++++++++++++++++++ t/03_vertical_align.t | 70 +++++++++++++++++++++++++ 2 files changed, 171 insertions(+) diff --git a/lib/pgFormatter/Beautify.pm b/lib/pgFormatter/Beautify.pm index d219a33..321e88b 100755 --- a/lib/pgFormatter/Beautify.pm +++ b/lib/pgFormatter/Beautify.pm @@ -938,6 +938,107 @@ sub _render_sql_tokens { return $rendered; } +=head2 _pad_right + +Pad a string with spaces until it reaches the requested width. + +The helper returns the original string unchanged when it is already at least as +wide as the requested width. Alignment uses spaces deliberately: tabs would +make the result depend on the tab width configured by the editor displaying the +SQL. + +=cut + +sub _pad_right { + my ( $self, $value, $width ) = @_; + + $value = '' if ( !defined $value ); + $width = length($value) if ( !defined $width ); + + my $padding = $width - length($value); + return $value if ( $padding <= 0 ); + + return $value . ( ' ' x $padding ); +} + +=head2 _align_create_table_column_group + +Align a group of single-line CREATE TABLE column definitions. + +The method aligns the column names and the beginning of each remainder, while +preserving indentation, token order, trailing commas, and unsupported lines. +It only returns rendered lines; it does not modify the formatter output. + +=cut + +sub _align_create_table_column_group { + my ( $self, $lines ) = @_; + + return [] if ( !$lines || !@{$lines} ); + + my @rows; + my $name_width = 0; + my $declaration_width = 0; + my $alignable_count = 0; + + for my $line ( @{$lines} ) { + my $column = $self->_parse_create_table_column($line); + + if ( !$column ) { + push @rows, { original => $line }; + next; + } + + $column->{'declaration'} = + $self->_render_sql_tokens( $column->{'declaration_tokens'} ); + $column->{'remainder'} = + $self->_render_sql_tokens( $column->{'remainder_tokens'} ); + + my $current_name_width = length( $column->{'name'} ); + $name_width = $current_name_width + if ( $current_name_width > $name_width ); + + my $current_declaration_width = length( $column->{'declaration'} ); + $declaration_width = $current_declaration_width + if ( $current_declaration_width > $declaration_width ); + + push @rows, $column; + $alignable_count++; + } + + # A single definition has nothing to align with. Return a new array reference + # so callers can safely use the result without mutating their input array. + return [ @{$lines} ] if ( $alignable_count < 2 ); + + my @aligned_lines; + + for my $row (@rows) { + if ( exists $row->{'original'} ) { + push @aligned_lines, $row->{'original'}; + next; + } + + my $line = + $row->{'indent'} + . $self->_pad_right( $row->{'name'}, $name_width ) + . ' ' + . $row->{'declaration'}; + + if ( length( $row->{'remainder'} ) ) { + $line .= + ( ' ' x + ( $declaration_width - length( $row->{'declaration'} ) ) ) + . ' ' + . $row->{'remainder'}; + } + + $line .= $row->{'comma'}; + push @aligned_lines, $line; + } + + return \@aligned_lines; +} + sub _pop_level { my ( $self, $token, $last_token ) = @_; diff --git a/t/03_vertical_align.t b/t/03_vertical_align.t index 02ada60..aff5a46 100644 --- a/t/03_vertical_align.t +++ b/t/03_vertical_align.t @@ -175,4 +175,74 @@ is( 'renders an empty token list as an empty string' ); +is( + $beautifier->_pad_right( 'id', 5 ), + 'id ', + 'pads a value to the requested width' +); + +is( + $beautifier->_pad_right( 'identifier', 5 ), + 'identifier', + 'does not truncate a value wider than the requested width' +); + +is_deeply( + $beautifier->_align_create_table_column_group( + [ + ' id uuid primary key default gen_random_uuid(),', + ' parent_job_id uuid references queue_job (id) on delete set null,', + ' type queue_job_type not null,', + q{ status queue_job_status not null default 'queued',}, + ' created_at timestamptz not null default now()', + ] + ), + [ + ' id uuid primary key default gen_random_uuid(),', + ' parent_job_id uuid references queue_job (id) on delete set null,', + ' type queue_job_type not null,', + q{ status queue_job_status not null default 'queued',}, + ' created_at timestamptz not null default now()', + ], + 'aligns column names and the beginning of column constraints' +); + +is_deeply( + $beautifier->_align_create_table_column_group( + [ + ' id uuid primary key,', + ' constraint demo_pk primary key (id)', + ' descriptive_name text', + ] + ), + [ + ' id uuid primary key,', + ' constraint demo_pk primary key (id)', + ' descriptive_name text', + ], + 'preserves unsupported table constraints while aligning column definitions' +); + +my @single_column = (' id uuid primary key'); +my $single_result = + $beautifier->_align_create_table_column_group(\@single_column); + +is_deeply( + $single_result, + \@single_column, + 'leaves a single column unchanged' +); + +isnt( + $single_result, + \@single_column, + 'returns a new array reference for a single column' +); + +is_deeply( + $beautifier->_align_create_table_column_group([]), + [], + 'handles an empty group' +); + done_testing(); From 4ee99eb94329815873cb70a0e758df16e49f3ec7 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 9 Jul 2026 17:13:24 +0200 Subject: [PATCH 09/18] align DEFAULT clauses --- lib/pgFormatter/Beautify.pm | 94 +++++++++++++++++++++++++++++++++++++ t/03_vertical_align.t | 32 ++++++++++++- 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/lib/pgFormatter/Beautify.pm b/lib/pgFormatter/Beautify.pm index 321e88b..1b21e73 100755 --- a/lib/pgFormatter/Beautify.pm +++ b/lib/pgFormatter/Beautify.pm @@ -961,6 +961,98 @@ sub _pad_right { return $value . ( ' ' x $padding ); } +=head2 _align_create_table_column_keyword + +Align a top-level keyword within the remainder of parsed CREATE TABLE columns. + +For every matching row, the tokens before the keyword are treated as a prefix. +Shorter prefixes are padded so the keyword starts in the same output column. +Keywords nested inside parentheses or array brackets are ignored. + +The method updates the rendered remainder stored in each matching row. It does +not reorder tokens or affect rows that do not contain the requested keyword. + +=cut + +sub _align_create_table_column_keyword { + my ( $self, $rows, $keyword ) = @_; + + return if ( !$rows || !@{$rows} || !defined $keyword || $keyword eq '' ); + + my $keyword_upper = uc($keyword); + my $prefix_width = 0; + my @matches; + + for my $row ( @{$rows} ) { + next if ( exists $row->{'original'} ); + + my $tokens = $row->{'remainder_tokens'}; + next if ( !$tokens || !@{$tokens} ); + + my $parenthesis_depth = 0; + my $bracket_depth = 0; + my $keyword_index; + + for my $index ( 0 .. $#{$tokens} ) { + my $token = $tokens->[$index]; + + if ( + $parenthesis_depth == 0 + && $bracket_depth == 0 + && uc($token) eq $keyword_upper + ) + { + $keyword_index = $index; + last; + } + + $parenthesis_depth++ if ( $token eq '(' ); + $parenthesis_depth-- + if ( $token eq ')' && $parenthesis_depth > 0 ); + $bracket_depth++ if ( $token eq '[' ); + $bracket_depth-- if ( $token eq ']' && $bracket_depth > 0 ); + } + + next if ( !defined $keyword_index ); + + my @prefix_tokens = + $keyword_index > 0 ? @{$tokens}[ 0 .. $keyword_index - 1 ] : (); + my @clause_tokens = @{$tokens}[ $keyword_index .. $#{$tokens} ]; + + my $prefix = $self->_render_sql_tokens( \@prefix_tokens ); + my $clause = $self->_render_sql_tokens( \@clause_tokens ); + + my $current_width = length($prefix); + $prefix_width = $current_width if ( $current_width > $prefix_width ); + + push @matches, + { + row => $row, + prefix => $prefix, + clause => $clause, + }; + } + + return if ( @matches < 2 ); + + for my $match (@matches) { + my $prefix = $match->{'prefix'}; + + if ( length($prefix) ) { + $match->{'row'}->{'remainder'} = + $self->_pad_right( $prefix, $prefix_width ) + . ' ' + . $match->{'clause'}; + } + else { + $match->{'row'}->{'remainder'} = + ( ' ' x ( $prefix_width + 1 ) ) . $match->{'clause'}; + } + } + + return; +} + =head2 _align_create_table_column_group Align a group of single-line CREATE TABLE column definitions. @@ -1010,6 +1102,8 @@ sub _align_create_table_column_group { # so callers can safely use the result without mutating their input array. return [ @{$lines} ] if ( $alignable_count < 2 ); + $self->_align_create_table_column_keyword( \@rows, 'DEFAULT' ); + my @aligned_lines; for my $row (@rows) { diff --git a/t/03_vertical_align.t b/t/03_vertical_align.t index aff5a46..3a8fa6b 100644 --- a/t/03_vertical_align.t +++ b/t/03_vertical_align.t @@ -198,7 +198,7 @@ is_deeply( ] ), [ - ' id uuid primary key default gen_random_uuid(),', + ' id uuid primary key default gen_random_uuid(),', ' parent_job_id uuid references queue_job (id) on delete set null,', ' type queue_job_type not null,', q{ status queue_job_status not null default 'queued',}, @@ -223,6 +223,36 @@ is_deeply( 'preserves unsupported table constraints while aligning column definitions' ); +is_deeply( + $beautifier->_align_create_table_column_group( + [ + ' first_value text default current_user,', + ' second_value text not null default current_user,', + ' third_value text references app_user (username)', + ] + ), + [ + ' first_value text default current_user,', + ' second_value text not null default current_user,', + ' third_value text references app_user (username)', + ], + 'aligns DEFAULT without moving unrelated constraints' +); + +is_deeply( + $beautifier->_align_create_table_column_group( + [ + ' first_value text default current_user,', + ' second_value text references app_user (username)', + ] + ), + [ + ' first_value text default current_user,', + ' second_value text references app_user (username)', + ], + 'does not add padding when only one row contains DEFAULT' +); + my @single_column = (' id uuid primary key'); my $single_result = $beautifier->_align_create_table_column_group(\@single_column); From 9dffc43f9cc88b8d4ddc0d25df7a8ba0f28a6202 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 9 Jul 2026 17:56:49 +0200 Subject: [PATCH 10/18] Added comment support --- lib/pgFormatter/Beautify.pm | 308 ++++++++++++++++++++++++++++++++++-- t/03_vertical_align.t | 243 +++++++++++++++++++++++++++- 2 files changed, 535 insertions(+), 16 deletions(-) diff --git a/lib/pgFormatter/Beautify.pm b/lib/pgFormatter/Beautify.pm index 1b21e73..1c50a59 100755 --- a/lib/pgFormatter/Beautify.pm +++ b/lib/pgFormatter/Beautify.pm @@ -500,6 +500,11 @@ s/CODEPART[B]*(\d+)CODEPART[B]*/$self->{ 'placeholder_values' }[$1]/igs; # Replace any PGFESCQ2 by "" $self->{'content'} =~ s/PGFESCQ2/""/g; + if ( $self->{'vertical_align'} && $self->{'format'} eq 'text' ) { + $self->{'content'} = + $self->_align_create_table_columns( $self->{'content'} ); + } + return $self->{'content'}; } @@ -783,8 +788,9 @@ sub tokenize_sql { =head2 _parse_create_table_column Parse one single-line CREATE TABLE column definition into the parts needed by -vertical alignment. Returns undef for table constraints, comments, multiline -input, and definitions that do not contain both a column name and a data type. +vertical alignment. A trailing comment is stored separately from the SQL. +Returns undef for table constraints, embedded comments, multiline input, and +definitions that do not contain both a column name and a data type. This method only analyzes input. It does not change the formatted SQL. @@ -799,12 +805,29 @@ sub _parse_create_table_column { my $definition = $line; $definition =~ s/^\s+|\s+$//g; + my @tokens = $self->tokenize_sql($definition); + + # A trailing comment belongs to the complete column definition rather than + # to its data type or constraints. Store it separately so the SQL can be + # aligned first and comments can be placed in their own output column. + my $comment = ''; + if ( @tokens && $self->_is_comment( $tokens[-1] ) ) { + $comment = pop @tokens; + $comment =~ s/^\s+//; + } + + # Comments embedded in the middle of a definition remain unsupported. They + # can affect SQL structure, so preserving the original line is safer than + # attempting to rearrange it. + return if ( grep { $self->_is_comment($_) } @tokens ); + my $comma = ''; - $comma = ',' if ( $definition =~ s/,\s*$// ); + if ( @tokens && $tokens[-1] eq ',' ) { + pop @tokens; + $comma = ','; + } - my @tokens = $self->tokenize_sql($definition); return if ( @tokens < 2 ); - return if ( grep { $self->_is_comment($_) } @tokens ); my $name = shift @tokens; @@ -873,6 +896,7 @@ sub _parse_create_table_column { declaration_tokens => \@declaration_tokens, remainder_tokens => \@remainder_tokens, comma => $comma, + comment => $comment, }; } @@ -1053,6 +1077,264 @@ sub _align_create_table_column_keyword { return; } +=head2 _parenthesis_delta + +Return the net parenthesis depth change for one SQL line. + +The existing SQL tokenizer is reused so parentheses inside quoted strings and +comments are not mistaken for structural parentheses. + +=cut + +sub _parenthesis_delta { + my ( $self, $line ) = @_; + + return 0 if ( !defined $line || $line eq '' ); + + my $delta = 0; + for my $token ( $self->tokenize_sql($line) ) { + $delta++ if ( $token eq '(' ); + $delta-- if ( $token eq ')' ); + } + + return $delta; +} + +=head2 _create_table_start + +Inspect one formatted line for the beginning of a regular CREATE TABLE body. + +The return value is a three-element list: whether the line begins a supported +CREATE TABLE statement, whether its opening parenthesis is present, and the net +parenthesis depth on that line. When the opening parenthesis is absent it may +occur on the following line. + +CREATE TABLE AS, PARTITION OF, and typed-table forms are deliberately skipped +because their parentheses do not necessarily contain ordinary column +definitions. + +=cut + +sub _create_table_start { + my ( $self, $line ) = @_; + + return ( 0, 0, 0 ) if ( !defined $line || $line !~ /\S/ ); + + my @tokens = $self->tokenize_sql($line); + return ( 0, 0, 0 ) if ( !@tokens || uc( $tokens[0] ) ne 'CREATE' ); + + my $table_index; + for my $index ( 1 .. $#tokens ) { + my $token = uc( $tokens[$index] ); + + if ( $token eq 'TABLE' ) { + $table_index = $index; + last; + } + + return ( 0, 0, 0 ) + if ( $token !~ /^(?:GLOBAL|LOCAL|TEMP|TEMPORARY|UNLOGGED)$/ ); + } + + return ( 0, 0, 0 ) if ( !defined $table_index ); + + my $has_opening_parenthesis = 0; + + for my $index ( $table_index + 1 .. $#tokens ) { + my $token = uc( $tokens[$index] ); + + return ( 0, 0, 0 ) if ( $token =~ /^(?:AS|OF|PARTITION)$/ ); + + if ( $tokens[$index] eq '(' ) { + $has_opening_parenthesis = 1; + last; + } + } + + return ( + 1, + $has_opening_parenthesis, + $has_opening_parenthesis ? $self->_parenthesis_delta($line) : 0, + ); +} + +=head2 _align_create_table_body + +Align eligible top-level lines from one already formatted CREATE TABLE body. + +Only lines that begin and end at the table body's direct parenthesis depth are +sent to the column-group aligner. This excludes continuation lines belonging +to multiline CHECK expressions or other nested constructs while still allowing +single-line type modifiers and function calls. + +=cut + +sub _align_create_table_body { + my ( $self, $body ) = @_; + + return [] if ( !$body || !@{$body} ); + + my @result = map { $_->{'line'} } @{$body}; + my @candidate_indices; + my @candidate_lines; + + for my $index ( 0 .. $#{$body} ) { + my $entry = $body->[$index]; + next + if ( $entry->{'depth_before'} != 1 || $entry->{'depth_after'} != 1 ); + + push @candidate_indices, $index; + push @candidate_lines, $entry->{'line'}; + } + + return \@result if ( !@candidate_lines ); + + my $aligned = + $self->_align_create_table_column_group( \@candidate_lines ); + + for my $index ( 0 .. $#candidate_indices ) { + $result[ $candidate_indices[$index] ] = $aligned->[$index]; + } + + return \@result; +} + +=head2 _align_create_table_columns + +Apply vertical column alignment to regular CREATE TABLE statements in a fully +formatted SQL document. + +The method is a final text-only rendering pass. It preserves all lines outside +supported table bodies and delegates actual row formatting to the smaller +parser, renderer, and group-alignment helpers. + +=cut + +sub _align_create_table_columns { + my ( $self, $content ) = @_; + + return $content if ( !defined $content || $content eq '' ); + + my $newline = $content =~ /\r\n/ ? "\r\n" : "\n"; + my @lines = split( /\Q$newline\E/, $content, -1 ); + my @output; + my @body; + my $depth = 0; + my $inside_table = 0; + my $pending_table = 0; + + for my $line (@lines) { + if ($inside_table) { + my $depth_before = $depth; + my $depth_after = $depth + $self->_parenthesis_delta($line); + + if ( $depth_after <= 0 ) { + my $aligned_body = $self->_align_create_table_body( \@body ); + push @output, @{$aligned_body}, $line; + + @body = (); + $depth = 0; + $inside_table = 0; + next; + } + + push @body, + { + line => $line, + depth_before => $depth_before, + depth_after => $depth_after, + }; + $depth = $depth_after; + next; + } + + if ($pending_table) { + my @tokens = $self->tokenize_sql($line); + my $first_token = @tokens ? $tokens[0] : ''; + + push @output, $line; + + if ( $first_token eq '(' ) { + $depth = $self->_parenthesis_delta($line); + $inside_table = $depth > 0 ? 1 : 0; + $pending_table = 0; + } + elsif ( + $line =~ /\S/ + && !( @tokens && $self->_is_comment( $tokens[0] ) ) + ) + { + $pending_table = 0; + } + + next; + } + + my ( $is_create_table, $has_opening_parenthesis, $opening_depth ) = + $self->_create_table_start($line); + + push @output, $line; + next if ( !$is_create_table ); + + if ($has_opening_parenthesis) { + $depth = $opening_depth; + $inside_table = $depth > 0 ? 1 : 0; + } + else { + $pending_table = 1; + } + } + + # Incomplete input is still valid formatter input. Preserve any collected + # lines instead of dropping them when the closing parenthesis is missing. + if (@body) { + push @output, map { $_->{'line'} } @body; + } + + return join( $newline, @output ); +} + +=head2 _align_create_table_column_comments + +Append trailing comments at a shared output column. + +The SQL portion of every supported column is measured after name, declaration, +and DEFAULT alignment. Only rows containing comments receive padding, so rows +without comments do not gain trailing whitespace. Unsupported lines are ignored +when calculating the comment column. + +=cut + +sub _align_create_table_column_comments { + my ( $self, $rows ) = @_; + + return if ( !$rows || !@{$rows} ); + + my $sql_width = 0; + my $has_comment = 0; + + for my $row ( @{$rows} ) { + next if ( exists $row->{'original'} ); + + my $current_width = length( $row->{'rendered'} ); + $sql_width = $current_width if ( $current_width > $sql_width ); + $has_comment = 1 if ( length( $row->{'comment'} ) ); + } + + return if ( !$has_comment ); + + for my $row ( @{$rows} ) { + next if ( exists $row->{'original'} || !length( $row->{'comment'} ) ); + + $row->{'rendered'} = + $self->_pad_right( $row->{'rendered'}, $sql_width ) + . ' ' + . $row->{'comment'}; + } + + return; +} + =head2 _align_create_table_column_group Align a group of single-line CREATE TABLE column definitions. @@ -1104,13 +1386,8 @@ sub _align_create_table_column_group { $self->_align_create_table_column_keyword( \@rows, 'DEFAULT' ); - my @aligned_lines; - for my $row (@rows) { - if ( exists $row->{'original'} ) { - push @aligned_lines, $row->{'original'}; - next; - } + next if ( exists $row->{'original'} ); my $line = $row->{'indent'} @@ -1126,10 +1403,15 @@ sub _align_create_table_column_group { . $row->{'remainder'}; } - $line .= $row->{'comma'}; - push @aligned_lines, $line; + $row->{'rendered'} = $line . $row->{'comma'}; } + $self->_align_create_table_column_comments( \@rows ); + + my @aligned_lines = map { + exists $_->{'original'} ? $_->{'original'} : $_->{'rendered'} + } @rows; + return \@aligned_lines; } diff --git a/t/03_vertical_align.t b/t/03_vertical_align.t index 3a8fa6b..0e9bcfc 100644 --- a/t/03_vertical_align.t +++ b/t/03_vertical_align.t @@ -17,6 +17,7 @@ is_deeply( declaration_tokens => [qw(uuid primary key)], remainder_tokens => [ 'default', 'gen_random_uuid', '(', ')' ], comma => ',', + comment => '', }, 'parses PRIMARY KEY and DEFAULT separately' ); @@ -31,6 +32,7 @@ is_deeply( declaration_tokens => ['jsonb'], remainder_tokens => [ 'not', 'null', 'default', q{'{}'}, '::jsonb' ], comma => ',', + comment => '', }, 'parses multiple column constraints' ); @@ -45,6 +47,7 @@ is_deeply( declaration_tokens => [ 'numeric', '(', '10', ',', '2', ')' ], remainder_tokens => [], comma => ',', + comment => '', }, 'keeps type modifiers inside the declaration' ); @@ -60,6 +63,7 @@ is_deeply( [ 'public.custom_type', '[', ']', 'unique' ], remainder_tokens => [], comma => '', + comment => '', }, 'parses quoted names, qualified custom types, arrays, and UNIQUE' ); @@ -74,7 +78,8 @@ is_deeply( declaration_tokens => ['uuid'], remainder_tokens => [ 'references', 'app_user', '(', 'id', ')', 'on', 'delete', 'set', 'null' ], - comma => ',', + comma => ',', + comment => '', }, 'parses a REFERENCES constraint' ); @@ -100,11 +105,26 @@ ok( 'skips multiline definitions in the first implementation' ); +is_deeply( + $beautifier->_parse_create_table_column( + ' username varchar(150) not null unique, -- explanatory comment' + ), + { + indent => ' ', + name => 'username', + declaration_tokens => [ 'varchar', '(', '150', ')' ], + remainder_tokens => [ 'not', 'null', 'unique' ], + comma => ',', + comment => '-- explanatory comment', + }, + 'parses a trailing comment separately from the column SQL' +); + ok( !defined $beautifier->_parse_create_table_column( - ' username text -- explanatory comment' + ' username text /* explanation */ not null' ), - 'skips definitions containing comments in the first implementation' + 'skips comments embedded in the middle of a definition' ); is( @@ -207,6 +227,40 @@ is_deeply( 'aligns column names and the beginning of column constraints' ); +is_deeply( + $beautifier->_align_create_table_column_group( + [ + ' id serial primary key, -- some comment', + ' name varchar(100) not null, -- another comment', + ' email varchar(100) not null, -- third comment', + ' created_at timestamp with time zone default current_timestamp -- last comment', + ] + ), + [ + ' id serial primary key, -- some comment', + ' name varchar(100) not null, -- another comment', + ' email varchar(100) not null, -- third comment', + ' created_at timestamp with time zone default current_timestamp -- last comment', + ], + 'aligns trailing comments after the complete column SQL' +); + +is_deeply( + $beautifier->_align_create_table_column_group( + [ + ' id uuid primary key default gen_random_uuid(),', + ' username varchar(150) not null unique, -- only comment', + ' email varchar not null unique', + ] + ), + [ + ' id uuid primary key default gen_random_uuid(),', + ' username varchar(150) not null unique, -- only comment', + ' email varchar not null unique', + ], + 'aligns a lone trailing comment against the longest supported column' +); + is_deeply( $beautifier->_align_create_table_column_group( [ @@ -275,4 +329,187 @@ is_deeply( 'handles an empty group' ); +sub format_sql { + my ( $query, $vertical_align ) = @_; + + my $formatter = pgFormatter::Beautify->new( + query => $query, + vertical_align => $vertical_align, + uc_keywords => 1, + uc_types => 1, + uc_functions => 1, + no_space_function => 1, + no_extra_line => 1, + ); + + $formatter->beautify(); + return $formatter->content(); +} + +my $create_table_sql = <<'SQL'; +create table queue_job ( + id uuid primary key default gen_random_uuid(), + parent_job_id uuid references queue_job (id) on delete set null, + status queue_job_status not null default 'queued', + created_at timestamptz not null default now() +); +SQL + +is( + format_sql( $create_table_sql, 1 ), + <<'SQL', +create table queue_job( + id uuid primary key default gen_random_uuid(), + parent_job_id uuid references queue_job (id) on delete set null, + status queue_job_status not null default 'queued', + created_at timestamptz not null default now() +); +SQL + 'applies vertical alignment through the normal formatter output path' +); + +my $commented_table_sql = <<'SQL'; +create table example ( + id serial primary key, -- some comment + name varchar(100) not null, -- another comment + email varchar(100) not null, -- third comment + created_at timestamp with time zone default current_timestamp -- last comment +); +SQL + +is( + format_sql( $commented_table_sql, 1 ), + <<'SQL', +create table example( + id serial primary key, -- some comment + name varchar(100) not null, -- another comment + email varchar(100) not null, -- third comment + created_at timestamp with time zone default current_timestamp -- last comment +); +SQL + 'aligns trailing comments through the normal formatter output path' +); + +my $commented_aligned_once = format_sql( $commented_table_sql, 1 ); +is( + $beautifier->_align_create_table_columns($commented_aligned_once), + $commented_aligned_once, + 'keeps aligned trailing comments stable when alignment runs again' +); + +is( + format_sql( $create_table_sql, 0 ), + <<'SQL', +create table queue_job( + id uuid primary key default gen_random_uuid(), + parent_job_id uuid references queue_job(id) on delete set null, + status queue_job_status not null default 'queued', + created_at timestamptz not null default now() +); +SQL + 'leaves normal formatter output unchanged when vertical alignment is disabled' +); + +my $separate_parenthesis = <<'SQL'; +create table demo +( + id uuid primary key, + descriptive_name text +); +SQL + +is( + $beautifier->_align_create_table_columns($separate_parenthesis), + <<'SQL', +create table demo +( + id uuid primary key, + descriptive_name text +); +SQL + 'detects a CREATE TABLE opening parenthesis on the following line' +); + +my $nested_definition = <<'SQL'; +CREATE TABLE measurements ( + id uuid primary key, + score integer check ( + score >= 0 + and score <= 100 + ), + descriptive_name text +); +SQL + +is( + $beautifier->_align_create_table_columns($nested_definition), + <<'SQL', +CREATE TABLE measurements ( + id uuid primary key, + score integer check ( + score >= 0 + and score <= 100 + ), + descriptive_name text +); +SQL + 'preserves multiline nested definitions while aligning other top-level columns' +); + +my $create_table_as = <<'SQL'; +CREATE TABLE completed_jobs AS +SELECT id, status +FROM queue_job; +SQL + +is( + $beautifier->_align_create_table_columns($create_table_as), + $create_table_as, + 'skips CREATE TABLE AS statements' +); + +my $multiple_tables = <<'SQL'; +CREATE TABLE first_table ( + id uuid, + descriptive_name text +); + +CREATE TABLE second_table ( + key text, + considerably_longer_value jsonb +); +SQL + +is( + $beautifier->_align_create_table_columns($multiple_tables), + <<'SQL', +CREATE TABLE first_table ( + id uuid, + descriptive_name text +); + +CREATE TABLE second_table ( + key text, + considerably_longer_value jsonb +); +SQL + 'aligns multiple CREATE TABLE statements independently' +); + +my $aligned_once = + $beautifier->_align_create_table_columns($separate_parenthesis); +is( + $beautifier->_align_create_table_columns($aligned_once), + $aligned_once, + 'is idempotent when applied more than once' +); + +is( + $beautifier->_parenthesis_delta( + q{ note text default 'not structural (parenthesis)' -- ignored )} + ), + 0, + 'ignores parentheses contained in strings and comments' +); + done_testing(); From 8609b732306928244fdd283cbdfac0021a8db1eb Mon Sep 17 00:00:00 2001 From: david Date: Thu, 9 Jul 2026 23:46:33 +0200 Subject: [PATCH 11/18] Added doc and tests --- README | 20 ++++++++++++++++++++ doc/pg_format.conf.sample | 2 +- doc/pg_format.pod | 17 +++++++++++++++++ lib/pgFormatter/CLI.pm | 2 +- t/02_regress.t | 3 ++- t/regress_test.pl | 1 + t/test-files/ex81.sql | 14 ++++++++++++++ t/test-files/expected/ex81.sql | 14 ++++++++++++++ 8 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 t/test-files/ex81.sql create mode 100644 t/test-files/expected/ex81.sql diff --git a/README b/README index fa74124..8025763 100644 --- a/README +++ b/README @@ -84,6 +84,8 @@ DESCRIPTION --no-space-function : remove space between function call and the open parenthesis. --redundant-parenthesis: do not remove redundant parenthesis in DML. + --vertical-align : vertically align CREATE TABLE column definitions and + trailing comments. Disabled by default. Examples: @@ -265,6 +267,24 @@ SPECIAL FORMATTING in some cases they must be preseved. Using this option will keep redundant parenthesis untouched. + Option --vertical-align + Use this option to vertically align direct, single-line column + definitions inside CREATE TABLE statements. It aligns column names, data + types, column constraints such as DEFAULT, and trailing comments. For + example: + + CREATE TABLE example ( + id serial PRIMARY KEY, -- some comment + name varchar(100) NOT NULL, -- another comment + email varchar(100) NOT NULL, -- third comment + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP -- last comment + ); + + Table-level constraints and multiline column definitions are preserved + but are not included in the alignment calculation. The option is disabled + by default and only applies to text output. It can also be enabled in a + configuration file with vertical-align=1. + HINTS Configuration If the default settings of pg_format doesn't fit all your needs you can diff --git a/doc/pg_format.conf.sample b/doc/pg_format.conf.sample index 6782e95..b3fd6cb 100644 --- a/doc/pg_format.conf.sample +++ b/doc/pg_format.conf.sample @@ -86,5 +86,5 @@ wrap-comment=0 # Remove the space character between a function call and the open parenthesis that follow. no-space-function=0 -# Vertically align CREATE TABLE column definitions. +# Vertically align CREATE TABLE column definitions and trailing comments. vertical-align=0 diff --git a/doc/pg_format.pod b/doc/pg_format.pod index 84347b6..f55210e 100644 --- a/doc/pg_format.pod +++ b/doc/pg_format.pod @@ -278,6 +278,23 @@ By default, pgFormatter tries to remove redundant parenthesis in DML but in some cases they must be preseved. Using this option will keep redundant parenthesis untouched. +=head2 Option --vertical-align + +Use this option to vertically align direct, single-line column definitions +inside CREATE TABLE statements. It aligns column names, data types, column +constraints such as DEFAULT, and trailing comments. For example: + + CREATE TABLE example ( + id serial PRIMARY KEY, -- some comment + name varchar(100) NOT NULL, -- another comment + email varchar(100) NOT NULL, -- third comment + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP -- last comment + ); + +Table-level constraints and multiline column definitions are preserved but are +not included in the alignment calculation. The option is disabled by default +and only applies to text output. + =head1 HINTS =head2 Configuration diff --git a/lib/pgFormatter/CLI.pm b/lib/pgFormatter/CLI.pm index 434c668..12b06c7 100755 --- a/lib/pgFormatter/CLI.pm +++ b/lib/pgFormatter/CLI.pm @@ -324,7 +324,7 @@ Options: --no-space-function : remove space between function call and the open parenthesis. --redundant-parenthesis: do not remove redundant parenthesis in DML. - --vertical-align : vertically align CREATE TABLE column definitions. + --vertical-align : vertically align CREATE TABLE column definitions and trailing comments. Examples: cat samples/ex1.sql | $0 - diff --git a/t/02_regress.t b/t/02_regress.t index 22da5f6..7102678 100755 --- a/t/02_regress.t +++ b/t/02_regress.t @@ -1,4 +1,4 @@ -use Test::Simple tests => 82; +use Test::Simple tests => 83; use File::Temp qw/ tempfile /; my $pg_format = $ENV{PG_FORMAT} // './pg_format'; # set to the full path to 'pg_format' to test installed binary in /usr/bin @@ -28,6 +28,7 @@ foreach my $f (@files) $opt = "--keyword-case 2 --function-case 1 --comma-start --wrap-after 1 --wrap-limit 40 --tabs --spaces 4 " if ($f =~ m#/ex58.sql$#); $opt = "--no-space-function" if ($f =~ m#/ex70.sql$#); $opt = "--keyword-case 1 --type-case 1" if ($f =~ m#/ex71.sql$#); + $opt = "--vertical-align --no-extra-line" if ( $f =~ m#/ex81\.sql$# ); if ($f =~ m#/ex61.sql$#) { my ($fh, $tmpfile) = tempfile('tmp_pgformatXXXX', SUFFIX => '.lst', TMPDIR => 1, O_TEMPORARY => 1, UNLINK => 1 ); diff --git a/t/regress_test.pl b/t/regress_test.pl index 407d8cd..34a61d6 100644 --- a/t/regress_test.pl +++ b/t/regress_test.pl @@ -27,6 +27,7 @@ $opt = "--keyword-case 2 --function-case 1 --comma-start --wrap-after 1 --wrap-limit 40 --tabs --spaces 4 " if ($f =~ m#/ex58.sql$#); $opt = "--no-space-function" if ($f =~ m#/ex70.sql$#); $opt = "--keyword-case 1 --type-case 1" if ($f =~ m#/ex71.sql$#); + $opt = "--vertical-align --no-extra-line" if ( $f =~ m#/ex81\.sql$# ); if ($f =~ m#/ex61.sql$#) { my ($fh, $tmpfile) = tempfile('tmp_pgformatXXXX', SUFFIX => '.lst', TMPDIR => 1, O_TEMPORARY => 1, UNLINK => 1 ); diff --git a/t/test-files/ex81.sql b/t/test-files/ex81.sql new file mode 100644 index 0000000..18a854b --- /dev/null +++ b/t/test-files/ex81.sql @@ -0,0 +1,14 @@ +CREATE TABLE example ( + id serial PRIMARY KEY, -- some comment + name varchar(100) NOT NULL, -- another comment + email varchar(100) NOT NULL, -- third comment + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP -- last comment +); + +CREATE TABLE queue_job ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid (), + parent_job_id uuid REFERENCES queue_job (id) ON DELETE SET NULL, + status queue_job_status NOT NULL DEFAULT 'queued', + payload jsonb NOT NULL DEFAULT '{}'::jsonb, + CONSTRAINT queue_job_status_check CHECK (status <> 'invalid') +); diff --git a/t/test-files/expected/ex81.sql b/t/test-files/expected/ex81.sql new file mode 100644 index 0000000..9fbac1c --- /dev/null +++ b/t/test-files/expected/ex81.sql @@ -0,0 +1,14 @@ +CREATE TABLE example ( + id serial PRIMARY KEY, -- some comment + name varchar(100) NOT NULL, -- another comment + email varchar(100) NOT NULL, -- third comment + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP -- last comment +); + +CREATE TABLE queue_job ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + parent_job_id uuid REFERENCES queue_job (id) ON DELETE SET NULL, + status queue_job_status NOT NULL DEFAULT 'queued', + payload jsonb NOT NULL DEFAULT '{}'::jsonb, + CONSTRAINT queue_job_status_check CHECK (status <> 'invalid') +); From 808ab4bf1d9b6e7af7359cad02d42d740735ee72 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 9 Jul 2026 23:48:57 +0200 Subject: [PATCH 12/18] Fixed spacing --- t/02_regress.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/02_regress.t b/t/02_regress.t index 7102678..f1b742a 100755 --- a/t/02_regress.t +++ b/t/02_regress.t @@ -28,7 +28,7 @@ foreach my $f (@files) $opt = "--keyword-case 2 --function-case 1 --comma-start --wrap-after 1 --wrap-limit 40 --tabs --spaces 4 " if ($f =~ m#/ex58.sql$#); $opt = "--no-space-function" if ($f =~ m#/ex70.sql$#); $opt = "--keyword-case 1 --type-case 1" if ($f =~ m#/ex71.sql$#); - $opt = "--vertical-align --no-extra-line" if ( $f =~ m#/ex81\.sql$# ); + $opt = "--vertical-align --no-extra-line" if ($f =~ m#/ex81\.sql$#); if ($f =~ m#/ex61.sql$#) { my ($fh, $tmpfile) = tempfile('tmp_pgformatXXXX', SUFFIX => '.lst', TMPDIR => 1, O_TEMPORARY => 1, UNLINK => 1 ); From 4ea89b3c7faeab0e82f52670d30bd7d55ef03bd3 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 10 Jul 2026 00:18:44 +0200 Subject: [PATCH 13/18] Added new tests for spacing edgecases --- lib/pgFormatter/Beautify.pm | 18 ++++-- lib/pgFormatter/CGI.pm | 8 ++- lib/pgFormatter/CLI.pm | 11 ++-- t/03_vertical_align.t | 64 +++++++++++++++++-- t/04_vertical_align_integration.t | 103 ++++++++++++++++++++++++++++++ t/test-files/expected/ex81.sql | 2 +- 6 files changed, 190 insertions(+), 16 deletions(-) create mode 100644 t/04_vertical_align_integration.t diff --git a/lib/pgFormatter/Beautify.pm b/lib/pgFormatter/Beautify.pm index 1c50a59..1cfba1a 100755 --- a/lib/pgFormatter/Beautify.pm +++ b/lib/pgFormatter/Beautify.pm @@ -182,7 +182,7 @@ sub new { $self->set_defaults(); for my $key ( - qw( query spaces space break wrap keywords functions rules uc_keywords uc_functions uc_types no_comments no_grouping placeholder multiline separator comma comma_break format colorize format_type wrap_limit wrap_after wrap_comment numbering redshift no_extra_line keep_newline no_space_function redundant_parenthesis vertical_align) + qw( query spaces space break wrap keywords functions rules uc_keywords uc_functions uc_types no_comments no_grouping placeholder multiline separator comma comma_break format colorize format_type wrap_limit wrap_after wrap_comment numbering redshift no_extra_line keep_newline no_space_function redundant_parenthesis vertical_align) ) { $self->{$key} = $options{$key} if defined $options{$key}; @@ -947,12 +947,21 @@ sub _render_sql_tokens { } if ( $token eq '(' ) { - my $needs_space = + my $force_space = $space_before_parenthesis{ uc($previous_token) } || ( defined $before_previous && uc($before_previous) eq 'REFERENCES' ); - $rendered .= ( $needs_space ? ' ' : '' ) . $token; + # Preserve pgFormatter's existing parenthesis policy: known functions + # and data types stay attached, while unknown names keep a space unless + # --no-space-function was requested. + my $attach_parenthesis = + $self->{'no_space_function'} + || $self->_is_function($previous_token) + || $self->_is_type($previous_token); + + $rendered .= + ( $force_space || !$attach_parenthesis ? ' ' : '' ) . $token; next; } @@ -1118,7 +1127,8 @@ definitions. sub _create_table_start { my ( $self, $line ) = @_; - return ( 0, 0, 0 ) if ( !defined $line || $line !~ /\S/ ); + return ( 0, 0, 0 ) + if ( !defined $line || $line !~ /^\s*CREATE\b/i ); my @tokens = $self->tokenize_sql($line); return ( 0, 0, 0 ) if ( !@tokens || uc( $tokens[0] ) ne 'CREATE' ); diff --git a/lib/pgFormatter/CGI.pm b/lib/pgFormatter/CGI.pm index 271de53..131b812 100755 --- a/lib/pgFormatter/CGI.pm +++ b/lib/pgFormatter/CGI.pm @@ -196,6 +196,7 @@ sub set_config { $self->{'extra_keyword'} //= ''; $self->{'no_space_function'} //= 0; $self->{'redundant_parenthesis'} //= 0; + $self->{'vertical_align'} //= 0; # Backward compatibility $self->{'extra_keyword'} = 'redshift' @@ -262,7 +263,7 @@ sub get_params { my $cgi = $self->{'cgi'}; for my $param_name ( - qw( colorize spaces uc_keyword uc_function uc_type content nocomment nogrouping show_example anonymize separator comma comma_break format_type wrap_after original_content numbering redshift keep_newline no_space_function redundant_parenthesis) + qw( colorize spaces uc_keyword uc_function uc_type content nocomment nogrouping show_example anonymize separator comma comma_break format_type wrap_after original_content numbering redshift keep_newline no_space_function redundant_parenthesis vertical_align) ) { $self->{$param_name} = $cgi->param($param_name) @@ -323,7 +324,7 @@ sub get_api_params { my $postdata = $cgi->param('POSTDATA'); my $json_params = decode_json($postdata); for my $param_name ( - qw( colorize spaces uc_keyword uc_function uc_type content nocomment nogrouping show_example anonymize separator comma comma_break format_type wrap_after original_content numbering redshift keep_newline no_space_function redundant_parenthesis) + qw( colorize spaces uc_keyword uc_function uc_type content nocomment nogrouping show_example anonymize separator comma comma_break format_type wrap_after original_content numbering redshift keep_newline no_space_function redundant_parenthesis vertical_align) ) { $self->{$param_name} = $json_params->{$param_name} @@ -364,6 +365,8 @@ sub sanitize_params { if ( $self->{'no_space_function'} !~ /^(0|1)$/ ); $self->{'redundant_parenthesis'} = 0 if ( $self->{'redundant_parenthesis'} !~ /^(0|1)$/ ); + $self->{'vertical_align'} = 0 + if ( $self->{'vertical_align'} !~ /^(0|1)$/ ); if ( $self->{'show_example'} ) { $self->{'content'} = q{ @@ -412,6 +415,7 @@ sub beautify_query { $args{'keep_newline'} = 1 if $self->{'keep_newline'}; $args{'no_space_function'} = 1 if $self->{'no_space_function'}; $args{'redundant_parenthesis'} = 1 if $self->{'redundant_parenthesis'}; + $args{'vertical_align'} = 1 if $self->{'vertical_align'}; $self->{'content'} = &remove_extra_parenthesis( $self->{'content'} ) if ( !$self->{'redundant_parenthesis'} && $self->{'content'} ); diff --git a/lib/pgFormatter/CLI.pm b/lib/pgFormatter/CLI.pm index 12b06c7..4d29099 100755 --- a/lib/pgFormatter/CLI.pm +++ b/lib/pgFormatter/CLI.pm @@ -130,7 +130,7 @@ sub beautify { $args{'extra_keyword'} = $self->{'cfg'}->{'extra-keyword'}; $args{'no_space_function'} = $self->{'cfg'}->{'no-space-function'}; $args{'redundant_parenthesis'} = $self->{'cfg'}->{'redundant-parenthesis'}; - $args{'vertical_align'} = $self->{'cfg'}->{'vertical-align'}; + $args{'vertical_align'} = $self->{'cfg'}->{'vertical-align'}; # Backward compatibility $args{'extra_keyword'} = 'redshift' @@ -324,7 +324,8 @@ Options: --no-space-function : remove space between function call and the open parenthesis. --redundant-parenthesis: do not remove redundant parenthesis in DML. - --vertical-align : vertically align CREATE TABLE column definitions and trailing comments. + --vertical-align : vertically align CREATE TABLE column definitions and + trailing comments. Examples: cat samples/ex1.sql | $0 - @@ -391,7 +392,7 @@ sub get_command_line_args { 'wrap-limit|w=i', 'wrap-after|W=i', 'inplace|i!', 'extra-function=s', 'extra-keyword=s', 'no-space-function!', - 'redundant-parenthesis!', 'vertical-align!', + 'redundant-parenthesis!', 'vertical-align!', ); $self->show_help_and_die(1) unless GetOptions( \%cfg, @options ); @@ -461,9 +462,9 @@ sub get_command_line_args { $cfg{'redshift'} //= 0; $cfg{'no-extra-line'} //= 0; $cfg{'inplace'} //= 0; - $cfg{'extra-keyword'} //= ''; + $cfg{'extra-keyword'} //= ''; $cfg{'vertical-align'} //= 0; - $cfg{'extra-keyword'} = 'redshift' if ( $cfg{'redshift'} ); + $cfg{'extra-keyword'} = 'redshift' if ( $cfg{'redshift'} ); if ( $cfg{'tabs'} ) { $cfg{'spaces'} = 1; diff --git a/t/03_vertical_align.t b/t/03_vertical_align.t index 0e9bcfc..cb64f45 100644 --- a/t/03_vertical_align.t +++ b/t/03_vertical_align.t @@ -153,8 +153,31 @@ is( $beautifier->_render_sql_tokens( [ 'default', 'gen_random_uuid', '(', ')' ] ), + 'default gen_random_uuid ()', + 'preserves the default space before an unknown function parenthesis' +); + +my $compact_function_renderer = + pgFormatter::Beautify->new( no_space_function => 1 ); + +is( + $compact_function_renderer->_render_sql_tokens( + [ 'default', 'gen_random_uuid', '(', ')' ] + ), 'default gen_random_uuid()', - 'renders an extension function without a space before its parenthesis' + 'removes the space before an unknown function when configured' +); + +is( + $beautifier->_render_sql_tokens( [ 'default', 'lower', '(', 'name', ')' ] ), + 'default lower(name)', + 'keeps PostgreSQL internal functions attached to their parenthesis' +); + +is( + $beautifier->_render_sql_tokens( [ 'my_type', '(', '10', ')' ] ), + 'my_type (10)', + 'preserves spacing for an unknown parameterized type' ); is( @@ -218,7 +241,7 @@ is_deeply( ] ), [ - ' id uuid primary key default gen_random_uuid(),', + ' id uuid primary key default gen_random_uuid (),', ' parent_job_id uuid references queue_job (id) on delete set null,', ' type queue_job_type not null,', q{ status queue_job_status not null default 'queued',}, @@ -254,8 +277,8 @@ is_deeply( ] ), [ - ' id uuid primary key default gen_random_uuid(),', - ' username varchar(150) not null unique, -- only comment', + ' id uuid primary key default gen_random_uuid (),', + ' username varchar(150) not null unique, -- only comment', ' email varchar not null unique', ], 'aligns a lone trailing comment against the longest supported column' @@ -410,6 +433,39 @@ SQL 'leaves normal formatter output unchanged when vertical alignment is disabled' ); +sub format_sql_with_default_function_spacing { + my ( $query, $vertical_align ) = @_; + + my $formatter = pgFormatter::Beautify->new( + query => $query, + vertical_align => $vertical_align, + uc_keywords => 1, + uc_types => 1, + uc_functions => 1, + no_extra_line => 1, + ); + + $formatter->beautify(); + return $formatter->content(); +} + +my $default_spacing_disabled = + format_sql_with_default_function_spacing( $create_table_sql, 0 ); +my $default_spacing_enabled = + format_sql_with_default_function_spacing( $create_table_sql, 1 ); + +like( + $default_spacing_disabled, + qr/default gen_random_uuid \(\)/, + 'normal formatting keeps a space before an unknown function parenthesis' +); + +like( + $default_spacing_enabled, + qr/default gen_random_uuid \(\)/, + 'vertical alignment preserves unknown-function parenthesis spacing' +); + my $separate_parenthesis = <<'SQL'; create table demo ( diff --git a/t/04_vertical_align_integration.t b/t/04_vertical_align_integration.t new file mode 100644 index 0000000..1bdf94f --- /dev/null +++ b/t/04_vertical_align_integration.t @@ -0,0 +1,103 @@ +use strict; +use warnings; + +BEGIN { + package JSON; + + sub import { + my $caller = caller; + no strict 'refs'; + *{"${caller}::decode_json"} = sub { die "decode_json is not used here" }; + *{"${caller}::encode_json"} = sub { die "encode_json is not used here" }; + } + + $INC{'JSON.pm'} = __FILE__; + + package CGI; + + sub import { } + + $INC{'CGI.pm'} = __FILE__; +} + +use Test::More; +use File::Temp qw(tempfile); +use lib 'lib'; +use pgFormatter::CGI; + + +my ( $config_fh, $config_path ) = tempfile(); +print {$config_fh} <<'CONFIG'; +vertical-align=1 +keyword-case=1 +type-case=1 +function-case=1 +no-extra-line=1 +CONFIG +close($config_fh); + +my ( $sql_fh, $sql_path ) = tempfile( SUFFIX => '.sql' ); +print {$sql_fh} <<'SQL'; +CREATE TABLE configured_example ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + descriptive_name TEXT NOT NULL +); +SQL +close($sql_fh); + +open my $formatter_output, '-|', + $^X, '-Ilib', 'pg_format', '--config', $config_path, $sql_path + or die "could not run pg_format: $!"; + +my $configured_output = do { + local $/; + <$formatter_output>; +}; +close($formatter_output); + +is( $?, 0, 'pg_format accepts vertical-align from a configuration file' ); + +is( + $configured_output, + <<'SQL', +create table configured_example ( + id uuid primary key default gen_random_uuid (), + descriptive_name text not null +); +SQL + 'configuration-file vertical alignment reaches the formatter' +); + +my $cgi_formatter = pgFormatter::CGI->new(); + +is( + $cgi_formatter->{'vertical_align'}, + 0, + 'CGI vertical alignment is disabled by default' +); + +$cgi_formatter->{'content'} = <<'SQL'; +CREATE TABLE example ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + descriptive_name TEXT NOT NULL +); +SQL +$cgi_formatter->{'format'} = 'text'; +$cgi_formatter->{'colorize'} = 0; +$cgi_formatter->{'vertical_align'} = 1; + +$cgi_formatter->beautify_query(); + +is( + $cgi_formatter->{'content'}, + <<'SQL', +CREATE TABLE example ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid (), + descriptive_name text NOT NULL +); + +SQL + 'CGI forwards vertical alignment while preserving function spacing' +); + +done_testing(); diff --git a/t/test-files/expected/ex81.sql b/t/test-files/expected/ex81.sql index 9fbac1c..e849855 100644 --- a/t/test-files/expected/ex81.sql +++ b/t/test-files/expected/ex81.sql @@ -6,7 +6,7 @@ CREATE TABLE example ( ); CREATE TABLE queue_job ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + id uuid PRIMARY KEY DEFAULT gen_random_uuid (), parent_job_id uuid REFERENCES queue_job (id) ON DELETE SET NULL, status queue_job_status NOT NULL DEFAULT 'queued', payload jsonb NOT NULL DEFAULT '{}'::jsonb, From 22d420f4e3d6935804f94188883e26e5af5e6b56 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 10 Jul 2026 00:22:02 +0200 Subject: [PATCH 14/18] Simplified test --- t/03_vertical_align.t | 98 +++++++++++++++++++++++++++- t/04_vertical_align_integration.t | 103 ------------------------------ 2 files changed, 97 insertions(+), 104 deletions(-) delete mode 100644 t/04_vertical_align_integration.t diff --git a/t/03_vertical_align.t b/t/03_vertical_align.t index cb64f45..a81dea2 100644 --- a/t/03_vertical_align.t +++ b/t/03_vertical_align.t @@ -1,9 +1,30 @@ use strict; use warnings; +BEGIN { + package JSON; + + sub import { + my $caller = caller; + no strict 'refs'; + *{"${caller}::decode_json"} = sub { die "decode_json is not used here" }; + *{"${caller}::encode_json"} = sub { die "encode_json is not used here" }; + } + + $INC{'JSON.pm'} = __FILE__; + + package CGI; + + sub import { } + + $INC{'CGI.pm'} = __FILE__; +} + use Test::More; +use File::Temp qw(tempfile); use lib 'lib'; use pgFormatter::Beautify; +use pgFormatter::CGI; my $beautifier = pgFormatter::Beautify->new(); @@ -568,4 +589,79 @@ is( 'ignores parentheses contained in strings and comments' ); -done_testing(); +# Verify the option also reaches the formatter through public configuration paths. +my ( $config_fh, $config_path ) = tempfile(); +print {$config_fh} <<'CONFIG'; +vertical-align=1 +keyword-case=1 +type-case=1 +function-case=1 +no-extra-line=1 +CONFIG +close($config_fh); + +my ( $sql_fh, $sql_path ) = tempfile( SUFFIX => '.sql' ); +print {$sql_fh} <<'SQL'; +CREATE TABLE configured_example ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + descriptive_name TEXT NOT NULL +); +SQL +close($sql_fh); + +open my $formatter_output, '-|', + $^X, '-Ilib', 'pg_format', '--config', $config_path, $sql_path + or die "could not run pg_format: $!"; + +my $configured_output = do { + local $/; + <$formatter_output>; +}; +close($formatter_output); + +is( $?, 0, 'pg_format accepts vertical-align from a configuration file' ); + +is( + $configured_output, + <<'SQL', +create table configured_example ( + id uuid primary key default gen_random_uuid (), + descriptive_name text not null +); +SQL + 'configuration-file alignment preserves default function spacing' +); + +my $cgi_formatter = pgFormatter::CGI->new(); + +is( + $cgi_formatter->{'vertical_align'}, + 0, + 'CGI vertical alignment is disabled by default' +); + +$cgi_formatter->{'content'} = <<'SQL'; +CREATE TABLE example ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + descriptive_name TEXT NOT NULL +); +SQL +$cgi_formatter->{'format'} = 'text'; +$cgi_formatter->{'colorize'} = 0; +$cgi_formatter->{'vertical_align'} = 1; + +$cgi_formatter->beautify_query(); + +is( + $cgi_formatter->{'content'}, + <<'SQL', +CREATE TABLE example ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid (), + descriptive_name text NOT NULL +); + +SQL + 'CGI forwards alignment while preserving default function spacing' +); + +done_testing(); \ No newline at end of file diff --git a/t/04_vertical_align_integration.t b/t/04_vertical_align_integration.t deleted file mode 100644 index 1bdf94f..0000000 --- a/t/04_vertical_align_integration.t +++ /dev/null @@ -1,103 +0,0 @@ -use strict; -use warnings; - -BEGIN { - package JSON; - - sub import { - my $caller = caller; - no strict 'refs'; - *{"${caller}::decode_json"} = sub { die "decode_json is not used here" }; - *{"${caller}::encode_json"} = sub { die "encode_json is not used here" }; - } - - $INC{'JSON.pm'} = __FILE__; - - package CGI; - - sub import { } - - $INC{'CGI.pm'} = __FILE__; -} - -use Test::More; -use File::Temp qw(tempfile); -use lib 'lib'; -use pgFormatter::CGI; - - -my ( $config_fh, $config_path ) = tempfile(); -print {$config_fh} <<'CONFIG'; -vertical-align=1 -keyword-case=1 -type-case=1 -function-case=1 -no-extra-line=1 -CONFIG -close($config_fh); - -my ( $sql_fh, $sql_path ) = tempfile( SUFFIX => '.sql' ); -print {$sql_fh} <<'SQL'; -CREATE TABLE configured_example ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - descriptive_name TEXT NOT NULL -); -SQL -close($sql_fh); - -open my $formatter_output, '-|', - $^X, '-Ilib', 'pg_format', '--config', $config_path, $sql_path - or die "could not run pg_format: $!"; - -my $configured_output = do { - local $/; - <$formatter_output>; -}; -close($formatter_output); - -is( $?, 0, 'pg_format accepts vertical-align from a configuration file' ); - -is( - $configured_output, - <<'SQL', -create table configured_example ( - id uuid primary key default gen_random_uuid (), - descriptive_name text not null -); -SQL - 'configuration-file vertical alignment reaches the formatter' -); - -my $cgi_formatter = pgFormatter::CGI->new(); - -is( - $cgi_formatter->{'vertical_align'}, - 0, - 'CGI vertical alignment is disabled by default' -); - -$cgi_formatter->{'content'} = <<'SQL'; -CREATE TABLE example ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - descriptive_name TEXT NOT NULL -); -SQL -$cgi_formatter->{'format'} = 'text'; -$cgi_formatter->{'colorize'} = 0; -$cgi_formatter->{'vertical_align'} = 1; - -$cgi_formatter->beautify_query(); - -is( - $cgi_formatter->{'content'}, - <<'SQL', -CREATE TABLE example ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid (), - descriptive_name text NOT NULL -); - -SQL - 'CGI forwards vertical alignment while preserving function spacing' -); - -done_testing(); From a82acc26d74819521aab2f8f4d50dbe708835d6c Mon Sep 17 00:00:00 2001 From: david Date: Fri, 10 Jul 2026 15:51:12 +0200 Subject: [PATCH 15/18] Consolidated a lot of tests These tests were written for local development and used for sainaty checks --- t/03_vertical_align.t | 658 +++--------------------------------------- 1 file changed, 38 insertions(+), 620 deletions(-) diff --git a/t/03_vertical_align.t b/t/03_vertical_align.t index a81dea2..8f09b42 100644 --- a/t/03_vertical_align.t +++ b/t/03_vertical_align.t @@ -1,465 +1,15 @@ use strict; use warnings; -BEGIN { - package JSON; - - sub import { - my $caller = caller; - no strict 'refs'; - *{"${caller}::decode_json"} = sub { die "decode_json is not used here" }; - *{"${caller}::encode_json"} = sub { die "encode_json is not used here" }; - } - - $INC{'JSON.pm'} = __FILE__; - - package CGI; - - sub import { } - - $INC{'CGI.pm'} = __FILE__; -} - use Test::More; -use File::Temp qw(tempfile); use lib 'lib'; use pgFormatter::Beautify; -use pgFormatter::CGI; - -my $beautifier = pgFormatter::Beautify->new(); - -is_deeply( - $beautifier->_parse_create_table_column( - ' id uuid primary key default gen_random_uuid(),' - ), - { - indent => ' ', - name => 'id', - declaration_tokens => [qw(uuid primary key)], - remainder_tokens => [ 'default', 'gen_random_uuid', '(', ')' ], - comma => ',', - comment => '', - }, - 'parses PRIMARY KEY and DEFAULT separately' -); - -is_deeply( - $beautifier->_parse_create_table_column( - q{ payload jsonb not null default '{}'::jsonb,} - ), - { - indent => ' ', - name => 'payload', - declaration_tokens => ['jsonb'], - remainder_tokens => [ 'not', 'null', 'default', q{'{}'}, '::jsonb' ], - comma => ',', - comment => '', - }, - 'parses multiple column constraints' -); - -is_deeply( - $beautifier->_parse_create_table_column( - ' amount numeric(10, 2),' - ), - { - indent => ' ', - name => 'amount', - declaration_tokens => [ 'numeric', '(', '10', ',', '2', ')' ], - remainder_tokens => [], - comma => ',', - comment => '', - }, - 'keeps type modifiers inside the declaration' -); - -is_deeply( - $beautifier->_parse_create_table_column( - ' "quoted column" public.custom_type[] unique' - ), - { - indent => ' ', - name => '"quoted column"', - declaration_tokens => - [ 'public.custom_type', '[', ']', 'unique' ], - remainder_tokens => [], - comma => '', - comment => '', - }, - 'parses quoted names, qualified custom types, arrays, and UNIQUE' -); - -is_deeply( - $beautifier->_parse_create_table_column( - ' owner_id uuid references app_user (id) on delete set null,' - ), - { - indent => ' ', - name => 'owner_id', - declaration_tokens => ['uuid'], - remainder_tokens => - [ 'references', 'app_user', '(', 'id', ')', 'on', 'delete', 'set', 'null' ], - comma => ',', - comment => '', - }, - 'parses a REFERENCES constraint' -); - -for my $table_constraint ( - 'constraint demo_pk primary key (id)', - 'primary key (id)', - 'foreign key (owner_id) references app_user (id)', - 'unique (correlation_key)', - 'check (id is not null)', - ) -{ - ok( - !defined $beautifier->_parse_create_table_column($table_constraint), - "skips table constraint: $table_constraint" - ); -} - -ok( - !defined $beautifier->_parse_create_table_column( - " score integer check (\n score >= 0\n )" - ), - 'skips multiline definitions in the first implementation' -); - -is_deeply( - $beautifier->_parse_create_table_column( - ' username varchar(150) not null unique, -- explanatory comment' - ), - { - indent => ' ', - name => 'username', - declaration_tokens => [ 'varchar', '(', '150', ')' ], - remainder_tokens => [ 'not', 'null', 'unique' ], - comma => ',', - comment => '-- explanatory comment', - }, - 'parses a trailing comment separately from the column SQL' -); - -ok( - !defined $beautifier->_parse_create_table_column( - ' username text /* explanation */ not null' - ), - 'skips comments embedded in the middle of a definition' -); - -is( - $beautifier->_render_sql_tokens( [qw(uuid primary key)] ), - 'uuid primary key', - 'renders a simple declaration' -); - -is( - $beautifier->_render_sql_tokens( - [ 'numeric', '(', '10', ',', '2', ')' ] - ), - 'numeric(10, 2)', - 'renders a parameterized type' -); - -is( - $beautifier->_render_sql_tokens( - [ 'public.custom_type', '[', ']' ] - ), - 'public.custom_type[]', - 'renders an array type' -); - -is( - $beautifier->_render_sql_tokens( - [ 'default', 'gen_random_uuid', '(', ')' ] - ), - 'default gen_random_uuid ()', - 'preserves the default space before an unknown function parenthesis' -); - -my $compact_function_renderer = - pgFormatter::Beautify->new( no_space_function => 1 ); - -is( - $compact_function_renderer->_render_sql_tokens( - [ 'default', 'gen_random_uuid', '(', ')' ] - ), - 'default gen_random_uuid()', - 'removes the space before an unknown function when configured' -); - -is( - $beautifier->_render_sql_tokens( [ 'default', 'lower', '(', 'name', ')' ] ), - 'default lower(name)', - 'keeps PostgreSQL internal functions attached to their parenthesis' -); - -is( - $beautifier->_render_sql_tokens( [ 'my_type', '(', '10', ')' ] ), - 'my_type (10)', - 'preserves spacing for an unknown parameterized type' -); - -is( - $beautifier->_render_sql_tokens( - [ 'references', 'app_user', '(', 'id', ')', 'on', 'delete', 'set', 'null' ] - ), - 'references app_user (id) on delete set null', - 'keeps a space before a referenced column list' -); - -is( - $beautifier->_render_sql_tokens( - [ 'not', 'null', 'default', q{'{}'}, '::jsonb' ] - ), - q{not null default '{}'::jsonb}, - 'renders a cast without surrounding spaces' -); - -is( - $beautifier->_render_sql_tokens( - [ 'check', '(', 'score', '>=', '0', ')' ] - ), - 'check (score >= 0)', - 'keeps a space after CHECK before a parenthesis' -); - -is( - $beautifier->_render_sql_tokens( - [ 'generated', 'always', 'as', '(', 'lower', '(', 'name', ')', ')', 'stored' ] - ), - 'generated always as (lower(name)) stored', - 'renders nested generated-column expressions' -); - -is( - $beautifier->_render_sql_tokens([]), - '', - 'renders an empty token list as an empty string' -); - -is( - $beautifier->_pad_right( 'id', 5 ), - 'id ', - 'pads a value to the requested width' -); - -is( - $beautifier->_pad_right( 'identifier', 5 ), - 'identifier', - 'does not truncate a value wider than the requested width' -); - -is_deeply( - $beautifier->_align_create_table_column_group( - [ - ' id uuid primary key default gen_random_uuid(),', - ' parent_job_id uuid references queue_job (id) on delete set null,', - ' type queue_job_type not null,', - q{ status queue_job_status not null default 'queued',}, - ' created_at timestamptz not null default now()', - ] - ), - [ - ' id uuid primary key default gen_random_uuid (),', - ' parent_job_id uuid references queue_job (id) on delete set null,', - ' type queue_job_type not null,', - q{ status queue_job_status not null default 'queued',}, - ' created_at timestamptz not null default now()', - ], - 'aligns column names and the beginning of column constraints' -); - -is_deeply( - $beautifier->_align_create_table_column_group( - [ - ' id serial primary key, -- some comment', - ' name varchar(100) not null, -- another comment', - ' email varchar(100) not null, -- third comment', - ' created_at timestamp with time zone default current_timestamp -- last comment', - ] - ), - [ - ' id serial primary key, -- some comment', - ' name varchar(100) not null, -- another comment', - ' email varchar(100) not null, -- third comment', - ' created_at timestamp with time zone default current_timestamp -- last comment', - ], - 'aligns trailing comments after the complete column SQL' -); - -is_deeply( - $beautifier->_align_create_table_column_group( - [ - ' id uuid primary key default gen_random_uuid(),', - ' username varchar(150) not null unique, -- only comment', - ' email varchar not null unique', - ] - ), - [ - ' id uuid primary key default gen_random_uuid (),', - ' username varchar(150) not null unique, -- only comment', - ' email varchar not null unique', - ], - 'aligns a lone trailing comment against the longest supported column' -); - -is_deeply( - $beautifier->_align_create_table_column_group( - [ - ' id uuid primary key,', - ' constraint demo_pk primary key (id)', - ' descriptive_name text', - ] - ), - [ - ' id uuid primary key,', - ' constraint demo_pk primary key (id)', - ' descriptive_name text', - ], - 'preserves unsupported table constraints while aligning column definitions' -); - -is_deeply( - $beautifier->_align_create_table_column_group( - [ - ' first_value text default current_user,', - ' second_value text not null default current_user,', - ' third_value text references app_user (username)', - ] - ), - [ - ' first_value text default current_user,', - ' second_value text not null default current_user,', - ' third_value text references app_user (username)', - ], - 'aligns DEFAULT without moving unrelated constraints' -); - -is_deeply( - $beautifier->_align_create_table_column_group( - [ - ' first_value text default current_user,', - ' second_value text references app_user (username)', - ] - ), - [ - ' first_value text default current_user,', - ' second_value text references app_user (username)', - ], - 'does not add padding when only one row contains DEFAULT' -); - -my @single_column = (' id uuid primary key'); -my $single_result = - $beautifier->_align_create_table_column_group(\@single_column); - -is_deeply( - $single_result, - \@single_column, - 'leaves a single column unchanged' -); - -isnt( - $single_result, - \@single_column, - 'returns a new array reference for a single column' -); - -is_deeply( - $beautifier->_align_create_table_column_group([]), - [], - 'handles an empty group' -); sub format_sql { - my ( $query, $vertical_align ) = @_; - - my $formatter = pgFormatter::Beautify->new( - query => $query, - vertical_align => $vertical_align, - uc_keywords => 1, - uc_types => 1, - uc_functions => 1, - no_space_function => 1, - no_extra_line => 1, - ); - - $formatter->beautify(); - return $formatter->content(); -} - -my $create_table_sql = <<'SQL'; -create table queue_job ( - id uuid primary key default gen_random_uuid(), - parent_job_id uuid references queue_job (id) on delete set null, - status queue_job_status not null default 'queued', - created_at timestamptz not null default now() -); -SQL - -is( - format_sql( $create_table_sql, 1 ), - <<'SQL', -create table queue_job( - id uuid primary key default gen_random_uuid(), - parent_job_id uuid references queue_job (id) on delete set null, - status queue_job_status not null default 'queued', - created_at timestamptz not null default now() -); -SQL - 'applies vertical alignment through the normal formatter output path' -); - -my $commented_table_sql = <<'SQL'; -create table example ( - id serial primary key, -- some comment - name varchar(100) not null, -- another comment - email varchar(100) not null, -- third comment - created_at timestamp with time zone default current_timestamp -- last comment -); -SQL - -is( - format_sql( $commented_table_sql, 1 ), - <<'SQL', -create table example( - id serial primary key, -- some comment - name varchar(100) not null, -- another comment - email varchar(100) not null, -- third comment - created_at timestamp with time zone default current_timestamp -- last comment -); -SQL - 'aligns trailing comments through the normal formatter output path' -); - -my $commented_aligned_once = format_sql( $commented_table_sql, 1 ); -is( - $beautifier->_align_create_table_columns($commented_aligned_once), - $commented_aligned_once, - 'keeps aligned trailing comments stable when alignment runs again' -); - -is( - format_sql( $create_table_sql, 0 ), - <<'SQL', -create table queue_job( - id uuid primary key default gen_random_uuid(), - parent_job_id uuid references queue_job(id) on delete set null, - status queue_job_status not null default 'queued', - created_at timestamptz not null default now() -); -SQL - 'leaves normal formatter output unchanged when vertical alignment is disabled' -); - -sub format_sql_with_default_function_spacing { - my ( $query, $vertical_align ) = @_; - + my ($query) = @_; my $formatter = pgFormatter::Beautify->new( query => $query, - vertical_align => $vertical_align, + vertical_align => 1, uc_keywords => 1, uc_types => 1, uc_functions => 1, @@ -470,44 +20,33 @@ sub format_sql_with_default_function_spacing { return $formatter->content(); } -my $default_spacing_disabled = - format_sql_with_default_function_spacing( $create_table_sql, 0 ); -my $default_spacing_enabled = - format_sql_with_default_function_spacing( $create_table_sql, 1 ); - -like( - $default_spacing_disabled, - qr/default gen_random_uuid \(\)/, - 'normal formatting keeps a space before an unknown function parenthesis' -); - -like( - $default_spacing_enabled, - qr/default gen_random_uuid \(\)/, - 'vertical alignment preserves unknown-function parenthesis spacing' +my $input = <<'SQL'; +create table example ( + id uuid primary key default gen_random_uuid(), + name varchar(100) not null, -- a comment + owner_id uuid references app_user (id), + created_at timestamptz not null default now(), + constraint example_name_unique unique (name) ); +SQL -my $separate_parenthesis = <<'SQL'; -create table demo -( - id uuid primary key, - descriptive_name text +my $expected = <<'SQL'; +create table example ( + id uuid primary key default gen_random_uuid (), + name varchar(100) not null, -- a comment + owner_id uuid references app_user (id), + created_at timestamptz not null default now(), + constraint example_name_unique unique (name) ); SQL is( - $beautifier->_align_create_table_columns($separate_parenthesis), - <<'SQL', -create table demo -( - id uuid primary key, - descriptive_name text -); -SQL - 'detects a CREATE TABLE opening parenthesis on the following line' + format_sql($input), + $expected, + 'aligns columns, defaults, references, and comments' ); -my $nested_definition = <<'SQL'; +my $unsupported = <<'SQL'; CREATE TABLE measurements ( id uuid primary key, score integer check ( @@ -516,152 +55,31 @@ CREATE TABLE measurements ( ), descriptive_name text ); -SQL - -is( - $beautifier->_align_create_table_columns($nested_definition), - <<'SQL', -CREATE TABLE measurements ( - id uuid primary key, - score integer check ( - score >= 0 - and score <= 100 - ), - descriptive_name text -); -SQL - 'preserves multiline nested definitions while aligning other top-level columns' -); -my $create_table_as = <<'SQL'; -CREATE TABLE completed_jobs AS -SELECT id, status -FROM queue_job; +CREATE TABLE copied AS +SELECT id FROM measurements; SQL -is( - $beautifier->_align_create_table_columns($create_table_as), - $create_table_as, - 'skips CREATE TABLE AS statements' -); - -my $multiple_tables = <<'SQL'; -CREATE TABLE first_table ( - id uuid, - descriptive_name text -); - -CREATE TABLE second_table ( - key text, - considerably_longer_value jsonb -); -SQL - -is( - $beautifier->_align_create_table_columns($multiple_tables), - <<'SQL', -CREATE TABLE first_table ( - id uuid, - descriptive_name text -); - -CREATE TABLE second_table ( - key text, - considerably_longer_value jsonb -); -SQL - 'aligns multiple CREATE TABLE statements independently' -); - -my $aligned_once = - $beautifier->_align_create_table_columns($separate_parenthesis); -is( - $beautifier->_align_create_table_columns($aligned_once), - $aligned_once, - 'is idempotent when applied more than once' -); - -is( - $beautifier->_parenthesis_delta( - q{ note text default 'not structural (parenthesis)' -- ignored )} - ), - 0, - 'ignores parentheses contained in strings and comments' -); - -# Verify the option also reaches the formatter through public configuration paths. -my ( $config_fh, $config_path ) = tempfile(); -print {$config_fh} <<'CONFIG'; -vertical-align=1 -keyword-case=1 -type-case=1 -function-case=1 -no-extra-line=1 -CONFIG -close($config_fh); - -my ( $sql_fh, $sql_path ) = tempfile( SUFFIX => '.sql' ); -print {$sql_fh} <<'SQL'; -CREATE TABLE configured_example ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - descriptive_name TEXT NOT NULL -); -SQL -close($sql_fh); - -open my $formatter_output, '-|', - $^X, '-Ilib', 'pg_format', '--config', $config_path, $sql_path - or die "could not run pg_format: $!"; - -my $configured_output = do { - local $/; - <$formatter_output>; -}; -close($formatter_output); - -is( $?, 0, 'pg_format accepts vertical-align from a configuration file' ); - -is( - $configured_output, - <<'SQL', -create table configured_example ( - id uuid primary key default gen_random_uuid (), - descriptive_name text not null -); -SQL - 'configuration-file alignment preserves default function spacing' -); - -my $cgi_formatter = pgFormatter::CGI->new(); +my $beautifier = pgFormatter::Beautify->new(); +my $result = $beautifier->_align_create_table_columns($unsupported); -is( - $cgi_formatter->{'vertical_align'}, - 0, - 'CGI vertical alignment is disabled by default' +like( + $result, + qr/score integer check \(\n score >= 0\n and score <= 100\n \),/, + 'preserves multiline definitions' ); -$cgi_formatter->{'content'} = <<'SQL'; -CREATE TABLE example ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - descriptive_name TEXT NOT NULL +like( + $result, + qr/CREATE TABLE copied AS\nSELECT id FROM measurements;/, + 'skips CREATE TABLE AS' ); -SQL -$cgi_formatter->{'format'} = 'text'; -$cgi_formatter->{'colorize'} = 0; -$cgi_formatter->{'vertical_align'} = 1; - -$cgi_formatter->beautify_query(); +my $aligned = format_sql($input); is( - $cgi_formatter->{'content'}, - <<'SQL', -CREATE TABLE example ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid (), - descriptive_name text NOT NULL -); - -SQL - 'CGI forwards alignment while preserving default function spacing' + $beautifier->_align_create_table_columns($aligned), + $aligned, + 'is idempotent' ); -done_testing(); \ No newline at end of file +done_testing(); From dcfdee2eb6cfb4c821c3fd0642b4f9e209bb7851 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 10 Jul 2026 16:18:17 +0200 Subject: [PATCH 16/18] Readded space that devides options and examples --- lib/pgFormatter/CLI.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pgFormatter/CLI.pm b/lib/pgFormatter/CLI.pm index 4d29099..7a15baa 100755 --- a/lib/pgFormatter/CLI.pm +++ b/lib/pgFormatter/CLI.pm @@ -326,6 +326,7 @@ Options: --redundant-parenthesis: do not remove redundant parenthesis in DML. --vertical-align : vertically align CREATE TABLE column definitions and trailing comments. + Examples: cat samples/ex1.sql | $0 - From 3d3d3e1c1e03ca74d147bf85755f43b45080a9fc Mon Sep 17 00:00:00 2001 From: david Date: Fri, 10 Jul 2026 18:55:23 +0200 Subject: [PATCH 17/18] Simplified test Added the other table because it was my exact usecase --- t/02_regress.t | 2 +- t/test-files/ex81.sql | 8 -------- t/test-files/expected/ex81.sql | 8 -------- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/t/02_regress.t b/t/02_regress.t index f1b742a..b807ddb 100755 --- a/t/02_regress.t +++ b/t/02_regress.t @@ -28,7 +28,7 @@ foreach my $f (@files) $opt = "--keyword-case 2 --function-case 1 --comma-start --wrap-after 1 --wrap-limit 40 --tabs --spaces 4 " if ($f =~ m#/ex58.sql$#); $opt = "--no-space-function" if ($f =~ m#/ex70.sql$#); $opt = "--keyword-case 1 --type-case 1" if ($f =~ m#/ex71.sql$#); - $opt = "--vertical-align --no-extra-line" if ($f =~ m#/ex81\.sql$#); + $opt = "--vertical-align" if ($f =~ m#/ex81\.sql$#); if ($f =~ m#/ex61.sql$#) { my ($fh, $tmpfile) = tempfile('tmp_pgformatXXXX', SUFFIX => '.lst', TMPDIR => 1, O_TEMPORARY => 1, UNLINK => 1 ); diff --git a/t/test-files/ex81.sql b/t/test-files/ex81.sql index 18a854b..58437b4 100644 --- a/t/test-files/ex81.sql +++ b/t/test-files/ex81.sql @@ -4,11 +4,3 @@ CREATE TABLE example ( email varchar(100) NOT NULL, -- third comment created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP -- last comment ); - -CREATE TABLE queue_job ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid (), - parent_job_id uuid REFERENCES queue_job (id) ON DELETE SET NULL, - status queue_job_status NOT NULL DEFAULT 'queued', - payload jsonb NOT NULL DEFAULT '{}'::jsonb, - CONSTRAINT queue_job_status_check CHECK (status <> 'invalid') -); diff --git a/t/test-files/expected/ex81.sql b/t/test-files/expected/ex81.sql index e849855..82d4e88 100644 --- a/t/test-files/expected/ex81.sql +++ b/t/test-files/expected/ex81.sql @@ -4,11 +4,3 @@ CREATE TABLE example ( email varchar(100) NOT NULL, -- third comment created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP -- last comment ); - -CREATE TABLE queue_job ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid (), - parent_job_id uuid REFERENCES queue_job (id) ON DELETE SET NULL, - status queue_job_status NOT NULL DEFAULT 'queued', - payload jsonb NOT NULL DEFAULT '{}'::jsonb, - CONSTRAINT queue_job_status_check CHECK (status <> 'invalid') -); From 862091402e697f89b6c24816a66179ac712589c5 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 10 Jul 2026 18:58:16 +0200 Subject: [PATCH 18/18] Readded needed arg Otherwise results in error --- t/02_regress.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/02_regress.t b/t/02_regress.t index b807ddb..f1b742a 100755 --- a/t/02_regress.t +++ b/t/02_regress.t @@ -28,7 +28,7 @@ foreach my $f (@files) $opt = "--keyword-case 2 --function-case 1 --comma-start --wrap-after 1 --wrap-limit 40 --tabs --spaces 4 " if ($f =~ m#/ex58.sql$#); $opt = "--no-space-function" if ($f =~ m#/ex70.sql$#); $opt = "--keyword-case 1 --type-case 1" if ($f =~ m#/ex71.sql$#); - $opt = "--vertical-align" if ($f =~ m#/ex81\.sql$#); + $opt = "--vertical-align --no-extra-line" if ($f =~ m#/ex81\.sql$#); if ($f =~ m#/ex61.sql$#) { my ($fh, $tmpfile) = tempfile('tmp_pgformatXXXX', SUFFIX => '.lst', TMPDIR => 1, O_TEMPORARY => 1, UNLINK => 1 );