diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ffee7f6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.0.18] - 2024-06-28 + +Added: + +- Basic SQL type checking for project schema +- F# project generation with `selectAll` queries for all views and tables + +Fixed: + +- `mig relations` command. +- `mig log` command when SQLite file does not exists diff --git a/Cli/Program.fs b/Cli/Program.fs index 0412a9d..64507de 100644 --- a/Cli/Program.fs +++ b/Cli/Program.fs @@ -30,6 +30,7 @@ type MigArgs = | [] DbSchema of ParseResults | [] Relations of ParseResults | [] Export of ParseResults + | [] GenFs of ParseResults | [] ProjectPath of path: string interface IArgParserTemplate with @@ -46,6 +47,13 @@ type MigArgs = | Relations _ -> "shows the relations (tables + views) type signatures in the database or project" | Export _ -> "exports the content of a relation as an insert statement" | ProjectPath _ -> "project path" + | GenFs _ -> "generates an F# project with queries to the database" + +and GenFsArgs = + | [] DummyGenFs + + interface IArgParserTemplate with + member _.Usage = "generates an F# project with queries to the database" and VersionArgs = | [] Dummy @@ -224,7 +232,6 @@ let main (args: string array) = match Lib.loadProjectFromDir path with | Ok p -> - match command with | Some(DbSchema flags) -> dumpSchema p flags | Some(Commit flags) -> commit p flags @@ -238,6 +245,7 @@ let main (args: string array) = Assembly.GetExecutingAssembly().GetName().Version.ToString() |> printfn "%s" 0 | Some(Export args) -> exportRelation p args + | Some(GenFs _) -> Cli.generateFsProj p | _ -> Print.printRed "no command given" 1 diff --git a/Lib/Cli.fs b/Lib/Cli.fs index 0ce4221..7491b1e 100644 --- a/Lib/Cli.fs +++ b/Lib/Cli.fs @@ -317,3 +317,6 @@ let exportRelation (p: Project) (relation: string) = | None -> Print.printError $"relation {relation} not found" 1 + +let generateFsProj (p: Project) = + FsGeneration.Main.generateDatabaseProj None p diff --git a/Lib/DbProject/BuildProject.fs b/Lib/DbProject/BuildProject.fs index 993f618..3082966 100644 --- a/Lib/DbProject/BuildProject.fs +++ b/Lib/DbProject/BuildProject.fs @@ -43,7 +43,8 @@ let mergeTomlSql (p: DbTomlFile) (src: SqlFile) = inits = p.inits reports = p.reports pullScript = p.pullScript - schemaVersion = p.schemaVersion } + schemaVersion = p.schemaVersion + includeFsFiles = p.includeFsFiles } let buildProject (reader: string -> string) (p: DbTomlFile) = let parse (file, sql) = diff --git a/Lib/DbProject/ParseDbToml.fs b/Lib/DbProject/ParseDbToml.fs index b485378..7d956b9 100644 --- a/Lib/DbProject/ParseDbToml.fs +++ b/Lib/DbProject/ParseDbToml.fs @@ -45,6 +45,9 @@ let tableInit = "table_init" [] let reportTable = "report" +[] +let includeFsFiles = "include_fs_files" + let tryGet (t: Tomlyn.Model.TomlTable) (key: string) = if t.ContainsKey(key) then Some(t[key]) else None @@ -118,6 +121,8 @@ let parseDbToml (source: string) = | Some v -> v | _ -> MalformedProject $"no {versionRemarks} field defined in db.toml" |> raise + let included = tryGetArray doc includeFsFiles + match tryGetString doc dbFileKey with | None -> MalformedProject $"no {dbFileKey} defined" |> raise | Some f -> @@ -130,7 +135,8 @@ let parseDbToml (source: string) = inits = inits pullScript = script schemaVersion = version - versionRemarks = remarks } + versionRemarks = remarks + includeFsFiles = included } let parseDbTomlFile (path: string) = try diff --git a/Lib/DbUtil.fs b/Lib/DbUtil.fs index 9c3bd02..3838379 100644 --- a/Lib/DbUtil.fs +++ b/Lib/DbUtil.fs @@ -92,13 +92,4 @@ let loadFromRes (asm: Assembly) (namespaceForResx: string) (file: string) = (namespaceDotFile, file.ReadToEnd()) with ex -> FailedLoadResFile $"failed loading resource file {namespaceDotFile}: {ex.Message}" - |> raise - -type ReaderExecuter = - abstract member ExecuteReader: string -> IDataReader - -type SqliteReaderExecuter(connection: SqliteConnection, transaction: SqliteTransaction) = - interface ReaderExecuter with - member _.ExecuteReader(sql: string) = - let command = new SqliteCommand(sql, connection, transaction) - command.ExecuteReader() + |> raise \ No newline at end of file diff --git a/Lib/Execution/Store/Get.fs b/Lib/Execution/Store/Get.fs index 1e4f366..982e21e 100644 --- a/Lib/Execution/Store/Get.fs +++ b/Lib/Execution/Store/Get.fs @@ -54,15 +54,17 @@ let getStepReason (conn: SqliteConnection) (migrationId: int64) (stepIndex: int6 /// let getMigrations (conn: SqliteConnection) = let migrations = - select { - for m in migrationTable do - orderByDescending m.date - } - |> conn.SelectAsync - |> Async.AwaitTask - |> Async.RunSynchronously - |> Seq.toList - + try + select { + for m in migrationTable do + orderByDescending m.date + } + |> conn.SelectAsync + |> Async.AwaitTask + |> Async.RunSynchronously + |> Seq.toList + with :? System.AggregateException as _ -> + [] migrations |> List.map (fun (m: StoredMigration) -> diff --git a/Lib/FsGeneration/FsprojFile.fs b/Lib/FsGeneration/FsprojFile.fs new file mode 100644 index 0000000..855bff0 --- /dev/null +++ b/Lib/FsGeneration/FsprojFile.fs @@ -0,0 +1,54 @@ +// Copyright 2023 Luis Ángel Méndez Gort + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module internal Migrate.FsGeneration.FsprojFile + +open System.IO +open System.Xml.Linq +open Migrate.Types + +let projectToFsproj (p: Project) = + + let includeQuery = + XElement(XName.Get "Compile", XAttribute(XName.Get "Include", "Query.fs")) + + let fs = + p.includeFsFiles + |> List.map (fun f -> XElement(XName.Get "Compile", XAttribute(XName.Get "Include", f))) + + XElement( + XName.Get "Project", + XAttribute(XName.Get "Sdk", "Microsoft.NET.Sdk"), + + XElement(XName.Get "PropertyGroup", XElement(XName.Get "TargetFramework", "net8.0")), + + XElement(XName.Get "ItemGroup", includeQuery :: fs), + + XElement( + XName.Get "ItemGroup", + XElement( + XName.Get "PackageReference", + XAttribute(XName.Get "Include", "Microsoft.Data.Sqlite"), + XAttribute(XName.Get "Version", "8.0.6") + ), + XElement( + XName.Get "PackageReference", + XAttribute(XName.Get "Include", "MigrateLib"), + XAttribute(XName.Get "Version", "0.0.18") + ) + ) + ) + +let saveXmlTo (dir: string) (xml: XElement) = + Path.Join(dir, "Database.fsproj") |> xml.Save diff --git a/Lib/FsGeneration/Main.fs b/Lib/FsGeneration/Main.fs new file mode 100644 index 0000000..7e7f032 --- /dev/null +++ b/Lib/FsGeneration/Main.fs @@ -0,0 +1,36 @@ +// Copyright 2023 Luis Ángel Méndez Gort + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module internal Migrate.FsGeneration.Main + +open System.IO +open Migrate.Types +open Migrate.Checks.Types + +let generateDatabaseProj (dir: string option) (p: Project) = + let dir = Option.defaultValue (Directory.GetCurrentDirectory()) dir + p |> FsprojFile.projectToFsproj |> FsprojFile.saveXmlTo dir + let rs, errs = typeCheck p.source + + match errs with + | [] -> + let queryFs = + rs |> relationTypes |> QueryModule.queryModule |> QueryModule.toFsString + + let queryPath = Path.Join(dir, "Query.fs") + File.WriteAllText(queryPath, queryFs) + 1 + | _ -> + errs |> String.concat "\n" |> LamgEnv.errPrint + 0 diff --git a/Lib/FsGeneration/FsGeneration.fs b/Lib/FsGeneration/QueryModule.fs similarity index 95% rename from Lib/FsGeneration/FsGeneration.fs rename to Lib/FsGeneration/QueryModule.fs index aef0fb2..b0f62ea 100644 --- a/Lib/FsGeneration/FsGeneration.fs +++ b/Lib/FsGeneration/QueryModule.fs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -module Migrate.FsGeneration.FsGeneration +module internal Migrate.FsGeneration.QueryModule open System open Migrate.Types @@ -87,7 +87,8 @@ let queryModule (rs: Relation list) = Oak() { TopLevelModule "Database.Query" { - yield Open "Migrate.DbUtil" + yield Open "System" + yield Open "Migrate.FsGeneration.Util" for x in rs |> List.map relationToFsRecord do yield x diff --git a/Lib/FsGeneration/Util.fs b/Lib/FsGeneration/Util.fs new file mode 100644 index 0000000..b08ac59 --- /dev/null +++ b/Lib/FsGeneration/Util.fs @@ -0,0 +1,27 @@ +// Copyright 2023 Luis Ángel Méndez Gort + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module Migrate.FsGeneration.Util + +open System.Data +open Microsoft.Data.Sqlite + +type ReaderExecuter = + abstract member ExecuteReader: string -> IDataReader + +type SqliteReaderExecuter(connection: SqliteConnection, transaction: SqliteTransaction) = + interface ReaderExecuter with + member _.ExecuteReader(sql: string) = + let command = new SqliteCommand(sql, connection, transaction) + command.ExecuteReader() diff --git a/Lib/Lib.fs b/Lib/Lib.fs index ef5a3c4..fb56966 100644 --- a/Lib/Lib.fs +++ b/Lib/Lib.fs @@ -1,23 +1,37 @@ +// Copyright 2023 Luis Ángel Méndez Gort + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + module Migrate.Lib /// /// Convenience function for running commits when using Migrate as a library /// let commitQuiet (p: Types.Project) = - Migrate.Execution.Commit.migrateAndCommit p true + Execution.Commit.migrateAndCommit p true /// /// Convenience function for loading project files from assembly resources /// Raises MalformedProject in case of failure /// let loadResourceFile (asm: System.Reflection.Assembly) (prefix: string) (file: string) = - Migrate.DbProject.LoadProjectFiles.loadResourceFile asm prefix file + DbProject.LoadProjectFiles.loadResourceFile asm prefix file /// /// Loads a project using a custom file reader /// let loadProjectWith (loadFile: string -> string) = - Migrate.DbProject.LoadProjectFiles.loadProjectWith loadFile + DbProject.LoadProjectFiles.loadProjectWith loadFile /// /// Loads a project from a directory if specified or the current one instead @@ -27,3 +41,6 @@ let loadProjectFromDir (dir: string option) = DbProject.LoadProjectFiles.loadProjectFromDir dir |> Ok with e -> Error e.Message + +let generateDatabaseProj (dir: string option) (p: Types.Project) = + FsGeneration.Main.generateDatabaseProj dir p diff --git a/Lib/Lib.fsproj b/Lib/Lib.fsproj index a2a9e39..5536b9d 100644 --- a/Lib/Lib.fsproj +++ b/Lib/Lib.fsproj @@ -45,8 +45,11 @@ - - + + + + + @@ -63,6 +66,9 @@ - + + + + \ No newline at end of file diff --git a/Lib/Types.fs b/Lib/Types.fs index 6f09f41..213d7a2 100644 --- a/Lib/Types.fs +++ b/Lib/Types.fs @@ -77,6 +77,7 @@ type Report = { src: string; dest: string } type Project = { dbFile: string source: SqlFile + includeFsFiles: string list syncs: string list inits: string list reports: Report list @@ -124,6 +125,12 @@ type DbTomlFile = /// Remarks about the version /// versionRemarks: string + + /// + /// F# files in the database project directory written by the user, + /// to be included in the generated Database.fsproj file + /// + includeFsFiles: string list } type SqlStep = { sql: string; error: string option } diff --git a/Test/Calculation.fs b/Test/Calculation.fs index 764ab50..cce1145 100644 --- a/Test/Calculation.fs +++ b/Test/Calculation.fs @@ -33,7 +33,8 @@ let emptyProject = syncs = [] inits = [] reports = [] - pullScript = None } + pullScript = None + includeFsFiles = [] } let schemaWithOneTable (tableName: string) = { emptySchema with diff --git a/Test/CheckTypes.fs b/Test/CheckTypes.fs index 5d9eae3..c41c0fa 100644 --- a/Test/CheckTypes.fs +++ b/Test/CheckTypes.fs @@ -37,6 +37,7 @@ let exampleProject = syncs = [] inits = [] pullScript = None + includeFsFiles = [] source = { tables = [ { name = "table0" diff --git a/Test/DbProject.fs b/Test/DbProject.fs index 09639ba..f498067 100644 --- a/Test/DbProject.fs +++ b/Test/DbProject.fs @@ -62,6 +62,7 @@ let parseProjectSrc () = inits = [] files = [ "file0.sql"; "file1.sql" ] pullScript = None + includeFsFiles = [] reports = [ { src = "source_relation" dest = "destination_relation" } ] } @@ -96,6 +97,7 @@ let wrapWithProject () = syncs = [ "table0" ] inits = [] files = [ "file0.sql"; "file1.sql" ] + includeFsFiles = [] pullScript = None reports = [ { src = "source_relation" @@ -117,6 +119,7 @@ let wrapWithProject () = dbFile = "/data/db.sqlite3" syncs = [ "table0" ] inits = [] + includeFsFiles = [] source = src pullScript = None reports = diff --git a/Test/Execution.fs b/Test/Execution.fs index eb59d3d..d27572d 100644 --- a/Test/Execution.fs +++ b/Test/Execution.fs @@ -33,6 +33,7 @@ let emptyProject: Project = syncs = [] inits = [] reports = [] + includeFsFiles = [] pullScript = None } let schema0 = diff --git a/Test/Report.fs b/Test/Report.fs index 0d29f40..d778718 100644 --- a/Test/Report.fs +++ b/Test/Report.fs @@ -28,6 +28,7 @@ let exampleProject = reports = [ { src = "rel0"; dest = "rel0_report" } ] syncs = [] inits = [] + includeFsFiles = [] pullScript = None source = { tables = diff --git a/Test/Util.fs b/Test/Util.fs index a33675d..3dc3476 100644 --- a/Test/Util.fs +++ b/Test/Util.fs @@ -65,5 +65,6 @@ let projectWithOneTable = source = schemaWithOneTable syncs = [ "table0" ] inits = [] + includeFsFiles = [] reports = [] pullScript = None }