Your partner for
Web-based business solutions...

Home
Introduction
Enterprise Manager
Stored Procedures
User-Defined Functions
Accessing MetaData
Accessing User Data
Using with VFP
Introduction to Stored Procedures and User Defined Functions in SQL Server Contact Us
The Atlanta FoxPro Users Group

February 19, 2004


Using SQL Server 2000, we will discuss and demonstrate several examples and types of stored procedures and user-defined functions. Then, we'll demonstrate setting up a connection to the database from FoxPro, executing stored procedures from FoxPro and utilizing the stored procedure result set in FoxPro.

"Where is all the knowledge we lost with information?" -- T.S. Elliot

Introduction Contact Us

"Those parts of the system that you can hit with a hammer are called hardware; those program instructions that you can only curse at are called software." -- Anonymous

Why is it important to FoxPro developers?
  • Bypasses a major FoxPro weakness.
  • Client/server capabilities.
  • Career-enhancing: being a developer with DBA skills is very sought-after; the trend will get stronger with Yukon. It's easier for a developer to become a DBA than for a DBA to become developer.

Resources:

What we're not going to cover:
  • Versions other than SQL Server 2000
  • Security and roles
  • SQL Injection
  • Index tuning
  • Other performance optimizations

SQL Server and MSDE
MSDE 2000 is a redistributable, scaled-down version of SQL Server 2000 and was introduced to provide application developers a database engine that is more powerful than the Jet engine and at the same time expandable to SQL Server. It is ideal for client applications requiring an embedded database and websites serving up to 25 concurrent users.

MSDE 2000 provides a cost-effective option for developers who need a database server that can be easily distributed and installed. Because it is fully compatible with SQL Server, developers can easily target both SQL Server and MSDE 2000 with the same application code base. This provides a seamless upgrade path from MSDE 2000 to SQL Server if an application grows beyond the storage and scalability limits of MSDE 2000.

Unlike SQL Server, MSDE does not have its own GUI interface (Enterprise Manager) for database design. MSDE ships with only osql utility and it's very difficult to create and manage databases from the command prompt. You can only create and manage MSDE databases via third-party tools and applications

MSDE is intended for single user or small workgroup environments. The following are some of the MSDE limitations in comparison with SQL Server:
  • No Enterprise Manager, Query Analyzer, Profiler or Index Tuning Wizard
  • Only supoprts 2GB RAM, 2 CPUs, 2GB database size limit
  • Only five concurrent batch workloads or 25 concurrent connections for websites
  • No Database Server Failover Support
  • No Full-text search, English Query or OLAP support
  • No Import and Export Wizards
  • No Full or Bulk-Logged recovery model support (only Simple)
  • No SQL Books Online

Alternatives to Enterprise Manager

Get SQL Server with an MSDN subscription or with Small Business Server
Enterprise Manager Contact Us
The Enterprise Manager is a graphical interface tool used to administer your SQL Servers. You can use the Enterprise Manager to configure SQL Server options, create/edit/view databases, perform maintenance and backups, and do quite a few more tasks that we will look at over the next few weeks.

A good introduction is here.

  • Organization
  • Tables and Indexes
  • Stored Procedures, Views and User-Defined Functions
  • Other Areas
Stored Procedures Contact Us
Stored procedures are collections of SQL statements and control-of-flow language. Stored procedures differ from ordinary SQL statements and from batches of SQL statements in that they are pre-compiled. The first time you run a procedure, SQL Server's query processor analyzes it and prepares an execution plan that is ultimately stored in a system table. The subsequent execution of the procedure is according to the stored plan. Since most of the query processing work has already been performed, stored procedures execute almost instantaneously.

Stored procedures are extremely similar to the constructs seen in other programming languages. They accept data in the form of input parameters that are specified at execution time. These input parameters (if implemented) are utilized in the execution of a series of statements that produce some result. This result is returned to the calling environment through the use of a recordset, output parameters and/or a return code.

