Skip to content

Instantly share code, notes, and snippets.

@ImaginaryDevelopment
Last active December 28, 2015 18:06
Show Gist options
  • Save ImaginaryDevelopment/b3053d5fb87cb6f4d9a9 to your computer and use it in GitHub Desktop.
Save ImaginaryDevelopment/b3053d5fb87cb6f4d9a9 to your computer and use it in GitHub Desktop.
T4 to generate F# types from a db
<#@ template debug="True" language="C#" hostspecific="True" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="System.Data.Entity.Design" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data.Entity.Design.PluralizationServices" #>
<#@ include file="DataModelToF.ttinclude" #><#
var cString = "Data Source=Prog7-Pc;Initial Catalog=PmRewriteApplicationDatabase;App=FsTypeProvider;User Id=;Password=;";
var tables = new []{
"Appointments",
"AppointmentTypes",
"AppointmentTypeMappings",
"AuthorizationExceptions",
"B2BEligibility",
"Charges",
"ChargeProfiles",
"Claims",
"ClaimFilingIndicator",
"Codes",
"CodeChargeMappings",
"GuarantorProfilesInfo",
"PatientsInfo",
"Payers",
"Users",
};
var blacklist = new []{
"Payments"
};
var targetProjectName = "Pm.Schema";
var doMultiFile = true;
var generatePartials = false;
var generateValueRecords = false;
var currentDir = Path.GetDirectoryName(Host.TemplateFile);
// for each table in the dbproj that aren't in the tables list
var dbPath = Path.GetFullPath(Path.Combine(currentDir, "..", "..", "..", "devDb"));
var allTables = (Directory.Exists(dbPath)
? GetTables(dbPath).Select(t => {t.GenerateFull = tables.Contains(t.Table); return t; })
: tables.Select(t => new TableInfo{Schema="dbo", Table=t, GenerateFull = true}))
.Where(t => !blacklist.Contains(t.Table))
.Where(t => generatePartials || t.GenerateFull)
.ToArray();
if(Directory.Exists(dbPath))
{
#>found dbProjectFolder at <#= dbPath #>
<#
foreach(var tablePath in allTables)
// TODO: generate just the records and helper
{#>
<#= tablePath.Schema + "." + tablePath.Table + "," + tablePath.Path #>
<#
}
} else {#>
didn't find it at <#= dbPath #>
<#}
var manager = Generate(Host, Dte, GenerationEnvironment, targetProjectName, allTables, generateValueRecords, cString);
manager.Process(doMultiFile);
#>
<#+
public static class BHelpers
{
public static string After(string text, string delimiter, int padding = 0)
{
return text.Substring(text.IndexOf(delimiter) + delimiter.Length + padding);
}
public static string Before(string text, string delimiter)
{
return text.Substring(0, text.IndexOf(delimiter));
}
}
#>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="System.Data.Entity.Design" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data.Entity.Design.PluralizationServices" #>
<# EnvDTE.DTE Dte;
//this relies on the nuget packages: T4EnvDte and T4MultiFile
#>
<#@ include file="MultipleOutputHelper.ttinclude" #>
<#@ include file="EnvDteHelper.ttinclude" #>
Main File Output
<#+
public static Manager Generate(ITextTemplatingEngineHost host, EnvDTE.DTE dte, StringBuilder generationEnvironment, string targetProjectName, IEnumerable<TableInfo> tables, bool generateValueRecords, string cString)
{
generationEnvironment.AppendLine(host.TemplateFile);
Action<int,string> appendLine = (indentLevels, text) => generationEnvironment.AppendLine(String.Join(string.Empty, Enumerable.Repeat(" ", indentLevels)) + text);
var useOptions = false;
var manager = Manager.Create(host, generationEnvironment);
var projects = RecurseSolutionProjects(dte);
var targetProject = projects.First(p => p.Name == targetProjectName);
var targetProjectFolder = Path.GetDirectoryName(targetProject.FullName);
var pluralizer = PluralizationService.CreateService(new CultureInfo("en")); // https://msdn.microsoft.com/en-us/library/system.data.entity.design.pluralizationservices.pluralizationservice(v=vs.110).aspx
generationEnvironment.AppendLine("Main file output");
foreach(var p in projects)
{
generationEnvironment.AppendLine(p.Name + " " + p.FullName);
}
using(var cn = new System.Data.SqlClient.SqlConnection(cString))
{
cn.Open();
foreach(var tableInfo in tables)
{
manager.StartNewFile(Path.Combine(targetProjectFolder, tableInfo.Table + ".generated.fs"), targetProject);
var typeName = pluralizer.Singularize(tableInfo.Table);
var columns = new List<ColumnDescription>();
var identities = new List<string>();
using(var cmd= new System.Data.SqlClient.SqlCommand("sp_help '" + tableInfo.Schema + "." + tableInfo.Table + "'", cn))
using(var r = cmd.ExecuteReader())
{
r.NextResult(); // ignore the first tables
while(r.Read()) // column info
{
// columns and info
var columnName = r["Column_name"].ToString();
var type = r["Type"].ToString();
// var computed = r["Computed"];
var length = Convert.ToInt32(r["Length"]);
// var prec = r["Prec"];
columns.Add(new ColumnDescription{ColumnName= columnName, Type= type, Length= length, Nullable = r["Nullable"].ToString() == "yes"});
}
r.NextResult();
while(r.Read()) // identities
{
if(r["Seed"] != System.DBNull.Value) // only valid identities (sql uses the identity column to say there are none defined instead of an empty set)
identities.Add(r["Identity"].ToString());
}
}
foreach(var c in columns)
if (identities.Contains(c.ColumnName))
c.IsIdentity = true;
columns = columns.OrderBy(c => c.ColumnName).ToList();
generationEnvironment.AppendLine("namespace Pm.Schema.DataModels." + pluralizer.Pluralize(typeName) + " // Generated by item in namespace " + manager.DefaultProjectNamespace );
generationEnvironment.AppendLine(string.Empty);
generationEnvironment.AppendLine("open System");
generationEnvironment.AppendLine("open System.ComponentModel");
generationEnvironment.AppendLine("open System.Linq.Expressions");
generationEnvironment.AppendLine(string.Empty);
generationEnvironment.AppendLine("open FSharp.NullHelpers");
generationEnvironment.AppendLine("open Pm.Schema.BReusable");
GenerateInterface(typeName, columns, appendLine, writeable:false, useOptions:useOptions);
if(tableInfo.GenerateFull)
GenerateInterface(typeName, columns, appendLine, writeable:true, useOptions:useOptions);
GenerateRecords(typeName, columns, appendLine, useOptions, generateValueRecords);
GenerateModule(typeName, columns, tableInfo.Schema, tableInfo.Table, appendLine, useOptions);
if(tableInfo.GenerateFull)
{
GenerateClass(typeName, columns, appendLine, useOptions);
}
manager.EndBlock();
}
}
return manager;
}
public static void GenerateInterface(string typeName, IEnumerable<ColumnDescription> columns, Action<int,string> appendLine, bool writeable, bool useOptions)
{
appendLine(0, GenerateTypeComment(columns.Count()));
appendLine(0, "type I" + typeName + (writeable ? "RW" : string.Empty) + " =");
if(writeable)
appendLine(1,"inherit I"+typeName);
foreach(var cd in columns)
{
appendLine(1, GenerateColumnComment(cd));
appendLine(1, "abstract member " + cd.ColumnName + ":" + MapSqlType(cd.Type, cd.Nullable, useOptions) + " with get" + (writeable ? ",set" : string.Empty));
}
appendLine(0, string.Empty);
}
public static void GenerateRecords(string typeName, IEnumerable<ColumnDescription> columns, Action<int,string> appendLine, bool useOptions, bool generateValueRecords)
{
if(generateValueRecords)
{
appendLine(0, GenerateTypeComment(columns.Count()));
appendLine(0, "[<StructuralEquality;NoComparison>]");
appendLine(0, "type " + typeName + "ValueRecord =");
appendLine(1, "{");
foreach(var cd in columns.Where(cd => !cd.IsPrimaryKey))
{
appendLine(1, GenerateColumnComment(cd));
appendLine(1, cd.ColumnName + ":" + MapSqlType(cd.Type, cd.Nullable, useOptions));
}
appendLine(1, "}");
appendLine(0, string.Empty);
}
appendLine(0, GenerateTypeComment(columns.Count()));
if (!useOptions)
{
appendLine(0, "[<NoComparison>]");
}
appendLine(0, "type " + typeName + "Record =");
appendLine(1, "{");
foreach(var cd in columns)
{
appendLine(1, GenerateColumnComment(cd));
appendLine(1, cd.ColumnName + ":" + MapSqlType(cd.Type, cd.Nullable, useOptions));
}
appendLine(1, "}");
appendLine(1, "interface I" + typeName + " with");
foreach(var cd in columns )
{
appendLine(2, "member x." + cd.ColumnName + " with get () = x." + cd.ColumnName);
}
appendLine(0, string.Empty);
appendLine(1, "static member Zero () = ");
appendLine(2, "{");
foreach(var cd in columns )
{
var mapped = MapSqlType(cd.Type, cd.Nullable, useOptions);
appendLine(2, cd.ColumnName + " = " + GetDefaultValue(mapped));
}
appendLine(2, "}");
appendLine(0, string.Empty);
}
public static void GenerateModule(string typeName, IEnumerable<ColumnDescription> columns, string schemaName, string tableName, Action<int,string> appendLine, bool useOptions)
{
var camelType = ToCamel(typeName);
appendLine(0, "module " + typeName + "Helpers =");
appendLine(1, "open Microsoft.FSharp.Core.Operators.Unchecked");
appendLine(0, string.Empty);
appendLine(1, "let tableName = \"" + typeName + "\"");
appendLine(1, "let ToRecord (i" + typeName + ":I" + typeName + ") =");
appendLine(2, "{");
foreach(var cd in columns )
{
var mapped = MapSqlType(cd.Type, cd.Nullable, useOptions);
appendLine(2, cd.ColumnName + " = i" + typeName + "." + cd.ColumnName);
}
appendLine(2, "}");
appendLine(0, string.Empty);
appendLine(1, "let toRecord (" + camelType + ":I" + typeName + ") =");
appendLine(2, "{");
foreach(var cd in columns )
{
var mapped = MapSqlType(cd.Type, cd.Nullable, useOptions);
appendLine(2, cd.ColumnName + " = " + camelType + "." + cd.ColumnName);
}
appendLine(2, "}");
appendLine(0,string.Empty);
appendLine(1, "let FromF (camelTypeF:Func<string,obj option>) =");
appendLine(2, "{");
var nonNullables = new [] {"string","String","byte[]"};
foreach(var cd in columns )
{
var mapped = MapSqlType(cd.Type, cd.Nullable, useOptions);
var converter = MapConverter(cd.Type, cd.Nullable, useOptions);
appendLine(2, cd.ColumnName + " = ");
appendLine(3, "match camelTypeF.Invoke \"" + cd.ColumnName + "\" with " + "// " + mapped);
if(cd.Nullable && !nonNullables.Contains(mapped))
appendLine(3, "|Some x -> Nullable (Convert." + converter + " x )");
else
appendLine(3, "|Some x -> Convert." + converter + " x ");
appendLine(3, "|None -> Unchecked.defaultof<_>");
}
appendLine(2, "}");
appendLine(0, string.Empty);
appendLine(1, "let inline toRecordStp (" + camelType + ": ^a) =");
appendLine(2, "{");
foreach(var cd in columns )
{
var mapped = MapSqlType(cd.Type,cd.Nullable,useOptions);
appendLine(2, cd.ColumnName + " = (^a: (member " + cd.ColumnName + ": _) " + camelType + ")");
}
appendLine(2, "}");
var canDoInsert = !columns.Any(c => c.Type == "image" || c.Type == "byte[]");
if(canDoInsert)
{
appendLine(0, string.Empty);
appendLine(1, "let createInsert (r:I" + typeName + ") =");
var needsQuotedTypes = new []{"varchar","char","nvarchar","nchar","datetime","xml","datetime2"};
Func<ColumnDescription,bool> needsQuoted = cd => needsQuotedTypes.Contains(cd.Type.ToLower());
var needsQuoteF = columns.Any(c=> !c.IsIdentity && needsQuoted(c));
if (needsQuoteF)
appendLine(2, "let quoted (s:string) = \"'\" + s.Replace(\"'\",\"''\") + \"'\" //" + columns.First(c => !c.IsIdentity && needsQuoted(c)).Type.ToLower());
Func<ColumnDescription,string,string> mapValue = (cd, prefix) => {
switch(cd.Type.ToLower()){
case "varchar":
return "if String.IsNullOrEmpty "
+ prefix
+ cd.ColumnName
+ " then \"null\" else quoted "
+ prefix
+ cd.ColumnName;
case "int":
return cd.Nullable
? "if isNull (box "
+ prefix
+ cd.ColumnName
+ ") then \"null\" else "
+ prefix
+ cd.ColumnName
+ " |> string"
: prefix
+ cd.ColumnName
+ " |> string";
default:
return cd.Nullable
? "if isNull (box "
+ prefix
+ cd.ColumnName
+ ") then \"null\" else "
+ prefix
+ cd.ColumnName
+ " |> string |> quoted"
: prefix
+ cd.ColumnName
+ " |> string |> quoted";
}
};
appendLine(2, "[");
foreach(var cd in columns.Where(c => !c.IsIdentity)) // inserts shouldn't include identity columns
{
var mapped = "\"" + cd.ColumnName + "\", "
+ mapValue(cd, "r.");
appendLine(3, mapped);
}
appendLine(2, "]");
appendLine(2, "|> fun pairs -> sprintf \"insert into "
+ schemaName
+ "."
+ tableName
+ "(%s) values (%s)\" (String.Join(\",\", pairs |> Seq.map fst )) (String.Join(\",\", pairs |> Seq.map snd))" );
}
appendLine(0, string.Empty);
}
public static void GenerateClass(string typeName, IEnumerable<ColumnDescription> columns, Action<int,string> appendLine, bool useOptions)
{
appendLine(0, GenerateTypeComment(columns.Count()));
appendLine(0, "type "+ typeName + "N (model:" + typeName + "Record) = ");
appendLine(0, string.Empty);
appendLine(1, "let propertyChanged = new Event<_, _>()");
appendLine(0, string.Empty);
foreach(var cd in columns) // https://fadsworld.wordpress.com/2011/05/18/f-quotations-for-inotifypropertychanged/
{
var camel = MapFieldNameFromType(cd.ColumnName);
appendLine(1, "let mutable " + camel + " = model." + cd.ColumnName);
}
appendLine(0, string.Empty);
appendLine(1, "interface I" + typeName + " with");
foreach(var cd in columns)
{
appendLine(2, GenerateColumnComment(cd));
appendLine(2, "member x." + cd.ColumnName + " with get () = x." + cd.ColumnName);
}
appendLine(0, string.Empty);
appendLine(1, "interface I" + typeName + "RW with" );
foreach(var cd in columns)
{
appendLine(2, GenerateColumnComment(cd));
appendLine(2, "member x." + cd.ColumnName + " with get () = x." + cd.ColumnName + " and set v = x." + cd.ColumnName + " <- v");
}
appendLine(0, string.Empty);
appendLine(1, "member x.MakeRecord () =");
appendLine(2, "{");
foreach(var cd in columns)
{
appendLine(2, cd.ColumnName + " = x." + cd.ColumnName);
}
appendLine(2, "}");
appendLine(0, string.Empty);
appendLine(1, "interface INotifyPropertyChanged with");
appendLine(2, "[<CLIEvent>]");
appendLine(2, "member x.PropertyChanged = propertyChanged.Publish");
appendLine(0, string.Empty);
appendLine(1, "abstract member RaisePropertyChanged : string -> unit");
appendLine(1, "default x.RaisePropertyChanged(propertyName : string) = propertyChanged.Trigger(x, PropertyChangedEventArgs(propertyName))");
appendLine(0, string.Empty);
appendLine(1, "abstract member SetAndNotify<'t> : string * 't byref * 't -> bool");
appendLine(1, "default x.SetAndNotify<'t> (propertyName, field: 't byref, value:'t) =");
appendLine(2, "if obj.ReferenceEquals(box field,box value) then false");
appendLine(2, "else");
appendLine(3, "field <- value");
appendLine(3, "x.RaisePropertyChanged(propertyName)");
appendLine(3, "true");
appendLine(0, string.Empty);
appendLine(1, "abstract member SetAndNotify<'t,'b> : string * 'b * 't Action * 't -> bool");
appendLine(1, "default x.SetAndNotify<'t,'b> (propertyName, baseValue:'b, baseSetter: 't Action, value:'t) =");
appendLine(2, "if obj.ReferenceEquals(box baseValue,box value) then false");
appendLine(2, "else");
appendLine(3, "baseSetter.Invoke value");
appendLine(3, "x.RaisePropertyChanged(propertyName)");
appendLine(3, "true");
foreach(var cd in columns)
{
var camel = MapFieldNameFromType(cd.ColumnName);
appendLine(0, string.Empty);
appendLine(1, GenerateColumnComment(cd));
appendLine(1, "member x."+ cd.ColumnName);
appendLine(2, "with get() = " + camel);
appendLine(2, "and set v = ");
appendLine(3, camel +" <- v");
appendLine(3, "x.RaisePropertyChanged \"" + cd.ColumnName +"\"");
}
}
public class TableInfo
{
public string Path {get;set;}
public string Schema {get;set;}
public string Table {get;set;}
public bool GenerateFull {get;set;}
}
public class ColumnDescription
{
public string ColumnName {get;set;}
public string Type {get;set;}
public int Length {get;set;}
public bool Nullable {get;set;}
public bool IsPrimaryKey {get;set;}
public bool IsIdentity {get;set;}
}
public static IEnumerable<TableInfo> GetTables(string dbPath)
{
return
Directory.GetFiles(dbPath, "*.table.sql", SearchOption.AllDirectories)
.Select(tp => new TableInfo{
Path=BHelpers.After(tp,"\\devDb\\"),
Schema=BHelpers.Before(BHelpers.After(tp, "Schemas\\"), "\\"),
Table=Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(tp))
});
}
static string MapNullableType(string targetType, bool nullable, bool useOptions)
{
return targetType + (nullable ? (useOptions ? " option" : " Nullable") : string.Empty);
}
static string MapSqlType(string type, bool nullable, bool useOptions)
{
switch (type.ToLower()){
case "char":
case "nchar":
case "nvarchar":
case "xml":
case "varchar": return "string";
case "bit": return MapNullableType("bool", nullable, useOptions);
case "timestamp": return MapNullableType("Int64", nullable, useOptions);
case "date":
case "datetime":
case "datetime2":
case "smalldatetime": return MapNullableType("DateTime", nullable, useOptions);
case "image": return "byte[]";
case "uniqueidentifier" : return MapNullableType("Guid", nullable, useOptions);
case "int": return MapNullableType("int", nullable, useOptions);
case "decimal": return MapNullableType("decimal", nullable, useOptions);
case "float" : return MapNullableType("float", nullable, useOptions);
default : return type ?? string.Empty;
}
}
static string MapConverter(string type, bool nullable, bool useOptions)
{
switch (type.ToLower()){
case "char":
case "nchar":
case "nvarchar":
case "xml":
case "varchar": return "ToString";
case "bit": return "ToBoolean";
case "image": return "ToBinaryData";
case "date":
case "datetime":
case "datetime2":
case "smalldatetime": return "ToDateTime";
case "uniqueidentifier" : return "ToGuid";
case "timestamp" : return "ToInt64";
case "int": return "ToInt32";
case "decimal": return "ToDecimal";
case "float" : return "ToDouble";
default : return type ?? string.Empty;
}
}
static string MapFieldNameFromType(string columnName)
{
var camel = ToCamel(columnName);
if(camel == "type")
return "type'";
return camel;
}
static string GetDefaultValue(string mappedType)
{
if(mappedType.EndsWith("Nullable"))
return "Nullable()";
if(mappedType.EndsWith("option"))
return "None";
switch(mappedType.ToLower()){
case "int": return "0";
case "int64": return "0L";
case "bool": return "false";
case "decimal": return "0m";
case "float": return "0.";
case "datetime": return "System.DateTime.MinValue";
case "uniqueidentifier" : return "Guid.Empty";
default : return "null";
}
}
static string GenerateColumnComment(ColumnDescription cd)
{
return "/// " + (cd.Type ?? "null") + " (" + cd.Length + ") " + (cd.Nullable? "null" : "not null") + (cd.IsIdentity ? " identity" : string.Empty);
}
static string GenerateTypeComment(int columnCount)
{
return "/// " + columnCount + " properties";
}
static string ToCamel(string s) // https://github.com/ayoung/Newtonsoft.Json/blob/master/Newtonsoft.Json/Utilities/StringUtils.cs
{
if (string.IsNullOrEmpty(s))
return s;
if (!char.IsUpper(s[0]))
return s;
string camelCase = char.ToLower(s[0], CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture);
if (s.Length > 1)
camelCase += s.Substring(1);
return camelCase;
}
#>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment