How to programmatically export SQL schema using Microsoft.SqlServer.Management.Smo - sql-server

I've built a simple utility in C# that uses Microsoft.SqlServer.Management.Smo objects to export a database schema. This has been working great until I added a full text index.
When it exports the SQL for creating a full text index it does not include the columns that are defined in the index. For example, suppose I have a table named "Recipes" with 2 columns included in the FT index named "RecipeName" "Description". Dumping the schema using my utility produces the following SQL:
CREATE FULLTEXT INDEX ON [dbo].[Recipes]
KEY INDEX [PK_Recipes]ON ([ft], FILEGROUP [PRIMARY])
WITH (CHANGE_TRACKING = AUTO, STOPLIST = SYSTEM)
What I expect to be dumped is this (notice the columns):
CREATE FULLTEXT INDEX ON [dbo].[Recipes](
[Description] LANGUAGE [English],
[RecipeName] LANGUAGE [English]
)
KEY INDEX [PK_Recipes]ON ([ft], FILEGROUP [PRIMARY])
WITH (CHANGE_TRACKING = AUTO, STOPLIST = SYSTEM)
Here's the C# that generates the schema from a database file:
static void GenerateScript(string sourceDbPath, string destinationScriptPath)
{
try
{
string connString = string.Format(#"Data Source=.;AttachDbFilename={0};Integrated Security=True;User Instance=True", sourceDbPath);
SqlConnection sqlConn = new SqlConnection(connString);
ServerConnection serverConn = new ServerConnection(sqlConn);
Server server = new Server(serverConn);
Database database = server.Databases[sourceDbPath];
Transfer transfer = new Transfer(database);
ScriptingOptions options = new ScriptingOptions();
options.AppendToFile = false; // Overwrite file
options.ClusteredIndexes = true;
options.Indexes = true;
options.DriAll = true;
options.Triggers = true;
options.Bindings = true;
options.Default = true;
options.IncludeDatabaseContext = false;
options.IncludeHeaders = true;
options.FullTextIndexes = true;
options.SchemaQualify = true;
options.SchemaQualifyForeignKeysReferences = true;
options.ScriptSchema = true;
options.ScriptData = false;
options.ScriptDrops = false;
options.FileName = destinationScriptPath;
transfer.Options = options;
transfer.CopyAllFullTextCatalogs = true;
transfer.CopyAllFullTextStopLists = true;
transfer.CopyAllTables = true;
transfer.ScriptTransfer();
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
Environment.Exit(-1);
}
}
Can anyone spot what I'm missing?

I was unable to find a combination of scripting options that would include the columns in the full text index create statement.
So instead I queried the database for the full text index information by executing the sp_help_fulltext_tables and sp_help_fulltext_columns stored procs. This allowed me to construct the statements by hand.
Not ideal but it works.

Related

C# SMO Script user permissions using script option permissions

When using scripting option Permissions in SQL Server 2008 and above, the database creation script does not get scripted. Instead, an ALTER DATABASE statement is scripted.
I am using SQL Server 2008 and higher. Using C# SMO .Net assemblies v12.0.2000.8.
These are my script options:
ScriptingOptions so = new ScriptingOptions();
so.DriAll = true;
so.EnforceScriptingOptions = false;
so.IncludeDatabaseContext = true;
so.AllowSystemObjects = false;
so.IncludeHeaders = true;
so.IncludeIfNotExists = true;
so.Indexes = true;
so.NoCommandTerminator = false;
so.NoFileGroup = true;
// TODO: script user permissions
so.Permissions = true;
so.PrimaryObject = true;
so.SchemaQualify = true;
so.ScriptOwner = true;
so.ScriptSchema = true;
so.ScriptDrops = false;
so.Triggers = true;
//so.ExtendedProperties = true;
so.WithDependencies = false;
so.ScriptBatchTerminator = true;
so.TargetServerVersion = SqlServerVersion.Version105;
so.ContinueScriptingOnError = true;
I do not use a Transfer Object.
When I set Permissions to false, the CREATE DATABASE statement is generated. When I set Permissions to true, an ALTER DATABASE statement is scripted instead.
I want to be able to script the user permissions AND have the CREATE DATABASE statement included in the script instead of the ALTER DATABASE statement.

How do I make sure alla data is restored using SMO?

A restore performed with C# code using SMO objects restores fine when original database is in SIMPLE recovery mode. We got problems with one database where the restore missed all content from a certain table, where all data was inserted late in process. The database showed to be in BULK LOGGED recovery mode. After changing to SIMPLE and doing a new backup, it restored fine, using our code.
We have tried different settings on the restore object, but found none that fixes the problem. We are under the impression that the restoring ignores data in the log.
The basic restore looks like this:
sqlServer = new Server(new ServerConnection(instanceName));
restore = GetRestore();
restore.PercentComplete += PercentCompleteAction;
restore.Complete += CompleteAction;
restore.SqlRestore(sqlServer);
the GetRestore function is basically implemented like this:
restore = new Restore();
var deviceItem = new BackupDeviceItem(backupFileName, DeviceType.File);
restore.Devices.Add(deviceItem);
restore.Database = newDatabaseName;
restore.NoRecovery = false;
restore.Action = RestoreActionType.Database;
restore.ReplaceDatabase = false;
return restore;
There are no error messages - just missing content in one table.
Added try:
I took a guess at the solution below, but it didn't help:
restore.ReplaceDatabase = false;
restore.NoRecovery = true;
restore.Action = RestoreActionType.Database;
restore.SqlRestore(sqlServer);
restore.ReplaceDatabase = true;
restore.NoRecovery = true;
restore.Action = RestoreActionType.Log;
restore.SqlRestore(sqlServer);
restore.ReplaceDatabase = true;
restore.NoRecovery = false;
restore.Action = RestoreActionType.Files;
restore.SqlRestore(sqlServer);
You need select a correct FileNumber in your Log file, see this method can solve your problem:
public static void restaurarBackup(string pathFileBak)
{
SqlConnection conn = new SqlConnection(Connection.getCon());
Server smoServer = new Server(new ServerConnection(conn));
string localFilePath = pathFileBak;
string db_name = "ETrade";
string defaultFolderEtrade = #"C:\ETrade\";
Restore rs = new Restore();
rs.NoRecovery = false;
rs.ReplaceDatabase = true;
BackupDeviceItem bdi = default(BackupDeviceItem);
bdi = new BackupDeviceItem(localFilePath, DeviceType.File);
rs.Devices.Add(bdi);
DataTable dt = rs.ReadFileList(smoServer);
foreach (DataRow r in dt.Rows)
{
string logicalFilename = r.ItemArray[dt.Columns["LogicalName"].Ordinal].ToString();
string physicalFilename = defaultFolderEtrade + Path.GetFileName(r.ItemArray[dt.Columns["PhysicalName"].Ordinal].ToString());
rs.RelocateFiles.Add(new RelocateFile(logicalFilename, physicalFilename));
}
DataTable backupHeaders = rs.ReadBackupHeader(smoServer);
rs.FileNumber = Convert.ToInt32(backupHeaders.AsEnumerable().Max(backupInfo => backupInfo["Position"]));
rs.Database = db_name;
smoServer.KillAllProcesses(rs.Database);
Microsoft.SqlServer.Management.Smo.Database db = smoServer.Databases[rs.Database];
db.DatabaseOptions.UserAccess = DatabaseUserAccess.Single;
rs.SqlRestore(smoServer);
db = smoServer.Databases[rs.Database];
db.SetOnline();
smoServer.Refresh();
db.Refresh();
}
If you want look backups in your Log file, see this query:
SELECT database_name, name, backup_start_date,
backup_finish_date, datediff(mi, backup_start_date, backup_finish_date) [tempo (min)],
position, first_lsn, last_lsn, server_name, recovery_model,
type, cast(backup_size/1024/1024 as numeric(15,2)) [Tamanho (MB)], B.is_copy_only
FROM msdb.dbo.backupset B

SSIS - Sending email different recipients different data

I'm new in SSIS.
I have a table with 1500 rows and I need to send emails from that table but each recipients has 15 rows from the table.
So I need to send different data to different emails from the same query.
Can you guys could help me please?
Thanks in advance.
Leo
-------------------update------------------------
Guys I could create a SSIS package to send email to different recipients the problem is: sample: 2 different users is receiving emails for the number of rows that they have in database...that's terrible each customer has 15 lines it will be 15 emails can I send just one email for customer contains the whole data?
Thanks in advance...
This is going to vary somewhat based on the query and other specifications, but at a high level you're probably going to want to follow these steps for sending the emails using SSIS. This example assumes that the emails are stored in a column within this table. As others have pointed out, using sp_send_dbmail will likely be your best option.
Create two string variables. One will hold the email addresses and the other will be for the SQL for sp_send_dbmail (more on this below). Create an additional variable of the object type that will hold the list of emails during execution.
Modify the string variable that will hold the SQL for sp_send_dbmail to be an expression using the variable with the email names. Depending on the query, you may need to add additional variables for other parameters in this query. An example of this variable is at the end of this post.
Have an initial Execute SQL Task that queries the table and retrieves the email addresses. Make sure to get all rows for each email. Set the ResultSet property to full and on the Result Set pane, add the object variable with 0 as the Result Name.
Next add a Foreach Loop, use the Foreach ADO Enumerator type, and select the object variable from the last Execute SQL Task for the source variable. The Enumeration Mode can be left as the "Rows in the first table" option.
On the Variable Mappings pane, add the string variable (for the email addresses) and set the index to 0. This will hold the email addresses for each execution of sp_send_dbmail.
Within the Foreach Loop, add an Execute SQL Task. For this, you will need to set the SQLSourceType to variable and use a variable holding the SQL with sp_send_dbmail.
Make sure that you have Database Mail properly configured for the account and profile used, including membership in the DatabaseMailUserRole role in msdb. You may also need to use the three-part name (database.schema.table) for your table.
Example SQL Variable Expression:
Note the double-quotes in the #query parameter around the email variable in addition to the quotes from concatenating the expression. You can either use two single quotes or precede a double-quote with a \ in the query to use a double-quote as part of the expression.
"DECLARE #Title varchar(100)
SET #Title = 'Email Title'
EXEC MSDB.DBO.SP_SEND_DBMAIL #profile_name = 'Your Profile',
#recipients = 'YourEmail#test.org',
#query = 'SELECT * FROM YourDatabase.YourSchema.YourTable WHERE EmailColumn = ""
+ #[User::VariableWithEmailAddress] + ""',
#query_result_no_padding = 1, #subject = #Title ; "
I have a package that sole role is to send emails from my packages and record the results in to a table. I use this package over and over from any package that sends mail.
It is simply a script task, that takes parameters and does the work:
The script to process:
public void Main()
{
//Read variables
#region ReadVariables
string cstr = Dts.Variables["connString"].Value.ToString();
//string sender = (string)Dts.Variables["User::Sender"].Value;
string title = (string)Dts.Variables["$Package::Title"].Value;
string priority = (string)Dts.Variables["$Package::Priority"].Value;
string body = (string)Dts.Variables["$Package::Body"].Value;
string source = Dts.Variables["$Package::Source"].Value.ToString();
string directTo = Dts.Variables["$Package::DirectMail"].Value.ToString();
string groups = Dts.Variables["$Package::MailGroups"].Value.ToString();
#endregion
//Send Email
#region SendMail
MailMessage mail = new MailMessage();
//mail.From = new MailAddress(sender);
mail.Subject = title;
mail.Body = body;
mail.IsBodyHtml = true;
switch(priority.ToUpper())
{
case "HIGH":
mail.Priority= MailPriority.High;
priority = "High";
break;
default:
mail.Priority=MailPriority.Normal;
priority = "Normal";
break;
}
DataTable dt = new DataTable(); //This is going to be a full distribution list
//Fill table with group email
if (groups.Split(',').Length > 0)
{
foreach (string group in groups.Split(','))
{
string strCmd = "mail.spGetEmailAddressesByGroup";
using (OleDbConnection conn = new OleDbConnection(cstr))
{
using (OleDbCommand cmd = new OleDbCommand(strCmd, conn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("A", group);
OleDbDataAdapter da = new OleDbDataAdapter(cmd);
da.Fill(dt);
}
}
}
}
//add the directs to email
if (directTo.Split(',').Length > 0)
{
foreach (string m in directTo.Split(','))
{
if (m != "")
{
DataRow dr = dt.NewRow();
dr[0] = "TO";
dr[1] = m;
dt.Rows.Add(dr);
}
}
}
//Add from and reply to defaults
DataRow dr2 = dt.NewRow();
dr2[0] = "REPLYTO";
dr2[1] = ""; //WHERE DO YOU WANT REPLIES
dt.Rows.Add(dr2);
DataRow dr3 = dt.NewRow();
dr3[0] = "FROM";
dr3[1] = ""; //ENTER WHO YOU WANT THE EMAIL TO COME FROM
dt.Rows.Add(dr3);
//Bind dt to mail
foreach (DataRow dr in dt.Rows)
{
switch (dr[0].ToString().ToUpper())
{
case "TO":
mail.To.Add(new MailAddress(dr[1].ToString()));
dr[0] = "To";
break;
case "CC":
mail.CC.Add(new MailAddress(dr[1].ToString()));
dr[0] = "Cc";
break;
case "BCC":
mail.Bcc.Add(new MailAddress(dr[1].ToString()));
dr[0] = "Bcc";
break;
case "REPLYTO":
mail.ReplyToList.Add(new MailAddress(dr[1].ToString()));
dr[0] = "ReplyTo";
break;
case "FROM":
mail.From = new MailAddress(dr[1].ToString());
dr[0] = "From";
break;
case "SENDER":
mail.Sender = new MailAddress(dr[1].ToString());
dr[0] = "Sender";
break;
default:
dr[0] = "NotSent";
break;
}
}
try
{
SmtpClient smtp = new SmtpClient();
smtp.Port = 25;
smtp.DeliveryMethod = SmtpDeliveryMethod.Network;
smtp.UseDefaultCredentials = false;
smtp.Host = ""; //ENTER YOUR IP / SERVER
smtp.Send(mail);
}
catch (Exception e)
{
}
#endregion
//Record email as sent //I WILL NOT BE PROVIDING THIS PART
//#region RecordEmailInDB
That's just to send mails, I have many packages that build the emails to send. Most are variables to parameters on the call. The most complicated is the building of the Email Body and this is where your specific question comes into play.
This is a sample control flow:
There's a data flow that queries the details that need to be sent and are recorded into an object. As well as a record counter.
Back to control flow. There is a precendence constraint set to rowcount >0.
The I have a script task to build the body basically. And I have a class that converts the ADO Object into an HTML table.
public string BuildHTMLTablefromDataTable(DataTable t)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.Append("<table border='1'><tr style='background-color: #1A5276; color:#FFFFFF;'>");
foreach (DataColumn c in t.Columns)
{
sb.Append("<th align='left'>");
sb.Append(c.ColumnName);
sb.Append("</th>");
}
sb.Append("</tr>");
int rc = 0;
foreach (DataRow r in t.Rows)
{
rc++;
//every other row switches from white to gray
string OpeningTR = "<tr style='background-color: " + ((rc % 2 == 1) ? "#E5E7E9;'>" : "#FCF3CF;'>");
sb.Append(OpeningTR);
foreach (DataColumn c in t.Columns)
{
sb.Append("<td align='left'>");
sb.Append(System.Web.HttpUtility.HtmlEncode(
r[c.ColumnName] == null ? String.Empty : r[c.ColumnName].ToString()
)); //This will handle any invalid characcters and convert null to empty string
sb.Append("</td>");
}
sb.Append("</tr>");
}
sb.Append("</table>");
return sb.ToString();
}
public string BuildBody(DataTable dt)
{
string body = "<P>The following are vouchers that are not in the voucher table but in the GL:</p>";
DataView v = new DataView(dt);
body += BuildHTMLTablefromDataTable(dt); //v.ToTable(true, "Name", "LastVisit", "DaysUntilTimeout", "ExpDate", "RoleName"));
return body;
}
public void Main()
{
#region Read Variables
System.Data.OleDb.OleDbDataAdapter da = new System.Data.OleDb.OleDbDataAdapter();
DataTable dt = new DataTable();
da.Fill(dt, Dts.Variables["User::Changes"].Value);
#endregion
string body = BuildBody(dt);
Dts.Variables["User::Body"].Value = body;
Dts.TaskResult = (int)ScriptResults.Success;
}
Finally I will call the SendMail package and pass the parameters.
For your purpose you will need to have a foreach around this package and adjust your where clause for the person on each pass.
This is an example of an email sent (Body only):

How do I retrieve a key field that is a primary key and automatically created in SQL Server

What I am trying to do is create a record in 2 tables, Communities and CommunityTeams. Each of these have a primary key ID which is set as a Identity 1.1 in SQL Server. Now, I would like to capture the key of Communities as a foreign key in CommunityTeams, but I have no way of knowing what that ID is.
Here is my code in ASP.NET MVC and Entity Framework:
if (ModelState.IsValid)
{
// Community Info
model.CommunityType = Convert.ToInt32(fc["communityType"]);
model.ComunityName = fc["communityName"];
model.CommunityCity = fc["communityCity"];
model.CommunityState = fc["communityState"];
model.CommunityCounty = fc["communityCounty"];
model.Population = Convert.ToInt32(fc["communityPop"]);
// Save to Database
model.Active = true;
model.DateCreated = DateTime.Now;
model.CreatedBy = User.Identity.Name;
model.Application_Complete = true;
model.Application_Date = DateTime.Now;
model.Payment_Complete = true;
model.Payment_Date = DateTime.Now;
model.Renewal = true;
model.Renewal_Date = DateTime.Now;
team.TeamLeader = true;
team.Admin = true;
var user = User.Identity.Name;
team.UserName = user.ToString();
team.CommunityId = 1;
db.CommunityTeams.Add(team);
db.Communities.Add(model);
db.SaveChanges();
return RedirectToAction("Index", "Habitats");
}
I will admit that you have a navigation property to Community in your CommunityTeam entity.
Replace team.CommunityId = 1; by team.Community = model;. Then simply add the team entity, EF will create both model and team.
db.CommunityTeams.Add(team);
db.SaveChanges();
You can also split the save in two parts by calling db.SaveChanges(); between the two Add call.
The first save will create the Community entity, EF will fill your primary key automatically so you can use it in Team entity for the second save.

Fastest way to record all DocIds and FileNames from dtSearch in SQL database

I am using dtSearch on combination with a SQL database and would like to maintain a table that includes all DocIds and their related FileNames. From there, I will add a column with my foreign key to allow me to combine text and database searches.
I have code to simply return all the records in the index and add them one by one to the DB. This, however, takes FOREVER, and doesn't address the issue of how to simply append new records as they are added to the index. But just in case it helps:
MyDatabaseContext db = new StateScapeEntities();
IndexJob ij = new dtSearch.Engine.IndexJob();
ij.IndexPath = #"d:\myindex";
IndexInfo indexInfo = dtSearch.Engine.IndexJob.GetIndexInfo(#"d:\myindex");
bool jobDone = ij.Execute();
SearchResults sr = new SearchResults();
uint n = indexInfo.DocCount;
for (int i = 1; i <= n; i++)
{
sr.AddDoc(ij.IndexPath, i, null);
}
for (int i = 1; i <= n; i++)
{
sr.GetNthDoc(i - 1);
//IndexDocument is defined elsewhere
IndexDocument id = new IndexDocument();
id.DocId = sr.CurrentItem.DocId;
id.FilePath = sr.CurrentItem.Filename;
if (id.FilePath != null)
{
db.IndexDocuments.Add(id);
db.SaveChanges();
}
}
To keep the DocId in the index you must use the flag dtsIndexKeepExistingDocIds in the IndexJob
You can also look the dtSearch Text Retrieval Engine Programmer's Reference when the DocID is changed
When a document is added to an index, it is assigned a DocId, and DocIds are always numbered sequentially.
When a document is reindexed, the old DocId is cancelled and a new DocId is assigned.
When an index is compressed, all DocIds in the index are renumbered to remove the cancelled DocIds unless the dtsIndexKeepExistingDocIds flag is set in IndexJob.
When an index is merged into another index, DocIds in the target index are never changed. The documents merged into the target index will all be assigned new, sequentially-numbered DocIds, unless (a) the dtsIndexKeepExistingDocIds flag is set in IndexJob and (b) the indexes have non-overlapping ranges of doc ids.
To improve your speed you can search for the word "xfirstword" and get all documents in an index.
You can also look to the faq How to retrieve all documents in an index
So, I used part of user2172986's response, but combined it with some additional code to get the solution to my question. I did indeed have to set the dtsKeepExistingDocIds flag in my index update routine.
From there, I only wanted to add the newly created DocIds to my SQL database. For that, I used the following code:
string indexPath = #"d:\myindex";
using (IndexJob ij = new dtSearch.Engine.IndexJob())
{
//make sure the updated index doesn't change DocIds
ij.IndexingFlags = IndexingFlags.dtsIndexKeepExistingDocIds;
ij.IndexPath = indexPath;
ij.ActionAdd = true;
ij.FoldersToIndex.Add( indexPath + "<+>");
ij.IncludeFilters.Add( "*");
bool jobDone = ij.Execute();
}
//create a DataTable to hold results
DataTable newIndexDoc = MakeTempIndexDocTable(); //this is a custom method not included in this example; just creates a DataTable with the appropriate columns
//connect to the DB;
MyDataBase db = new MyDataBase(); //again, custom code not included - link to EntityFramework entity
//get the last DocId in the DB?
int lastDbDocId = db.IndexDocuments.OrderByDescending(i => i.DocId).FirstOrDefault().DocId;
//get the last DocId in the Index
IndexInfo indexInfo = dtSearch.Engine.IndexJob.GetIndexInfo(indexPath);
uint latestIndexDocId = indexInfo.LastDocId;
//create a searchFilter
dtSearch.Engine.SearchFilter sf = new SearchFilter();
int indexId = sf.AddIndex(indexPath);
//only select new records (from one greater than the last DocId in the DB to the last DocId in the index itself
sf.SelectItems(indexId, lastDbDocId + 1, int.Parse(latestIndexDocId.ToString()), true);
using (SearchJob sj = new dtSearch.Engine.SearchJob())
{
sj.SetFilter(sf);
//return every document in the specified range (using xfirstword)
sj.Request = "xfirstword";
// Specify the path to the index to search here
sj.IndexesToSearch.Add(indexPath);
//additional flags and limits redacted for clarity
sj.Execute();
// Store the error message in the status
//redacted for clarity
SearchResults results = sj.Results;
int startIdx = 0;
int endIdx = results.Count;
if (startIdx==endIdx)
return;
for (int i = startIdx; i < endIdx; i++)
{
results.GetNthDoc(i);
IndexDocument id = new IndexDocument();
id.DocId = results.CurrentItem.DocId;
id.FileName= results.CurrentItem.Filename;
if (id.FileName!= null)
{
DataRow row = newIndexDoc.NewRow();
row["DocId"] = id.DocId;
row["FileName"] = id.FileName;
newIndexDoc.Rows.Add(row);
}
}
newIndexDoc.AcceptChanges();
//SqlBulkCopy
using (SqlConnection connection =
new SqlConnection(db.Database.Connection.ConnectionString))
{
connection.Open();
using (SqlBulkCopy bulkCopy = new SqlBulkCopy(connection))
{
bulkCopy.DestinationTableName =
"dbo.IndexDocument";
try
{
// Write from the source to the destination.
bulkCopy.WriteToServer(newIndexDoc);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
newIndexDoc.Clear();
db.UpdateIndexDocument();
}
Here is my new solution with AddDoc method from the SearchResults interface:
First get the StartingDocID and the LastDocID from the IndexInfo and walk the loop like this:
function GetFilename(paDocID: Integer): String;
var
lCOMSearchResults: ISearchResults;
lSearchResults_Count: Integer;
begin
if Assigned(prCOMServer) then
begin
lCOMSearchResults := prCOMServer.NewSearchResults as ISearchResults;
lCOMSearchResults.AddDoc(GetIndexPath(prIndexContent), paDocID, 0);
lSearchResults_Count := lCOMSearchResults.Count;
if lSearchResults_Count = 1 then
begin
lCOMSearchResults.GetNthDoc(0);
Result := lCOMSearchResults.DocDetailItem['_Filename'];
end;
end;
end

Resources