From 385311dc6b4a2e610cdd8a2698955b23943a8275 Mon Sep 17 00:00:00 2001 From: Zaid Date: Sun, 6 Dec 2020 22:40:23 +0100 Subject: [PATCH 1/3] Introducing Ubik v3.18 as a dotnet CLI tool --- README.md | 32 +++++++- build.fsx | 20 +++++ src/Ubik/Program.fs | 171 ++++++++++++++++++++++++++++++++++--------- src/Ubik/Ubik.fsproj | 8 +- 4 files changed, 196 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 90ba7a6..f5ec422 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,14 @@ Analyzer that provides embedded **SQL syntax analysis** when writing queries usi - Free (MIT licensed) - Supports VS Code with [Ionide](https://github.com/ionide/ionide-vscode-fsharp) via F# Analyzers SDK - Supports Visual Studio +- Supports CLI (via Ubik) ## NuGet | Package | Stable | Prerelease | | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | NpgsqlFSharpAnalyzer | [![NuGet Badge](https://buildstats.info/nuget/NpgsqlFSharpAnalyzer)](https://www.nuget.org/packages/NpgsqlFSharpAnalyzer/) | [![NuGet Badge](https://buildstats.info/nuget/NpgsqlFSharpAnalyzer?includePreReleases=true)](https://www.nuget.org/packages/NpgsqlFSharpAnalyzer/) | - +| Ubik | [![NuGet Badge](https://buildstats.info/nuget/Ubik)](https://www.nuget.org/packages/Ubik/) | [![NuGet Badge](https://buildstats.info/nuget/Ubik?includePreReleases=true)](https://www.nuget.org/packages/Ubik/) | ## Using The Analyzer (Visual Studio) @@ -73,6 +74,35 @@ Make sure you have these settings in Ionide for FSharp ``` Which instructs Ionide to load the analyzers from the directory of the analyzers into which `NpgsqlFSharpAnalyzer` was installed. +# Using CLI with Ubik + +### 1 - Configure the connection string to your development database +The analyzer requires a connection string that points to the database you are developing against. You can configure this connection string by either creating a file called `NPGSQL_FSHARP` (without extension) somewhere next to your F# project or preferably in the root of your workspace. This file should contain that connection string and nothing else. An example of the contents of such file: +``` +Host=localhost; Username=postgres; Password=postgres; Database=databaseName +``` +> Remember to add an entry in your .gitingore file to make sure you don't commit the connection string to your source version control system. + +Another way to configure the connection string is by setting the value of an environment variable named `NPGSQL_FSHARP` that contains the connection string. + +The analyzer will try to locate and read the file first, then falls back to using the environment variable. + +### 2 - Install Ubik as a dotnet CLI tool +``` +dotnet tool install ubik --global +``` +### 3 - Run Ubik in the directory of the project you want to analyze +```bash +cd ./path/to/project +ubik + +ubik ./path/to/Project.fsproj + +ubik ./File1.fs ./AnotherFile.fs + +ubik --version +``` + ### Writing Long Multi-line Queries When it is not convenient to write a query inline like this: diff --git a/build.fsx b/build.fsx index 0548c7e..688b433 100644 --- a/build.fsx +++ b/build.fsx @@ -315,6 +315,21 @@ let publishToNuget _ = if exitCode <> 0 then failwith "Could not publish package" +let packUbik _ = + Shell.cleanDir (__SOURCE_DIRECTORY__ "dist") + let args = + [ + "pack" + "--configuration Release" + sprintf "--output %s" (__SOURCE_DIRECTORY__ "dist") + ] + + let exitCode = Shell.Exec("dotnet", String.concat " " args, "src" "Ubik") + if exitCode <> 0 + then failwith "dotnet pack failed" + +Target.create "PackUbik" packUbik +Target.create "PublishUbik" publishToNuget //----------------------------------------------------------------------------- // Target Declaration @@ -352,6 +367,11 @@ Target.create "PackNoTests" dotnetPack ==> "PublishToNuGet" ==> "Release" +"DotnetBuild" +==> "DotnetTest" +==> "PackUbik" +==> "PublishUbik" + "DotnetRestore" ==> "WatchTests" diff --git a/src/Ubik/Program.fs b/src/Ubik/Program.fs index 89cc6aa..67727f8 100644 --- a/src/Ubik/Program.fs +++ b/src/Ubik/Program.fs @@ -1,57 +1,75 @@ open System open System.IO +open System.Linq open Npgsql.FSharp.Analyzers.Core open Spectre.Console open System.Xml +open FSharp.Compiler.Text +open FSharp.Data.LiteralProviders let resolveFile (path: string) = if Path.IsPathRooted path then path else Path.GetFullPath (Path.Combine(Environment.CurrentDirectory, path)) +type FilePath = + | File of path: string + | InvalidFile of fileName: string + +type CliArgs = + | Version + | InvalidArgs of error:string + | Project of projectPath:string + | Files of fsharpFiles: FilePath[] + let getProject (args: string []) = try match args with | [| |] -> Directory.GetFiles(Environment.CurrentDirectory, "*.fsproj") |> Array.tryHead - |> Option.map (fun projectPath -> resolveFile projectPath) + |> Option.map (fun projectPath -> Project(resolveFile projectPath)) + |> function + | Some project -> project + | None -> + Directory.GetFiles(Environment.CurrentDirectory, "*.fs") + |> Array.map File + |> Files + + | [| "--version" |] -> Version | multipleArgs -> - let firstArg = multipleArgs.[0] - if firstArg.EndsWith(".fsproj") then - Some (resolveFile firstArg) - else - Directory.GetFiles(resolveFile firstArg, "*.fsproj") + if multipleArgs.Length = 1 && multipleArgs.[0].EndsWith ".fsproj" then + Project (resolveFile multipleArgs.[0]) + else if Directory.Exists(resolveFile multipleArgs.[0]) then + Directory.GetFiles(resolveFile multipleArgs.[0], "*.fsproj") |> Array.tryHead - |> Option.map (fun projectPath -> resolveFile projectPath) - with - | error -> None + |> Option.map (fun projectPath -> Project(resolveFile projectPath)) + |> function + | Some project -> project + | None -> + Directory.GetFiles(Environment.CurrentDirectory, "*.fs") + |> Array.map File + |> Files + else + multipleArgs + |> Array.filter (fun file -> file.EndsWith ".fs") + |> Array.map (fun file -> try File (resolveFile file) with _ -> InvalidFile file) + |> Files -[] -let main argv = - match getProject argv with - | None -> - printfn "No project file found in the current directory" - 1 - - | Some project -> - AnsiConsole.MarkupLine("Analyzing [blue]{0}[/]", project) - - let document = XmlDocument() - document.LoadXml(File.ReadAllText project) - - let fsharpFileNodes = document.GetElementsByTagName("Compile") - let fsharpFiles = [ - for item in 0 .. fsharpFileNodes.Count - 1 -> - let relativePath = fsharpFileNodes.[item].Attributes.["Include"].InnerText - let projectParent = Directory.GetParent project - Path.Combine(projectParent.FullName, relativePath) - ] + with + | error -> InvalidArgs error.Message - for file in fsharpFiles do - AnsiConsole.MarkupLine("Analyzing file [green]{0}[/]", file) - match Project.context file with +let analyzeFiles (fsharpFiles: FilePath[]) = + let errorCount = ResizeArray() + for fsharpFile in fsharpFiles do + match fsharpFile with + | InvalidFile nonExistingFile -> + AnsiConsole.MarkupLine("Analyzing file did not exist [red]{0}[/]", nonExistingFile) + errorCount.Add(1) + | File fsharpFile -> + AnsiConsole.MarkupLine("Analyzing file [green]{0}[/]", fsharpFile) + match Project.context fsharpFile with | None -> () | Some context -> let syntacticBlocks = SyntacticAnalysis.findSqlOperations context @@ -74,6 +92,93 @@ let main argv = |> List.distinctBy (fun message -> message.Range) for message in messages do - AnsiConsole.MarkupLine("Error [red]{0}[/]", message.Message) + let range = message.Range + let source = SourceText.ofString(File.ReadAllText fsharpFile) + if range.StartLine = range.EndLine then + let marker = + source.GetLineString(range.StartLine - 1) + |> Seq.mapi (fun index token -> + if index >= range.StartColumn && index < range.EndColumn + then "[orange1]^[/]" + else " " + ) + |> String.concat "" + + let original = + source.GetLineString(range.StartLine - 1) + |> Seq.mapi (fun index token -> + if index >= range.StartColumn && index < range.EndColumn + then "[orange1]" + token.ToString().EscapeMarkup() + "[/]" + else token.ToString().EscapeMarkup() + ) + |> String.concat "" + + let before = (range.StartLine - 1).ToString() + let current = range.StartLine.ToString() + let after = (range.StartLine + 1).ToString() + + let maxLength = List.max [ before.Length; current.Length; after.Length ] + + AnsiConsole.MarkupLine(" [blue]{0} |[/]", before.PadLeft(maxLength, ' ')) + AnsiConsole.MarkupLine(" [blue]{0} |[/] {1}", current.PadLeft(maxLength, ' '), original) + AnsiConsole.MarkupLine(" [blue]{0} |[/] {1}", after.PadLeft(maxLength, ' '), marker) + AnsiConsole.MarkupLine("[orange1]{0}[/]", message.Message) + errorCount.Add(1) + else + let lines = [range.StartLine-1 .. range.EndLine+1] + let maxLength = + lines + |> List.map (fun line -> line.ToString().Length) + |> List.max + + for line in lines do + if line = range.StartLine - 1 || line = range.EndLine + 1 + then AnsiConsole.MarkupLine(" [blue]{0} |[/] ", line.ToString().PadLeft(maxLength, ' ')) + else AnsiConsole.MarkupLine(" [blue]{0} |[/] {1}", line.ToString().PadLeft(maxLength, ' '), source.GetLineString(line - 1).EscapeMarkup()) + + AnsiConsole.MarkupLine("[orange1]{0}[/]", message.Message) + errorCount.Add(1) + let exitCode = errorCount.Sum() + Console.WriteLine() + if exitCode = 0 + then AnsiConsole.MarkupLine("[green]No errors found[/]") + elif exitCode = 1 + then AnsiConsole.MarkupLine("[orange1]Found 1 error[/]", exitCode) + else AnsiConsole.MarkupLine("[orange1]Found {0} errors[/]", exitCode) + exitCode + +let [] projectFile = TextFile<"./Ubik.fsproj">.Text + +let projectVersion = + let doc = XmlDocument() + use content = new MemoryStream(Text.Encoding.UTF8.GetBytes projectFile) + doc.Load(content) + doc.GetElementsByTagName("Version").[0].InnerText + +[] +let main argv = + match getProject argv with + | InvalidArgs error -> + AnsiConsole.MarkupLine("[red]{0}: {1}[/]", "Error occured while reading CLI arguments: ", error) + 1 + + | Version -> + printfn "%s" projectVersion 0 + + | Files files -> analyzeFiles files + + | Project project -> + AnsiConsole.MarkupLine("Analyzing project [blue]{0}[/]", project) + + let document = XmlDocument() + document.LoadXml(File.ReadAllText project) + + let fsharpFileNodes = document.GetElementsByTagName("Compile") + analyzeFiles [| + for item in 0 .. fsharpFileNodes.Count - 1 -> + let relativePath = fsharpFileNodes.[item].Attributes.["Include"].InnerText + let projectParent = Directory.GetParent project + File(Path.Combine(projectParent.FullName, relativePath)) + |] diff --git a/src/Ubik/Ubik.fsproj b/src/Ubik/Ubik.fsproj index df7b4b7..6bec114 100644 --- a/src/Ubik/Ubik.fsproj +++ b/src/Ubik/Ubik.fsproj @@ -3,6 +3,12 @@ Exe netcoreapp3.1 + ubik + true + true + Major + 3.18.0 + Initial release of Ubik @@ -17,7 +23,7 @@ - + From e06231910314439545a22bad74899e40a4c2e458 Mon Sep 17 00:00:00 2001 From: Zaid Date: Sun, 6 Dec 2020 23:14:28 +0100 Subject: [PATCH 2/3] Ubik v3.19 Provide a nice error message when there is no connection string file or environment variable --- src/Ubik/Program.fs | 5 ++++- src/Ubik/Ubik.fsproj | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Ubik/Program.fs b/src/Ubik/Program.fs index 67727f8..8b4cfef 100644 --- a/src/Ubik/Program.fs +++ b/src/Ubik/Program.fs @@ -77,7 +77,10 @@ let analyzeFiles (fsharpFiles: FilePath[]) = let messages = let connectionString = SqlAnalyzer.tryFindConnectionString context.FileName if isNull connectionString || String.IsNullOrWhiteSpace connectionString then - [ ] + [ + for block in syntacticBlocks -> + SqlAnalysis.createWarning "Missing environment variable 'NPGSQL_FSHARP'. Please set that variable to the connection string of your development database or put the connection string in a file called 'NPGSQL_FSHARP' next to your project file or in your workspace root." block.range + ] else match SqlAnalysis.databaseSchema connectionString with | Result.Error connectionError -> diff --git a/src/Ubik/Ubik.fsproj b/src/Ubik/Ubik.fsproj index 6bec114..759cd6f 100644 --- a/src/Ubik/Ubik.fsproj +++ b/src/Ubik/Ubik.fsproj @@ -7,8 +7,8 @@ true true Major - 3.18.0 - Initial release of Ubik + 3.19.0 + Provide a nice error message when there is no connection string file or environment variable From 1ca2fd7c363bbda49a95df672a7a6b58613d76e6 Mon Sep 17 00:00:00 2001 From: Zaid Date: Tue, 8 Dec 2020 07:36:26 +0100 Subject: [PATCH 3/3] v3.20 Correctly retain selected non-nullability for SELECTed column when casted to another type --- RELEASE_NOTES.md | 9 ++- src/FParsec/AssemblyInfo.fs | 20 +++--- src/NpgsqlFSharpAnalyzer.Core/AssemblyInfo.fs | 20 +++--- src/NpgsqlFSharpAnalyzer.Core/SqlAnalysis.fs | 68 ++++++++++++++++++- src/NpgsqlFSharpAnalyzer/AssemblyInfo.fs | 20 +++--- src/NpgsqlFSharpParser/AssemblyInfo.fs | 20 +++--- src/NpgsqlFSharpParser/Parser.fs | 1 + .../source.extension.vsixmanifest | 2 +- src/Ubik/AssemblyInfo.fs | 20 +++--- src/Ubik/Ubik.fsproj | 4 +- .../AssemblyInfo.fs | 20 +++--- tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs | 20 ++++++ tests/examples/hashing/AssemblyInfo.fs | 20 +++--- .../castingNonNullableStaysNonNullable.fs | 21 ++++++ tests/examples/hashing/examples.fsproj | 1 + 15 files changed, 189 insertions(+), 77 deletions(-) create mode 100644 tests/examples/hashing/castingNonNullableStaysNonNullable.fs diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f6538ba..f57951c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,10 +1,13 @@ -### 3.18.0 - 2020-09-15 +### 3.20.0 - 2020-12-08 +* Correctly retain selected column non-nullability when casted or aliased to another type + +### 3.18.0 - 2020-12-06 * Analyze SQL blocks from within lambda expressions -### 3.17.0 - 2020-09-15 +### 3.17.0 - 2020-12-06 * Support for datetimeOffset and datetimeOffsetOrNone when reading columns of type timestamptz -### 3.16.0 - 2020-09-15 +### 3.16.0 - 2020-12-06 * Analyze top level do expressions ### 3.15.0 - 2020-09-15 diff --git a/src/FParsec/AssemblyInfo.fs b/src/FParsec/AssemblyInfo.fs index bc891f6..4ef05e8 100644 --- a/src/FParsec/AssemblyInfo.fs +++ b/src/FParsec/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] -[] -[] -[] +[] +[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FParsec" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.18.0" - let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.18.0" - let [] AssemblyInformationalVersion = "3.18.0" + let [] AssemblyVersion = "3.20.0" + let [] AssemblyMetadata_ReleaseDate = "2020-12-08T00:00:00.0000000" + let [] AssemblyFileVersion = "3.20.0" + let [] AssemblyInformationalVersion = "3.20.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" + let [] AssemblyMetadata_GitHash = "e06231910314439545a22bad74899e40a4c2e458" diff --git a/src/NpgsqlFSharpAnalyzer.Core/AssemblyInfo.fs b/src/NpgsqlFSharpAnalyzer.Core/AssemblyInfo.fs index 20437bd..f276d71 100644 --- a/src/NpgsqlFSharpAnalyzer.Core/AssemblyInfo.fs +++ b/src/NpgsqlFSharpAnalyzer.Core/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] -[] -[] -[] +[] +[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "NpgsqlFSharpAnalyzer.Core" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.18.0" - let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.18.0" - let [] AssemblyInformationalVersion = "3.18.0" + let [] AssemblyVersion = "3.20.0" + let [] AssemblyMetadata_ReleaseDate = "2020-12-08T00:00:00.0000000" + let [] AssemblyFileVersion = "3.20.0" + let [] AssemblyInformationalVersion = "3.20.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" + let [] AssemblyMetadata_GitHash = "e06231910314439545a22bad74899e40a4c2e458" diff --git a/src/NpgsqlFSharpAnalyzer.Core/SqlAnalysis.fs b/src/NpgsqlFSharpAnalyzer.Core/SqlAnalysis.fs index d3718bc..f2f677d 100644 --- a/src/NpgsqlFSharpAnalyzer.Core/SqlAnalysis.fs +++ b/src/NpgsqlFSharpAnalyzer.Core/SqlAnalysis.fs @@ -343,12 +343,78 @@ module SqlAnalysis = | _ -> None + // When casting columns during a select statement, if the original column is non-nullable + // then the computed column somehow becomes nullable when we ask the database for its type + // for example + // user_id int not null -> inferred non-nullable + // user_id::text -> inferred nullable + // + // This function resolves the correct nullability and infers non-nullable columns to be returned as such + let resolveColumnNullability (commandText: string) (schema: DbSchemaLookups) (columns: Column list) = + match Parser.parse commandText with + | Result.Ok (Expr.SelectQuery selectQuery) -> + let schemaColumns = + schema.Columns + |> Seq.map (fun pair -> pair.Value) + |> Seq.toList + + columns + |> List.map (fun column -> + let originalColumnNameAndAlias = + selectQuery.Columns + |> List.tryPick (function + // find columns expressions of the shape + // {originalName}::{typeName} AS {alias} + | Expr.As(Expr.TypeCast(Expr.Ident originalName, Expr.Ident typeName), Expr.Ident alias) -> + Some (originalName, alias, false) + // 1::{typeName} AS {alias} + | Expr.As(Expr.TypeCast(Expr.Integer _, Expr.Ident typeName), Expr.Ident alias) -> + Some (column.Name, column.Name, true) + // {originalName}::{typeName} + | Expr.TypeCast(Expr.Ident originalName, Expr.Ident typeName) -> + Some (originalName, originalName, false) + // 1::{typeName} + | Expr.TypeCast(Expr.Integer _, Expr.Ident typeName) -> + Some (column.Name, column.Name, true) + // "text"::{typeName} + | Expr.TypeCast(Expr.StringLiteral _, Expr.Ident typeName) -> + Some (column.Name, column.Name, true) + // 1 as {alias} + | Expr.As(Expr.Integer _, Expr.Ident alias) -> + Some (alias, alias, true) + // "text" AS alias + | Expr.As(Expr.StringLiteral _, Expr.Ident alias) -> + Some (alias, alias, true) + | _ -> + None + ) + + match originalColumnNameAndAlias with + | None -> column + | Some (name, alias, isConst) when isConst-> { column with Nullable = false } + | Some (name, alias, isConst) -> + schemaColumns + |> List.tryFind (fun columnSchema -> columnSchema.Name = name && alias = column.Name) + |> function + | None -> column + | Some columnSchema -> + { column with + Nullable = columnSchema.Nullable + DefaultConstraint = columnSchema.DefaultConstraint + PartOfPrimaryKey = columnSchema.PartOfPrimaryKey + BaseTableName = column.BaseTableName + BaseSchemaName = column.BaseSchemaName } + ) + | _ -> + columns + let extractParametersAndOutputColumns(connectionString, commandText, dbSchemaLookups) = try let parameters, output, enums = InformationSchema.extractParametersAndOutputColumns(connectionString, commandText, false, dbSchemaLookups) let parametersWithNullability = determineParameterNullability parameters dbSchemaLookups commandText let potentiallyMissingColumns = missingInsertColumns dbSchemaLookups commandText - Result.Ok (parametersWithNullability, output, potentiallyMissingColumns) + let rewrittenColumns = resolveColumnNullability commandText dbSchemaLookups output + Result.Ok (parametersWithNullability, rewrittenColumns, potentiallyMissingColumns) with | :? PostgresException as databaseError -> // errors such as syntax errors are reported here diff --git a/src/NpgsqlFSharpAnalyzer/AssemblyInfo.fs b/src/NpgsqlFSharpAnalyzer/AssemblyInfo.fs index 05f6c84..54fa713 100644 --- a/src/NpgsqlFSharpAnalyzer/AssemblyInfo.fs +++ b/src/NpgsqlFSharpAnalyzer/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] -[] -[] -[] +[] +[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "NpgsqlFSharpAnalyzer" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.18.0" - let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.18.0" - let [] AssemblyInformationalVersion = "3.18.0" + let [] AssemblyVersion = "3.20.0" + let [] AssemblyMetadata_ReleaseDate = "2020-12-08T00:00:00.0000000" + let [] AssemblyFileVersion = "3.20.0" + let [] AssemblyInformationalVersion = "3.20.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" + let [] AssemblyMetadata_GitHash = "e06231910314439545a22bad74899e40a4c2e458" diff --git a/src/NpgsqlFSharpParser/AssemblyInfo.fs b/src/NpgsqlFSharpParser/AssemblyInfo.fs index d717dbe..8a5c30c 100644 --- a/src/NpgsqlFSharpParser/AssemblyInfo.fs +++ b/src/NpgsqlFSharpParser/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] -[] -[] -[] +[] +[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "NpgsqlFSharpParser" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.18.0" - let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.18.0" - let [] AssemblyInformationalVersion = "3.18.0" + let [] AssemblyVersion = "3.20.0" + let [] AssemblyMetadata_ReleaseDate = "2020-12-08T00:00:00.0000000" + let [] AssemblyFileVersion = "3.20.0" + let [] AssemblyInformationalVersion = "3.20.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" + let [] AssemblyMetadata_GitHash = "e06231910314439545a22bad74899e40a4c2e458" diff --git a/src/NpgsqlFSharpParser/Parser.fs b/src/NpgsqlFSharpParser/Parser.fs index 781b44c..dc073a6 100644 --- a/src/NpgsqlFSharpParser/Parser.fs +++ b/src/NpgsqlFSharpParser/Parser.fs @@ -255,6 +255,7 @@ let updateQuery = opp.AddOperator(InfixOperator("AND", spaces, 7, Associativity.Left, fun left right -> Expr.And(left, right))) opp.AddOperator(InfixOperator("AS", spaces, 6, Associativity.Left, fun left right -> Expr.As(left, right))) +opp.AddOperator(InfixOperator("as", spaces, 6, Associativity.Left, fun left right -> Expr.As(left, right))) opp.AddOperator(InfixOperator("OR", notFollowedBy (text "DER BY"), 6, Associativity.Left, fun left right -> Expr.Or(left, right))) opp.AddOperator(InfixOperator("IN", spaces, 8, Associativity.Left, fun left right -> Expr.In(left, right))) opp.AddOperator(InfixOperator(">", spaces, 9, Associativity.Left, fun left right -> Expr.GreaterThan(left, right))) diff --git a/src/NpgsqlFSharpVs/source.extension.vsixmanifest b/src/NpgsqlFSharpVs/source.extension.vsixmanifest index da6b243..e1b4a8f 100644 --- a/src/NpgsqlFSharpVs/source.extension.vsixmanifest +++ b/src/NpgsqlFSharpVs/source.extension.vsixmanifest @@ -3,7 +3,7 @@ xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011"> - + NpgsqlFSharpVs F# Analyzer for embedded SQL syntax analysis, type-checking for parameters and result sets and nullable column detection when writing queries using Npgsql.FSharp. https://github.com/Zaid-Ajaj/Npgsql.FSharp.Analyzer diff --git a/src/Ubik/AssemblyInfo.fs b/src/Ubik/AssemblyInfo.fs index 0efe71c..d05fd93 100644 --- a/src/Ubik/AssemblyInfo.fs +++ b/src/Ubik/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] -[] -[] -[] +[] +[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Ubik" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.18.0" - let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.18.0" - let [] AssemblyInformationalVersion = "3.18.0" + let [] AssemblyVersion = "3.20.0" + let [] AssemblyMetadata_ReleaseDate = "2020-12-08T00:00:00.0000000" + let [] AssemblyFileVersion = "3.20.0" + let [] AssemblyInformationalVersion = "3.20.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" + let [] AssemblyMetadata_GitHash = "e06231910314439545a22bad74899e40a4c2e458" diff --git a/src/Ubik/Ubik.fsproj b/src/Ubik/Ubik.fsproj index 759cd6f..61c00a4 100644 --- a/src/Ubik/Ubik.fsproj +++ b/src/Ubik/Ubik.fsproj @@ -7,8 +7,8 @@ true true Major - 3.19.0 - Provide a nice error message when there is no connection string file or environment variable + 3.20.0 + Correctly retain selected column non-nullability when casted or aliased to another type diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/AssemblyInfo.fs b/tests/NpgsqlFSharpAnalyzer.Tests/AssemblyInfo.fs index a5473c9..18e5666 100644 --- a/tests/NpgsqlFSharpAnalyzer.Tests/AssemblyInfo.fs +++ b/tests/NpgsqlFSharpAnalyzer.Tests/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] -[] -[] -[] +[] +[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "NpgsqlFSharpAnalyzer.Tests" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.18.0" - let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.18.0" - let [] AssemblyInformationalVersion = "3.18.0" + let [] AssemblyVersion = "3.20.0" + let [] AssemblyMetadata_ReleaseDate = "2020-12-08T00:00:00.0000000" + let [] AssemblyFileVersion = "3.20.0" + let [] AssemblyInformationalVersion = "3.20.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" + let [] AssemblyMetadata_GitHash = "e06231910314439545a22bad74899e40a4c2e458" diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs b/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs index 9d573ef..2ec54e4 100644 --- a/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs +++ b/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs @@ -262,6 +262,26 @@ let tests = Expect.isEmpty messages "No errors returned" } + test "Semantic analysis: casting non-nullable columns stays non-nullable" { + use db = createTestDatabase() + + Sql.connect db.ConnectionString + |> Sql.query "CREATE TABLE users (user_id bigserial primary key, username text not null)" + |> Sql.executeNonQuery + |> raiseWhenFailed + + match context (find "../examples/hashing/castingNonNullableStaysNonNullable.fs") with + | None -> failwith "Could not crack project" + | Some context -> + match SqlAnalysis.databaseSchema db.ConnectionString with + | Result.Error connectionError -> + failwith connectionError + | Result.Ok schema -> + let blocks = SyntacticAnalysis.findSqlOperations context + let messages = blocks |> List.collect (fun block -> SqlAnalysis.analyzeOperation block db.ConnectionString schema) + Expect.isEmpty messages "No errors returned" + } + test "Semantic analysis: incorrect queries in executeTranscation are detected" { use db = createTestDatabase() diff --git a/tests/examples/hashing/AssemblyInfo.fs b/tests/examples/hashing/AssemblyInfo.fs index c68e32c..5c9f922 100644 --- a/tests/examples/hashing/AssemblyInfo.fs +++ b/tests/examples/hashing/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] -[] -[] -[] +[] +[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "examples" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.18.0" - let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.18.0" - let [] AssemblyInformationalVersion = "3.18.0" + let [] AssemblyVersion = "3.20.0" + let [] AssemblyMetadata_ReleaseDate = "2020-12-08T00:00:00.0000000" + let [] AssemblyFileVersion = "3.20.0" + let [] AssemblyInformationalVersion = "3.20.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" + let [] AssemblyMetadata_GitHash = "e06231910314439545a22bad74899e40a4c2e458" diff --git a/tests/examples/hashing/castingNonNullableStaysNonNullable.fs b/tests/examples/hashing/castingNonNullableStaysNonNullable.fs new file mode 100644 index 0000000..fb2ee47 --- /dev/null +++ b/tests/examples/hashing/castingNonNullableStaysNonNullable.fs @@ -0,0 +1,21 @@ +module castingNonNullableStaysNonNullable + +open Npgsql.FSharp.Tasks + +let userIds connection = + connection + |> Sql.connect + |> Sql.query "SELECT user_id::text AS useridtext FROM users" + |> Sql.execute (fun read -> read.text "useridtext") + +let moreIds connection = + connection + |> Sql.connect + |> Sql.query "SELECT user_id::text as useridtext FROM users" + |> Sql.execute (fun read -> read.text "useridtext") + +let withoutAlias connection = + connection + |> Sql.connect + |> Sql.query "SELECT user_id::text FROM users" + |> Sql.execute (fun read -> read.text "user_id") diff --git a/tests/examples/hashing/examples.fsproj b/tests/examples/hashing/examples.fsproj index 6e4c3cd..6311395 100644 --- a/tests/examples/hashing/examples.fsproj +++ b/tests/examples/hashing/examples.fsproj @@ -5,6 +5,7 @@ +