First cut of migration program

This commit is contained in:
Daniel J. Summers 2025-01-30 21:28:04 -05:00
parent 42e3a58131
commit 194cd2b5cc
6 changed files with 208 additions and 10 deletions

View File

@ -5,6 +5,31 @@ open Npgsql
open Npgsql.FSharp
open PrayerTracker.Entities
/// Table names
[<RequireQualifiedAccess>]
module Table =
/// The church table
[<Literal>]
let Church = "church"
/// The small group table
[<Literal>]
let Group = "small_group"
/// The small group member table
[<Literal>]
let Member = "member"
/// The prayer request table
[<Literal>]
let Request = "prayer_request"
/// The user table
[<Literal>]
let User = "pt_user"
/// Helper functions for the PostgreSQL data implementation
[<AutoOpen>]
module private Helpers =
@ -100,6 +125,7 @@ module private Helpers =
IsAdmin = row.bool "is_admin"
PasswordHash = row.string "password_hash"
LastSeen = row.fieldValueOrNone<Instant> "last_seen"
SmallGroups = []
}

View File

@ -127,6 +127,15 @@ type RequestSort =
| _ -> invalidArg "code" $"Unknown code {code}"
/// Type for a time zone ID
type TimeZoneId =
| TimeZoneId of string
override this.ToString() =
match this with
| TimeZoneId it -> it
open System
/// PK type for the Church entity
@ -173,15 +182,6 @@ type SmallGroupId =
| SmallGroupId guid -> guid
/// PK type for the TimeZone entity
type TimeZoneId =
| TimeZoneId of string
override this.ToString() =
match this with
| TimeZoneId it -> it
/// PK type for the User entity
type UserId =
| UserId of Guid
@ -523,6 +523,9 @@ type User =
/// The last time the user was seen (set whenever the user is loaded into a session)
LastSeen: Instant option
/// The small groups to which this user is authorized
SmallGroups: SmallGroupId list
}
/// The full name of the user
@ -536,7 +539,8 @@ type User =
Email = ""
IsAdmin = false
PasswordHash = ""
LastSeen = None }
LastSeen = None
SmallGroups = [] }
/// Cross-reference between user and small group

View File

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="BitBadger.Documents.Postgres" Version="3.1.0" />
<PackageReference Include="BitBadger.Documents.Sqlite" Version="4.0.1" />
<PackageReference Include="Giraffe" Version="7.0.2" />
<PackageReference Include="NodaTime" Version="3.2.0" />
<PackageReference Include="Npgsql.FSharp" Version="5.7.0" />

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PrayerTracker.Data\PrayerTracker.Data.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BitBadger.Documents.Postgres" Version="4.0.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,141 @@