Benefits:
  • Precompiled:
    SQL Server compiles each stored procedure once and then reutilizes the execution plan. This results in tremendous performance boosts when stored procedures are called repeatedly.
  • Reduced client/server traffic:
    If network bandwidth is a concern in your environment, you'll be happy to learn that stored procedures can reduce long SQL queries to a single line that is transmitted over the wire.
  • Efficient reuse of code:
    Multiple users and client programs can use stored procedures. If you utilize them in a planned manner, you'll find the development cycle takes less time.
  • Enhanced security controls:
    You can grant users permission to execute a stored procedure independently of underlying table permissions.
  • They allow faster execution:
    If the operation requires a large amount of Transact-SQL code or is performed repetitively, stored procedures can be faster than batches of Transact-SQL code. They are parsed and optimized when they are created, and an in-memory version of the procedure can be used after the procedure is executed the first time. Transact-SQL statements repeatedly sent from the client each time they run are compiled and optimized every time SQL Server executes them.
  • Another benefit is that you can execute a stored procedure on either a local or remote SQL Server. This enables you to run processes on other machines and work with information across servers, not just local databases.
  • An application program written in Visual FoxPro can also execute stored procedures, providing an optimum solution between the client-side software and SQL Server.

Creating and Running a Stored Procedure
create procedure all_employees 
as select * from employees 
go 

exec all_employees 

name department badge 
-------------------- -------------------- ----------- 
Brat Smith Sales 1234 
Karen Jones Sales 5514 
( 2 row(s) affected) 
When you submit a stored procedure to the system, SQL Server compiles and verifies the routines within it. If any problems are found, the procedure is rejected and you'll need to determine what the problem is prior to resubmitting the routine. If your stored procedure references another, as yet unimplemented stored procedure, you'll receive a warning message, but the routine will still be installed.

Other Important Items:
  • Query Analyzer is indispenable for testing stored procedure logic before implementing it.
  • SET NOCOUNT ON:
    Each time a statement in a stored procedure is executed, a message is sent to the client, called DONE_IN_PROC. This message also indicates the number of records that have been affected by the executed statement. If you have one or two statements on a stored procedure it probably doesn't matter, but if you have a complex collection of statements in your stored procedure, you should really consider adding SET NOCOUNT ON at the beginning of the stored procedure, eliminating all this "chatter" about "records being affected" between the client and the server. This can provide a significant performance boost because network traffic is greatly reduced.
  • @@RECORDCOUNT
  • @@IDENTITY and NEWID()
User-Defined Functions Contact Us
A UDF is a module, which is attached to a database in SQL Server. UDF's can accept parameters and can return a value. If you're familiar with stored procedures, then you'll be glad to know that UDF’s can contain code in a similar way to stored procedures. UDF's, however are limited to modifying only local variables (those whose lifetime is within the scope of the UDF), meaning that you can’t use SELECT, INSERT, DELETE, etc statements to modify any tables outside of the scope of the UDF. They are also limited to the types of variables you may return. For example - text, ntext, and image datatypes can not be returned.

What good are UDF's if you can't modify relational data then? Well, they're great for creating functions that need to work with data from databases, but don't actually need to modify the database. You can still retrieve and manipulate tables but can't physically commit changes to them.

OrderStatus is an example of implementing a business rule with a UDF.

CREATE FUNCTION OrderStatus
(@Shipped int, @ShipDate datetime)

RETURNS char

AS  

BEGIN 
   DECLARE @RET char(1)
   SET @RET = 'Z'
   
   IF @Shipped = 1 OR @ShipDate <> '1900-01-01'
      BEGIN
         SET @RET = 'B'
      END
   ELSE
      BEGIN
         IF @Shipped = 1 AND @ShipDate = '1900-01-01'
            BEGIN
               SET @RET = 'S'
            END
         ELSE
            BEGIN
               IF @Shipped = 0
                  BEGIN
                     SET @RET = 'L'
                  END
            END
       END
   RETURN @RET
END
GetActBags is an example of UDF that utilizes a query (that itself calls another UDF) to return a single value.

CREATE FUNCTION GetActBags
(@ProductCode varchar(20), @Grade varchar(20))

RETURNS float

AS  

