Skip to content

Commit 481ddf3

Browse files
committed
feature: allow to pass options to format command
1 parent 2d31427 commit 481ddf3

4 files changed

Lines changed: 184 additions & 36 deletions

File tree

src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/Command/FormatSqlCommand.php

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
namespace Flow\Bridge\Symfony\PostgreSqlBundle\Command;
66

77
use function Flow\Filesystem\DSL\{native_local_filesystem, path};
8-
use function Flow\PostgreSql\DSL\sql_format;
9-
use function Flow\Types\DSL\type_string;
8+
use function Flow\PostgreSql\DSL\{sql_deparse_options, sql_format};
9+
use function Flow\Types\DSL\{type_boolean, type_integer, type_string};
10+
use Flow\Bridge\Symfony\PostgreSqlBundle\Sql\SqlFileFinder;
1011
use Flow\Filesystem\Local\NativeLocalFilesystem;
11-
use Flow\Filesystem\Path as FsPath;
12+
use Flow\PostgreSql\DeparseOptions;
1213
use Symfony\Component\Console\Attribute\AsCommand;
1314
use Symfony\Component\Console\Command\Command;
1415
use Symfony\Component\Console\Input\{InputArgument, InputInterface, InputOption};
@@ -31,28 +32,35 @@ protected function configure() : void
3132
->addArgument('sql', InputArgument::OPTIONAL, 'Raw SQL string to format. Ignored when --path is used.')
3233
->addOption('path', 'p', InputOption::VALUE_REQUIRED, 'Path, directory or glob pattern (only files with .sql extension are processed)')
3334
->addOption('write', 'w', InputOption::VALUE_NONE, 'Write formatted SQL back to source files instead of printing to stdout')
34-
->addOption('check', null, InputOption::VALUE_NONE, 'Exit with non-zero status when any input is not already formatted (does not modify files)');
35+
->addOption('check', null, InputOption::VALUE_NONE, 'Exit with non-zero status when any input is not already formatted (does not modify files)')
36+
->addOption('indent-size', null, InputOption::VALUE_REQUIRED, 'Number of spaces used for indentation', 4)
37+
->addOption('max-line-length', null, InputOption::VALUE_REQUIRED, 'Maximum line length before wrapping', 80)
38+
->addOption('no-pretty-print', null, InputOption::VALUE_NONE, 'Disable pretty-printing (output single-line SQL)')
39+
->addOption('commas-start-of-line', null, InputOption::VALUE_NONE, 'Place commas at the start of the line instead of the end')
40+
->addOption('trailing-newline', null, InputOption::VALUE_NONE, 'Append a trailing newline to formatted SQL');
3541
}
3642

