Skip to content

cli ergonomics #172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 29, 2025
Merged

cli ergonomics #172

merged 7 commits into from
Jan 29, 2025

Conversation

psteinroe
Copy link
Collaborator

@psteinroe psteinroe commented Jan 21, 2025

goal of this pr is to gear the cli towards the use case we want to support initially: running a check command on migration file(s).

after a discussion with @juleswritescode, this is the plan:

We will have one single pgtoools check command. by default, it will scan everything for sql files and lint it. it can be filtered by path, e.g. pgtools check supabase/migrations to just lint the migration directory. We will have a migrations-dir config option that will enable extra filtering features for files within the directory, primarily --after and maybe --last-nth. Furthermore, we will also have --staged and --changed to easily lint just the relevant migrations in a CI environment or a pre-commit hook. The existing --include and --exclude filters will continue to work.

To enable --after, we will pass a --migrations-pattern.

how popular tools store migrations:

  • drizzle: migration.sql in a directory with the timestamp and the name. (link)
  • prisma: same as drizzle (link)
  • supabase: migrations dir with sql files
  • typeorm: stores migrations in ts files -> not supported for now

UPDATE: I removed --migrations-pattern again - we just try both and check if any yield an expected value. Added migration_dir as well as after :)

I initially tried to put it into get_files_to_process but that only looks at the inputs from the user and we need the traversal output to look at each file recursively. the right place for that is can_handle, which calls is_path_ignored from the workspace. the downside of my approach is that we check ignore patterns also for files, whereas before we just checked it for directories and symlinks.... an alternative would be a new workspace api aka is_migration_ignored, but that would conflict with is_path_ignored imo.

@psteinroe psteinroe marked this pull request as ready for review January 27, 2025 21:29
Comment on lines +459 to +465

let is_valid_file = self.fs.path_is_file(path)
&& path
.extension()
.is_some_and(|ext| ext == "sql" || ext == "pg");

if self.fs.path_is_dir(path) || self.fs.path_is_symlink(path) || is_valid_file {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here is the relevant part in can_handle

/// Check whether a file is ignored in the top-level config `files.ignore`/`files.include`
fn is_ignored(&self, path: &Path) -> bool {
let file_name = path.file_name().and_then(|s| s.to_str());
// Never ignore PGLSP's config file regardless `include`/`ignore`
(file_name != Some(ConfigName::pglsp_toml())) &&
// Apply top-level `include`/`ignore
(self.is_ignored_by_top_level_config(path))
(self.is_ignored_by_top_level_config(path) || self.is_ignored_by_migration_config(path))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_ignored is called by is_path_ignored and we add the new check here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very sweet!

use tempfile::TempDir;

fn setup() -> TempDir {
TempDir::new().expect("Failed to create temp dir")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tempfile is required to have proper test that work with canonicalize

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very cool!

pglsp.toml Outdated
Comment on lines 16 to 18
[migrations]
migrations_dir = "migrations"
after = 20230912094327
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested it locally but removed the migrations dir again

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we remove it? or comment out the properties?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point :D

Copy link
Collaborator

@juleswritescode juleswritescode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very beautiful PR and great description! Love it 😍

pglsp.toml Outdated
Comment on lines 16 to 18
[migrations]
migrations_dir = "migrations"
after = 20230912094327
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we remove it? or comment out the properties?

Comment on lines 98 to 100
fn test_get_migration_non_migration_file() {
let migrations_dir = PathBuf::from("/tmp/migrations");
let path = migrations_dir.join("not_a_migration.sql");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully understand what we're checking here – are we checking what happens if the .sql file is empty? or if it doesn't match the pattern?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesnt match the pattern since it doesnt contain a timestamp

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clarified it in the test name 👍

use tempfile::TempDir;

fn setup() -> TempDir {
TempDir::new().expect("Failed to create temp dir")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very cool!

.and_then(|os_str| os_str.to_str())
.and_then(|file_name| {
let mut parts = file_name.splitn(2, '_');
let timestamp = parts.next()?.parse().ok()?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really liked the explanation in the PR description about how popular tools all use similar migrations formats. Should we add a comment somewhere here that we expect a <TimestampInMs>_<rest> format?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, added comments

@@ -56,7 +62,7 @@ impl CommandRunner for CheckCommandPayload {
}

fn should_write(&self) -> bool {
self.write || self.fix
false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sanity check: did you just change this for debugging?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, I removed all the --fix stuff because ware not fixing anything. its a leftover from biomes check command.

pub struct MigrationsConfiguration {
/// The directory where the migration files are stored
#[partial(bpaf(long("migrations-dir")))]
pub migrations_dir: String,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we use bpaf-derive, but if we do, we could also parse into a PathBuf.

Examples of that were hard to find, so maybe that's not the recommended way – I'm not sure

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that would be preferred, but biomes Merge does not work with PathBuf :(

/// Check whether a file is ignored in the top-level config `files.ignore`/`files.include`
fn is_ignored(&self, path: &Path) -> bool {
let file_name = path.file_name().and_then(|s| s.to_str());
// Never ignore PGLSP's config file regardless `include`/`ignore`
(file_name != Some(ConfigName::pglsp_toml())) &&
// Apply top-level `include`/`ignore
(self.is_ignored_by_top_level_config(path))
(self.is_ignored_by_top_level_config(path) || self.is_ignored_by_migration_config(path))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very sweet!


Some(&migration.timestamp <= ignore_before)
})
.unwrap_or(false)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if we pass a migrations_dir but no after? Wouldn't the and_then return None, so the function would always return false?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, and we do not want to ignore any migration file if no after is specified do we?

@psteinroe psteinroe merged commit 3f8b479 into main Jan 29, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants