Create a Basic SQL Server 2005 Trigger to Send E-mail Alerts

Filed under: Quick Code, SQL Server — Written by Chrissy on Tuesday, February 5th, 2008 @ 11:07 am

For as many times as I have read about sending e-mails using SQL Server triggers, I've rarely come across actual code samples. After someone asked for a "Triggers for Dummies" example in a Facebook SQL group, I created the following example which uses a trigger to alert a manager that an expensive item has been entered into inventory.

First, if SQL Mail isn't enabled and a profile hasn't been created, we must do so.

Enable SQL Mail/Create Profile

--// First, enable SQL SMail
use master
go
sp_configure 'show advanced options',1
go
reconfigure with override
go
sp_configure 'Database Mail XPs',1
go
reconfigure
go
 
--//Now create the mail profile. CHANGE @email_address,@display_name and @mailserver_name VALUES to support your environment
EXECUTE msdb.dbo.sysmail_add_account_sp
@account_name = 'DBMailAccount',
@email_address = 'sqlserver@domain.com',
@display_name = 'SQL Server Mailer',
@mailserver_name = 'exchangeServer'
 
EXECUTE msdb.dbo.sysmail_add_profile_sp
@profile_name = 'DBMailProfile'
 
EXECUTE msdb.dbo.sysmail_add_profileaccount_sp
@profile_name = 'DBMailProfile',
@account_name = 'DBMailAccount',
@sequence_number = 1 ;

Now that SQL will support sending e-mails, let's create the sample table. This is not a useful or well designed table by any means -- it's just a simple example table:

CREATE TABLE

CREATE TABLE dbo.inventory (
item varchar(50),
price money
)
GO

Now that SQL mail and the table are setup, we will create a trigger that does the following:

  • Creates an AFTER INSERT trigger named expensiveInventoryMailer on the inventory table. This means that the trigger will be executed after the data has been entered.
  • Checks for items being entered that have a price of $1000 or more
  • If there is a match, an email is sent using the SQL Mail profile we used above.

The Actual Trigger

CREATE TRIGGER expensiveInventoryMailer ON dbo.inventory AFTER INSERT AS
 
DECLARE @price money
DECLARE @item varchar(50)
 
SET @price  = (SELECT price FROM inserted)
SET @item = (SELECT item FROM inserted)
 
IF @price >= 1000
  BEGIN
    DECLARE @msg varchar(500)
    SET @msg = 'Expensive item "' + @item + '" entered into inventory at $' + CAST(@price as varchar(10)) + '.'
    --// CHANGE THE VALUE FOR @recipients
    EXEC msdb.dbo.sp_send_dbmail @recipients=N'manager@domain.com', @body= @msg,  @subject = 'SQL Server Trigger Mail', @profile_name = 'DBMailProfile'
  END
GO

The only way to test a trigger is to add actual data, so let's do that here:
insert into inventory (item,price) values ('Vase',100)
insert into inventory (item,price) values ('Oven',1000)

Your email should arrive very quickly. If it doesn't, check the SQL Server mail log in SQL Management Studio by running SELECT * FROM sysmail_allitems.

Have fun!

Aurgasm.us and The Hott Infinite Scrollage

Filed under: Quick Code — Written by Chrissy on Wednesday, October 31st, 2007 @ 7:04 pm

While visiting one of the best music discovery blogs ever, aurgasm.us, I noticed that the author, Paul Irish, had implemented a really slick Infinite Scroll AJAX technique which smoothly loads the next set(s) of posts without loading a new page.


This blog is awesome and the
author wrote some sweet code.

I was so impressed by the cross-browser compatibility and usability that after checking the copyright and seeing it was GPL'd, I added a slightly modified version of Paul's code to my own blog. I then changed the AJAX loading gif to one that I found @ this ajax gif generator and turned the code into a Wordpress plugin. In talking to Paul, however, I discovered that he and his friend are working on a plug-in too and theirs will actually have options. So I won't be releasing mine but if anyone can't wait until theirs is released, you can download my super simple version here. Your posts must be in the content div (which is generally the case with themes) for this to work. You may also want to remove any Next and Previous prompts.

You can see this plugin in action on the homepage of this blog or at the original source, aurgasm.us.

PowerShell: Parse an RSS Feed and E-mail/Text Yourself the Results

Filed under: PowerShell, Quick Code — Written by Chrissy on Thursday, October 25th, 2007 @ 1:30 pm

I'm so excited -- my best friend and tech partner-in-crime, Brandon, recently picked up PowerShell after I gave him a copy of Wrox's Professional PowerShell. He totally loves it and has already created a few new PowerShell scripts and converted other ASP.NET/VBScript scripts. One of his favorite scripts checks the Woot.com RSS feed and e-mails him with the daily item. He reduced a 25 line VBScript to a 5 line PowerShell script which goes something like this:

$rssUrl = "http://www.woot.com/blog/rss.aspx"
$blog = [xml](new-object System.Net.WebClient).DownloadString($rssUrl)
$results = $blog.rss.channel.item[0].title
 
$smtpmail = [System.Net.Mail.SMTPClient]("smtp.san.rr.com")
$smtpmail.Send("me@netnerds.net", "xxxxxxxxxx@vtext.com ", "Woot Deal", $results)

Five silly lines! PowerShell is just so straightforward:

1) Assign URL to a variable
2) Create a new WebClient and pull the RSS Feed into it.
3) Assign the value of the first title element to a variable
4) Create a SMTP Client and assign it the value of the smtp host
5) Access the Send Method and pass it (From, To, Subject, Body)

He then used Task Scheduler (tasksch) to run the script each night.

Another script he wrote checks for backups older than seven days then deletes them. If you use this script, be sure to remove the -whatif if you are happy with the results.

foreach ($file in (get-childitem -Path C:\backups\))
{
  $curTime = [System.DateTime]::get_now()
  if ($file.LastWriteTime -gt $curTime.Add("-7"))
   {
    remove-item -recurse $file -whatif
   }
}

