Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
428 changes: 283 additions & 145 deletions src/DbTests/DbGen.fs

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions src/DbTests/DbTests.fsproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<!-- Target a framework compatible with netstandard2.1 and one only compatible with netstandard2.0 -->
Expand Down Expand Up @@ -36,6 +36,7 @@
<None Include="SQL\OverriddenDtoParamName.sql" />
<None Include="SQL\DynamicSqlWithDeclaration.sql" />
<None Include="SQL\DynamicSqlWithoutDeclaration.sql" />
<Content Include="SQL\TempTable.sql" />
<None Include="facil.yaml" />
<Compile Include="Config.fs" />
<Compile Include="Utils.fs" />
Expand All @@ -47,12 +48,12 @@
<Compile Include="ConfigTests.fs" />
<Compile Include="OutParameterAndReturnValueTests.fs" />
<Compile Include="MiscTests.fs" />
<Compile Include="TempTableTests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Expecto" Version="9.0.2" />
<PackageReference Include="Facil" Version="*" />
<PackageReference Condition="'$(TargetFramework)' == 'net5.0'" Include="FSharp.Control.AsyncSeq" Version="3.0.3" />
<PackageReference Include="Hedgehog" Version="0.8.4" />
<PackageReference Include="Hedgehog.Experimental" Version="0.2.3" />
Expand All @@ -62,6 +63,13 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Unquote" Version="5.0.0" />
<PackageReference Include="YoloDev.Expecto.TestSdk" Version="0.9.2" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="2.1.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Facil.Runtime\Facil.Runtime.fsproj" />
</ItemGroup>
<Target Name="CodeGen" AfterTargets="BeforeBuild">
<Exec Command="dotnet run -p $(MSBuildThisFileDirectory)..\Facil.Generator -- $(MSBuildThisFileDirectory)" ConsoleToMsBuild="false" IgnoreExitCode="true" />
</Target>
</Project>
1 change: 1 addition & 0 deletions src/DbTests/SQL/TempTable.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT Id, [Name] FROM #Temp
48 changes: 48 additions & 0 deletions src/DbTests/TempTableTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module TempTableTests

open Expecto
open Hedgehog

[<Tests>]
let tests =
testList "TempTable tests" [
testCase "Load and read from the temp table with dto" <| fun () ->
let data =
seq {
{| Id = 1; Name = "name" |}
}

let res =
DbGen.Scripts.SQL.TempTable
.WithConnection(Config.connStr)
.WithParameters({| data = data |})
.Execute()
|> Seq.toList


Expect.equal res.Length 1 "Wrong length"
let head = res.[0]

Expect.equal head.Id 1 "Wrong Id"
Expect.equal head.Name "name" "Wrong Name"

testCase "Load and read from the temp table" <| fun () ->
let data =
seq {
DbGen.Scripts.SQL.TempTabledata(Id = 1, Name = "name")
}

let res =
DbGen.Scripts.SQL.TempTable
.WithConnection(Config.connStr)
.WithParameters(data = data)
.Execute()
|> Seq.toList


Expect.equal res.Length 1 "Wrong length"
let head = res.[0]

Expect.equal head.Id 1 "Wrong Id"
Expect.equal head.Name "name" "Wrong Name"
]
5 changes: 5 additions & 0 deletions src/DbTests/facil.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ rulesets:
col1Filter:
type: NVARCHAR(42)

- for: SQL/TempTable.sql
params:
data:
tempTable: "CREATE TABLE #Temp (Id INT NOT NULL, [Name] NVARCHAR(100) NOT NULL)"

tableDtos:
- include: .*
except: WithoutDto
Expand Down
4 changes: 4 additions & 0 deletions src/Facil.Generator/Config.fs
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,15 @@ type ScriptParameterDto = {
nullable: bool option
``type``: string option
dtoName: string option
tempTable: string option
}


type ScriptParameter = {
Nullable: bool option
Type: string option
DtoName: string option
TempTable: string option
}


Expand Down Expand Up @@ -416,13 +418,15 @@ module ScriptParameter =
Nullable = dto.nullable
Type = dto.``type``
DtoName = dto.dtoName
TempTable = dto.tempTable
}


let merge (p1: ScriptParameter) (p2: ScriptParameter) = {
Nullable = p2.Nullable |> Option.orElse p1.Nullable
Type = p2.Type |> Option.orElse p1.Type
DtoName = p2.DtoName |> Option.orElse p1.DtoName
TempTable = p2.TempTable |> Option.orElse p1.TempTable
}


Expand Down
96 changes: 75 additions & 21 deletions src/Facil.Generator/Db.fs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
module internal Facil.Db
module internal rec Facil.Db

open System
open System.Collections.Generic
open System.Data
open System.IO
open Microsoft.Data.SqlClient
open Microsoft.SqlServer.TransactSql.ScriptDom
open System.Text.RegularExpressions


let adjustSizeForDbType (dbType: SqlDbType) (size: int16) =
Expand Down Expand Up @@ -51,21 +52,43 @@ let getScriptParameters (cfg: RuleSet) (sysTypeIdLookup: Map<int, string>) (tabl
let sourceToUse =
(script.Source, (Map.toList rule.Parameters))
||> List.fold (fun source (paramName, p) ->
if not (paramsWithFirstUsageOffset.ContainsKey $"@{paramName}") then
logWarning $"Script '{script.GlobMatchOutput}' has a matching rule with parameter '@{paramName}' that is not used in the script. Ignoring parameter."
source
else
match p.Type with
| None -> source
| Some typeDef -> $"DECLARE @{paramName} {typeDef} = {facilTempVarPrefix}{paramName}\n{source}"
if not (paramsWithFirstUsageOffset.ContainsKey $"@{paramName}") then
logWarning $"Script '{script.GlobMatchOutput}' has a matching rule with parameter '@{paramName}' that is not used in the script. Ignoring parameter."
source
else
match p.Type with
| None -> source
| Some typeDef -> $"DECLARE @{paramName} {typeDef} = {facilTempVarPrefix}{paramName}\n{source}"
)
let parameters = ResizeArray()

rule.Parameters
|> Map.iter(fun paramName value ->
match value.TempTable with
| Some source ->
let t = getTempTable2 sysTypeIdLookup source conn

parameters.Add(
{
Name = paramName
SortKey = 0
Size = 0s
Precision = 0uy
Scale = 0uy
FSharpDefaultValueString = None
TypeInfo = TempTable t
IsOutput = false
IsCursorRef = false
}
)
| _ -> ()
)

use cmd = conn.CreateCommand()
cmd.CommandText <- "sys.sp_describe_undeclared_parameters"
cmd.CommandType <- CommandType.StoredProcedure
cmd.Parameters.AddWithValue("@tsql", sourceToUse) |> ignore
use reader = cmd.ExecuteReader()
let parameters = ResizeArray()
while reader.Read() do

let paramName =
Expand Down Expand Up @@ -106,7 +129,7 @@ let getScriptParameters (cfg: RuleSet) (sysTypeIdLookup: Map<int, string>) (tabl
Size =
reader.["suggested_max_length"]
|> unbox<int16>
|> adjustSizeForDbType (match typeInfo with Scalar ti -> ti.SqlDbType | Table _ -> SqlDbType.Structured)
|> adjustSizeForDbType (match typeInfo with Scalar ti -> ti.SqlDbType | Table _ -> SqlDbType.Structured | TempTable _ -> SqlDbType.Structured)
Precision = reader.["suggested_precision"] |> unbox<byte>
Scale = reader.["suggested_scale"] |> unbox<byte>
FSharpDefaultValueString =
Expand All @@ -129,13 +152,14 @@ let getScriptParameters (cfg: RuleSet) (sysTypeIdLookup: Map<int, string>) (tabl



let getColumnsFromSpDescribeFirstResultSet (sysTypeIdLookup: Map<int, string>) (executable: Choice<StoredProcedure, Script>) (conn: SqlConnection) =
let getColumnsFromSpDescribeFirstResultSet (sysTypeIdLookup: Map<int, string>) (executable: Choice<StoredProcedure, Script, TempTable>) (conn: SqlConnection) =
use cmd = conn.CreateCommand()
cmd.CommandText <- "sys.sp_describe_first_result_set"
cmd.CommandType <- CommandType.StoredProcedure
match executable with
| Choice1Of2 sproc -> cmd.Parameters.AddWithValue("@tsql", sproc.SchemaName + "." + sproc.Name) |> ignore
| Choice2Of2 script -> cmd.Parameters.AddWithValue("@tsql", script.Source) |> ignore
| Choice1Of3 sproc -> cmd.Parameters.AddWithValue("@tsql", sproc.SchemaName + "." + sproc.Name) |> ignore
| Choice2Of3 script -> cmd.Parameters.AddWithValue("@tsql", script.Source) |> ignore
| Choice3Of3 temp -> cmd.Parameters.AddWithValue("@tsql", $"SELECT * FROM #{temp.Name}") |> ignore
use reader = cmd.ExecuteReader()
let cols = ResizeArray()
while reader.Read() do
Expand Down Expand Up @@ -163,22 +187,26 @@ let getColumnsFromSpDescribeFirstResultSet (sysTypeIdLookup: Map<int, string>) (
if cols.Count = 0 then None else Seq.toList cols |> List.sortBy (fun c -> c.SortKey) |> Some


let getColumnsFromSetFmtOnlyOn (executable: Choice<StoredProcedure, Script>) (conn: SqlConnection) =
let getColumnsFromSetFmtOnlyOn (executable: Choice<StoredProcedure, Script, TempTable>) (conn: SqlConnection) =
use cmd = conn.CreateCommand()
match executable with
| Choice1Of2 sproc ->
| Choice1Of3 sproc ->
cmd.CommandText <- sproc.SchemaName + "." + sproc.Name
cmd.CommandType <- CommandType.StoredProcedure
for param in sproc.Parameters do
match param.TypeInfo with
| Scalar ti -> cmd.Parameters.Add(param.Name, ti.SqlDbType) |> ignore
| Table tt -> cmd.Parameters.Add(param.Name, SqlDbType.Structured, TypeName = $"{tt.SchemaName}.{tt.Name}") |> ignore
| Choice2Of2 script ->
| TempTable _ -> ()
| Choice2Of3 script ->
cmd.CommandText <- script.Source
for param in script.Parameters do
match param.TypeInfo with
| Scalar ti -> cmd.Parameters.Add(param.Name, ti.SqlDbType) |> ignore
| Table tt -> cmd.Parameters.Add(param.Name, SqlDbType.Structured, TypeName = $"{tt.SchemaName}.{tt.Name}") |> ignore
| TempTable _ -> ()
| Choice3Of3 temp ->
cmd.CommandText <- $"SELECT * FROM #{temp.Name}"
use reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly)
match reader.GetSchemaTable() with
| null -> None
Expand Down Expand Up @@ -213,8 +241,9 @@ let getColumns (conn: SqlConnection) sysTypeIdLookup executable =
with ex ->
let executableName =
match executable with
| Choice1Of2 sp -> $"stored procedure %s{sp.SchemaName}.%s{sp.Name}"
| Choice2Of2 s -> $"script {s.GlobMatchOutput}"
| Choice1Of3 sp -> $"stored procedure %s{sp.SchemaName}.%s{sp.Name}"
| Choice2Of3 s -> $"script {s.GlobMatchOutput}"
| Choice3Of3 t -> $"temp table {t.Name}"
raise <| Exception($"Error getting output columns for {executableName}", ex)


Expand Down Expand Up @@ -360,7 +389,7 @@ let getStoredProcedures sysTypeIdLookup (tableTypesByUserId: Map<int, TableType>
Size =
reader.["max_length"]
|> unbox<int16>
|> adjustSizeForDbType (match typeInfo with Scalar ti -> ti.SqlDbType | Table _ -> SqlDbType.Structured)
|> adjustSizeForDbType (match typeInfo with Scalar ti -> ti.SqlDbType | Table _ -> SqlDbType.Structured | TempTable _ -> SqlDbType.Structured)
Precision = reader.["precision"] |> unbox<byte>
Scale = reader.["scale"] |> unbox<byte>
FSharpDefaultValueString = None // Added later
Expand Down Expand Up @@ -411,7 +440,7 @@ let getStoredProcedures sysTypeIdLookup (tableTypesByUserId: Map<int, TableType>
)
// Add result sets
|> List.map (fun sproc ->
{ sproc with ResultSet = getColumns conn sysTypeIdLookup (Choice1Of2 sproc) }
{ sproc with ResultSet = getColumns conn sysTypeIdLookup (Choice1Of3 sproc) }
)


Expand Down Expand Up @@ -475,6 +504,24 @@ let getTableDtos (sysTypeIdLookup: Map<int, string>) (conn: SqlConnection) =
raise <| Exception("Error getting table DTOs", ex)



let getTempTable2 sysTypeIdLookup (source : string) (conn: SqlConnection) =
let tempTable =
{ TempTable.Name = Regex("(#[a-z0-9\\-_]+)", RegexOptions.IgnoreCase).Match(source).Groups.[1].Value
Source = source
Columns = []}

use cmd = conn.CreateCommand()
cmd.CommandText <-
let src = tempTable.Source.Replace(tempTable.Name, "#" + tempTable.Name)
$"IF OBJECT_ID('tempdb.dbo.#{tempTable.Name}', 'U') IS NOT NULL DROP TABLE #{tempTable.Name};\n\r{src}"

cmd.ExecuteNonQuery() |> ignore

{ tempTable with
Columns = getColumns conn sysTypeIdLookup (Choice3Of3 tempTable) |> Option.defaultValue [] }


let getEverything (cfg: RuleSet) fullYamlPath (scriptsWithoutParamsOrResultSets: Script list) (conn: SqlConnection) =

let sysTypeIdLookup = getSysTypeIdLookup conn
Expand All @@ -485,14 +532,21 @@ let getEverything (cfg: RuleSet) fullYamlPath (scriptsWithoutParamsOrResultSets:

let scripts =
scriptsWithoutParamsOrResultSets
|> List.map(fun script ->
{ script with Source = script.Source.Replace("#", "##") }
)
|> List.map (fun script ->
let parameters = getScriptParameters cfg sysTypeIdLookup tableTypesByUserId script conn
{ script with Parameters = parameters }
)
|> List.map (fun script ->
let resultSet = getColumns conn sysTypeIdLookup (Choice2Of2 script)
let resultSet = getColumns conn sysTypeIdLookup (Choice2Of3 script)
{ script with ResultSet = resultSet }
)
|> List.map(fun script ->
// Clean up any temp tables name swapping.
{ script with Source = script.Source.Replace("##", "#") }
)
|> List.filter (fun s ->

let hasUnsupportedParameter =
Expand Down
6 changes: 6 additions & 0 deletions src/Facil.Generator/Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,16 @@ type TableType = {
Columns: TableTypeColumn list
}

type TempTable = {
Name : string
Source : string
Columns: OutputColumn list
}

type ParameterTypeInfo =
| Scalar of SqlTypeInfo
| Table of TableType
| TempTable of TempTable


/// A parameter for a stored procedure, script, or similar.
Expand Down
Loading