diff --git a/Cargo.lock b/Cargo.lock index f9c464468..e52723863 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2232,6 +2232,8 @@ name = "pg_cli" version = "0.0.0" dependencies = [ "anyhow", + "biome_deserialize", + "biome_deserialize_macros", "bpaf", "crossbeam", "dashmap 5.5.3", @@ -2658,6 +2660,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "tempfile", "text-size", "tokio", "toml", @@ -3788,12 +3791,13 @@ checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" [[package]] name = "tempfile" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand 2.3.0", + "getrandom", "once_cell", "rustix 0.38.42", "windows-sys 0.59.0", diff --git a/crates/pg_cli/Cargo.toml b/crates/pg_cli/Cargo.toml index d49036985..d04f88c5d 100644 --- a/crates/pg_cli/Cargo.toml +++ b/crates/pg_cli/Cargo.toml @@ -12,31 +12,33 @@ version = "0.0.0" [dependencies] -anyhow = { workspace = true } -bpaf = { workspace = true, features = ["bright-color"] } -crossbeam = { workspace = true } -dashmap = "5.5.3" -hdrhistogram = { version = "7.5.4", default-features = false } -path-absolutize = { version = "3.1.1", optional = false, features = ["use_unix_paths_on_wasm"] } -pg_analyse = { workspace = true } -pg_configuration = { workspace = true } -pg_console = { workspace = true } -pg_diagnostics = { workspace = true } -pg_flags = { workspace = true } -pg_fs = { workspace = true } -pg_lsp = { workspace = true } -pg_text_edit = { workspace = true } -pg_workspace = { workspace = true } -quick-junit = "0.5.0" -rayon = { workspace = true } -rustc-hash = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["io-std", "io-util", "net", "time", "rt", "sync", "rt-multi-thread", "macros"] } -tracing = { workspace = true } -tracing-appender = "0.2.3" -tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } -tracing-tree = "0.4.0" +anyhow = { workspace = true } +biome_deserialize = { workspace = true } +biome_deserialize_macros = { workspace = true } +bpaf = { workspace = true, features = ["bright-color"] } +crossbeam = { workspace = true } +dashmap = "5.5.3" +hdrhistogram = { version = "7.5.4", default-features = false } +path-absolutize = { version = "3.1.1", optional = false, features = ["use_unix_paths_on_wasm"] } +pg_analyse = { workspace = true } +pg_configuration = { workspace = true } +pg_console = { workspace = true } +pg_diagnostics = { workspace = true } +pg_flags = { workspace = true } +pg_fs = { workspace = true } +pg_lsp = { workspace = true } +pg_text_edit = { workspace = true } +pg_workspace = { workspace = true } +quick-junit = "0.5.0" +rayon = { workspace = true } +rustc-hash = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["io-std", "io-util", "net", "time", "rt", "sync", "rt-multi-thread", "macros"] } +tracing = { workspace = true } +tracing-appender = "0.2.3" +tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } +tracing-tree = "0.4.0" [target.'cfg(unix)'.dependencies] libc = "0.2.161" diff --git a/crates/pg_cli/src/commands/check.rs b/crates/pg_cli/src/commands/check.rs index 72f4d0076..ea6084af9 100644 --- a/crates/pg_cli/src/commands/check.rs +++ b/crates/pg_cli/src/commands/check.rs @@ -1,5 +1,6 @@ use crate::cli_options::CliOptions; use crate::{CliDiagnostic, Execution, TraversalMode}; +use biome_deserialize::Merge; use pg_configuration::PartialConfiguration; use pg_console::Console; use pg_fs::FileSystem; @@ -9,9 +10,6 @@ use std::ffi::OsString; use super::{get_files_to_process_with_cli_options, CommandRunner}; pub(crate) struct CheckCommandPayload { - pub(crate) write: bool, - pub(crate) fix: bool, - pub(crate) unsafe_: bool, pub(crate) configuration: Option, pub(crate) paths: Vec, pub(crate) stdin_file_path: Option, @@ -26,12 +24,20 @@ impl CommandRunner for CheckCommandPayload { fn merge_configuration( &mut self, loaded_configuration: LoadedConfiguration, - fs: &DynRef<'_, dyn FileSystem>, - console: &mut dyn Console, + _fs: &DynRef<'_, dyn FileSystem>, + _console: &mut dyn Console, ) -> Result { - let LoadedConfiguration { configuration, .. } = loaded_configuration; + let LoadedConfiguration { + configuration: mut fs_configuration, + .. + } = loaded_configuration; + + if let Some(configuration) = self.configuration.clone() { + // overwrite fs config with cli args + fs_configuration.merge_with(configuration); + } - Ok(configuration) + Ok(fs_configuration) } fn get_files_to_process( @@ -56,7 +62,7 @@ impl CommandRunner for CheckCommandPayload { } fn should_write(&self) -> bool { - self.write || self.fix + false } fn get_execution( diff --git a/crates/pg_cli/src/commands/mod.rs b/crates/pg_cli/src/commands/mod.rs index c01cd6a45..6b47ecc65 100644 --- a/crates/pg_cli/src/commands/mod.rs +++ b/crates/pg_cli/src/commands/mod.rs @@ -11,7 +11,7 @@ use pg_console::Console; use pg_fs::FileSystem; use pg_workspace::configuration::{load_configuration, LoadedConfiguration}; use pg_workspace::settings::PartialConfigurationExt; -use pg_workspace::workspace::{FixFileMode, UpdateSettingsParams}; +use pg_workspace::workspace::UpdateSettingsParams; use pg_workspace::{DynRef, Workspace, WorkspaceError}; use std::ffi::OsString; use std::path::PathBuf; @@ -33,18 +33,6 @@ pub enum PgLspCommand { /// Runs everything to the requested files. #[bpaf(command)] Check { - /// Writes safe fixes, formatting and import sorting - #[bpaf(long("write"), switch)] - write: bool, - - /// Allow to do unsafe fixes, should be used with `--write` or `--fix` - #[bpaf(long("unsafe"), switch)] - unsafe_: bool, - - /// Alias for `--write`, writes safe fixes, formatting and import sorting - #[bpaf(long("fix"), switch, hide_usage)] - fix: bool, - #[bpaf(external(partial_configuration), hide_usage, optional)] configuration: Option, #[bpaf(external, hide_usage)] @@ -401,165 +389,9 @@ fn get_files_to_process_with_cli_options( } } -/// Holds the options to determine the fix file mode. -pub(crate) struct FixFileModeOptions { - write: bool, - suppress: bool, - suppression_reason: Option, - fix: bool, - unsafe_: bool, -} - -/// - [Result]: if the given options are incompatible -/// - [Option]: if no fixes are requested -/// - [FixFileMode]: if safe or unsafe fixes are requested -pub(crate) fn determine_fix_file_mode( - options: FixFileModeOptions, - console: &mut dyn Console, -) -> Result, CliDiagnostic> { - let FixFileModeOptions { - write, - fix, - suppress, - suppression_reason: _, - unsafe_, - } = options; - - check_fix_incompatible_arguments(options)?; - - let safe_fixes = write || fix; - let unsafe_fixes = (write || safe_fixes) && unsafe_; - - if unsafe_fixes { - Ok(Some(FixFileMode::SafeAndUnsafeFixes)) - } else if safe_fixes { - Ok(Some(FixFileMode::SafeFixes)) - } else if suppress { - Ok(Some(FixFileMode::ApplySuppressions)) - } else { - Ok(None) - } -} - -/// Checks if the fix file options are incompatible. -fn check_fix_incompatible_arguments(options: FixFileModeOptions) -> Result<(), CliDiagnostic> { - let FixFileModeOptions { - write, - suppress, - suppression_reason, - fix, - .. - } = options; - if write && fix { - return Err(CliDiagnostic::incompatible_arguments("--write", "--fix")); - } else if suppress && write { - return Err(CliDiagnostic::incompatible_arguments( - "--suppress", - "--write", - )); - } else if suppress && fix { - return Err(CliDiagnostic::incompatible_arguments("--suppress", "--fix")); - } else if !suppress && suppression_reason.is_some() { - return Err(CliDiagnostic::unexpected_argument( - "--reason", - "`--reason` is only valid when `--suppress` is used.", - )); - }; - Ok(()) -} - #[cfg(test)] mod tests { use super::*; - use pg_console::BufferConsole; - - #[test] - fn incompatible_arguments() { - { - let (write, suppress, suppression_reason, fix, unsafe_) = - (true, false, None, true, false); - assert!(check_fix_incompatible_arguments(FixFileModeOptions { - write, - suppress, - suppression_reason, - fix, - unsafe_ - }) - .is_err()); - } - } - - #[test] - fn safe_fixes() { - let mut console = BufferConsole::default(); - - for (write, suppress, suppression_reason, fix, unsafe_) in [ - (true, false, None, false, false), // --write - (false, false, None, true, false), // --fix - ] { - assert_eq!( - determine_fix_file_mode( - FixFileModeOptions { - write, - suppress, - suppression_reason, - fix, - unsafe_ - }, - &mut console - ) - .unwrap(), - Some(FixFileMode::SafeFixes) - ); - } - } - - #[test] - fn safe_and_unsafe_fixes() { - let mut console = BufferConsole::default(); - - for (write, suppress, suppression_reason, fix, unsafe_) in [ - (true, false, None, false, true), // --write --unsafe - (false, false, None, true, true), // --fix --unsafe - ] { - assert_eq!( - determine_fix_file_mode( - FixFileModeOptions { - write, - suppress, - suppression_reason, - fix, - unsafe_ - }, - &mut console - ) - .unwrap(), - Some(FixFileMode::SafeAndUnsafeFixes) - ); - } - } - - #[test] - fn no_fix() { - let mut console = BufferConsole::default(); - - let (write, suppress, suppression_reason, fix, unsafe_) = - (false, false, None, false, false); - assert_eq!( - determine_fix_file_mode( - FixFileModeOptions { - write, - suppress, - suppression_reason, - fix, - unsafe_ - }, - &mut console - ) - .unwrap(), - None - ); - } /// Tests that all CLI options adhere to the invariants expected by `bpaf`. #[test] diff --git a/crates/pg_cli/src/execute/traverse.rs b/crates/pg_cli/src/execute/traverse.rs index 98a831c83..873b89053 100644 --- a/crates/pg_cli/src/execute/traverse.rs +++ b/crates/pg_cli/src/execute/traverse.rs @@ -456,7 +456,13 @@ impl TraversalContext for TraversalOptions<'_, '_> { fn can_handle(&self, pglsp_path: &PgLspPath) -> bool { let path = pglsp_path.as_path(); - if self.fs.path_is_dir(path) || self.fs.path_is_symlink(path) { + + 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 { // handle: // - directories // - symlinks @@ -476,7 +482,7 @@ impl TraversalContext for TraversalOptions<'_, '_> { } // bail on fifo and socket files - if !self.fs.path_is_file(path) { + if !is_valid_file { return false; } diff --git a/crates/pg_cli/src/lib.rs b/crates/pg_cli/src/lib.rs index 8bed85095..8dabff3ad 100644 --- a/crates/pg_cli/src/lib.rs +++ b/crates/pg_cli/src/lib.rs @@ -67,9 +67,6 @@ impl<'app> CliSession<'app> { let result = match command { PgLspCommand::Version(_) => commands::version::full_version(self), PgLspCommand::Check { - write, - fix, - unsafe_, cli_options, configuration, paths, @@ -81,9 +78,6 @@ impl<'app> CliSession<'app> { self, &cli_options, CheckCommandPayload { - write, - fix, - unsafe_, configuration, paths, stdin_file_path, diff --git a/crates/pg_configuration/src/database.rs b/crates/pg_configuration/src/database.rs index 3302e5846..9b35ae61b 100644 --- a/crates/pg_configuration/src/database.rs +++ b/crates/pg_configuration/src/database.rs @@ -1,10 +1,10 @@ -use biome_deserialize_macros::Partial; +use biome_deserialize_macros::{Merge, Partial}; use bpaf::Bpaf; use serde::{Deserialize, Serialize}; /// The configuration of the database connection. #[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize)] -#[partial(derive(Bpaf, Clone, Eq, PartialEq))] +#[partial(derive(Bpaf, Clone, Eq, PartialEq, Merge))] #[partial(serde(rename_all = "snake_case", default, deny_unknown_fields))] pub struct DatabaseConfiguration { /// The host of the database. diff --git a/crates/pg_configuration/src/files.rs b/crates/pg_configuration/src/files.rs index 5ae70dab1..039d785be 100644 --- a/crates/pg_configuration/src/files.rs +++ b/crates/pg_configuration/src/files.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU64; use biome_deserialize::StringSet; -use biome_deserialize_macros::Partial; +use biome_deserialize_macros::{Merge, Partial}; use bpaf::Bpaf; use serde::{Deserialize, Serialize}; @@ -12,7 +12,7 @@ pub const DEFAULT_FILE_SIZE_LIMIT: NonZeroU64 = /// The configuration of the filesystem #[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize)] -#[partial(derive(Bpaf, Clone, Eq, PartialEq))] +#[partial(derive(Bpaf, Clone, Eq, PartialEq, Merge))] #[partial(serde(rename_all = "snake_case", default, deny_unknown_fields))] pub struct FilesConfiguration { /// The maximum allowed size for source code files in bytes. Files above diff --git a/crates/pg_configuration/src/lib.rs b/crates/pg_configuration/src/lib.rs index 6d2e5f600..9dd64f892 100644 --- a/crates/pg_configuration/src/lib.rs +++ b/crates/pg_configuration/src/lib.rs @@ -8,6 +8,7 @@ pub mod database; pub mod diagnostics; pub mod files; pub mod generated; +pub mod migrations; pub mod vcs; pub use crate::diagnostics::ConfigurationDiagnostic; @@ -21,12 +22,15 @@ pub use analyser::{ RuleConfiguration, RuleFixConfiguration, RulePlainConfiguration, RuleSelector, RuleWithFixOptions, RuleWithOptions, Rules, }; -use biome_deserialize_macros::Partial; +use biome_deserialize_macros::{Merge, Partial}; use bpaf::Bpaf; use database::{ partial_database_configuration, DatabaseConfiguration, PartialDatabaseConfiguration, }; use files::{partial_files_configuration, FilesConfiguration, PartialFilesConfiguration}; +use migrations::{ + partial_migrations_configuration, MigrationsConfiguration, PartialMigrationsConfiguration, +}; use serde::{Deserialize, Serialize}; use vcs::VcsClientKind; @@ -37,7 +41,7 @@ pub const VERSION: &str = match option_env!("PGLSP_VERSION") { /// The configuration that is contained inside the configuration file. #[derive(Clone, Debug, Default, Deserialize, Eq, Partial, PartialEq, Serialize)] -#[partial(derive(Bpaf, Clone, Eq, PartialEq))] +#[partial(derive(Bpaf, Clone, Eq, PartialEq, Merge))] #[partial(cfg_attr(feature = "schema", derive(schemars::JsonSchema)))] #[partial(serde(deny_unknown_fields, rename_all = "snake_case"))] pub struct Configuration { @@ -52,6 +56,13 @@ pub struct Configuration { )] pub files: FilesConfiguration, + /// Configure migrations + #[partial( + type, + bpaf(external(partial_migrations_configuration), optional, hide_usage) + )] + pub migrations: MigrationsConfiguration, + /// The configuration for the linter #[partial(type, bpaf(external(partial_linter_configuration), optional))] pub linter: LinterConfiguration, @@ -72,6 +83,7 @@ impl PartialConfiguration { ignore: Some(Default::default()), ..Default::default() }), + migrations: None, vcs: Some(PartialVcsConfiguration { enabled: Some(false), client_kind: Some(VcsClientKind::Git), diff --git a/crates/pg_configuration/src/migrations.rs b/crates/pg_configuration/src/migrations.rs new file mode 100644 index 000000000..2c7842203 --- /dev/null +++ b/crates/pg_configuration/src/migrations.rs @@ -0,0 +1,17 @@ +use biome_deserialize_macros::{Merge, Partial}; +use bpaf::Bpaf; +use serde::{Deserialize, Serialize}; + +/// The configuration of the filesystem +#[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize, Default)] +#[partial(derive(Bpaf, Clone, Eq, PartialEq, Merge))] +#[partial(serde(rename_all = "snake_case", default, deny_unknown_fields))] +pub struct MigrationsConfiguration { + /// The directory where the migration files are stored + #[partial(bpaf(long("migrations-dir")))] + pub migrations_dir: String, + + /// Ignore any migrations before this timestamp + #[partial(bpaf(long("after")))] + pub after: u64, +} diff --git a/crates/pg_workspace/Cargo.toml b/crates/pg_workspace/Cargo.toml index 69ccc89ca..159ac509c 100644 --- a/crates/pg_workspace/Cargo.toml +++ b/crates/pg_workspace/Cargo.toml @@ -39,6 +39,7 @@ tree-sitter.workspace = true tree_sitter_sql.workspace = true [dev-dependencies] +tempfile = "3.15.0" [lib] doctest = false diff --git a/crates/pg_workspace/src/settings.rs b/crates/pg_workspace/src/settings.rs index 376a4aa68..e016e4bb9 100644 --- a/crates/pg_workspace/src/settings.rs +++ b/crates/pg_workspace/src/settings.rs @@ -4,13 +4,17 @@ use std::{ borrow::Cow, num::NonZeroU64, path::{Path, PathBuf}, + str::FromStr, sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}, }; use ignore::gitignore::{Gitignore, GitignoreBuilder}; use pg_configuration::{ - database::PartialDatabaseConfiguration, diagnostics::InvalidIgnorePattern, - files::FilesConfiguration, ConfigurationDiagnostic, LinterConfiguration, PartialConfiguration, + database::PartialDatabaseConfiguration, + diagnostics::InvalidIgnorePattern, + files::FilesConfiguration, + migrations::{self, MigrationsConfiguration, PartialMigrationsConfiguration}, + ConfigurationDiagnostic, LinterConfiguration, PartialConfiguration, }; use pg_fs::FileSystem; @@ -27,6 +31,9 @@ pub struct Settings { /// Linter settings applied to all files in the workspace pub linter: LinterSettings, + + /// Migrations settings + pub migrations: Option, } #[derive(Debug)] @@ -99,6 +106,14 @@ impl Settings { to_linter_settings(working_directory.clone(), LinterConfiguration::from(linter))?; } + // Migrations settings + if let Some(migrations) = configuration.migrations { + self.migrations = to_migration_settings( + working_directory.clone(), + MigrationsConfiguration::from(migrations), + ); + } + Ok(()) } @@ -304,6 +319,32 @@ pub struct FilesSettings { pub git_ignore: Option, } +/// Migration settings +#[derive(Debug, Default)] +pub struct MigrationSettings { + pub path: Option, + pub after: Option, +} + +impl From for MigrationSettings { + fn from(value: PartialMigrationsConfiguration) -> Self { + Self { + path: value.migrations_dir.map(PathBuf::from), + after: value.after, + } + } +} + +fn to_migration_settings( + working_directory: Option, + conf: MigrationsConfiguration, +) -> Option { + working_directory.map(|working_directory| MigrationSettings { + path: Some(working_directory.join(conf.migrations_dir)), + after: Some(conf.after), + }) +} + /// Limit the size of files to 1.0 MiB by default pub(crate) const DEFAULT_FILE_SIZE_LIMIT: NonZeroU64 = // SAFETY: This constant is initialized with a non-zero value diff --git a/crates/pg_workspace/src/workspace/server.rs b/crates/pg_workspace/src/workspace/server.rs index d1173c5fa..2f7a8fbae 100644 --- a/crates/pg_workspace/src/workspace/server.rs +++ b/crates/pg_workspace/src/workspace/server.rs @@ -1,4 +1,10 @@ -use std::{fs, future::Future, panic::RefUnwindSafe, path::Path, sync::RwLock}; +use std::{ + fs, + future::Future, + panic::RefUnwindSafe, + path::{Path, PathBuf}, + sync::RwLock, +}; use analyser::AnalyserVisitorBuilder; use change::StatementChange; @@ -33,6 +39,7 @@ use super::{ mod analyser; mod change; mod document; +mod migration; mod pg_query; mod tree_sitter; @@ -150,13 +157,28 @@ impl WorkspaceServer { Ok(()) } + fn is_ignored_by_migration_config(&self, path: &Path) -> bool { + let set = self.settings(); + set.as_ref() + .migrations + .as_ref() + .and_then(|migration_settings| { + let ignore_before = migration_settings.after.as_ref()?; + let migrations_dir = migration_settings.path.as_ref()?; + let migration = migration::get_migration(path, migrations_dir)?; + + Some(&migration.timestamp <= ignore_before) + }) + .unwrap_or(false) + } + /// 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)) } /// Check whether a file is ignored in the top-level config `files.ignore`/`files.include` diff --git a/crates/pg_workspace/src/workspace/server/migration.rs b/crates/pg_workspace/src/workspace/server/migration.rs new file mode 100644 index 000000000..baa736dd8 --- /dev/null +++ b/crates/pg_workspace/src/workspace/server/migration.rs @@ -0,0 +1,121 @@ +use std::path::Path; + +#[derive(Debug)] +pub(crate) struct Migration { + pub(crate) timestamp: u64, + pub(crate) name: String, +} + +/// Get the migration associated with a path, if it is a migration file +pub(crate) fn get_migration(path: &Path, migrations_dir: &Path) -> Option { + // Check if path is a child of the migration directory + let is_child = path + .canonicalize() + .ok() + .and_then(|canonical_child| { + migrations_dir + .canonicalize() + .ok() + .map(|canonical_dir| canonical_child.starts_with(&canonical_dir)) + }) + .unwrap_or(false); + + if !is_child { + return None; + } + + // we are trying to match patterns used by popular migration tools + + // in the "root" pattern, all files are directly within the migrations directory + // and their names follow _.sql. + // this is used by supabase + let root_migration = path + .file_name() + .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()?; + let name = parts.next()?.to_string(); + Some(Migration { timestamp, name }) + }); + + if root_migration.is_some() { + return root_migration; + } + + // in the "subdirectory" pattern, each migration is in a subdirectory named _ + // this is used by prisma and drizzle + path.parent() + .and_then(|parent| parent.file_name()) + .and_then(|os_str| os_str.to_str()) + .and_then(|dir_name| { + let mut parts = dir_name.splitn(2, '_'); + let timestamp = parts.next()?.parse().ok()?; + let name = parts.next()?.to_string(); + Some(Migration { timestamp, name }) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::PathBuf; + use tempfile::TempDir; + + fn setup() -> TempDir { + TempDir::new().expect("Failed to create temp dir") + } + + #[test] + fn test_get_migration_root_pattern() { + let temp_dir = setup(); + let migrations_dir = temp_dir.path().to_path_buf(); + let path = migrations_dir.join("1234567890_create_users.sql"); + fs::write(&path, "").unwrap(); + + let migration = get_migration(&path, &migrations_dir); + + assert!(migration.is_some()); + let migration = migration.unwrap(); + assert_eq!(migration.timestamp, 1234567890); + assert_eq!(migration.name, "create_users.sql"); + } + + #[test] + fn test_get_migration_subdirectory_pattern() { + let temp_dir = setup(); + let migrations_dir = temp_dir.path().to_path_buf(); + let subdir = migrations_dir.join("1234567890_create_users"); + fs::create_dir(&subdir).unwrap(); + let path = subdir.join("up.sql"); + fs::write(&path, "").unwrap(); + + let migration = get_migration(&path, &migrations_dir); + + assert!(migration.is_some()); + let migration = migration.unwrap(); + assert_eq!(migration.timestamp, 1234567890); + assert_eq!(migration.name, "create_users"); + } + + #[test] + fn test_get_migration_not_timestamp_in_filename() { + let migrations_dir = PathBuf::from("/tmp/migrations"); + let path = migrations_dir.join("not_a_migration.sql"); + + let migration = get_migration(&path, &migrations_dir); + + assert!(migration.is_none()); + } + + #[test] + fn test_get_migration_outside_migrations_dir() { + let migrations_dir = PathBuf::from("/tmp/migrations"); + let path = PathBuf::from("/tmp/other/1234567890_create_users.sql"); + + let migration = get_migration(&path, &migrations_dir); + + assert!(migration.is_none()); + } +} diff --git a/pglsp.toml b/pglsp.toml index b2273c4c9..ea3943611 100644 --- a/pglsp.toml +++ b/pglsp.toml @@ -13,6 +13,10 @@ username = "postgres" password = "postgres" database = "postgres" +# [migrations] +# migrations_dir = "migrations" +# after = 20230912094327 + [linter] enabled = true