Skip to content
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

Create a migration command to migrate the data from V2 to V3 #38

Merged
merged 5 commits into from
Jan 15, 2025

Conversation

Dracks
Copy link
Owner

@Dracks Dracks commented Jan 15, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Added optional description field for bank transactions.
    • Introduced database migration support from Mr. Scrooge V2.
    • Enhanced error handling with new error codes and messages.
    • Added a new resource file for testing purposes.
  • Documentation

    • Updated README with migration instructions and file format details.
    • Added section on migrating from Mr. Scrooge V2.
  • Refactor

    • Improved database configuration management.
    • Restructured error context reporting.
    • Added common interface for graph label classes.
  • Tests

    • Added comprehensive tests for V2 database migration.
    • Updated existing test cases for improved coverage.
    • Modified tests for user creation and migration command functionality.
  • Chores

    • Added a task for creating and using enums in the database.

Copy link

coderabbitai bot commented Jan 15, 2025

Walkthrough

This pull request introduces a comprehensive set of changes to the Mr. Scrooge server, focusing on database migration functionality, error handling, and model enhancements. The modifications include adding a new migration service for transitioning data from version 2, expanding error codes, introducing optional fields in bank transaction models, and updating various configuration and test files to support the migration process. Additionally, it enhances testing capabilities by including a new SQLite resource file.

Changes

File Change Summary
Package.swift Added SQLite test resource file old.sqlite3 to test target
README.md Updated migration instructions, added "How to Migrate from Mr Scrooge V2" section, modified server run commands
src/swift-server/configure.swift Introduced getDbConfig() function for database configuration
src/swift-server/core/error.swift Added new error codes (E10019-E10031) with messages
src/swift-server/bank-transaction/bank-transaction.models.swift Added optional description field to BankTransaction
src/swift-server/django-migration/django-migration.service.swift New service for handling database migration from Django/V2
src/swift-server/django-migration/v2-migrate.command.swift Added V2MigrateCommand for executing database migration
src/swift-server/graph/graph.models.swift Introduced AbstractGroupLabels protocol

Sequence Diagram

sequenceDiagram
    participant User
    participant V2MigrateCommand
    participant DjangoMigrationService
    participant Database

    User->>V2MigrateCommand: Execute migration
    V2MigrateCommand->>Database: Connect to old database
    V2MigrateCommand->>DjangoMigrationService: Initialize migration
    DjangoMigrationService->>Database: Migrate tags to labels
    DjangoMigrationService->>Database: Migrate rules
    DjangoMigrationService->>Database: Migrate transactions
    DjangoMigrationService->>Database: Migrate graphs
    V2MigrateCommand->>User: Display migration complete
Loading

Possibly related PRs

  • updating the client to fix stuff #31: The main PR introduces a new resource file for testing, while the retrieved PR modifies the test workflow configuration. Both involve changes related to testing, indicating a potential connection in the context of enhancing test capabilities.

Poem

🐰 Migrating data, hop by hop,
From Django's realm to our new shop
Transactions dance, labels take flight
Version two becomes version bright!
A rabbit's journey of code delight 🚀


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (10)
src/swift-server/django-migration/django-migration.service.swift (2)

174-175: Address the TODO for transforming parent-child relationships.

There's a TODO comment indicating that transforming the parent-child relation is missing in the migrateTagsToRules function. This is crucial for maintaining data integrity during migration.

Would you like assistance in implementing the parent-child relationship transformation? I can help draft the necessary code or open a GitHub issue to track this task.


549-556: Avoid extending Array with a custom map function that includes an index.

Extending Array with a map function that includes an index can lead to confusion and potential conflicts with existing standard library methods. Instead, consider using Swift's built-in enumerated() method to achieve similar functionality more idiomatically.

Apply this diff to refactor the code:

-extension Array {
-    @inlinable public func map<T>(_ transform: (Element, Int) throws -> T) throws -> [T] {
-        var counter = Counter()
-        return try map { try transform($0, counter.next()) }
-    }
-}

Replace usages of the custom map function with enumerated().map:

let newLabels = try tagsList.enumerated().map { index, tag in
    // existing transformation logic
}
src/swift-server/django-migration/v2-migrate.command.swift (2)

7-13: Consider adding validation for the oldDbUrl format.

The command accepts a database URL without format validation. While the validation happens in getDbConfig, early validation in the command would provide better user experience.

 @Argument(name: "dbUrl", help: "The url where to find the old Database")
-var oldDbUrl: String
+var oldDbUrl: String {
+    didSet {
+        guard oldDbUrl.starts(with: "sqlite://") || oldDbUrl.starts(with: "postgres://") else {
+            fatalError("Invalid database URL format. Must start with 'sqlite://' or 'postgres://'")
+        }
+    }
+}

38-44: Add progress logging for long-running migrations.

The migration process might take a while for large datasets, but there's no progress indication.

 let migrator = DjangoMigrationService(
   owner: try groupOwner.requireID(), app: application, oldDb: sql)

+context.console.print("Starting migration...")
+context.console.print("Migrating tags to labels...")
 try await migrator.migrateTagsToLabels()
+context.console.print("Migrating tags to rules...")
 try await migrator.migrateTagsToRules()
+context.console.print("Migrating transactions...")
 try await migrator.migrateTransactions()
+context.console.print("Migrating graphs...")
 try await migrator.migrateGraphs()
src/swift-server/configure.swift (2)

Line range hint 9-40: Enhance error context for database configuration failures.

The error handling could be more informative by including additional context.

-throw Exception(.E10019, context: ["dbUrl": dbUrl])
+throw Exception(.E10019, context: [
+    "dbUrl": dbUrl,
+    "supportedPrefixes": "sqlite://, postgres://",
+    "error": "Invalid database URL format"
+])

73-73: Consider adding command documentation.

The v2_migrate command is registered without any inline documentation about its usage.

-app.asyncCommands.use(V2MigrateCommand(), as: "v2_migrate")
+// Register v2_migrate command for migrating data from MrScrooge V2
+// Usage: swift run MrScroogeServer v2_migrate <old-db-url> <group-owner-id>
+app.asyncCommands.use(V2MigrateCommand(), as: "v2_migrate")
src/swift-server-tests/migrate-v2-command.tests.swift (3)

35-40: Remove or uncomment the commented code.

The file contains commented-out code blocks that should either be removed if unnecessary or uncommented if they provide value to the tests.

Also applies to: 49-53


44-44: Document magic numbers in assertions.

Several assertions use magic numbers without explanation:

  • 3 labels
  • 3 rules
  • 6 conditions
  • 296 transactions

Consider adding comments explaining the expected counts or extracting them as named constants.

+// Expected counts from V2 database
+private let expectedLabelCount = 3
+private let expectedRuleCount = 3
+private let expectedConditionCount = 6
+private let expectedTransactionCount = 296

-XCTAssertEqual(labels.count, 3, "Labels number is not valid")
+XCTAssertEqual(labels.count, expectedLabelCount, "Labels number is not valid")

Also applies to: 58-58, 68-69, 75-75


77-81: Remove commented-out filter condition.

The commented-out filter condition on line 78 should be removed if unnecessary.

 let november9Transaction = try await BankTransaction.query(on: app.db)
-			// .filter(\.$groupOwner.$id == groupId)
 			.filter(\.$_date == "2019-11-09")
 			.with(\.$labels)
 			.first()
README.md (1)

27-27: Fix markdown formatting.

The emphasis markers should not have spaces inside them.

-** IMPORTANT **
+**IMPORTANT**
🧰 Tools
🪛 Markdownlint (0.37.0)

27-27: null
Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


27-27: null
Spaces inside emphasis markers

(MD037, no-space-in-emphasis)

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50b628d and 5eeb197.

📒 Files selected for processing (14)
  • Package.swift (1 hunks)
  • README.md (2 hunks)
  • src/swift-server-tests/app-test.swift (1 hunks)
  • src/swift-server-tests/migrate-v2-command.tests.swift (1 hunks)
  • src/swift-server-tests/profile.tests.swift (1 hunks)
  • src/swift-server/bank-transaction/bank-transaction.models.swift (1 hunks)
  • src/swift-server/configure.swift (3 hunks)
  • src/swift-server/core/error.swift (2 hunks)
  • src/swift-server/core/exception.swift (1 hunks)
  • src/swift-server/django-migration/django-migration.service.swift (1 hunks)
  • src/swift-server/django-migration/v2-migrate.command.swift (1 hunks)
  • src/swift-server/graph/graph.models.swift (3 hunks)
  • src/swift-server/user/user.service.swift (1 hunks)
  • todo.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/swift-server-tests/profile.tests.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
src/swift-server/bank-transaction/bank-transaction.models.swift

[Warning] 46-46: TODOs should be resolved (change the name to comment)

(todo)

src/swift-server-tests/migrate-v2-command.tests.swift

[Warning] 112-112: TODOs should be resolved (Add the validations)

(todo)

🪛 Markdownlint (0.37.0)
README.md

27-27: null
Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


27-27: null
Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


23-23: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Test
🔇 Additional comments (15)
src/swift-server/django-migration/django-migration.service.swift (2)

34-47: Verify @unchecked Sendable conformance for TagModel.

The TagModel class is marked as @unchecked Sendable, which tells the compiler to assume it's safe for concurrent use without verification. Ensure that all the stored properties of TagModel are safe to access from multiple threads, and consider using Sendable where possible to let the compiler check for concurrency safety.


49-97: Verify @unchecked Sendable conformance for FilterModel.

The FilterModel class is marked as @unchecked Sendable. Ensure that the properties and methods within FilterModel are safe for concurrent use, and there are no race conditions or thread safety issues.

src/swift-server/django-migration/v2-migrate.command.swift (1)

20-29: 🛠️ Refactor suggestion

Consider adding error handling for database connection failures.

The code handles SQL database type mismatch but doesn't explicitly handle connection failures.

Add explicit error handling:

 application.databases.use(dbFactory, as: oldDbId, isDefault: false)
+
+guard let _ = try? application.db(oldDbId).query("SELECT 1").wait() else {
+    throw Exception(.E10021, context: ["oldDbUrl": signature.oldDbUrl, "error": "Failed to connect to database"])
+}
Package.swift (1)

101-101: Verify test database initialization.

The old.sqlite3 test file is added but we should verify its initialization in tests.

✅ Verification successful

Test database resource is properly configured

The database file exists at the correct location and is properly configured as a processed resource in Package.swift. No additional initialization is required for test resources.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if the test database is properly initialized in tests
rg -A 5 "old\.sqlite3" src/swift-server-tests

Length of output: 47


Script:

#!/bin/bash
# Find the test database file location
fd "old.sqlite3"

# Search for references to old.sqlite3 in the entire codebase
rg -g '!{*.lock,Package.resolved}' "old\.sqlite3"

Length of output: 168

src/swift-server/graph/graph.models.swift (3)

75-78: LGTM! Good abstraction for group labels.

The introduction of AbstractGroupLabels protocol promotes code reuse and consistency between GraphGroupLabels and GraphHorizontalGroupLabels.


136-136: Verify protocol conformance implementation.

Both GraphGroupLabels and GraphHorizontalGroupLabels conform to AbstractGroupLabels, but their initializer signatures differ from the protocol requirement. The protocol requires init(graphId: Graph.IDValue, labelId: Label.IDValue, order: Int) but the implementations use GraphGroup.IDValue and GraphHorizontalGroup.IDValue respectively.

Please verify if this difference in ID types is intentional and doesn't cause type mismatches during migration.

Also applies to: 160-160


Line range hint 126-126: LGTM! Simplified error handling.

Good simplification by directly using getApp() which already includes the necessary nil check.

src/swift-server/core/error.swift (2)

5-7: LGTM! Comprehensive error codes for migration scenarios.

The new error codes provide good coverage for various migration scenarios with clear, descriptive messages.


61-89: ⚠️ Potential issue

Add missing error message for E10030.

Error code E10030 is defined in the enum but missing from the errorDictionary.

Add the missing entry:

 	.E10029: ErrorInfo(message: "Retrieving the groups of a graph, we get multiple groups"),
 	.E10030: ErrorInfo(
 		message: "We didn't found the group related to a graph in the old database"),
+	.E10031: ErrorInfo(message: "Your error message here"),
 ]

Likely invalid or redundant comment.

src/swift-server-tests/migrate-v2-command.tests.swift (1)

112-112: Address the TODO comment.

The TODO comment "Add the validations" should be addressed to ensure complete test coverage.

Would you like me to help implement the missing validations or create a GitHub issue to track this task?

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 112-112: TODOs should be resolved (Add the validations)

(todo)

todo.md (1)

30-30: Verify if this task belongs in the current PR.

This new task about creating and using enums in the database appears to be unrelated to the current PR's objective of creating a migration command from V2 to V3. Consider moving it to a separate PR or issue to maintain focus on the migration functionality.

README.md (2)

12-12: Verify QIF file format support status.

The change from "credit" to "TBD" suggests that QIF file support is no longer available. Please clarify if this is temporary during migration or if support has been removed permanently.


16-24: LGTM! Clear migration instructions.

The migration section provides clear step-by-step instructions with all necessary details for migrating from V2 to V3.

🧰 Tools
🪛 Markdownlint (0.37.0)

23-23: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

src/swift-server-tests/app-test.swift (2)

126-126: LGTM! Good simplification of error handling.

The change to use getApp() directly removes redundant error checking while maintaining the same level of safety.


126-126: Consider adding migration-specific test cases.

Given that this PR introduces a V2 to V3 migration command, consider adding test cases that verify:

  • Data migration scenarios
  • Edge cases during migration
  • Validation of migrated data integrity

This will help ensure the reliability of the migration process.

Would you like me to help create test cases for the migration scenarios?

src/swift-server/core/exception.swift Show resolved Hide resolved
src/swift-server/user/user.service.swift Show resolved Hide resolved
README.md Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (9)
src/swift-server/django-migration/django-migration.service.swift (3)

548-555: Refactor to use enumerated() instead of custom map extension

The custom map extension with an index parameter duplicates functionality provided by the standard library's enumerated() method. Utilizing enumerated() improves code readability and leverages well-tested standard library features.

You can refactor the code as follows:

-public struct Counter {
-    var count: Int = 0
-    public init() {}
-    @usableFromInline mutating func next() -> Int {
-        count = count + 1
-        return count
-    }
-}
-
-extension Array {
-    @inlinable public func map<T>(_ transform: (Element, Int) throws -> T)
-        throws -> [T]
-    {
-        var counter = Counter()
-        return try map { try transform($0, counter.next()) }
-    }
-}

Replace usage of the custom map with enumerated():

let newLabels = try tagsList.enumerated().map { index, tag in
    // Use index as needed
}

65-68: Ensure locale-independent parsing of Double values from String

The getCondDouble() method converts conditional strings to Double using the default initializer. This can lead to issues if the environment's locale uses a comma as the decimal separator.

Modify the code to use a NumberFormatter with a fixed locale:

func getCondDouble() throws -> Double {
-    if let d = Double(conditional) {
+    let formatter = NumberFormatter()
+    formatter.locale = Locale(identifier: "en_US_POSIX")
+    if let d = formatter.number(from: conditional)?.doubleValue {
        return d
    }
    throw Exception(.E10023, context: ["filterId": id as Any, "tagId": tagId])
}

This ensures consistent parsing of numeric strings regardless of the environment's locale settings.


281-301: Optimize transaction migration with batch operations

In migrateTransaction(rds:), each transaction and its related labels are saved individually. This could impact performance when migrating a large number of records.

Consider using batch inserts or wrapping the save operations within a database transaction to improve performance:

try await app.db.transaction { db in
    try await transaction.save(on: db)
    // Save labels within the transaction
}

Batch operations reduce the number of database round-trips and can significantly speed up the migration process.

README.md (3)

23-23: Specify language identifiers in fenced code blocks

Code blocks without language identifiers do not benefit from syntax highlighting, which can improve readability.

Add the appropriate language identifier after the triple backticks. For example:

-```
 swift run
-```
+```bash
swift run
+```

Apply this change to all code blocks in the README.

Also applies to: 46-46, 50-50, 55-55, 61-61, 66-66, 71-71

🧰 Tools
🪛 Markdownlint (0.37.0)

23-23: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)


27-27: Correct Markdown formatting for emphasis

There are spaces inside the emphasis markers, which can prevent proper rendering.

Change:

-** IMPORTANT **: The web-client must have been launched...
+**IMPORTANT**: The web-client must have been launched...

Removing the spaces ensures that the text is correctly emphasized.

🧰 Tools
🪛 Markdownlint (0.37.0)

27-27: null
Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


27-27: null
Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


54-54: Fix grammatical error in the admin user creation description

The phrase "by default will be demo:demo" is grammatically incorrect.

Modify the sentence for clarity:

-3. Create an admin user (by default will be demo:demo):
+3. Create an admin user (default credentials are demo:demo):

This correction improves readability and ensures correct grammar.

🧰 Tools
🪛 LanguageTool

[grammar] ~54-~54: The past participle is required after “will be”, alternatively you could omit the “be”.
Context: .... Create an admin user (by default will be demo:demo): ``` ./serverDebug create_user --...

(BE_VBP_IN)

src/swift-server/configure.swift (3)

Line range hint 9-40: Add documentation for error codes and enhance URL validation.

The new getDbConfig function provides good separation of concerns, but could benefit from:

  1. Documentation for error codes (E10019, E10003)
  2. More robust PostgreSQL URL validation
  3. Input sanitization for the file path
+/// Configures the database connection based on the provided URL.
+/// - Parameter dbUrl: Database URL in format "sqlite://path" or "postgres://connection_string"
+/// - Throws: Exception with code:
+///   - E10019: Invalid database URL format
+///   - E10003: Unsupported database type
+/// - Returns: Tuple of database configuration factory and database ID
 func getDbConfig(url dbUrl: String) throws -> (DatabaseConfigurationFactory, DatabaseID) {
+    // Sanitize input
+    guard let sanitizedUrl = dbUrl.trimmingCharacters(in: .whitespacesAndNewlines)
+        .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
+        throw Exception(.E10019, context: ["dbUrl": dbUrl])
+    }
+
     if !dbUrl.starts(with: "sqlite://") && !dbUrl.starts(with: "postgres://") {

41-45: Consider adding logging and making the default database path configurable.

While the function is well-structured, consider:

  1. Adding logging for successful database configuration
  2. Making the default SQLite path configurable via environment variables
 func configureDb(_ app: Application) async throws {
-    let dbUrl = Environment.get("DB_URL") ?? "sqlite://db.sqlite3"
+    let defaultDbPath = Environment.get("DEFAULT_DB_PATH") ?? "db.sqlite3"
+    let dbUrl = Environment.get("DB_URL") ?? "sqlite://\(defaultDbPath)"
     let (dbFactory, dbId) = try getDbConfig(url: dbUrl)
     app.databases.use(dbFactory, as: dbId)
+    app.logger.info("Database configured successfully", metadata: ["type": "\(dbId)"])
 }

Line range hint 9-73: Consider documenting the V2 to V3 migration architecture.

The changes introduce a solid foundation for database configuration and migration support. Consider adding:

  1. Documentation describing the overall V2 to V3 migration architecture
  2. Migration rollback strategy
  3. Data validation steps during migration
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5eeb197 and 0e06e85.

📒 Files selected for processing (4)
  • README.md (2 hunks)
  • src/swift-server/configure.swift (3 hunks)
  • src/swift-server/django-migration/django-migration.service.swift (1 hunks)
  • src/swift-server/django-migration/v2-migrate.command.swift (1 hunks)
🧰 Additional context used
🪛 Markdownlint (0.37.0)
README.md

27-27: null
Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


27-27: null
Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


23-23: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)


46-46: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)


50-50: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)


55-55: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)


61-61: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)


66-66: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)


71-71: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 LanguageTool
README.md

[grammar] ~54-~54: The past participle is required after “will be”, alternatively you could omit the “be”.
Context: .... Create an admin user (by default will be demo:demo): ``` ./serverDebug create_user --...

(BE_VBP_IN)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Test
🔇 Additional comments (1)
src/swift-server/django-migration/django-migration.service.swift (1)

34-47: Evaluate the use of @unchecked Sendable on model classes

The application of @unchecked Sendable to model classes like TagModel, FilterModel, RawDataSource, and Graph bypasses the compiler's concurrency safety checks. This could lead to data races if instances of these models are accessed from multiple tasks concurrently without proper synchronization.

Please ensure that:

  • The properties within these models are safe to be shared across concurrent contexts.
  • Appropriate synchronization mechanisms are in place if these models are mutated concurrently.
  • If the models are only used within controlled contexts where concurrency issues are unlikely, document the reasoning for using @unchecked Sendable.

Also applies to: 49-97, 189-276, 324-470

Comment on lines 7 to 19
@Argument(name: "dbUrl", help: "The url where to find the old Database")
var oldDbUrl: String {
didSet {
guard
oldDbUrl.starts(with: "sqlite://")
|| oldDbUrl.starts(with: "postgres://")
else {
fatalError(
"Invalid database URL format. Must start with 'sqlite://' or 'postgres://'"
)
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid using fatalError for argument validation

Using fatalError for input validation causes the program to crash and can be uninformative to the user. Instead, throw a CommandError to provide a clear error message.

Refactor the validation logic:

struct Signature: CommandSignature {
    @Argument(name: "dbUrl", help: "The url where to find the old Database")
    var oldDbUrl: String

+   func validate() throws {
+       guard oldDbUrl.starts(with: "sqlite://") || oldDbUrl.starts(with: "postgres://") else {
+           throw CommandError(
+               identifier: "invalidDbUrl",
+               reason: "Invalid database URL format. Must start with 'sqlite://' or 'postgres://'",
+               suggestion: "Check the database URL and try again."
+           )
+       }
+   }

    @Argument(
        name: "group-owner-id",
        help: "The group owner Id used for the imported data")
    var groupOwnerId: UUID
}

Then, call try signature.validate() at the beginning of the run method:

func run(using context: CommandContext, signature: Signature) async throws {
    try signature.validate()
    // Rest of the code...
}

This approach provides a better user experience by displaying a descriptive error without crashing the application.

Comment on lines +71 to +73
app.asyncCommands.use(CreateUserCommand(), as: "create_user")
app.asyncCommands.use(DemoDataCommand(), as: "demo_data")
app.asyncCommands.use(V2MigrateCommand(), as: "v2_migrate")
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Update outdated test reference to match new command name

The command changes are properly implemented, but there's one remaining reference to the old command name:

  • Update src/swift-server-tests/app-test.swift to use "create_user" instead of "demo_user" in the test arguments
🔗 Analysis chain

Verify command changes and migration implementation.

The command registration changes indicate a shift from demo to production functionality. Please verify:

  1. That all references to the old 'demo_user' command have been updated
  2. The implementation of V2MigrateCommand handles all necessary data migration cases
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining references to 'demo_user' command
rg "demo_user" --type swift

# Find and analyze the V2MigrateCommand implementation
ast-grep --pattern 'struct V2MigrateCommand {
  $$$
}'

Length of output: 154


Script:

#!/bin/bash
# Try finding V2MigrateCommand as either struct or class
rg "V2MigrateCommand" -A 5 --type swift

# Look for command usage patterns
ast-grep --pattern 'app.asyncCommands.use($_, as: $_)'

Length of output: 2265

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
src/swift-server/django-migration/v2-migrate.command.swift (2)

19-30: Consider enhancing URL validation.

While the current validation checks for URL scheme, consider validating the complete URL structure to ensure it contains a valid path.

 func validate() throws {
+    guard let url = URL(string: oldDbUrl) else {
+        throw InvalidArgument(
+            argument: "dbUrl",
+            reason: "Invalid URL format"
+        )
+    }
     guard
-        oldDbUrl.starts(with: "sqlite://")
-            || oldDbUrl.starts(with: "postgres://")
+        url.scheme == "sqlite"
+            || url.scheme == "postgres"
     else {
         throw InvalidArgument(
             argument: "dbUrl",
             reason:
                 "Invalid database URL format. Must start with 'sqlite://' or 'postgres://'"
         )
     }
+    guard !url.path.isEmpty else {
+        throw InvalidArgument(
+            argument: "dbUrl",
+            reason: "Database path is required"
+        )
+    }
 }

64-82: Consider adding progress tracking and rollback capability.

While the migration steps are well-organized, consider:

  1. Adding progress tracking (e.g., number of items migrated)
  2. Implementing rollback capability for failed migrations

Example for progress tracking:

 context.console.print("Migrating transactions...")
-try await migrator.migrateTransactions()
+let count = try await migrator.migrateTransactions()
+context.console.print("Migrated \(count) transactions")
src/swift-server-tests/migrate-v2-command.tests.swift (4)

35-40: Remove or uncomment test assertions.

The commented assertions for console output validation should either be removed or uncommented if they're still relevant.


49-53: Remove unused commented code.

The commented label dictionary code should be removed if it's no longer needed.


194-194: Remove commented assertion.

The commented assertion XCTAssertNil(console.testOutputQueue) should be removed if it's no longer needed.


168-205: Consider adding more error test cases.

While testing invalid URL format is good, consider adding tests for:

  1. Non-existent database file
  2. Invalid group owner ID format
  3. Database connection failures
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e06e85 and 9600d16.

📒 Files selected for processing (3)
  • src/swift-server-tests/app-test.swift (1 hunks)
  • src/swift-server-tests/migrate-v2-command.tests.swift (1 hunks)
  • src/swift-server/django-migration/v2-migrate.command.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/swift-server-tests/app-test.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
src/swift-server-tests/migrate-v2-command.tests.swift

[Warning] 111-111: TODOs should be resolved (Add the validations)

(todo)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Test
🔇 Additional comments (4)
src/swift-server/django-migration/v2-migrate.command.swift (2)

5-8: LGTM! Well-structured error type.

The InvalidArgument error type provides clear error context with both the argument name and reason.


37-49: LGTM! Robust database setup with proper error handling.

The code properly validates inputs, configures the database, and includes appropriate type checking.

src/swift-server-tests/migrate-v2-command.tests.swift (2)

72-110: LGTM! Comprehensive transaction testing.

The tests thoroughly validate transaction properties, including dates, values, and label associations.


111-111: Address TODO comment for additional validations.

The TODO comment indicates missing validations. What specific validations need to be added?

Would you like me to help implement the additional graph validations?

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 111-111: TODOs should be resolved (Add the validations)

(todo)

Comment on lines 50 to 63
guard
let groupOwner = try await UserGroup.query(on: application.db)
.filter(
\.$id == signature.groupOwnerId
).first()
else {
throw InvalidArgument(
argument: "groupId",
reason: "Group Owner Id not found in the DB")
}
let migrator = DjangoMigrationService(
owner: try groupOwner.requireID(), app: application, oldDb: oldDb,
oldSqlDb: sql)

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding transaction boundary.

While the group owner validation is good, consider wrapping the entire migration process in a transaction to ensure data consistency in case of failures.

 guard
     let groupOwner = try await UserGroup.query(on: application.db)
         .filter(
             \.$id == signature.groupOwnerId
         ).first()
 else {
     throw InvalidArgument(
         argument: "groupId",
         reason: "Group Owner Id not found in the DB")
 }
+try await application.db.transaction { transaction in
     let migrator = DjangoMigrationService(
-        owner: try groupOwner.requireID(), app: application, oldDb: oldDb,
+        owner: try groupOwner.requireID(), app: application, oldDb: transaction,
         oldSqlDb: sql)
+    // Move migration steps inside transaction
+}

Committable suggestion skipped: line range outside the PR's diff.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/swift-server/django-migration/django-migration.service.swift (5)

16-16: Consider making PAGE_SIZE configurable.

The fixed page size of 100 might need adjustment based on the data volume and available memory. Consider making it configurable through environment variables or constructor parameters.

-static let PAGE_SIZE = 100
+static let PAGE_SIZE: Int = {
+    if let envSize = Environment.get("MIGRATION_PAGE_SIZE"),
+       let size = Int(envSize) {
+        return size
+    }
+    return 100
+}()

64-73: Enhance number parsing error handling.

The number parsing logic could be more robust. Consider adding the actual value to the error context and supporting different number formats.

 func getCondDouble() throws -> Double {
     let formatter = NumberFormatter()
     formatter.locale = Locale(identifier: "en_US_POSIX")
     if let d = formatter.number(from: conditional)?
         .doubleValue
     {
         return d
     }
-    throw Exception(.E10023, context: ["filterId": id as Any, "tagId": tagId])
+    throw Exception(.E10023, context: [
+        "filterId": id as Any,
+        "tagId": tagId,
+        "value": conditional,
+        "locale": "en_US_POSIX"
+    ])
 }

307-324: Enhance progress reporting with ETA and rate.

The current progress reporting is basic. Consider adding estimated time remaining and migration rate information.

 func migrateTransactions() async throws {
     let transactionsQuery = OldDb.RawDataSource.query(on: oldDb).sort(.id)
     var finish = false
     var pageIdx = 1
     var done = 0
+    let startTime = Date()
     while !finish {
         let page = try await transactionsQuery.page(
             withIndex: pageIdx, size: DjangoMigrationService.PAGE_SIZE)
         pageIdx = page.metadata.page + 1
         finish = pageIdx > page.metadata.pageCount
         for rds in page.items {
             try await migrateTransaction(rds: rds)
             done = done + 1
         }
-        print("Migrated \(done)/\(page.metadata.total)")
+        let elapsed = Date().timeIntervalSince(startTime)
+        let rate = Double(done) / elapsed
+        let remaining = Double(page.metadata.total - done) / rate
+        print("Migrated \(done)/\(page.metadata.total) at \(Int(rate))/s, ETA: \(Int(remaining))s")
     }
 }

476-541: Add rollback capability for graph migration.

The graph migration process lacks rollback capability. If an error occurs mid-way through the migration of a graph and its related entities, the database could be left in an inconsistent state.

Consider implementing the migration within a transaction:

 func migrateGraphs() async throws {
     let graphs = try await OldDb.Graph.query(on: oldDb).all()
+    try await app.db.transaction { tx in
         for graph in graphs {
-            try await migrateGraph(graph: graph)
+            try await migrateGraph(graph: graph, on: tx)
         }
+    }
 }

346-379: Simplify enum mapping using string-based raw values.

The manual enum mapping could be simplified by making the enums conform to String and using raw values.

-func getKindEnum() throws -> GraphKind {
-    switch kind {
-    case "pie":
-        return .pie
-    case "bar":
-        return .bar
-    case "line":
-        return .line
-    default:
-        throw Exception(
-            .E10027, context: ["graphId": id as Any, "kind": kind])
-    }
-}
+func getKindEnum() throws -> GraphKind {
+    guard let kind = GraphKind(rawValue: kind) else {
+        throw Exception(.E10027, context: ["graphId": id as Any, "kind": kind])
+    }
+    return kind
+}
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9600d16 and 91db949.

📒 Files selected for processing (1)
  • src/swift-server/django-migration/django-migration.service.swift (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Test
🔇 Additional comments (1)
src/swift-server/django-migration/django-migration.service.swift (1)

34-34: Verify thread safety with @unchecked Sendable.

The models are marked as @unchecked Sendable which bypasses the compiler's thread safety checks. Ensure that all properties are actually thread-safe or properly synchronized.

Also applies to: 49-49

✅ Verification successful

@unchecked Sendable usage is appropriate here

The models are used exclusively for database operations through Vapor's ORM during migration, with no concurrent mutations observed. The thread safety is properly managed by the framework's database query interface.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for potential concurrent access to these models
rg -l "async.*TagModel|async.*FilterModel" 

Length of output: 44


Script:

#!/bin/bash
# Find all usages of TagModel and FilterModel
rg "TagModel|FilterModel" -A 3 -B 3

# Find property declarations within these models
ast-grep --pattern 'class $_ {
  $$$
  var $_: $_ 
  $$$
}'

Length of output: 8019

Comment on lines +264 to +279
func getRelations(on oldDb: SQLDatabase) async throws -> [TagsRelation] {
let tableName = DjangoMigrationService.OldDb.TableNames.rdsToTags.rawValue
let tagsList = try await oldDb.select().column(
SQLColumn(
SQLLiteral.all,
table: SQLIdentifier(tableName))
).from(
tableName
).where(
SQLColumn("raw_data_source_id", table: tableName), .equal,
SQLLiteral.numeric("\(try requireID())")
).all()
return try tagsList.map {
try .init(row: $0)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Protect against SQL injection in raw SQL queries.

The manual construction of SQL queries using string interpolation is vulnerable to SQL injection. Use parameterized queries instead.

-            ).where(
-                SQLColumn("raw_data_source_id", table: tableName), .equal,
-                SQLLiteral.numeric("\(try requireID())")
+            ).where(SQLBind(try requireID()))

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +178 to +189
func migrateTagsToRules() async throws {
var tagsList = try await OldDb.TagModel.query(on: oldDb).filter(
\.$parent.$id == nil
).all()
var tagToRuleDictId: [Int64: UUID] = [:]
while !tagsList.isEmpty {
let newIds = try await migrateTags(tagsList, withDict: &tagToRuleDictId)
tagsList = try await OldDb.TagModel.query(on: oldDb).filter(
\.$parent.$id ~~ newIds
).all()
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add depth limit for recursive tag migration.

The recursive approach for migrating nested tags could potentially cause stack overflow with deeply nested hierarchies. Consider adding a maximum depth limit.

 func migrateTagsToRules() async throws {
+    let maxDepth = 100  // Configurable maximum depth
+    var currentDepth = 0
     var tagsList = try await OldDb.TagModel.query(on: oldDb).filter(
         \.$parent.$id == nil
     ).all()
     var tagToRuleDictId: [Int64: UUID] = [:]
     while !tagsList.isEmpty {
+        currentDepth += 1
+        guard currentDepth <= maxDepth else {
+            throw Exception(.E10099, context: ["depth": currentDepth])
+        }
         let newIds = try await migrateTags(tagsList, withDict: &tagToRuleDictId)
         tagsList = try await OldDb.TagModel.query(on: oldDb).filter(
             \.$parent.$id ~~ newIds
         ).all()
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func migrateTagsToRules() async throws {
var tagsList = try await OldDb.TagModel.query(on: oldDb).filter(
\.$parent.$id == nil
).all()
var tagToRuleDictId: [Int64: UUID] = [:]
while !tagsList.isEmpty {
let newIds = try await migrateTags(tagsList, withDict: &tagToRuleDictId)
tagsList = try await OldDb.TagModel.query(on: oldDb).filter(
\.$parent.$id ~~ newIds
).all()
}
}
func migrateTagsToRules() async throws {
let maxDepth = 100 // Configurable maximum depth
var currentDepth = 0
var tagsList = try await OldDb.TagModel.query(on: oldDb).filter(
\.$parent.$id == nil
).all()
var tagToRuleDictId: [Int64: UUID] = [:]
while !tagsList.isEmpty {
currentDepth += 1
guard currentDepth <= maxDepth else {
throw Exception(.E10099, context: ["depth": currentDepth])
}
let newIds = try await migrateTags(tagsList, withDict: &tagToRuleDictId)
tagsList = try await OldDb.TagModel.query(on: oldDb).filter(
\.$parent.$id ~~ newIds
).all()
}
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (7)
src/swift-server/django-migration/v2-migrate.command.swift (5)

5-8: Enhance error handling by conforming to LocalizedError.

Consider enhancing the InvalidArgument error type by conforming to LocalizedError and adding a suggestion field for better user guidance.

-struct InvalidArgument: Error {
+struct InvalidArgument: LocalizedError {
 	var argument: String
 	var reason: String
+	var suggestion: String?
+
+	var errorDescription: String? {
+		return reason
+	}
+
+	var failureReason: String? {
+		return "Invalid argument: \(argument)"
+	}
+
+	var recoverySuggestion: String? {
+		return suggestion
+	}
}

19-30: Enhance URL and UUID validation.

The current validation only checks URL prefixes. Consider adding:

  1. Full URL format validation
  2. UUID format validation for groupOwnerId
 func validate() throws {
     guard
-        oldDbUrl.starts(with: "sqlite://")
-            || oldDbUrl.starts(with: "postgres://")
+        let url = URL(string: oldDbUrl),
+        let scheme = url.scheme,
+        ["sqlite", "postgres"].contains(scheme)
     else {
         throw InvalidArgument(
             argument: "dbUrl",
-            reason:
-                "Invalid database URL format. Must start with 'sqlite://' or 'postgres://'"
+            reason: "Invalid database URL format",
+            suggestion: "URL must be a valid sqlite:// or postgres:// URL"
         )
     }
+    
+    guard UUID(uuidString: groupOwnerId.uuidString) != nil else {
+        throw InvalidArgument(
+            argument: "group-owner-id",
+            reason: "Invalid UUID format",
+            suggestion: "Provide a valid UUID for the group owner"
+        )
+    }
 }

33-35: Enhance command help text with more details.

The current help text could be more informative about the required arguments, their formats, and what data will be migrated.

 var help: String {
-    "Migrates all data from the old MrScrooge V2 Database"
+    """
+    Migrates data from MrScrooge V2 to V3 Database.
+    
+    Arguments:
+      dbUrl: URL of the V2 database (sqlite:// or postgres://)
+      group-owner-id: UUID of the group owner for the imported data
+    
+    Migrates:
+      - Tags to Labels
+      - Tags to Rules
+      - Transactions
+      - Graphs
+    """
 }

68-77: Enhance progress reporting with completion percentage.

Consider adding progress indicators for better user feedback during migration.

-context.console.print("Starting migration...")
+let console = context.console
+console.output("Starting migration...", style: .info)
+
+func reportProgress(_ step: String) {
+    console.output("[\(Date())] \(step)", style: .info)
+}

-context.console.print("Migrating tags to labels...")
+reportProgress("Migrating tags to labels...")
 try await migrator.migrateTagsToLabels()
-context.console.print("Migrating tags to rules...")
+reportProgress("Migrating tags to rules...")
 try await migrator.migrateTagsToRules()
-context.console.print("Migrating transactions...")
+reportProgress("Migrating transactions...")
 try await migrator.migrateTransactions()
-context.console.print("Migrating graphs...")
+reportProgress("Migrating graphs...")
 try await migrator.migrateGraphs()

80-85: Enhance error handling with more specific error cases.

The error handling could be more comprehensive to handle different types of errors.

 } catch let error as Exception {
-    context.console.error(String(describing: error))
+    context.console.output("Migration failed: \(error.localizedDescription)", style: .error)
 } catch let error as InvalidArgument {
-    context.console.error("Invalid argument: \(error.argument)")
-    context.console.error("Error: \(error.reason)")
+    context.console.output("Invalid argument: \(error.argument)", style: .error)
+    context.console.output("Error: \(error.reason)", style: .error)
+    if let suggestion = error.suggestion {
+        context.console.output("Suggestion: \(suggestion)", style: .info)
+    }
+} catch {
+    context.console.output("Unexpected error: \(error.localizedDescription)", style: .error)
 }
src/swift-server/django-migration/django-migration.service.swift (2)

5-36: Consider marking the class as final and adding documentation.

The DjangoMigrationService class could benefit from:

  1. Being marked as final since it's not designed for inheritance
  2. Adding documentation comments explaining its purpose and requirements
+/// Service responsible for migrating data from Django V2 to V3.
+/// Handles the migration of tags, rules, transactions, and graphs.
+final
 class DjangoMigrationService {

312-329: Enhance progress reporting for better monitoring.

The current progress reporting is basic. Consider:

  1. Using a proper logging framework instead of print
  2. Adding time estimates for completion
  3. Including error counts in the progress
-      print("Migrated \(done)/\(page.metadata.total)")
+      app.logger.info("Migration progress: \(done)/\(page.metadata.total) transactions processed")
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91db949 and f98fc2d.

📒 Files selected for processing (2)
  • src/swift-server/django-migration/django-migration.service.swift (1 hunks)
  • src/swift-server/django-migration/v2-migrate.command.swift (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Test
🔇 Additional comments (2)
src/swift-server/django-migration/django-migration.service.swift (2)

183-194: Add depth limit for recursive tag migration.

The recursive approach for migrating nested tags could potentially cause stack overflow with deeply nested hierarchies.


269-284: Protect against SQL injection in raw SQL queries.

The manual construction of SQL queries using string interpolation is vulnerable to SQL injection.

Comment on lines +37 to +48
func run(using context: CommandContext, signature: Signature) async throws {
do {
try signature.validate()
let (dbFactory, _) = try getDbConfig(url: signature.oldDbUrl)
let application = context.application
let oldDbId: DatabaseID = .init(string: "old")
application.databases.use(dbFactory, as: oldDbId, isDefault: false)
let oldDb = application.db(oldDbId)

guard let sql = oldDb as? SQLDatabase else {
throw Exception(.E10020, context: ["oldDbUrl": signature.oldDbUrl])
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve database resource management.

Consider adding cleanup for the old database connection and enhancing error handling for database setup.

 func run(using context: CommandContext, signature: Signature) async throws {
     do {
         try signature.validate()
         let (dbFactory, _) = try getDbConfig(url: signature.oldDbUrl)
         let application = context.application
         let oldDbId: DatabaseID = .init(string: "old")
-        application.databases.use(dbFactory, as: oldDbId, isDefault: false)
-        let oldDb = application.db(oldDbId)
+        defer {
+            application.databases.shutdown()
+        }
+        
+        do {
+            application.databases.use(dbFactory, as: oldDbId, isDefault: false)
+            let oldDb = application.db(oldDbId)
+        } catch {
+            throw Exception(.E10021, context: ["error": error.localizedDescription])
+        }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +482 to +539
private func migrateGraph(graph oldGraph: OldDb.Graph) async throws {
let labelFilterId: UUID?
if let tagFilter = oldGraph.tagFilter {
labelFilterId = try getLabelId(tagFilter)
} else {
labelFilterId = nil
}
let oldGraphId = try oldGraph.requireID()
let graph = try Graph(
groupOwnerId: groupOwnerId, name: oldGraph.name,
kind: oldGraph.getKindEnum(),
labelFilterId: labelFilterId,
dateRange: oldGraph.getDateRangeEnum())

try await graph.save(on: newDb)
let graphId = try graph.requireID()

let oldGroup = try await OldDb.GraphGroup.query(
table: .graphGroup, for: oldGraphId, on: oldSqlDb)
guard let oldGroup else {
throw Exception(.E10030, context: ["oldGraphId": oldGraphId])
}
let group = try GraphGroup(
graphId: graphId, group: oldGroup.getGroupType(),
hideOthers: oldGroup.hideOthers)
try await group.save(on: newDb)

let oldGroupTags = try await OldDb.GraphGroupTag.query(
table: .graphGroupTag, for: oldGroup.id, on: oldSqlDb)
let groupLabels: [GraphGroupLabels] = try oldGroupTags.enumerated().map {
.init(graphId: graphId, labelId: try getLabelId($1.tagId), order: $0)
}
for groupLabel in groupLabels {
try await groupLabel.save(on: newDb)
}

let oldHorizontalGroup = try await OldDb.GraphGroup.query(
table: .graphHorizontalGroup, for: oldGraphId, on: oldSqlDb)
if let oldHorizontalGroup {
let horizontalGroup = GraphHorizontalGroup(
graphId: graphId, group: try oldHorizontalGroup.getGroupType(),
hideOthers: oldHorizontalGroup.hideOthers,
accumulate: oldHorizontalGroup.accumulate ?? false)
try await horizontalGroup.save(on: newDb)

let oldHorizontalGroupTags = try await OldDb.GraphGroupTag.query(
table: .graphHorizontalGroupTag, for: oldGroup.id, on: oldSqlDb)
let horizontalGroupLabels: [GraphHorizontalGroupLabels] =
try oldHorizontalGroupTags.enumerated().map {
.init(
graphId: graphId, labelId: try getLabelId($1.tagId),
order: $0)
}
for groupLabel in horizontalGroupLabels {
try await groupLabel.save(on: newDb)
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Wrap graph migration in a database transaction.

The graph migration process involves multiple related entities. If any step fails, the database could be left in an inconsistent state. Consider wrapping the entire migration in a transaction.

 private func migrateGraph(graph oldGraph: OldDb.Graph) async throws {
+    try await newDb.transaction { tx in
       let labelFilterId: UUID?
       if let tagFilter = oldGraph.tagFilter {
         labelFilterId = try getLabelId(tagFilter)
       } else {
         labelFilterId = nil
       }
       // ... rest of the migration code ...
+    }
 }

Committable suggestion skipped: line range outside the PR's diff.

}

extension DjangoMigrationService.OldDb {
final class TagModel: Model, @unchecked Sendable {
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

@unchecked Sendable usage requires synchronization mechanisms

The models contain mutable state (var properties and collections) and are used in concurrent contexts (TaskGroup). The current @unchecked Sendable implementation bypasses compiler safety checks without proper synchronization, which could lead to data races. Consider:

  • Using immutable properties (let) where possible
  • Adding synchronization for mutable state
  • Implementing proper Sendable conformance instead of @unchecked
🔗 Analysis chain

Verify the safety of @unchecked Sendable usage.

The models are marked as @unchecked Sendable which bypasses the compiler's sendability checking. This could lead to data races if the models are used across concurrent contexts.

Also applies to: 54-54, 198-198, 333-333

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for concurrent access to these models
rg -A 5 "async.*\b(TagModel|FilterModel|RawDataSource|Graph)\b.*\{" 

Length of output: 69


Script:

#!/bin/bash
# Check model implementations and concurrent usage patterns
echo "=== Model Implementations ==="
ast-grep --pattern 'class $_ : Model {
  $$$
}'

echo -e "\n=== Concurrent Access Patterns ==="
# Look for Task, async, or concurrent usage
rg -B 2 -A 2 "(Task|DispatchQueue|actor).*\b(TagModel|FilterModel|RawDataSource|Graph)\b"

echo -e "\n=== Mutable Properties ==="
# Look for var declarations in models
ast-grep --pattern 'class $_ : Model {
  $$$
  var $_: $_ 
  $$$
}'

Length of output: 792


Script:

#!/bin/bash
# Check for property declarations in models
echo "=== Property Declarations ==="
ast-grep --pattern 'class $_ : Model {
  $$$
  let $_: $_
  $$$
}'

echo -e "\n=== Any Property Modifiers ==="
rg "^[[:space:]]*(private|public|internal)?\s*(var|let)\s+\w+\s*:" src/swift-server/django-migration/django-migration.service.swift

Length of output: 1513

@Dracks Dracks merged commit 7be33b4 into mr-scrooge-3.0.0 Jan 15, 2025
2 of 4 checks passed
@Dracks Dracks deleted the feat/db-migrate branch January 15, 2025 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant