Port 7.7 - WIP
This commit is contained in:
959
src/Umbraco.Core/Persistence/LocalDb.cs
Normal file
959
src/Umbraco.Core/Persistence/LocalDb.cs
Normal file
@@ -0,0 +1,959 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Umbraco.Core.Persistence
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages LocalDB databases.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Latest version is SQL Server 2016 Express LocalDB,
|
||||
/// see https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb
|
||||
/// which can be installed by downloading the Express installer from https://www.microsoft.com/en-us/sql-server/sql-server-downloads
|
||||
/// (about 5MB) then select 'download media' to download SqlLocalDB.msi (about 44MB), which you can execute. This installs
|
||||
/// LocalDB only. Though you probably want to install the full Express. You may also want to install SQL Server Management
|
||||
/// Studio which can be used to connect to LocalDB databases.</para>
|
||||
/// <para>See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this.</para>
|
||||
/// </remarks>
|
||||
internal class LocalDb
|
||||
{
|
||||
private int _version;
|
||||
private bool _hasVersion;
|
||||
|
||||
#region Availability & Version
|
||||
|
||||
/// <summary>
|
||||
/// Gets the LocalDb installed version.
|
||||
/// </summary>
|
||||
/// <remarks>If more than one version is installed, returns the highest available. Returns
|
||||
/// the major version as an integer e.g. 11, 12...</remarks>
|
||||
/// <exception cref="InvalidOperationException">Thrown when LocalDb is not available.</exception>
|
||||
public int Version
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureVersion();
|
||||
if (_version <= 0)
|
||||
throw new InvalidOperationException("LocalDb is not available.");
|
||||
return _version;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the LocalDb version is detected.
|
||||
/// </summary>
|
||||
private void EnsureVersion()
|
||||
{
|
||||
if (_hasVersion) return;
|
||||
DetectVersion();
|
||||
_hasVersion = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether LocalDb is available.
|
||||
/// </summary>
|
||||
public bool IsAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureVersion();
|
||||
return _version > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that LocalDb is available.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when LocalDb is not available.</exception>
|
||||
private void EnsureAvailable()
|
||||
{
|
||||
if (IsAvailable == false)
|
||||
throw new InvalidOperationException("LocalDb is not available.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects LocalDb installed version.
|
||||
/// </summary>
|
||||
/// <remarks>If more than one version is installed, the highest available is detected.</remarks>
|
||||
private void DetectVersion()
|
||||
{
|
||||
_hasVersion = true;
|
||||
_version = -1;
|
||||
|
||||
var programFiles = Environment.GetEnvironmentVariable("ProgramFiles");
|
||||
if (programFiles == null) return;
|
||||
|
||||
// detect 14, 13, 12, 11
|
||||
for (var i = 14; i > 10; i--)
|
||||
{
|
||||
var path = Path.Combine(programFiles, string.Format(@"Microsoft SQL Server\{0}0\Tools\Binn\SqlLocalDB.exe", i));
|
||||
if (File.Exists(path) == false) continue;
|
||||
_version = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Instances
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of existing LocalDb instances.
|
||||
/// </summary>
|
||||
/// <returns>The name of existing LocalDb instances.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when LocalDb is not available.</exception>
|
||||
public string[] GetInstances()
|
||||
{
|
||||
EnsureAvailable();
|
||||
string output, error;
|
||||
var rc = ExecuteSqlLocalDb("i", out output, out error); // info
|
||||
if (rc != 0 || error != string.Empty) return null;
|
||||
return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a LocalDb instance exists.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The name of the instance.</param>
|
||||
/// <returns>A value indicating whether a LocalDb instance with the specified name exists.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when LocalDb is not available.</exception>
|
||||
public bool InstanceExists(string instanceName)
|
||||
{
|
||||
EnsureAvailable();
|
||||
var instances = GetInstances();
|
||||
return instances != null && instances.Contains(instanceName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a LocalDb instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The name of the instance.</param>
|
||||
/// <returns>A value indicating whether the instance was created without errors.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when LocalDb is not available.</exception>
|
||||
public bool CreateInstance(string instanceName)
|
||||
{
|
||||
EnsureAvailable();
|
||||
string output, error;
|
||||
return ExecuteSqlLocalDb(string.Format("c \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops a LocalDb instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The name of the instance.</param>
|
||||
/// <returns>A value indicating whether the instance was dropped without errors.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when LocalDb is not available.</exception>
|
||||
/// <remarks>
|
||||
/// When an instance is dropped all the attached database files are deleted.
|
||||
/// Successful if the instance does not exist.
|
||||
/// </remarks>
|
||||
public bool DropInstance(string instanceName)
|
||||
{
|
||||
EnsureAvailable();
|
||||
var instance = GetInstance(instanceName);
|
||||
if (instance == null) return true;
|
||||
instance.DropDatabases(); // else the files remain
|
||||
|
||||
// -i force NOWAIT, -k kills
|
||||
string output, error;
|
||||
return ExecuteSqlLocalDb(string.Format("p \"{0}\" -i", instanceName), out output, out error) == 0 && error == string.Empty
|
||||
&& ExecuteSqlLocalDb(string.Format("d \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops a LocalDb instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The name of the instance.</param>
|
||||
/// <returns>A value indicating whether the instance was stopped without errors.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when LocalDb is not available.</exception>
|
||||
/// <remarks>
|
||||
/// Successful if the instance does not exist.
|
||||
/// </remarks>
|
||||
public bool StopInstance(string instanceName)
|
||||
{
|
||||
EnsureAvailable();
|
||||
if (InstanceExists(instanceName) == false) return true;
|
||||
|
||||
// -i force NOWAIT, -k kills
|
||||
string output, error;
|
||||
return ExecuteSqlLocalDb(string.Format("p \"{0}\" -i", instanceName), out output, out error) == 0 && error == string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops a LocalDb instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The name of the instance.</param>
|
||||
/// <returns>A value indicating whether the instance was started without errors.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when LocalDb is not available.</exception>
|
||||
/// <remarks>
|
||||
/// Failed if the instance does not exist.
|
||||
/// </remarks>
|
||||
public bool StartInstance(string instanceName)
|
||||
{
|
||||
EnsureAvailable();
|
||||
if (InstanceExists(instanceName) == false) return false;
|
||||
string output, error;
|
||||
return ExecuteSqlLocalDb(string.Format("s \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a LocalDb instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The name of the instance.</param>
|
||||
/// <returns>The instance with the specified name if it exists, otherwise null.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when LocalDb is not available.</exception>
|
||||
public Instance GetInstance(string instanceName)
|
||||
{
|
||||
EnsureAvailable();
|
||||
return InstanceExists(instanceName) ? new Instance(instanceName) : null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Databases
|
||||
|
||||
/// <summary>
|
||||
/// Represents a LocalDb instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// LocalDb is assumed to be available, and the instance is assumed to exist.
|
||||
/// </remarks>
|
||||
public class Instance
|
||||
{
|
||||
private readonly string _masterCstr;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the instance.
|
||||
/// </summary>
|
||||
public string InstanceName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Instance"/> class.
|
||||
/// </summary>
|
||||
/// <param name="instanceName"></param>
|
||||
public Instance(string instanceName)
|
||||
{
|
||||
InstanceName = instanceName;
|
||||
_masterCstr = string.Format(@"Server=(localdb)\{0};Integrated Security=True;", instanceName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a LocalDb connection string.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <returns>The connection string for the specified database.</returns>
|
||||
/// <remarks>
|
||||
/// The database should exist in the LocalDb instance.
|
||||
/// </remarks>
|
||||
public string GetConnectionString(string databaseName)
|
||||
{
|
||||
return _masterCstr + string.Format(@"Database={0};", databaseName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a LocalDb connection string for an attached database.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <param name="filesPath">The directory containing database files.</param>
|
||||
/// <returns>The connection string for the specified database.</returns>
|
||||
/// <remarks>
|
||||
/// The database should not exist in the LocalDb instance.
|
||||
/// It will be attached with its name being its MDF filename (full path), uppercased, when
|
||||
/// the first connection is opened, and remain attached until explicitely detached.
|
||||
/// </remarks>
|
||||
public string GetAttachedConnectionString(string databaseName, string filesPath)
|
||||
{
|
||||
string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename;
|
||||
GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename);
|
||||
|
||||
return _masterCstr + string.Format(@"AttachDbFileName='{0}';", mdfFilename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of existing databases.
|
||||
/// </summary>
|
||||
/// <returns>The name of existing databases.</returns>
|
||||
public string[] GetDatabases()
|
||||
{
|
||||
var userDatabases = new List<string>();
|
||||
|
||||
using (var conn = new SqlConnection(_masterCstr))
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var databases = new Dictionary<string, string>();
|
||||
|
||||
SetCommand(cmd, @"
|
||||
SELECT name, filename FROM sys.sysdatabases");
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
databases[reader.GetString(0)] = reader.GetString(1);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var database in databases)
|
||||
{
|
||||
var dbname = database.Key;
|
||||
|
||||
if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb")
|
||||
continue;
|
||||
|
||||
// fixme - shall we deal with stale databases?
|
||||
// fixme - is it always ok to assume file names?
|
||||
//var mdf = database.Value;
|
||||
//var ldf = mdf.Replace(".mdf", "_log.ldf");
|
||||
//if (staleOnly && File.Exists(mdf) && File.Exists(ldf))
|
||||
// continue;
|
||||
|
||||
//ExecuteDropDatabase(cmd, dbname, mdf, ldf);
|
||||
//count++;
|
||||
|
||||
userDatabases.Add(dbname);
|
||||
}
|
||||
}
|
||||
|
||||
return userDatabases.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a database exists.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <returns>A value indicating whether a database with the specified name exists.</returns>
|
||||
/// <remarks>
|
||||
/// A database exists if it is registered in the instance, and its files exist. If the database
|
||||
/// is registered but some of its files are missing, the database is dropped.
|
||||
/// </remarks>
|
||||
public bool DatabaseExists(string databaseName)
|
||||
{
|
||||
using (var conn = new SqlConnection(_masterCstr))
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var mdf = GetDatabase(cmd, databaseName);
|
||||
if (mdf == null) return false;
|
||||
|
||||
// it can exist, even though its files have been deleted
|
||||
// if files exist assume all is ok (should we try to connect?)
|
||||
var ldf = GetLogFilename(mdf);
|
||||
if (File.Exists(mdf) && File.Exists(ldf))
|
||||
return true;
|
||||
|
||||
ExecuteDropDatabase(cmd, databaseName, mdf, ldf);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new database.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <param name="filesPath">The directory containing database files.</param>
|
||||
/// <returns>A value indicating whether the database was created without errors.</returns>
|
||||
/// <remarks>
|
||||
/// Failed if a database with the specified name already exists in the instance,
|
||||
/// or if the database files already exist in the specified directory.
|
||||
/// </remarks>
|
||||
public bool CreateDatabase(string databaseName, string filesPath)
|
||||
{
|
||||
string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename;
|
||||
GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename);
|
||||
|
||||
using (var conn = new SqlConnection(_masterCstr))
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var mdf = GetDatabase(cmd, databaseName);
|
||||
if (mdf != null) return false;
|
||||
|
||||
// cannot use parameters on CREATE DATABASE
|
||||
// ie "CREATE DATABASE @0 ..." does not work
|
||||
SetCommand(cmd, string.Format(@"
|
||||
CREATE DATABASE {0}
|
||||
ON (NAME=N{1}, FILENAME={2})
|
||||
LOG ON (NAME=N{3}, FILENAME={4})",
|
||||
QuotedName(databaseName),
|
||||
QuotedName(databaseName, '\''), QuotedName(mdfFilename, '\''),
|
||||
QuotedName(logName, '\''), QuotedName(ldfFilename, '\'')));
|
||||
|
||||
var unused = cmd.ExecuteNonQuery();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops a database.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <returns>A value indicating whether the database was dropped without errors.</returns>
|
||||
/// <remarks>
|
||||
/// Successful if the database does not exist.
|
||||
/// Deletes the database files.
|
||||
/// </remarks>
|
||||
public bool DropDatabase(string databaseName)
|
||||
{
|
||||
using (var conn = new SqlConnection(_masterCstr))
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
SetCommand(cmd, @"
|
||||
SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)",
|
||||
databaseName);
|
||||
|
||||
var mdf = GetDatabase(cmd, databaseName);
|
||||
if (mdf == null) return true;
|
||||
|
||||
ExecuteDropDatabase(cmd, databaseName, mdf);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops stale databases.
|
||||
/// </summary>
|
||||
/// <returns>The number of databases that were dropped.</returns>
|
||||
/// <remarks>
|
||||
/// A database is considered stale when its files cannot be found.
|
||||
/// </remarks>
|
||||
public int DropStaleDatabases()
|
||||
{
|
||||
return DropDatabases(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops databases.
|
||||
/// </summary>
|
||||
/// <param name="staleOnly">A value indicating whether to delete only stale database.</param>
|
||||
/// <returns>The number of databases that were dropped.</returns>
|
||||
/// <remarks>
|
||||
/// A database is considered stale when its files cannot be found.
|
||||
/// </remarks>
|
||||
public int DropDatabases(bool staleOnly = false)
|
||||
{
|
||||
var count = 0;
|
||||
using (var conn = new SqlConnection(_masterCstr))
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var databases = new Dictionary<string, string>();
|
||||
|
||||
SetCommand(cmd, @"
|
||||
SELECT name, filename FROM sys.sysdatabases");
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
databases[reader.GetString(0)] = reader.GetString(1);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var database in databases)
|
||||
{
|
||||
var dbname = database.Key;
|
||||
|
||||
if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb")
|
||||
continue;
|
||||
|
||||
var mdf = database.Value;
|
||||
var ldf = mdf.Replace(".mdf", "_log.ldf");
|
||||
if (staleOnly && File.Exists(mdf) && File.Exists(ldf))
|
||||
continue;
|
||||
|
||||
ExecuteDropDatabase(cmd, dbname, mdf, ldf);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches a database.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <returns>The directory containing the database files.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when a database with the specified name does not exist.</exception>
|
||||
public string DetachDatabase(string databaseName)
|
||||
{
|
||||
using (var conn = new SqlConnection(_masterCstr))
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var mdf = GetDatabase(cmd, databaseName);
|
||||
if (mdf == null)
|
||||
throw new InvalidOperationException("Database does not exist.");
|
||||
|
||||
DetachDatabase(cmd, databaseName);
|
||||
|
||||
return Path.GetDirectoryName(mdf);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a database.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <param name="filesPath">The directory containing database files.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when a database with the specified name already exists.</exception>
|
||||
public void AttachDatabase(string databaseName, string filesPath)
|
||||
{
|
||||
using (var conn = new SqlConnection(_masterCstr))
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var mdf = GetDatabase(cmd, databaseName);
|
||||
if (mdf != null)
|
||||
throw new InvalidOperationException("Database already exists.");
|
||||
|
||||
AttachDatabase(cmd, databaseName, filesPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file names of a database.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <param name="mdfName">The MDF logical name.</param>
|
||||
/// <param name="ldfName">The LDF logical name.</param>
|
||||
/// <param name="mdfFilename">The MDF filename.</param>
|
||||
/// <param name="ldfFilename">The LDF filename.</param>
|
||||
public void GetFilenames(string databaseName,
|
||||
out string mdfName, out string ldfName,
|
||||
out string mdfFilename, out string ldfFilename)
|
||||
{
|
||||
using (var conn = new SqlConnection(_masterCstr))
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
GetFilenames(cmd, databaseName, out mdfName, out ldfName, out mdfFilename, out ldfFilename);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills all existing connections.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
public void KillConnections(string databaseName)
|
||||
{
|
||||
using (var conn = new SqlConnection(_masterCstr))
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
SetCommand(cmd, @"
|
||||
DECLARE @sql VARCHAR(MAX);
|
||||
SELECT @sql = COALESCE(@sql,'') + 'kill ' + CONVERT(VARCHAR, SPId) + ';'
|
||||
FROM master.sys.sysprocesses
|
||||
WHERE DBId = DB_ID(@0) AND SPId <> @@SPId;
|
||||
EXEC(@sql);",
|
||||
databaseName);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a database.
|
||||
/// </summary>
|
||||
/// <param name="cmd">The Sql Command.</param>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <returns>The full filename of the MDF file, if the database exists, otherwise null.</returns>
|
||||
private static string GetDatabase(SqlCommand cmd, string databaseName)
|
||||
{
|
||||
SetCommand(cmd, @"
|
||||
SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)",
|
||||
databaseName);
|
||||
|
||||
string mdf = null;
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
mdf = reader.GetString(1) ?? string.Empty;
|
||||
while (reader.Read())
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return mdf;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops a database and its files.
|
||||
/// </summary>
|
||||
/// <param name="cmd">The Sql command.</param>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <param name="mdf">The name of the database (MDF) file.</param>
|
||||
/// <param name="ldf">The name of the log (LDF) file.</param>
|
||||
private static void ExecuteDropDatabase(SqlCommand cmd, string databaseName, string mdf, string ldf = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// cannot use parameters on ALTER DATABASE
|
||||
// ie "ALTER DATABASE @0 ..." does not work
|
||||
SetCommand(cmd, string.Format(@"
|
||||
ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE",
|
||||
QuotedName(databaseName)));
|
||||
|
||||
var unused1 = cmd.ExecuteNonQuery();
|
||||
}
|
||||
catch (SqlException e)
|
||||
{
|
||||
if (e.Message.Contains("Unable to open the physical file") && e.Message.Contains("Operating system error 2:"))
|
||||
{
|
||||
// quite probably, the files were missing
|
||||
// yet, it should be possible to drop the database anyways
|
||||
// but we'll have to deal with the files
|
||||
}
|
||||
else
|
||||
{
|
||||
// no idea, throw
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// cannot use parameters on DROP DATABASE
|
||||
// ie "DROP DATABASE @0 ..." does not work
|
||||
SetCommand(cmd, string.Format(@"
|
||||
DROP DATABASE {0}",
|
||||
QuotedName(databaseName)));
|
||||
|
||||
var unused2 = cmd.ExecuteNonQuery();
|
||||
|
||||
// be absolutely sure
|
||||
if (File.Exists(mdf)) File.Delete(mdf);
|
||||
ldf = ldf ?? GetLogFilename(mdf);
|
||||
if (File.Exists(ldf)) File.Delete(ldf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the log (LDF) filename corresponding to a database (MDF) filename.
|
||||
/// </summary>
|
||||
/// <param name="mdfFilename">The MDF filename.</param>
|
||||
/// <returns></returns>
|
||||
private static string GetLogFilename(string mdfFilename)
|
||||
{
|
||||
if (mdfFilename.EndsWith(".mdf") == false)
|
||||
throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", "mdfFilename");
|
||||
return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches a database.
|
||||
/// </summary>
|
||||
/// <param name="cmd">The Sql command.</param>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
private static void DetachDatabase(SqlCommand cmd, string databaseName)
|
||||
{
|
||||
// cannot use parameters on ALTER DATABASE
|
||||
// ie "ALTER DATABASE @0 ..." does not work
|
||||
SetCommand(cmd, string.Format(@"
|
||||
ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE",
|
||||
QuotedName(databaseName)));
|
||||
|
||||
var unused1 = cmd.ExecuteNonQuery();
|
||||
|
||||
SetCommand(cmd, @"
|
||||
EXEC sp_detach_db @dbname=@0",
|
||||
databaseName);
|
||||
|
||||
var unused2 = cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a database.
|
||||
/// </summary>
|
||||
/// <param name="cmd">The Sql command.</param>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <param name="filesPath">The directory containing database files.</param>
|
||||
private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath)
|
||||
{
|
||||
string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename;
|
||||
GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename);
|
||||
|
||||
// cannot use parameters on CREATE DATABASE
|
||||
// ie "CREATE DATABASE @0 ..." does not work
|
||||
SetCommand(cmd, string.Format(@"
|
||||
CREATE DATABASE {0}
|
||||
ON (NAME=N{1}, FILENAME={2})
|
||||
LOG ON (NAME=N{3}, FILENAME={4})
|
||||
FOR ATTACH",
|
||||
QuotedName(databaseName),
|
||||
QuotedName(databaseName, '\''), QuotedName(mdfFilename, '\''),
|
||||
QuotedName(logName, '\''), QuotedName(ldfFilename, '\'')));
|
||||
|
||||
var unused = cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a database command.
|
||||
/// </summary>
|
||||
/// <param name="cmd">The command.</param>
|
||||
/// <param name="sql">The command text.</param>
|
||||
/// <param name="args">The command arguments.</param>
|
||||
/// <remarks>
|
||||
/// The command text must refer to arguments as @0, @1... each referring
|
||||
/// to the corresponding position in <paramref name="args"/>.
|
||||
/// </remarks>
|
||||
private static void SetCommand(SqlCommand cmd, string sql, params object[] args)
|
||||
{
|
||||
cmd.CommandType = CommandType.Text;
|
||||
cmd.CommandText = sql;
|
||||
cmd.Parameters.Clear();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
cmd.Parameters.AddWithValue("@" + i, args[i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file names of a database.
|
||||
/// </summary>
|
||||
/// <param name="cmd">The Sql command.</param>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <param name="mdfName">The MDF logical name.</param>
|
||||
/// <param name="ldfName">The LDF logical name.</param>
|
||||
/// <param name="mdfFilename">The MDF filename.</param>
|
||||
/// <param name="ldfFilename">The LDF filename.</param>
|
||||
private void GetFilenames(SqlCommand cmd, string databaseName,
|
||||
out string mdfName, out string ldfName,
|
||||
out string mdfFilename, out string ldfFilename)
|
||||
{
|
||||
mdfName = ldfName = mdfFilename = ldfFilename = null;
|
||||
|
||||
SetCommand(cmd, @"
|
||||
SELECT DB_NAME(database_id), type_desc, name, physical_name
|
||||
FROM master.sys.master_files
|
||||
WHERE database_id=DB_ID(@0)",
|
||||
databaseName);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var type = reader.GetString(1);
|
||||
if (type == "ROWS")
|
||||
{
|
||||
mdfName = reader.GetString(2);
|
||||
ldfName = reader.GetString(3);
|
||||
}
|
||||
else if (type == "LOG")
|
||||
{
|
||||
ldfName = reader.GetString(2);
|
||||
ldfFilename = reader.GetString(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy database files.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the source database.</param>
|
||||
/// <param name="filesPath">The directory containing source database files.</param>
|
||||
/// <param name="targetDatabaseName">The name of the target database.</param>
|
||||
/// <param name="targetFilesPath">The directory containing target database files.</param>
|
||||
/// <param name="sourceExtension">The source database files extension.</param>
|
||||
/// <param name="targetExtension">The target database files extension.</param>
|
||||
/// <param name="overwrite">A value indicating whether to overwrite the target files.</param>
|
||||
/// <param name="delete">A value indicating whether to delete the source files.</param>
|
||||
/// <remarks>
|
||||
/// The <paramref name="targetDatabaseName"/>, <paramref name="targetFilesPath"/>, <paramref name="sourceExtension"/>
|
||||
/// and <paramref name="targetExtension"/> parameters are optional. If they result in target being identical
|
||||
/// to source, no copy is performed. If <paramref name="delete"/> is false, nothing happens, otherwise the source
|
||||
/// files are deleted.
|
||||
/// If target is not identical to source, files are copied or moved, depending on the value of <paramref name="delete"/>.
|
||||
/// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp.
|
||||
/// </remarks>
|
||||
public void CopyDatabaseFiles(string databaseName, string filesPath,
|
||||
string targetDatabaseName = null, string targetFilesPath = null,
|
||||
string sourceExtension = null, string targetExtension = null,
|
||||
bool overwrite = false, bool delete = false)
|
||||
{
|
||||
var nop = (targetFilesPath == null || targetFilesPath == filesPath)
|
||||
&& (targetDatabaseName == null || targetDatabaseName == databaseName)
|
||||
&& (sourceExtension == null && targetExtension == null || sourceExtension == targetExtension);
|
||||
if (nop && delete == false) return;
|
||||
|
||||
string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename;
|
||||
GetDatabaseFiles(databaseName, filesPath,
|
||||
out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename);
|
||||
|
||||
if (sourceExtension != null)
|
||||
{
|
||||
mdfFilename += "." + sourceExtension;
|
||||
ldfFilename += "." + sourceExtension;
|
||||
}
|
||||
|
||||
if (nop)
|
||||
{
|
||||
// delete
|
||||
if (File.Exists(mdfFilename)) File.Delete(mdfFilename);
|
||||
if (File.Exists(ldfFilename)) File.Delete(ldfFilename);
|
||||
}
|
||||
else
|
||||
{
|
||||
// copy or copy+delete ie move
|
||||
string targetLogName, targetBaseFilename, targetLogFilename, targetMdfFilename, targetLdfFilename;
|
||||
GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath,
|
||||
out targetLogName, out targetBaseFilename, out targetLogFilename, out targetMdfFilename, out targetLdfFilename);
|
||||
|
||||
if (targetExtension != null)
|
||||
{
|
||||
targetMdfFilename += "." + targetExtension;
|
||||
targetLdfFilename += "." + targetExtension;
|
||||
}
|
||||
|
||||
if (delete)
|
||||
{
|
||||
if (overwrite && File.Exists(targetMdfFilename)) File.Delete(targetMdfFilename);
|
||||
if (overwrite && File.Exists(targetLdfFilename)) File.Delete(targetLdfFilename);
|
||||
File.Move(mdfFilename, targetMdfFilename);
|
||||
File.Move(ldfFilename, targetLdfFilename);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Copy(mdfFilename, targetMdfFilename, overwrite);
|
||||
File.Copy(ldfFilename, targetLdfFilename, overwrite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether database files exist.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the source database.</param>
|
||||
/// <param name="filesPath">The directory containing source database files.</param>
|
||||
/// <param name="extension">The database files extension.</param>
|
||||
/// <returns>A value indicating whether the database files exist.</returns>
|
||||
/// <remarks>
|
||||
/// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp.
|
||||
/// </remarks>
|
||||
public bool DatabaseFilesExist(string databaseName, string filesPath, string extension = null)
|
||||
{
|
||||
string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename;
|
||||
GetDatabaseFiles(databaseName, filesPath,
|
||||
out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename);
|
||||
|
||||
if (extension != null)
|
||||
{
|
||||
mdfFilename += "." + extension;
|
||||
ldfFilename += "." + extension;
|
||||
}
|
||||
|
||||
return File.Exists(mdfFilename) && File.Exists(ldfFilename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the database files.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">The name of the database.</param>
|
||||
/// <param name="filesPath">The directory containing database files.</param>
|
||||
/// <param name="logName">The name of the log.</param>
|
||||
/// <param name="baseFilename">The base filename (the MDF filename without the .mdf extension).</param>
|
||||
/// <param name="baseLogFilename">The base log filename (the LDF filename without the .ldf extension).</param>
|
||||
/// <param name="mdfFilename">The MDF filename.</param>
|
||||
/// <param name="ldfFilename">The LDF filename.</param>
|
||||
private static void GetDatabaseFiles(string databaseName, string filesPath,
|
||||
out string logName,
|
||||
out string baseFilename, out string baseLogFilename,
|
||||
out string mdfFilename, out string ldfFilename)
|
||||
{
|
||||
logName = databaseName + "_log";
|
||||
baseFilename = Path.Combine(filesPath, databaseName);
|
||||
baseLogFilename = Path.Combine(filesPath, logName);
|
||||
mdfFilename = baseFilename + ".mdf";
|
||||
ldfFilename = baseFilename + "_log.ldf";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SqlLocalDB
|
||||
|
||||
/// <summary>
|
||||
/// Executes the SqlLocalDB command.
|
||||
/// </summary>
|
||||
/// <param name="args">The arguments.</param>
|
||||
/// <param name="output">The command standard output.</param>
|
||||
/// <param name="error">The command error output.</param>
|
||||
/// <returns>The process exit code.</returns>
|
||||
/// <remarks>
|
||||
/// Execution is successful if the exit code is zero, and error is empty.
|
||||
/// </remarks>
|
||||
private int ExecuteSqlLocalDb(string args, out string output, out string error)
|
||||
{
|
||||
var programFiles = Environment.GetEnvironmentVariable("ProgramFiles");
|
||||
if (programFiles == null)
|
||||
{
|
||||
output = string.Empty;
|
||||
error = "SqlLocalDB.exe not found";
|
||||
return -1;
|
||||
}
|
||||
|
||||
var path = Path.Combine(programFiles, string.Format(@"Microsoft SQL Server\{0}0\Tools\Binn\SqlLocalDB.exe", _version));
|
||||
|
||||
var p = new Process
|
||||
{
|
||||
StartInfo =
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
FileName = path,
|
||||
Arguments = args,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
}
|
||||
};
|
||||
p.Start();
|
||||
output = p.StandardOutput.ReadToEnd();
|
||||
error = p.StandardError.ReadToEnd();
|
||||
p.WaitForExit();
|
||||
|
||||
return p.ExitCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a Unicode string with the delimiters added to make the input string a valid SQL Server delimited identifier.
|
||||
/// </summary>
|
||||
/// <param name="name">The name to quote.</param>
|
||||
/// <param name="quote">A quote character.</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This is a C# implementation of T-SQL QUOTEDNAME.
|
||||
/// <paramref name="quote"/> is optional, it can be '[' (default), ']', '\'' or '"'.
|
||||
/// </remarks>
|
||||
private static string QuotedName(string name, char quote = '[')
|
||||
{
|
||||
switch (quote)
|
||||
{
|
||||
case '[':
|
||||
case ']':
|
||||
return "[" + name.Replace("]", "]]") + "]";
|
||||
case '\'':
|
||||
return "'" + name.Replace("'", "''") + "'";
|
||||
case '"':
|
||||
return "\"" + name.Replace("\"", "\"\"") + "\"";
|
||||
default:
|
||||
throw new NotSupportedException("Not a valid quote character.");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user