BEGIN 
   DECLARE @RET real
   SELECT @RET = SUM(CASE ISNULL(PODetail.POUnits, 0)
                          WHEN 0 THEN 0
                          ELSE PODetail.POBOLWeight * dbo.WeightMult(GTable.TableValue, 'Bags (55.115)')
                     END)
      FROM PODetail
           LEFT OUTER JOIN GTable ON GTable.TableName = 'Units' AND GTable.TableKey = PODetail.POUnits
      WHERE PODetail.POProduct = @ProductCode AND PODetail.POGrade = @Grade
   RETURN @RET
END
Num2Words is a UDF version of a function that is common to almost every programming language.

CREATE FUNCTION dbo.Num2Words
 (@Number Numeric (38, 0))
 RETURNS VARCHAR(8000)
/*
* Converts a integer number as large as 34 digits into the equivalent words.
* The first letter is capitalized.
*
* Example:
SELECT dbo.Num2Words (1234567890) + CHAR(10)
       + dbo.Num2Words (0) + CHAR(10)
       + dbo.Num2Words (123) + CHAR(10)
SELECT dbo.Num2Words(76543210987654321098765432109876543210)
 
DECLARE @i numeric (38,0)
SET @i = 0
WHILE @I <= 1000
   BEGIN 
      PRINT convert (char(5), @i)  
            + convert(varchar(255), dbo.Num2Words(@i)) 
      SET @I  = @i + 1
   END
*/

AS

BEGIN
   DECLARE @inputNumber VARCHAR(38)
   DECLARE @NumbersTable TABLE (number CHAR(2), word VARCHAR(10))
   DECLARE @outputString VARCHAR(8000)
   DECLARE @length INT 
   DECLARE @counter INT 
   DECLARE @loops INT 
   DECLARE @position INT 
   DECLARE @chunk CHAR(3) -- for chunks of 3 numbers 
   DECLARE @tensones CHAR(2) 
   DECLARE @hundreds CHAR(1) 
   DECLARE @tens CHAR(1) 
   DECLARE @ones CHAR(1)

   IF @Number = 0 RETURN 'Zero'

   -- initialize the variables
   SELECT @inputNumber = CONVERT(varchar(38), @Number),
          @outputString = '',
          @counter = 1
   SELECT @length   = LEN(@inputNumber),
          @position = LEN(@inputNumber) - 2,
          @loops    = LEN(@inputNumber)/3

   -- make sure there is an extra loop added for the remaining numbers
   IF LEN(@inputNumber) % 3 <> 0 SET @loops = @loops + 1

   -- insert data for the numbers and words
   INSERT INTO @NumbersTable
     SELECT '00', ''
       UNION ALL SELECT '01', 'one'       UNION ALL SELECT '02', 'two'
       UNION ALL SELECT '03', 'three'     UNION ALL SELECT '04', 'four'
       UNION ALL SELECT '05', 'five'      UNION ALL SELECT '06', 'six'
       UNION ALL SELECT '07', 'seven'     UNION ALL SELECT '08', 'eight'
       UNION ALL SELECT '09', 'nine'      UNION ALL SELECT '10', 'ten'
       UNION ALL SELECT '11', 'eleven'    UNION ALL SELECT '12', 'twelve'
       UNION ALL SELECT '13', 'thirteen'  UNION ALL SELECT '14', 'fourteen'
       UNION ALL SELECT '15', 'fifteen'   UNION ALL SELECT '16', 'sixteen'
       UNION ALL SELECT '17', 'seventeen' UNION ALL SELECT '18', 'eighteen'
       UNION ALL SELECT '19', 'nineteen'  UNION ALL SELECT '20', 'twenty'
       UNION ALL SELECT '30', 'thirty'    UNION ALL SELECT '40', 'forty'
       UNION ALL SELECT '50', 'fifty'     UNION ALL SELECT '60', 'sixty'
       UNION ALL SELECT '70', 'seventy'   UNION ALL SELECT '80', 'eighty'
       UNION ALL SELECT '90', 'ninety'
        
   WHILE @counter <= @loops
      BEGIN
	      -- get chunks of 3 numbers at a time, padded with leading zeros
	      SET @chunk = RIGHT('000' + SUBSTRING(@inputNumber, @position, 3), 3)
      
	      IF @chunk <> '000'
            BEGIN
		         SELECT @tensones = SUBSTRING(@chunk, 2, 2)
		              , @hundreds = SUBSTRING(@chunk, 1, 1)
		              , @tens = SUBSTRING(@chunk, 2, 1)
		              , @ones = SUBSTRING(@chunk, 3, 1)
                    
		         -- If twenty or less, use the word directly from @NumbersTable
		         IF CONVERT(INT, @tensones) <= 20 OR @Ones='0'
                  BEGIN
			            SET @outputString = (SELECT word 
                                                  FROM @NumbersTable 
                                                  WHERE @tensones = number)
                               + CASE @counter
                                   WHEN  1 THEN '' -- No name
                                   WHEN  2 THEN ' thousand '    WHEN  3 THEN ' million '
                                   WHEN  4 THEN ' billion '     WHEN  5 THEN ' trillion '
                                   WHEN  6 THEN ' quadrillion ' WHEN  7 THEN ' quintillion '
                                   WHEN  8 THEN ' sextillion '  WHEN  9 THEN ' septillion '
                                   WHEN 10 THEN ' octillion '   WHEN 11 THEN ' nonillion '
                                   WHEN 12 THEN ' decillion '   WHEN 13 THEN ' undecillion '
                                   ELSE '' END
                                + @outputString
		             END
		          ELSE
                   BEGIN -- break down the ones and the tens separately
                      SET @outputString = ' ' 
                                 + (SELECT word 
                                       FROM @NumbersTable 
                                       WHERE @tens + '0' = number)
					                  + '-'
                                 + (SELECT word 
                                       FROM @NumbersTable 
                                       WHERE '0'+ @ones = number)
                            + CASE @counter
                                WHEN  1 THEN '' -- No name
                                WHEN  2 THEN ' thousand '    WHEN  3 THEN ' million '
                                WHEN  4 THEN ' billion '     WHEN  5 THEN ' trillion '
                                WHEN  6 THEN ' quadrillion ' WHEN  7 THEN ' quintillion '
                                WHEN  8 THEN ' sextillion '  WHEN  9 THEN ' septillion '
                                WHEN 10 THEN ' octillion '   WHEN 11 THEN ' nonillion '
                                WHEN 12 THEN ' decillion '   WHEN 13 THEN ' undecillion '
                                ELSE ''
                              END
                            + @outputString
		            END
            
		         -- now get the hundreds
		         IF @hundreds <> '0'
                  BEGIN
			            SET @outputString  = (SELECT word 
                                              FROM @NumbersTable 
                                              WHERE '0' + @hundreds = number)
 					                         + ' hundred ' 
                                        + @outputString
                  END
            END
      
         SELECT @counter = @counter + 1, @position = @position - 3
      END

   -- Remove any double spaces
   SET @outputString = LTRIM(RTRIM(REPLACE(@outputString, '  ', ' '))) 
   SET @outputstring = UPPER(LEFT(@outputstring, 1)) + SUBSTRING(@outputstring, 2, 8000)
   
   RETURN @outputString
END
Accessing MetaData Contact Us
FW_spGetRowCounts simply returns a list of all user tables and the number of rows in each.

CREATE PROCEDURE FW_spGetRowCounts
 AS
SET NOCOUNT ON

SELECT NAME, ROWS FROM SysIndexes WHERE IndID < 2 ORDER BY Name
FW_spGetSchema returns a schema listing (joined to a custom data dictionary table named FieldLabels) according to a passed field list for a particular table.

CREATE PROCEDURE FW_spGetSchema
@TableName varchar(40), @FldList varchar(1000)
 AS
SET NOCOUNT ON

IF @FldList = ''
   SELECT SysColumns.Name, SysTypes.Name AS Type, SysColumns.Length, SysColumns.IsNullable, SysIndexes.Rows,
          ISNULL(FieldLabels.FieldLabel, SysColumns.Name) AS FieldLabel
      FROM SysColumns
      INNER JOIN SysTypes ON SysTypes.XType = SysColumns.XType
      INNER JOIN SysObjects ON SysObjects.ID = SysColumns.ID AND SysObjects.Name=@TableName
      INNER JOIN SysIndexes ON SysIndexes.ID = SysColumns.ID AND SysIndexes.IndID < 2
      LEFT OUTER JOIN FieldLabels ON FieldLabels.TableName = SysObjects.Name AND
                      FieldLabels.TableColumn = SysColumns.Name
      ORDER BY SysColumns.ColOrder
   ELSE
      BEGIN
         DECLARE @OrdList varchar(300)
         DECLARE @SeqList varchar(300)
         SET @OrdList = @FldList
         SET @FldList = REPLACE(@FldList, '[', '')
         SET @FldList = REPLACE(@FldList, ']', '')
         SET @SeqList = @FldList
         SET @FldList = REPLACE(@FldList, ',', ''',''')
         DECLARE @SelStr varchar(2000)
         SET @SelStr = 'SELECT SysColumns.Name, SysTypes.Name AS Type, SysColumns.Length, ' +
                               'SysColumns.IsNullable, SysIndexes.Rows, ' +
                               'ISNULL(FieldLabels.FieldLabel, '' '') AS FieldLabel, ' +
                               'dbo.ItemIndex(''' + @SeqList + ''', SysColumns.Name) AS Seq ' +
                         'FROM SysColumns ' +
                           'INNER JOIN SysTypes ON SysTypes.XType = SysColumns.XType ' +
                           'INNER JOIN SysObjects ON SysObjects.ID = SysColumns.ID AND ' +
                                                    'SysObjects.Name=''' + @TableName + ''' ' +
                           'INNER JOIN SysIndexes ON SysIndexes.ID = SysColumns.ID AND ' +
                                                    'SysIndexes.IndID < 2 ' +
                           'LEFT OUTER JOIN FieldLabels ON FieldLabels.TableName = SysObjects.Name AND ' +
                                           'FieldLabels.TableColumn = SysColumns.Name ' +
                         'WHERE SysColumns.Name IN (''' + @FldList + ''') ' +
                         'ORDER BY Seq '

         EXEC(@SelStr)
      END
FW_spGetAllUserObjects simply lists all objects for all user tables (columns, indexes, restraints, etc.)

CREATE PROCEDURE FW_spGetAllUserObjects
 AS
SET NOCOUNT ON

SELECT SysObjects.Name AS TableName, SysColumns.Name,
       SysTypes.Name AS Type, SysColumns.Length, SysColumns.IsNullable
   FROM SysColumns 
   INNER JOIN SysTypes ON SysTypes.XType = SysColumns.XType
   INNER JOIN SysObjects ON SysObjects.ID = SysColumns.ID
   WHERE SysObjects.Category = 0
   ORDER BY SysObjects.Name, SysColumns.ColOrder
FW_spFindTextViewAndSP searches for a particular string in all view and stored procedure definitions.

CREATE PROCEDURE FW_spFindTextViewAndSP
@LOOKUP varchar(40)
 AS
SET NOCOUNT ON

SET @LOOKUP = '%' + RTRIM(@LOOKUP) + '%'
SELECT SysObjects.ID SPID, SysObjects.Name SPName, SysComments.Text SPText
    FROM SysObjects 
         INNER JOIN SysComments ON SysObjects.ID = SysComments.ID 
    WHERE (SysObjects.XType='V' OR SysObjects.XType='P') AND
           SysObjects.Name NOT LIKE 'dt%' AND SysComments.Text LIKE @LOOKUP
Accessing User Data Contact Us
RP_spGetRelatedOrders is a simple example of a parameterized user query.

CREATE PROCEDURE RP_spGetRelatedOrders
@OrderNumber varchar(10)
 AS
SET NOCOUNT ON

SELECT PODetail.POProduct, PODetail.POGrade, PODetail.POBOLWeight,PODetail.POUnits, PODetail.POPrice,
       OrderDetail.MasterID, OrderDetail.ProductQty, OrderDetail.ProductUnits, OrderDetail.ProductUnitPrice,
       OrderDetail.ProductDiscPct, OrderMaster.MasterCustID, OrderMaster.MasterCustPO, OrderMaster.MasterETS,
       Customers.CustName, OrderMaster.MasterReleaseDate, OrderMaster.MasterShipFrom, GTable.TableValue AS Units,
       OrderDetail.ProductQty * dbo.WeightMult(GTable.TableValue, 'Bags (55.115)') AS Bags
   FROM PODetail
        INNER JOIN OrderDetail ON OrderDetail.ProductCode = PODetail.POProduct
        INNER JOIN OrderMaster ON OrderMaster.MasterID = OrderDetail.MasterID
        INNER JOIN Customers   ON Customers.CustID = OrderMaster.MasterCustID
        INNER JOIN GTable      ON GTable.TableName = 'Units' AND
                                  GTable.TableKey = OrderDetail.ProductUnits
        INNER JOIN GTable AS GTable2 ON GTable2.TableName = 'Units' AND
                                        GTable2.TableKey = OrderDetail.ProductPriceUnits
   WHERE PONumber = @OrderNumber
   ORDER BY Customers.CustName, PONumber
RP_spQtyOnHandReport is a complex example of a report query.

CREATE PROCEDURE RP_spQtyOnHandReport
AS
SET NOCOUNT ON

SELECT PONumber, Vendor, Material, POProduct, POPrice,
       OrdBags, ROUND(OrdBags * 55.115, 0) AS OrdLbs,
       ActBags, ROUND(ActBags * 55.115, 0) AS ActLbs,
       ActBags - OrdBags AS RemBags, ROUND((ActBags - OrdBags) * 55.115, 0) AS RemLbs,
       PORemarks, WhseName 
   FROM (SELECT PODetail.PONumber, GTable4.TableValue AS Material, GTable2.TableValue AS Vendor,
                PODetail.POProduct, POPrice, PODetail.PORemarks, Warehouse.WhseName,
                SUM(CASE ISNULL(OrderDetail.ProductUnits, 0)
                      WHEN 0 THEN 0
                      ELSE OrderDetail.ProductQty * dbo.WeightMult(GTable.TableValue, 'Bags (55.115)')
                    END) AS OrdBags,  
                (SELECT SUM(CASE POActBags
                              WHEN 0 THEN 
                                 CASE ISNULL(PODet.POUnits, 0)
                                   WHEN 0 THEN 0
                                   ELSE PODet.POBOLWeight * dbo.WeightMult(GTable3.TableValue, 'Bags (55.115)')
                                 END
                              ELSE POActBags
                            END) AS POActBags
                   FROM PODetail AS PODet 
                        INNER JOIN GTable AS GTable3 ON GTable3.TableName = 'Units' AND
                                   GTable3.TableKey = PODet.POUnits
                   WHERE PODetail.PONumber  = PODet.PONumber AND
                         PODetail.POProduct = PODet.POProduct
                   GROUP BY PODet.PONumber, PODet.POProduct) AS ActBags
            FROM PODetail
                 LEFT OUTER JOIN OrderDetail ON OrderDetail.ProductCode = PODetail.POProduct
                 LEFT OUTER JOIN GTable ON GTable.TableName = 'Units' AND
                                 GTable.TableKey = OrderDetail.ProductUnits
                 INNER JOIN POMaster ON POMaster.PONumber = PODetail.PONumber
                 LEFT OUTER JOIN GTable AS GTable2 ON GTable2.tablename = 'Vendors' AND
                                                      CAST(GTable2.TableKey AS int) = POMaster.POVendorID
                 INNER JOIN GTable AS GTable4 ON GTable4.TableName = 'Materials' AND
                                                 GTable4.TableKey = PODetail.POMaterials
                 INNER JOIN Warehouse ON Warehouse.WhseID = PODetail.POWarehouse
            WHERE POMaster.POClosed = 0
            GROUP BY PODetail.PONumber, GTable4.TableValue, GTable2.TableValue, PODetail.POProduct,
                     PODetail.POPrice, GTable.TableValue, PODetail.PORemarks, Warehouse.WhseName) AS temp
   GROUP BY PONumber, Vendor, Material, POProduct, POPrice, OrdBags, ActBags, PORemarks, WhseName 
   HAVING ActBags > OrdBags OR OrdBags > ActBags * 1.1
   ORDER BY PONumber, Vendor, Material, POProduct
spFW_GetNameList is a simple example of a parameterized user query.

CREATE PROCEDURE spFW_GetNameList
@tStrTaxID varchar(20), @tStrState varchar(3), @tStrZip varchar(10), @tStrLastName varchar(30),
@tStrProviderType varchar(2), @IntRNCNameLengthToUseMin int, @RoutingTree varchar(500)
AS

SET NOCOUNT ON

DECLARE @lStrWhereStatement varchar(1000)
DECLARE @StrCommand varchar(1100)
DECLARE @lStrNameField varchar(30)
DECLARE @lStrAdditionalOrder varchar(100)

SET @lStrWhereStatement  = ' MasterNames.NetCode IN (' + @RoutingTree + ') '
SET @lStrAdditionalOrder = ''

IF @tStrTaxID <> ''
   BEGIN
      SET @lStrWhereStatement = @lStrWhereStatement + ' AND MasterNames.TaxID = ''' + @tStrTaxID + ' '''
   END

IF @tStrZip <> ''
   BEGIN
      SET @tStrZip            = LEFT(@tStrZip, 5)
      SET @lStrWhereStatement = @lStrWhereStatement + ' AND MasterNames.BZip LIKE ''' + @tStrZip + '%'' '
   END

IF @tStrLastName <> ''
   BEGIN
      IF @tStrProviderType = 'H'
         BEGIN
            SET @lStrNameField       = 'MasterNames.FAname'
            SET @lStrAdditionalOrder = @lStrAdditionalOrder + ' MasterNames.FaName,'
         END
      ELSE
         BEGIN
            SET @lStrNameField       = 'MasterNames.LName'
            SET @lStrAdditionalOrder = @lStrAdditionalOrder + ' MasterNames.LName,'
         END

      IF LEN(@tStrLastName) <= @IntRNCNameLengthToUseMin
         BEGIN
            SET @lStrWhereStatement = @lStrWhereStatement + ' AND ' +
                                      @lStrNameField + ' = ''' + @tStrLastName + ''' '
         END
      ELSE
         BEGIN
            SET @lStrWhereStatement = @lStrWhereStatement + ' AND ' +
                                      @lStrNameField + ' LIKE ''' + @tStrLastName + '%'' '
         END
   END

IF @tStrProviderType <> ''
   BEGIN
      IF @tStrProviderType='H'
         BEGIN
            SET @lStrWhereStatement = @lStrWhereStatement + ' AND MasterNames.LName = '''' '
         END
      ELSE
         BEGIN
            SET @lStrWhereStatement = @lStrWhereStatement + ' AND MasterNames.LName <> '''' '
         END
   END

SET @lStrWhereStatement = @lStrWhereStatement + ' AND MasterNames.BState = ''' + @tStrState + ''' '

SET @StrCommand = 'SELECT TOP 100 MasterNames.*, ' +
                          'dbo.ItemIndex(''' + @RoutingTree + ''', MasterNames.NetCode) as Seq ' +
                     'FROM MasterNames ' + 
                     'WHERE ' + @lStrWhereStatement + 
                     'ORDER BY Seq, ' + @lStrAdditionalOrder

EXEC(@StrCommand)
Using with VFP Contact Us
SQL pass-through (SPT) technology enables you to send SQL statements directly to a server. SPT statements, because they execute on the back-end server, are powerful ways to enhance the performance of your client/server applications.

The method illustrated in this section contrast with the CursorAdapter method, introduced in VFP 8.0. Some code and examples are here.

SPT technology includes the following characteristics:
  • You can use server-specific functionality, such as stored procedures and server-based intrinsic functions.
  • You can use extensions to SQL supported by the server, as well as data-definition, server administration, and security commands.
  • You have more control over SQL pass-through Update, Delete, and Insert statements.
  • You have more control over remote transactions.
  • Visual FoxPro can handle SQL pass-through queries that return more than a single result set.
  • By default, a SQL pass-through query always returns a non-updatable snapshot of remote data, which is stored in an active view cursor. You can make the cursor updatable by setting properties with the CURSORSETPROP( ) function.

The following code assumes a trusted connection to the SQL Server. If that is not the case, fill in the UID and PWD parameters with the user name and password, respectively.

lnConn = SQLStringConnect("DRIVER={SQL Server};SERVER=ServerName;UID=;PWD=;DATABASE=SQLDatabaseName")

IF lnConn <= 0
   =AERROR(laError)
   =MESSAGEBOX(laError(1, 2), 32, "Could not connect to SQL Server")
   RETURN "Could not connect to SQL Server"
ENDIF

lcSQL  = "EXEC spExampleProcedure " + ALLTRIM(STR(lnID))

=SQLSETPROP(lnConn, "asynchronous", .F.)

DO WHILE .T.
   lnReturn = SQLExec(lnConn, lcSQL)
   
   IF lnReturn <= 1
      EXIT
   ENDIF
ENDDO

IF lnReturn <= 0
   =AERROR(laError)
   =MESSAGEBOX(laError(1, 2), 32, "EXEC spExampleProcedure Failed")
   RETURN "EXEC spExampleProcedure failed: " + lcSQL
ENDIF

** Result set now resides in local cursor SQLResult

=SQLDisconnect(lnConn)
It is very important that a connection be released when it is no longer needed by the application because connections consume valuable resources on the server, and the number of connections may be limited by licensing constraints.

A class method that interacts with a SQL Server stored procedure:

*-----------------------------------------------------------------------------*
* Return a single SQL table record into an object collection                          
* Assumes public connection handle stored in .lnConn and a place to store the
* resulting record count (.lnDBFRecCount).
*-----------------------------------------------------------------------------*
   PROCEDURE GetSQLRecordObject(lcSQL AS STRING) AS OBJECT

      LOCAL lnSavSel, loReturnObject, lcErrMsg, lnTally

      lnSavSel = SELECT()

      WITH THIS
         lnTally = SQLExec(.lnConn, lcSQL)
         
         SELECT SQLResult

         IF RECCOUNT() > 0
            SELECT SQLResult
            SCATTER MEMO NAME loReturnObject
            .lnDBFRecCount = RECCOUNT()
         ELSE
            SCATTER MEMO NAME loReturnObject BLANK
            .lnDBFRecCount = 0
         ENDIF

         USE IN SQLResult

         IF VARTYPE(loReturnObject) <> "O"
            lcErrMsg = .ERRORMESSAGE("Could not create loReturnObject for " +;
                                     lcSQL + "'.")
            RETURN lcErrMsg
         ENDIF
      ENDWITH

      SELECT (lnSavSel)

      RETURN loReturnObject

   ENDPROC
Retrieving the SQL Server identity value for a newly-inserted record:

         lcSQL   = "INSERT Prospects " +;
                     "(Name, Addr1, Addr2, City, State, Postal, Fips, Phone, Fax) " +;
                    "VALUES " +;
                     "('" + STRTRAN(lcName, "'", "''") + "','" + STRTRAN(lcAddr1, "'", "''") + "','" +;
                            STRTRAN(lcAddr2, "'", "''") + "','" + STRTRAN(lcCity, "'", "''") + "','" +;
                            lcState + "','" + lcPostal + "','" + lcFips + "','" +;
                            lcPhone + "','" + lcFax + "') " +;
                   "SELECT TOP 1 @@IDENTITY AS ProspectID FROM Prospects"
         lnTally = SQLExec(.lnConn, lcSQL)
Linking to a FoxPro table from withing a SQL Server stored procedure:

DECLARE @ParmCity varchar(100)
DECLARE @SQLStr varchar(300)
SET @ParmCity = UPPER('Pensacola')
SET @SQLStr = 'SELECT Temp.*
                  FROM OPENROWSET(''VFPOLEDB.1'',
                            ''C:\HOME\ATLANTA FOXPRO USERS GROUP\'';'''';'''',
                            ''SELECT *
                                 FROM Zip
                                 WHERE City=''''' + @ParmCity + ''''''') AS Temp'
EXECUTE(@SQLStr)