For the record, I have Professional Windows PowerShell (Wrox/Andrew Watt), Windows PowerShell Cookbook (O'reilly/Lee Holmes, MSFT) and PowerShell in Action (Manning/Bruce Payette, MSFT). I suggest reading all three of them, in that order. The Wrox and O'reilly books provide the "how" while Payette's book provides the "why?". All three are very well written, especially the Cookbook, for which I was a technical editor ;)

PowerShell 1.0: Adding Virtual FTP Directories to IIS 6 or 7

Filed under: IIS, PowerShell, Quick Code — Written by Chrissy on Sunday, July 29th, 2007 @ 4:41 pm

While my firm explores using WebDAV and SharePoint 2007 for exchanging large amounts of files, we're temporarily using FTP dropboxes to fill the void. Last Monday, I setup 11 new accounts and it took a total of one hour to complete the same 15 step process (give or take) for each account. By the time I was finished, I decided automating FTP account creation would be my first PowerShell project. What you see below is part of that project.

The code below creates a virtual directory in the "Default FTP Site" of the machine that is running the PowerShell script. The virtual directory called "NewUser" is mapped to C:\FTP\NewUser and is set to be both readable and writable. For the record, I couldn't get WMI to work (get-wmiobject) and that's the reason I decided to use the .NET's Directory Services support.

$server = $env:computername
$service = New-Object System.DirectoryServices.DirectoryEntry("IIS://$server/MSFTPSVC")
$site = $service.psbase.children |Where-Object { $_.ServerComment -eq 'Default FTP Site' }
$site = New-Object System.DirectoryServices.DirectoryEntry($site.psbase.path+"/Root") # <-- IIS 6 requires this. Not sure why. Otherwise, it never appears to commit changes. This line is not required for IIS 7.
$virtualdir = $site.psbase.children.Add("NewUser","IIsFtpVirtualDir")
$virtualdir.psbase.CommitChanges()
$virtualdir.put("Path","C:\FTP\NewUser")
$virtualdir.put("AccessRead",$true)
$virtualdir.put("AccessWrite",$false)
$virtualdir.psbase.CommitChanges()
$service.psbase.refreshCache() # OPTIONAL

Alternatively, you could do go straight for the path if you know it (IIS 6 seems to like this):

$service = New-Object DirectoryServices.DirectoryEntry("IIS://localhost/MSFTPSVC/1/Root")
$virtualdir = $service.psbase.children.Add("NewUser", "IIsFtpVirtualDir")
$virtualdir.psbase.CommitChanges()
$virtualdir.put("Path","C:\FTP\NewUser")
$virtualdir.put("AccessRead",$true)
$virtualdir.put("AccessWrite",$false)
$virtualdir.psbase.CommitChanges()

If you would like to iterate through each of the virtual directories on your FTP server, you can use the following code:

$service = New-Object System.DirectoryServices.DirectoryEntry("IIS://$env:computername/MSFTPSVC")
$site = $service.psbase.children | Where-Object { $_.ServerComment -eq 'Default FTP Site' }
$virtualdirs = $site.psbase.children.Find("Root","IIsFtpVirtualDir").psbase.children
foreach ($virtualdir in $virtualdirs) {$virtualdir.psbase.name}

This code is likely applicable to many of the objects in the IIS ADSI provider. While I've only tested this on Vista (IIS 7), this should also work for Windows XP and 2003's IIS 6 as Vista uses IIS 6's MMC for management.

Also, if you are wondering how I know when to use psbase or psbase.children, I really don't. I just fumble around until I get it to work. The 4 lines above, specifically $virtualdirs = $site.psbase.children.Find("Root","IIsFtpVirtualDir").psbase.children took me about seven hours to figure out. I hear PowerShell 2.0 will have much better support for Directory Services and hopefully that will include support the IIS FTP service.

PowerShell: Set-Acl Does Not Appear to Work

Filed under: PowerShell, Quick Code, Security — Written by Chrissy on Saturday, July 28th, 2007 @ 7:32 am

If you've ever dealt with NTFS permissions in VBScript, you will no doubt appreciate just how easy PowerShell now makes it to manage access control lists. Basic examples in PowerShell books and around the 'net look something like this:

$directory = "Test"
$acl = Get-Acl $directory
$accessrule = New-Object system.security.AccessControl.FileSystemAccessRule("IUSR_CRACKLIN", "Modify", "Allow")
$acl.AddAccessRule($accessrule)
set-acl -aclobject $acl $directory

In the example above, user "IUSR_CRACKLIN" is given Modify access to the Test directory. Running the code above will not produce any errors but upon checking permission via the GUI, it seems as though the user was added, but no permissions were set.

I thought that perhaps this was an issue with Vista and I tried it on Windows Server 2003. And that's when I noticed that the directory had been given "Special Permissions." When I checked the Advanced permissions, I could see that Modify access had been assigned, but only to "This Folder." Other folders that had the checkboxes checked listed "This Folder, subfolders and files"

Since I wanted the Test directory permissions to match the others, I searched the Google to see which flags would give me "This Folder, subfolders and files." I found Damir Dobric's blog post titled "Directory Security and Access Rules which sported a handy reference table flags that must be set to achieve various scenarios.

Subfolders and Files only InheritanceFlags.ContainerInherit, InheritanceFlags.ObjectInherit, PropagationFlags.InheritOnly
This Folder, Subfolders and Files    InheritanceFlags.ContainerInherit, InheritanceFlags.ObjectInherit, PropagationFlags.None
This Folder, Subfolders and Files InheritanceFlags.ContainerInherit, InheritanceFlags.ObjectInherit, PropagationFlags.NoPropagateInherit
This folder and subfolders InheritanceFlags.ContainerInherit, PropagationFlags.None
Subfolders only InheritanceFlags.ContainerInherit, PropagationFlags.InheritOnly
This folder and files InheritanceFlags.ObjectInherit, PropagationFlags.None
This folder and files InheritanceFlags.ObjectInherit, PropagationFlags.NoPropagateInherit

So it setting the following should give me what I need:
InheritanceFlags.ContainerInherit, InheritanceFlags.ObjectInherit and PropagationFlags.None
.

$directory = "Test"
$inherit = [system.security.accesscontrol.InheritanceFlags]"ContainerInherit, ObjectInherit"
$propagation = [system.security.accesscontrol.PropagationFlags]"None"
$acl = Get-Acl $directory
$accessrule = New-Object system.security.AccessControl.FileSystemAccessRule("IUSR_CRACKLIN", "Modify", $inherit, $propagation, "Allow")
$acl.AddAccessRule($accessrule)
set-acl -aclobject $acl $directory

I then checked the permissions and voila:

Imagine that.. PowerShell can set any number of permissions with about 6 lines of code while VBScript requires over 36 lines JUST to set the constants needed for managing permissions. I'm so excited thinking about the possibilities: PowerShell + Windows Core + SSH is going to be awesome.

PowerShell 1.0: Find the Fully Qualified Domain Name of Current Active Directory Domain

Filed under: Active Directory, PowerShell, Quick Code — Written by Chrissy on Thursday, July 26th, 2007 @ 9:52 am

So I'm making the move to PowerShell. It's painful learning such alien (to me) concepts but books like Lee Holmes' PowerShell: The Definitive Guide help a ton. I was fortunate enough to be the editor for Chapters 1-5 and got a sneak preview. It's a fantastic book and can't wait to receive the title, complete with indexes! For now, I'm searching both the 36 Word documents and the sample code for solutions using Vista's built-in search functions.

My first task, which I'll explain in later posts, includes some AD stuff. One tiny part of the equation is dynamically finding the FQDN of the current Active Directory domain. This should be easy! Using RootDSE, it's super easy to find out the DistinguishedName or even the FQDN of the domain controller being queried, so wouldn't there be a similar entry for FQDN of the whole domain? Apparently not (or if it's there, I can't find it). I've spent the morning and part of last night digging through LDAP filters, looking on Google for examples of objectcategory=crossref, dnsroot, dnshostname, etc. But after finding this useful codeplex page, I played around with GetCurrentDomain() and realized that finding the DNS hostname for an AD domain all boils down to this one line:

$strDomainDNS = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name

Or this one liner, as Lee just suggested to me

$strDomainDNS = $env:USERDNSDOMAIN

Ahh! So it was that easy. Why, then, did it take 8 hours to find that? Now I wonder how to get the FQDN of any domain. Being a newb is hard work.

If you're wondering what dnshostname is for, it's to resolve the local machine you are working with. Michael at brnets.com provides the following example embedded in a big ol Exchange script:

$rootDSE = [adsi]"LDAP://RootDSE"
$DCDNShostname = $rootDSE.dnsHostName
$DCDNShostname

Oh, as a bonus, here's some another query you may find helpful. I started with benp's script then made it a little skinnier (and more prone to errors! ;))

Search for Active Directory User Object in the Current Domain

$domain = New-Object DirectoryServices.DirectoryEntry
$search = [System.DirectoryServices.DirectorySearcher]$domain
$search.Filter = "(&(objectClass=user)(sAMAccountname=Chrissy))"
$user = $search.FindOne().GetDirectoryEntry()
$user.Name

My original code made reference to GetDirectoryEntry() but John Brennan suggested looking for alternative approaches. Thanks for the tip, John!

T-SQL Equivalent of VBScript's FormatDateTime Function

Filed under: Quick Code, SQL Server, VBScript — Written by Chrissy on Tuesday, July 3rd, 2007 @ 8:17 am

Looking for the T-SQL (somewhat) equivalent to VBScript's FormatDateTime function? I've been too, for years. I finally found it within the CONVERT() function. As stated in SQL Server Books Online:

In CONVERT ( data_type [ ( length ) ] , expression [ , style ] ), style is the style of the date format used to convert datetime or smalldatetime data to character data (nchar, nvarchar, char, varchar, nchar, or nvarchar data types); or the string format used to convert float, real, money, or smallmoney data to character data (nchar, nvarchar, char, varchar, nchar, or nvarchar data types). When style is NULL, the result returned is also NULL.

Manuj Bahl wrote a nice article covering date and time manipulation in SQL Server 2000 and it in, he summarized BOL's table that lists the different style types. It went something like this:

Style ID Style Type
0 or 100 mon dd yyyy hh:miam (or pm)
101 mm/dd/yy
102 yy.mm.dd
103 dd/mm/yy
104 dd.mm.yy
105 dd-mm-yy
106 dd mon yy
107 mon dd, yy
108 hh:mm:ss
9 or 109 mon dd yyyy hh:mi:ss:mmmam (or pm)
110 mm-dd-yy
111 yy/mm/dd
112 yymmdd
13 or 113 dd mon yyyy hh:mm:ss:mmm(24h)
114 hh:mi:ss:mmm(24h)
20 or 120 yyyy-mm-dd hh:mi:ss(24h)
21 or 121 yyyy-mm-dd hh:mi:ss.mmm(24h)
126 yyyy-mm-dd thh:mm:ss.mmm(no spaces)
130 dd mon yyyy hh:mi:ss:mmmam
131 dd/mm/yy hh:mi:ss:mmmam

Try this out with GETDATE() by running the following statement:
SELECT CONVERT(VARCHAR,GETDATE(),7) AS currentdate

Your results should look something like this: Jul 03, 07 Nice! Have fun :)

T-SQL: Convert Seconds to Datetime / Get Total Seconds from Datetime Field

Filed under: Quick Code, SQL Server — Written by Chrissy on Tuesday, July 3rd, 2007 @ 8:06 am

For my final project in my database class at USF, I chose to create a database for a small record label looking to sell albums online as well as provide artist bios, tour information and news. The database, which was scaled down version of a project I worked on in the past, looked something like this:

The professor asked us to compose a few sample queries and, while looking at the songs table, I wondered if there was a way that I could use the information in the length column to determine how long each album ran. The song length column was created as datatype datetime for this very reason. Since SQL Server does't support just a time column, I used the dummy 1900-01-01 date when INSERTING my values. A typical song length would looks like this: 1900-01-01 00:02:11.000.

So I wanted to find the full time length of an album without text parsing to look for ":" and so on. Ultimately, I found this to be possible. Here's the query I came up with:

  SELECT CONVERT(VARCHAR,DATEADD(ss,SUM(DATEPART(HOUR, length)*3600 + DATEPART(MINUTE,length)*60 + DATEPART(SECOND,length)),0),114) AS AlbumLength
  FROM AlbumsTracks Where AlbumID = 1

What you see going on above is the following:
1. Using the DATEPART function to break down the song length into seconds. This is done by grabbing the various parts and multiplying them by 60 for minute or 3600 for hour.
2. Using DATEADD function to add those seconds to the date of "0" (1900-01-01).
3. Using the CONVERT function's ability to change datetime into various formats (kinda like VBScript's FORMATDATETIME), I went with style 114 or hh:mi:ss:mmm (24h))

The above query produced the result: 00:56:02:000. That's a 56 minute and 2 second running time. For more information on CONVERT's ability to manipulate datetime, check out SQL Server Books Online.

MSSQL: Parse Search Engine Querystrings for Search Terms

Filed under: Quick Code, SQL Server — Written by Chrissy on Thursday, February 22nd, 2007 @ 1:15 am

As a compliment to my MaxMind GeoIP Locator scripts and the parse top-level domain from URL function, I've created another function to parse the search term from an URL.

I've heard that T-SQL is a bit slow with parsing strings and wanted to test this myself so I created this in both C# and T-SQL. The difference is very noticeable, indeed: while the original, unoptimized function I wrote in T-SQL took 11 seconds to parse 10,000 referral URLs, the C# version took 4 seconds. The updated, more efficient function takes 6 seconds to run in T-SQL and 3 seconds in C#. Below are two truncated versions of the functions I created. I slimmed them down to make them easier to read on the blog but you can download the full versions here: qscleaner.cs and qscleaner.sql. If you do not have Visual Studio.net, scroll to the bottom of the post -- there I link the qscleaner.dll and give sample CREATE ASSEMBLY syntax.

QSCleaner.cs

/*
Created by Chrissy LeMaire (clemaire@gmail.com)
Website: http://netnerds.net/
 
! - THIS IS THE TRUNCATED VERSION created to keep the blog posting size down. It works but not as
!- precisely. Please download the full version @ http://blog.netnerds.net/code/qscleaner.cs
 
This C# UDF does the following
1. Guesses the search engine's search keyword delimiter
2. Finds the term and extracts it
3. Replaces querystring percentage encoding with its human readable equiv.
 
NO WARRANTIES, USE THIS AT YOUR OWN RISK, etc.
*/
 
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
 
public partial class UserDefinedFunctions
{
    [Microsoft.SqlServer.Server.SqlFunction]
    public static SqlString QSCleaner(string strURL)
    {
        int ampStart, slashStart, delimiterStart;
        string strQueryString = "";
        string strDelimiter = "";
 
        strURL = strURL.Replace("aq=", "aa=");
        strURL = strURL.Replace("as_q=", "aa=");
        strURL = strURL.Replace("as_oq=", "aa=");
        strURL = strURL.Replace("as_eq=", "aa=");
 
        while (strDelimiter.Length == 0)
        {
 
    // most used search delimiters
         if (strURL.IndexOf("q=") > 0)
        { strDelimiter = "q="; break; }
 
         if (strURL.IndexOf("p=") > 0)
        { strDelimiter = "p="; break; }
 
         if (strURL.IndexOf("query=") > 0)
        { strDelimiter = "="; break; }
 
         if (strURL.IndexOf("search=") > 0)
        { strDelimiter = "search="; break; }
 
         if (strURL.IndexOf("qry=") > 0)
        { strDelimiter = "qry="; break; }
 
         if (strURL.IndexOf("p=") > 0)
        { strDelimiter = "p="; break; }
 
         if (strURL.IndexOf("words=") > 0)
        { strDelimiter = "words="; break; }
 
         if (strURL.IndexOf("word=") > 0)
        { strDelimiter = "word="; break; }
 
         if (strURL.IndexOf("MT=") > 0)
        { strDelimiter = "MT="; break; }
 
         if (strURL.IndexOf("s=") > 0)
        { strDelimiter = "s="; break; }
 
         if (strURL.IndexOf("key=") > 0)
        { strDelimiter = "key="; break; }
        // end most used
 
                  //if by this time it isnt set.. it's not likely a search string
            if (strDelimiter.Length == 0) strDelimiter = "unknown";
        }
 
        if (strURL.IndexOf(strDelimiter) > 0)
        {
            try
            {
                delimiterStart = strURL.IndexOf(strDelimiter) + strDelimiter.Length;
                strQueryString = strURL.Substring(delimiterStart, (strURL.Length - delimiterStart));
                ampStart = strQueryString.IndexOf("&");
                if (ampStart > 0) strQueryString = strQueryString.Substring(0, ampStart);
 
                slashStart = strQueryString.IndexOf("/");
                if (slashStart > 0) strQueryString = strQueryString.Substring(0, slashStart - 1);
 
                // because I can't get System.Web.HttpUtility.UrlDecode to work
                strQueryString = strQueryString.Replace("%20", " ");
                strQueryString = strQueryString.Replace("%21", "!");
                strQueryString = strQueryString.Replace("%22", """);
                strQueryString = strQueryString.Replace("%23", "#");
                strQueryString = strQueryString.Replace("%24", "$");
                strQueryString = strQueryString.Replace("%25", "%");
                strQueryString = strQueryString.Replace("%26", "&");
                strQueryString = strQueryString.Replace("%27", "''");
                strQueryString = strQueryString.Replace("%28", "(");
                strQueryString = strQueryString.Replace("%29", ")");
                strQueryString = strQueryString.Replace("%2A", "*");
                strQueryString = strQueryString.Replace("%2B", "+");
                strQueryString = strQueryString.Replace("%2C", ",");
                strQueryString = strQueryString.Replace("%2D", "-");
                strQueryString = strQueryString.Replace("%2E", ".");
                strQueryString = strQueryString.Replace("%2F", "/");
                strQueryString = strQueryString.Replace("%3A", ":");
                strQueryString = strQueryString.Replace("%3B", ";");
                strQueryString = strQueryString.Replace("%3C", "<");
                strQueryString = strQueryString.Replace("%3D", "=");
                strQueryString = strQueryString.Replace("%3E", ">");
                strQueryString = strQueryString.Replace("%3F", "?");
                strQueryString = strQueryString.Replace("%40", "@");
                strQueryString = strQueryString.Replace("%5B", "[");
                strQueryString = strQueryString.Replace("%5C", "");
                strQueryString = strQueryString.Replace("%5D", "]");
                strQueryString = strQueryString.Replace("%5E", "^");
                strQueryString = strQueryString.Replace("%5F", "_");
                strQueryString = strQueryString.Replace("%60", "`");
                strQueryString = strQueryString.Replace("%7B", "{");
                strQueryString = strQueryString.Replace("%7C", "|");
                strQueryString = strQueryString.Replace("%7D", "}");
                strQueryString = strQueryString.Replace("+", " ");
            }
            catch
            {
                strQueryString = strURL;
            }
        }
        else
            strQueryString = strURL;
 
        return new SqlString(strQueryString);
    }
};

If you don't have access to SQL Server 2005 or SQL 2k5 CLR, you can still use a T-SQL function.

QSCleaner.sql

/*
Created by Chrissy LeMaire (clemaire@gmail.com)
Website: http://netnerds.net/
 
! - THIS IS THE TRUNCATED VERSION created to keep the blog posting size down. It works but not as
!- precisely. Please download the full version @ http://blog.netnerds.net/code/qscleaner.sql
 
This T-SQL Function does the following
1. Guesses the search engine's search keyword delimiter
2. Finds the term and extracts it
3. Replaces querystring percentage encoding with its human readable equiv.
 
NO WARRANTIES, USE THIS AT YOUR OWN RISK, etc.
*/
 
CREATE FUNCTION [dbo].[QSCleaner] (@strURL varchar(512))
RETURNS varchar(255)
AS
BEGIN
DECLARE @ampStart int
DECLARE @queryStr varchar(512)
DECLARE @slashStart int
DECLARE @strDelimiter varchar(64)
 
--Clean up some stuff that gets in the way of q=
SET @strURL = REPLACE(@strURL, 'aq=', 'aa=')
SET @strURL = REPLACE(@strURL, 'as_q=', 'aa=')
SET @strURL = REPLACE(@strURL, 'as_oq=', 'aa=')
SET @strURL = REPLACE(@strURL, 'as_eq=', 'aa=')
 
--Get Delimiter
WHILE @strDelimiter IS NULL
BEGIN
    -- most used search delimiters
    IF CHARINDEX('q=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'q='
    BREAK
    END
 
    IF CHARINDEX('p=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'p='
    BREAK
    END
 
    IF CHARINDEX('query=',@strURL) > 0
    BEGIN
    SET @strDelimiter = '='
    BREAK
    END
 
    IF CHARINDEX('search=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'search='
    BREAK
    END
 
    IF CHARINDEX('qry=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'qry='
    BREAK
    END
 
    IF CHARINDEX('p=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'p='
    BREAK
    END
 
    IF CHARINDEX('words=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'words='
    BREAK
    END
 
    IF CHARINDEX('word=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'word='
    BREAK
    END
 
    IF CHARINDEX('MT=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'MT='
    BREAK
    END
 
    IF CHARINDEX('s=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 's='
    BREAK
    END
 
    IF CHARINDEX('key=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'key='
    BREAK
    END
    -- end most used
 
IF @strDelimiter IS NULL SET @strDelimiter = ''
END
 
  IF CHARINDEX(@strDelimiter,@strURL) > 0
  BEGIN
  SET  @queryStr = SUBSTRING(@strURL,CHARINDEX(@strDelimiter,@strURL)+LEN(@strDelimiter),LEN(@strURL))
  SET  @ampStart = CHARINDEX('&',@queryStr)
  IF @ampStart > 0 SET @queryStr = LEFT(@queryStr,@ampStart-1)
 
  SET  @slashStart = CHARINDEX('/',@queryStr)
  IF @slashStart > 0 SET @queryStr = LEFT(@queryStr,@slashStart-1)
  SET @queryStr = REPLACE(@queryStr, '%20', ' ')
  SET @queryStr = REPLACE(@queryStr, '%21', '!')
  SET @queryStr = REPLACE(@queryStr, '%22', '"')
  SET @queryStr = REPLACE(@queryStr, '%23', '#')
  SET @queryStr = REPLACE(@queryStr, '%24', '$')
  SET @queryStr = REPLACE(@queryStr, '%25', '%')
  SET @queryStr = REPLACE(@queryStr, '%26', '&')
  SET @queryStr = REPLACE(@queryStr, '%27', '''')
  SET @queryStr = REPLACE(@queryStr, '%28', '(')
  SET @queryStr = REPLACE(@queryStr, '%29', ')')
  SET @queryStr = REPLACE(@queryStr, '%2A', '*')
  SET @queryStr = REPLACE(@queryStr, '%2B', '+')
  SET @queryStr = REPLACE(@queryStr, '%2C', ',')
  SET @queryStr = REPLACE(@queryStr, '%2D', '-')
  SET @queryStr = REPLACE(@queryStr, '%2E', '.')
  SET @queryStr = REPLACE(@queryStr, '%2F', '/')
  SET @queryStr = REPLACE(@queryStr, '%3A', ':')
  SET @queryStr = REPLACE(@queryStr, '%3B', ';')
  SET @queryStr = REPLACE(@queryStr, '%3C', '<')
  SET @queryStr = REPLACE(@queryStr, '%3D', '=')
  SET @queryStr = REPLACE(@queryStr, '%3E', '>')
  SET @queryStr = REPLACE(@queryStr, '%3F', '?')
  SET @queryStr = REPLACE(@queryStr, '%40', '@')
  SET @queryStr = REPLACE(@queryStr, '%5B', '[')
  SET @queryStr = REPLACE(@queryStr, '%5C', '')
  SET @queryStr = REPLACE(@queryStr, '%5D', ']')
  SET @queryStr = REPLACE(@queryStr, '%5E', '^')
  SET @queryStr = REPLACE(@queryStr, '%5F', '_')
  SET @queryStr = REPLACE(@queryStr, '%60', '`')
  SET @queryStr = REPLACE(@queryStr, '%7B', '{')
  SET @queryStr = REPLACE(@queryStr, '%7C', '|')
  SET @queryStr = REPLACE(@queryStr, '%7D', '}')
  SET @queryStr = REPLACE(@queryStr, '+', ' ')
 
  END
if @queryStr is null SET @queryStr = @strURL
RETURN  @queryStr
END

If you do not have VS.NET but still wish to use the C# version of qscleaner, you can download the dll here: qscleaner.dll or you can email me for the SLN file and compile it yourself using msbuild which is included in the .NET 2.0 framework. Anyway, save the dll to the DATA directory that contains your .mdf files (for example: C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data). To "install" the UDF, run the following:

CREATE ASSEMBLY QSCleaner
FROM 'C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data\qscleaner.dll'
WITH PERMISSION_SET = SAFE
GO
 
CREATE FUNCTION dbo.QSCleaner(@strURL nvarchar) RETURNS nvarchar
AS EXTERNAL NAME QSCleaner.UserDefinedFunctions.QSCleaner
GO

To call the function, create a query that's something like this: SELECT TOP 1000 dbo.qscleaner(referer) as keywords FROM hitcounter WHERE referer IS NOT NULL ORDER BY id DESC

Actually, I take it back. The above dll/assembly/function creation doesn't work -- I just receive 1000 h's all the way down. Compiling it via VS.NET works and the T-SQL function works but the manual creation does not. If anyone can shed some light, I'd appreciate it!

SQL Server 2005: Creating Your First C# CLR UDF in 10 Easy Steps (One of Which Includes Partying)

Filed under: Quick Code, SQL Server — Written by Chrissy on Wednesday, February 14th, 2007 @ 12:07 am

Well, 10 steps, give or take. This could have been executed in fewer steps had we not chosen such a useful function as an example <G>. This function, which is only supported in SQL Server 2005 with CLR enabled, esentially performs a reverse IP lookup (nslookup -type=ptr), uses System.Net.Dns.GetHostEntry and thus requires a bit more security configuration. So, let's begin.

1. In VS 2005, click File -> New -> Project -> Visual C# -> Database -> SQL Server Project -> OK.
2. Enter SQL Server name, credentials, and database name -> OK.
a. Accept the prompt which asks you if you want to enable CLR debugging in the database
3. In Solution Explorer, right click on the name of your SQL Server Project -> Add -> User Defined Function -> save it as "IPtoHostname.cs" (this will be the name of your function)
4. Copy and paste the following code into the IPtoHostname.cs window:

IPtoHostname.cs

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
 
public partial class UserDefinedFunctions
{
    [Microsoft.SqlServer.Server.SqlFunction]
    public static System.Data.SqlTypes.SqlString IPtoHostName(string ipaddr) // this is like T-SQL's CREATE FUNCTION dbo.IPtoHostName  (@strCCode varchar(xx))
    {
        try // This basically means "If the IP resolves properly and doesn't throw an exception, then..."
        {
            string myIP = System.Net.Dns.GetHostEntry(ipaddr).HostName.ToString();
            return new SqlString(myIP);
        }
        catch // "If it doesn't, just return the IP address"
        {
            return new SqlString(ipaddr);
        }
    }
};

Note: Again, because we use System.Net, we need to set some permissions which weakens the security of the given database. I haven't researched the implications of doing so but I figure if I'm going to run CLR stuff in my database, I'm willing to bend a little on security anyway.

5. Save your work. In Solution Explorer, right click on then name of your SQL Server Project and click Properties. Change the Permission Level to External.
6. Open SQL Server Management Studio and issue the following command, using the name of the database you selected in step 2.

EXEC sp_configure 'CLR ENABLED' , '1'
GO
RECONFIGURE
GO
ALTER DATABASE MyDBFromStep2 SET TRUSTWORTHY ON
GO

Note: In order to enable CLR or set the TRUSTWORTHYness of a database, it's likely that you must be a sys admin. And this is mostly for keywords to help Googlers... if you do not do the above, you will run into an error similiar to the following:

System.Security.SecurityException...

A .NET Framework error occurred during execution of user defined routine or aggregate 'IPtoHostName':
System.Security.SecurityException: Request for the permission of type 'System.Net.DnsPermission, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
System.Security.SecurityException:
   at System.Security.CodeAccessSecurityEngine.Check(Object demand, StackCrawlMark& stackMark, Boolean isPermSet)
   at System.Security.CodeAccessPermission.Demand()
   at System.Net.Dns.GetHostEntry(String hostNameOrAddress)
   at UserDefinedFunctions.IPtoHostName(String ipaddr)

7. Next, go back to Visual Studio, and select the "Debug" dropdown. Change that to "Release."
8. Save. Hit F5. You should have a successful build.
9. In SQL Management Studio, Query your database with the following statement:

select dbo.IPtoHostName('24.0.175.222') as hostname

See "c-24-0-175-222.hsd1.tx.comcast.net"
10. Laissez les bon temps rouler! (Party!)