open NodaTime
open Npgsql.FSharp
open PrayerTracker.Data
open PrayerTracker.Entities
module PgMappings =
/// Map a row to a Church instance
let mapToChurch (row : RowReader) =
{ Id = ChurchId (row.uuid "id")
Name = row.string "church_name"
City = row.string "city"
State = row.string "state"
HasVpsInterface = row.bool "has_vps_interface"
InterfaceAddress = row.stringOrNone "interface_address"
}
/// Map a row to a ListPreferences instance
let mapToListPreferences (row : RowReader) =
{ SmallGroupId = SmallGroupId (row.uuid "small_group_id")
DaysToKeepNew = row.int "days_to_keep_new"
DaysToExpire = row.int "days_to_expire"
LongTermUpdateWeeks = row.int "long_term_update_weeks"
EmailFromName = row.string "email_from_name"
EmailFromAddress = row.string "email_from_address"
Fonts = row.string "fonts"
HeadingColor = row.string "heading_color"
LineColor = row.string "line_color"
HeadingFontSize = row.int "heading_font_size"
TextFontSize = row.int "text_font_size"
GroupPassword = row.string "group_password"
IsPublic = row.bool "is_public"
PageSize = row.int "page_size"
TimeZoneId = TimeZoneId (row.string "time_zone_id")
RequestSort = RequestSort.Parse (row.string "request_sort")
DefaultEmailType = EmailFormat.Parse (row.string "default_email_type")
AsOfDateDisplay = AsOfDateDisplay.Parse (row.string "as_of_date_display")
}
/// Map a row to a Member instance
let mapToMember (row : RowReader) =
{ Id = MemberId (row.uuid "id")
SmallGroupId = SmallGroupId (row.uuid "small_group_id")
Name = row.string "member_name"
Email = row.string "email"
Format = row.stringOrNone "email_format" |> Option.map EmailFormat.Parse
}
/// Map a row to a Prayer Request instance
let mapToPrayerRequest (row : RowReader) =
{ Id = PrayerRequestId (row.uuid "id")
UserId = UserId (row.uuid "user_id")
SmallGroupId = SmallGroupId (row.uuid "small_group_id")
EnteredDate = row.fieldValue<Instant> "entered_date"
UpdatedDate = row.fieldValue<Instant> "updated_date"
Requestor = row.stringOrNone "requestor"
Text = row.string "request_text"
NotifyChaplain = row.bool "notify_chaplain"
RequestType = PrayerRequestType.Parse (row.string "request_type")
Expiration = Expiration.Parse (row.string "expiration")
}
/// Map a row to a Small Group instance
let mapToSmallGroup (row : RowReader) =
{ Id = SmallGroupId (row.uuid "id")
ChurchId = ChurchId (row.uuid "church_id")
Name = row.string "group_name"
Preferences = ListPreferences.Empty
}
/// Map a row to a Small Group instance with populated list preferences
let mapToSmallGroupWithPreferences (row : RowReader) =
{ mapToSmallGroup row with
Preferences = mapToListPreferences row
}
/// Map a row to a User instance
let mapToUser (row : RowReader) =
{ Id = UserId (row.uuid "id")
FirstName = row.string "first_name"
LastName = row.string "last_name"
Email = row.string "email"
IsAdmin = row.bool "is_admin"
PasswordHash = row.string "password_hash"
LastSeen = row.fieldValueOrNone<Instant> "last_seen"
SmallGroups = []
}
// TODO: Configure PostgreSQL and SQLite connections
task {
let source = BitBadger.Documents.Postgres.Configuration.dataSource ()
let! churches =
Sql.fromDataSource source
|> Sql.query "SELECT * FROM pt.church"
|> Sql.executeAsync PgMappings.mapToChurch
for church in churches do
do! BitBadger.Documents.Sqlite.Document.insert Table.Church church
printfn "Migrated %d churches" churches.Length
let! groups =
Sql.fromDataSource source
|> Sql.query "SELECT sg.*, lp.* FROM pt.small_group sg
INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id"
|> Sql.executeAsync PgMappings.mapToSmallGroupWithPreferences
for group in groups do
do! BitBadger.Documents.Sqlite.Document.insert Table.Group group
printfn "Migrated %d groups" groups.Length
let! members =
Sql.fromDataSource source
|> Sql.query "SELECT * from pt.member"
|> Sql.executeAsync PgMappings.mapToMember
for mbr in members do
do! BitBadger.Documents.Sqlite.Document.insert Table.Member mbr
printfn "Migrated %d members" members.Length
let! requests =
Sql.fromDataSource source
|> Sql.query "SELECT * from pt.prayer_request"
|> Sql.executeAsync PgMappings.mapToPrayerRequest
for request in requests do
do! BitBadger.Documents.Sqlite.Document.insert Table.Request request
printfn "Migrated %d requests" requests.Length
let! users =
Sql.fromDataSource source
|> Sql.query "SELECT * FROM pt.pt_user"
|> Sql.executeAsync PgMappings.mapToUser
for user in users do
let! groups =
Sql.fromDataSource source
|> Sql.query "SELECT small_group_id FROM pt.user_small_group WHERE user_id = :user_id"
|> Sql.parameters [ ":user_id", Sql.uuid user.Id.Value ]
|> Sql.executeAsync (fun row -> (row.uuid >> SmallGroupId) "small_group_id")
do! BitBadger.Documents.Sqlite.Document.insert Table.User { user with SmallGroups = groups }
printfn "Migrated %d users" users.Length
} |> Async.AwaitTask |> Async.RunSynchronously

View File

@ -16,6 +16,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "PrayerTracker.MigrateV9", "PrayerTracker.MigrateV9\PrayerTracker.MigrateV9.fsproj", "{CE7C5972-AC9A-44A8-8265-771483FD87DB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -38,6 +40,10 @@ Global
{2B5BA107-9BDA-4A1D-A9AF-AFEE6BF12270}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B5BA107-9BDA-4A1D-A9AF-AFEE6BF12270}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B5BA107-9BDA-4A1D-A9AF-AFEE6BF12270}.Release|Any CPU.Build.0 = Release|Any CPU
{CE7C5972-AC9A-44A8-8265-771483FD87DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE7C5972-AC9A-44A8-8265-771483FD87DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE7C5972-AC9A-44A8-8265-771483FD87DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE7C5972-AC9A-44A8-8265-771483FD87DB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE