66Dynamic creation of offline MDF
77===============================
88
9- Sometimes you don't want to have to be online just to compile your programs. With FSharp.Data.SqlClient you can use a local
10- .MDF file as the compile time connection string, and then change your connection string at runtime when you deploy your application.
9+ Sometimes you don't want to have to be online just to compile your programs, or
10+ you might not have access to your production database from your CI systems. With
11+ FSharp.Data.SqlClient you can use a local .MDF file as the compile-time
12+ connection string, and then use a different connection string when you deploy
13+ your application.
14+
15+ A connection string to a local .MDF file might look like this:
1116*)
1217
1318open FSharp.Data
1419
1520[<Literal>]
16- let connectionString = @" Data Source=(LocalDB)\v12.0;AttachDbFilename=C:\git\Project1\Database1.mdf;Integrated Security=True;Connect Timeout=10"
21+ let compileConnectionString =
22+ @" Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=C:\git\Project1\Database1.mdf;Integrated Security=True"
1723
1824(**
19- However, binary files like this are difficult to diff/merge when working with multiple developers. For this reason wouldn't it be nice
20- to store your schema in a plain text file, and have it dynamically create the MDF file for compile time?
25+ However, binary files like this are difficult to diff/merge when working with
26+ multiple developers, so you might not want to check them in. Wouldn't it be nice
27+ to store your schema in a plain text file, and have it dynamically create the
28+ MDF file for compile time?
2129
2230Well the following scripts can do that for your project.
2331
24- First create a file called `createdb.ps1`:
32+ First create a file called `createDb.ps1` and place it in an `SQL` subfolder in
33+ your project (you can place it in the project root to, if you want):
2534
26- # this is the name that Fsharp.Data.SqlClient TypeProvider expects it to be at build time
27- $new_db_name = "Database1"
35+ param(
36+ [Parameter(Mandatory=$true)][String]$DbName,
37+ [Parameter(Mandatory=$true)][String]$DbScript
38+ )
2839
2940 $detach_db_sql = @"
30- use master;
31- GO
32- EXEC sp_detach_db @dbname = N'$new_db_name';
33- GO
41+ IF (SELECT COUNT(*) FROM sys.databases WHERE name = '$DbName') > 0
42+ EXEC sp_detach_db @dbname = N'$DbName'
3443 "@
3544
3645 $detach_db_sql | Out-File "detachdb.sql"
37- sqlcmd -S "(localdb)\v11.0 " -i detachdb.sql
38- Remove-Item .\ detachdb.sql
46+ sqlcmd -S "(LocalDB)\MSSQLLocalDB " -i " detachdb.sql"
47+ Remove-Item " detachdb.sql"
3948
40- Remove-Item "$new_db_name .mdf"
41- Remove-Item "$new_db_name .ldf"
49+ if (Test-Path "$PSScriptRoot\$DbName.mdf") { Remove-Item "$PSScriptRoot\$DbName .mdf" }
50+ if (Test-Path "$PSScriptRoot\$DbName.ldf") { Remove-Item "$PSScriptRoot\$DbName .ldf" }
4251
4352 $create_db_sql = @"
44- USE master ;
45- GO
46- CREATE DATABASE $new_db_name
47- ON
48- ( NAME = Sales_dat,
49- FILENAME = '$PSScriptRoot\$new_db_name.mdf',
50- SIZE = 10,
51- MAXSIZE = 50,
52- FILEGROWTH = 5 )
53- LOG ON
54- ( NAME = Sales_log,
55- FILENAME = '$PSScriptRoot\$new_db_name.ldf',
56- SIZE = 5MB,
57- MAXSIZE = 25MB,
58- FILEGROWTH = 5MB ) ;
59- GO
53+ CREATE DATABASE $DbName
54+ ON (
55+ NAME = ${DbName}_dat,
56+ FILENAME = '$PSScriptRoot\$DbName.mdf'
57+ )
58+ LOG ON (
59+ NAME = ${DbName}_log,
60+ FILENAME = '$PSScriptRoot\$DbName.ldf'
61+ )
6062 "@
6163
6264 $create_db_sql | Out-File "createdb.sql"
63- sqlcmd -S "(localdb)\v11.0 " -i createdb.sql
64- Remove-Item .\ createdb.sql
65+ sqlcmd -S "(LocalDB)\MSSQLLocalDB " -i " createdb.sql"
66+ Remove-Item " createdb.sql"
6567
66- sqlcmd -S "(localdb)\v11.0 " -i schema.sql
68+ sqlcmd -S "(LocalDB)\MSSQLLocalDB " -i "$DbScript"
6769
6870 $detach_db_sql | Out-File "detachdb.sql"
69- sqlcmd -S "(localdb)\v11.0 " -i detachdb.sql
70- Remove-Item .\ detachdb.sql
71+ sqlcmd -S "(LocalDB)\MSSQLLocalDB " -i " detachdb.sql"
72+ Remove-Item " detachdb.sql"
7173
7274Then change your connection string to look like this
7375*)
7476
7577[<Literal>]
76- let connectionStringForCompileTime = @" Data Source=(LocalDB)\v12.0;AttachDbFilename=" + __ SOURCE_ DIRECTORY__ + @" \Database1.mdf;Integrated Security=True;Connect Timeout=10"
78+ let compileConnectionString =
79+ @" Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=" + __ SOURCE_ DIRECTORY__ + @" \Database1.mdf;Integrated Security=True;Connect Timeout=10"
7780
78- type Foo = SqlCommandProvider< " SELECT * FROM Foo" , connectionStringForCompileTime >
81+ type Foo = SqlCommandProvider< " SELECT * FROM Foo" , compileConnectionString >
7982
8083let myResults = ( new Foo( " Use your Runtime connectionString here" )) .Execute()
8184
8285(**
83- Lastly, edit your `.fsproj` file and add the following to the very end right before `</Project>`
84-
85- <Target Name="BeforeBuild">
86- <Message Text="Building out SQL Database: Database1.mdf" Importance="High" />
87- <Exec Command="PowerShell -NoProfile -ExecutionPolicy Bypass -Command "& { $(ProjectDir)Createdb.ps1 }"" />
86+ Lastly, edit your `.fsproj` file and add the following to the very end right
87+ before `</Project>`:
88+
89+ <ItemGroup>
90+ <SqlFiles Include="**\*.sql" />
91+ <BuildDbPsScript Include="SQL\createDb.ps1" />
92+ <BuildDbSqlScripts Include="SQL\create_myDb1.sql" DbName="Db1" />
93+ <BuildDbSqlScripts Include="SQL\create_myDb2.sql" DbName="Db2" />
94+ <UpToDateCheckInput Include="@(SqlFiles)" />
95+ <UpToDateCheckInput Include="@(BuildDbPsScript)" />
96+ <UpToDateCheckInput Include="@(BuildDbSqlScripts)" />
97+ <UpToDateCheckInput Include="@(BuildDbSqlScripts -> 'SQL\%(DbName).mdf')" />
98+ <UpToDateCheckInput Include="@(BuildDbSqlScripts -> 'SQL\%(DbName).ldf')" />
99+ </ItemGroup>
100+
101+ <Target Name="BuildDb" BeforeTargets="BeforeBuild" Inputs="@(BuildDbSqlScripts);@(BuildDbPsScript)" Outputs="SQL\%(BuildDbSqlScripts.DbName).mdf;SQL\%(BuildDbSqlScripts.DbName).ldf">
102+ <Message Text="DB files missing or outdated. Building out database %(BuildDbSqlScripts.DbName) using script %(BuildDbSqlScripts.Identity)" Importance="High" />
103+ <Exec Command="PowerShell -NoProfile -ExecutionPolicy Bypass -Command "& { @(BuildDbPsScript) -DbName %(BuildDbSqlScripts.DbName) -DbScript %(BuildDbSqlScripts.Identity) }"" />
88104 </Target>
89105
90- Now when you build, it will create a database named `Database1` and then look for a file called `schema.sql` which will be used
91- to create the database. It will then compile against this dynamically generated MDF file so you'll get full static type checking
92- without the hassle of having to have an internet connection, or deal with binary .MDF files!
106+ <Target Name="TouchProjectFileIfSqlOrDbChanged" BeforeTargets="BeforeBuild" Inputs="@(SqlFiles);@(BuildDbPsScript);@(BuildDbSqlScripts)" Outputs="$(MSBuildProjectFile)">
107+ <Message Text="SQL or DB files changed. Changing project file modification time to force recompilation." Importance="High" />
108+ <Exec Command="PowerShell -NoProfile -ExecutionPolicy Bypass -Command "(dir $(MSBuildProjectFile)).LastWriteTime = Get-Date"" />
109+ </Target>
93110
94- *)
111+ Now when you build, it will create the databases `SQL\Db1.mdf` and `SQL\Db2.mdf`
112+ using the scripts `SQL\create_myDb1.sql` and `SQL\create_myDb2.sql`. It will
113+ then compile against this dynamically generated MDF file so you'll get full
114+ static type checking without the hassle of having to have an internet
115+ connection, or deal with binary .MDF files!
116+
117+ Furthermore, the `.fsproj` edits above give the following benefits:
118+ * The DBs are rebuilt if their corresponding SQL scripts have changed, or if the
119+ PowerShell script has changed
120+ * The project is rebuilt if the PowerShell script has changed
121+ * The project is rebuilt if any SQL file has changed (both the database creation
122+ scripts, and any other SQL scripts that SqlClient might use though the
123+ `SqlFile` type provider)
124+ * Incremental build - each database is only built if its corresponding SQL
125+ script or the PowerShell script has changed
126+
127+ When it comes to actually making the database creation scripts (such as the
128+ `create_myDb1.sql` in the example above), you can do this if you use SQL Server
129+ Management Studio (SSMS):
130+ * Connect to the database you want to copy
131+ * Right-click the database and select Tasks -> Generate scripts
132+ * Select what you need to be exported (for example, everything except Users).
133+ * If SqlClient throws errors when connecting to your local database, you might
134+ be missing important objects from your database. Make sure everything you need
135+ is enabled in SSMS under Tools -> Options -> SQL Server Object Explorer ->
136+ Scripting. For example, if you have indexed views and use the `WITH
137+ (NOEXPAND)` hint in your SQL, you need the indexes too, which are not enabled
138+ by default. In this case, enable "Script indexes" under the "Table and view
139+ options" heading.
140+ *)
0 commit comments