3743
protected function execute(InputInterface $input, OutputInterface $output) : int
3844
{
3945
$pathOption = $input->getOption('path');
4046
$sqlArgument = $input->getArgument('sql');
41-
$write = (bool) $input->getOption('write');
42-
$check = (bool) $input->getOption('check');
47+
$write = type_boolean()->cast($input->getOption('write'));
48+
$check = type_boolean()->cast($input->getOption('check'));
4349

4450
if ($pathOption === null && $sqlArgument === null) {
4551
$output->writeln('<error>Either a SQL string argument or --path option must be provided.</error>');
4652

4753
return Command::FAILURE;
4854
}
4955

56+
$options = $this->buildDeparseOptions($input);
57+
5058
if ($pathOption !== null) {
51-
return $this->formatPath(type_string()->assert($pathOption), $write, $check, $output);
59+
return $this->formatPath(type_string()->assert($pathOption), $write, $check, $output, $options);
5260
}
5361

5462
$sql = type_string()->assert($sqlArgument);
55-
$formatted = sql_format($sql);
63+
$formatted = sql_format($sql, $options);
5664

5765
if ($check) {
5866
if (\trim($formatted) !== \trim($sql)) {
@@ -71,37 +79,19 @@ protected function execute(InputInterface $input, OutputInterface $output) : int
7179
return Command::SUCCESS;
7280
}
7381

74-
/**
75-
* @return list<FsPath>
76-
*/
77-
private function collectSqlFiles(FsPath $path) : array
82+
private function buildDeparseOptions(InputInterface $input) : DeparseOptions
7883
{
79-
$status = $this->filesystem->status($path);
80-
81-
if ($status !== null && $status->isDirectory()) {
82-
$path = path(\rtrim($path->path(), '/') . '/**/*.sql', $path->options());
83-
}
84-
85-
$files = [];
86-
87-
foreach ($this->filesystem->list($path) as $file) {
88-
if (!$file->isFile()) {
89-
continue;
90-
}
91-
92-
if ($file->path->extension() !== 'sql') {
93-
continue;
94-
}
95-
96-
$files[] = $file->path;
97-
}
98-
99-
return $files;
84+
return sql_deparse_options()
85+
->indentSize(type_integer()->cast($input->getOption('indent-size')))
86+
->maxLineLength(type_integer()->cast($input->getOption('max-line-length')))
87+
->prettyPrint(!type_boolean()->cast($input->getOption('no-pretty-print')))
88+
->commasStartOfLine(type_boolean()->cast($input->getOption('commas-start-of-line')))
89+
->trailingNewline(type_boolean()->cast($input->getOption('trailing-newline')));
10090
}
10191

102-
private function formatPath(string $pathString, bool $write, bool $check, OutputInterface $output) : int
92+
private function formatPath(string $pathString, bool $write, bool $check, OutputInterface $output, DeparseOptions $options) : int
10393
{
104-
$files = $this->collectSqlFiles(path($pathString));
94+
$files = (new SqlFileFinder(native_local_filesystem()))->find(path($pathString));
10595

10696
if ($files === []) {
10797
$output->writeln(\sprintf('<comment>No .sql files found at: %s</comment>', $pathString));
@@ -116,7 +106,7 @@ private function formatPath(string $pathString, bool $write, bool $check, Output
116106
$original = $this->filesystem->readFrom($file)->content();
117107

118108
try {
119-
$formatted = sql_format($original);
109+
$formatted = sql_format($original, $options);
120110
} catch (\Throwable $e) {
121111
$output->writeln(\sprintf('<error>Failed to format %s: %s</error>', $file->path(), $e->getMessage()));
122112

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Bridge\Symfony\PostgreSqlBundle\Sql;
6+
7+
use function Flow\Filesystem\DSL\path;
8+
use Flow\Filesystem\Local\NativeLocalFilesystem;
9+
use Flow\Filesystem\Path;
10+
11+
final readonly class SqlFileFinder
12+
{
13+
public function __construct(private NativeLocalFilesystem $filesystem)
14+
{
15+
}
16+
17+
/**
18+
* Resolves a Path (file, directory, or glob) into a list of `.sql` files.
19+
* Directories are scanned recursively.
20+
*
21+
* @return list<Path>
22+
*/
23+
public function find(Path $path) : array
24+
{
25+
$status = $this->filesystem->status($path);
26+
27+
if ($status !== null && $status->isDirectory()) {
28+
$path = path(\rtrim($path->path(), '/') . '/**/*.sql', $path->options());
29+
}
30+
31+
$files = [];
32+
33+
foreach ($this->filesystem->list($path) as $file) {
34+
if (!$file->isFile()) {
35+
continue;
36+
}
37+
38+
if ($file->path->extension() !== 'sql') {
39+
continue;
40+
}
41+
42+
$files[] = $file->path;
43+
}
44+
45+
return $files;
46+
}
47+
}

src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Integration/Command/FormatSqlCommandTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ public function test_check_option_succeeds_for_already_formatted_sql() : void
4141
self::assertStringContainsString('already formatted', $tester->getDisplay());
4242
}
4343

44+
public function test_commas_at_start_of_line_option_is_applied() : void
45+
{
46+
$tester = new CommandTester($this->context->command('flow.postgresql.command.sql_format'));
47+
$tester->execute([
48+
'sql' => 'SELECT id, name, email, created_at, updated_at FROM users',
49+
'--commas-start-of-line' => true,
50+
'--max-line-length' => '20',
51+
]);
52+
53+
self::assertSame(Command::SUCCESS, $tester->getStatusCode());
54+
self::assertMatchesRegularExpression('/\n\s*,\s*\w/', $tester->getDisplay());
55+
}
56+
4457
public function test_fails_when_no_input_provided() : void
4558
{
4659
$tester = new CommandTester($this->context->command('flow.postgresql.command.sql_format'));
@@ -102,6 +115,15 @@ public function test_formats_sql_string_argument() : void
102115
self::assertStringContainsString('FROM users', $tester->getDisplay());
103116
}
104117

118+
public function test_no_pretty_print_outputs_single_line_sql() : void
119+
{
120+
$tester = new CommandTester($this->context->command('flow.postgresql.command.sql_format'));
121+
$tester->execute(['sql' => 'SELECT id, name FROM users WHERE id = 1', '--no-pretty-print' => true]);
122+
123+
self::assertSame(Command::SUCCESS, $tester->getStatusCode());
124+
self::assertStringNotContainsString("\n ", $tester->getDisplay());
125+
}
126+
105127
public function test_reports_when_no_sql_files_found() : void
106128
{
107129
$tester = new CommandTester($this->context->command('flow.postgresql.command.sql_format'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Bridge\Symfony\PostgreSqlBundle\Tests\Integration\Sql;
6+
7+
use function Flow\Filesystem\DSL\native_local_filesystem;
8+
use Flow\Bridge\Symfony\PostgreSqlBundle\Sql\SqlFileFinder;
9+
use Flow\Bridge\Symfony\PostgreSqlBundle\Tests\Context\FilesystemContext;
10+
use PHPUnit\Framework\TestCase;
11+
12+
final class SqlFileFinderTest extends TestCase
13+
{
14+
private SqlFileFinder $finder;
15+
16+
private FilesystemContext $fs;
17+
18+
protected function setUp() : void
19+
{
20+
$this->fs = new FilesystemContext('flow_sql_finder_');
21+
$this->finder = new SqlFileFinder(native_local_filesystem());
22+
}
23+
24+
protected function tearDown() : void
25+
{
26+
$this->fs->cleanup();
27+
}
28+
29+
public function test_finds_single_sql_file_by_exact_path() : void
30+
{
31+
$file = $this->fs->writeFile('query.sql', 'SELECT 1');
32+
33+
$found = $this->finder->find($file);
34+
35+
self::assertCount(1, $found);
36+
self::assertSame($file->path(), $found[0]->path());
37+
}
38+
39+
public function test_finds_sql_files_recursively_in_directory() : void
40+
{
41+
$this->fs->writeFile('a.sql', 'SELECT 1');
42+
$this->fs->writeFile('nested/deep/b.sql', 'SELECT 2');
43+
$this->fs->writeFile('nested/c.sql', 'SELECT 3');
44+
45+
$paths = \array_map(static fn ($p) => \basename($p->path()), $this->finder->find($this->fs->path()));
46+
47+
\sort($paths);
48+
self::assertSame(['a.sql', 'b.sql', 'c.sql'], $paths);
49+
}
50+
51+
public function test_finds_sql_files_via_glob_pattern() : void
52+
{
53+
$this->fs->writeFile('one.sql', 'SELECT 1');
54+
$this->fs->writeFile('two.sql', 'SELECT 2');
55+
$this->fs->writeFile('skip.txt', 'nope');
56+
57+
$found = $this->finder->find($this->fs->path('*.sql'));
58+
59+
self::assertCount(2, $found);
60+
}
61+
62+
public function test_returns_empty_list_for_empty_directory() : void
63+
{
64+
self::assertSame([], $this->finder->find($this->fs->path()));
65+
}
66+
67+
public function test_skips_non_sql_extensions_in_directory() : void
68+
{
69+
$this->fs->writeFile('keep.sql', 'SELECT 1');
70+
$this->fs->writeFile('skip.txt', 'nope');
71+
$this->fs->writeFile('skip.md', 'nope');
72+
73+
$found = $this->finder->find($this->fs->path());
74+
75+
self::assertCount(1, $found);
76+
self::assertSame('keep.sql', \basename($found[0]->path()));
77+
}
78+
79+
public function test_skips_non_sql_files_matched_by_broad_glob() : void
80+
{
81+
$this->fs->writeFile('a.sql', 'SELECT 1');
82+
$this->fs->writeFile('b.txt', 'nope');
83+
84+
$found = $this->finder->find($this->fs->path('*'));
85+
86+
self::assertCount(1, $found);
87+
self::assertSame('a.sql', \basename($found[0]->path()));
88+
}
89+
}

0 commit comments

Comments
 (0)