You are on page 1of 263

Client/Server Applications

with
Visual FoxPro and SQL Server

Chuck Urwiler
Gary DeWitt
Mike Levy
Leslie Koorhan

Hentzenwerke Publishing
Published by:
Hentzenwerke Publishing
980 East Circle Drive
Whitefish Bay WI 53217 USA

Hentzenwerke Publishing books are available through booksellers and directly from the
publisher. Contact Hentzenwerke Publishing at:
414.332.9876
414.332.9463 (fax)
www.hentzenwerke.com
books@hentzenwerke.com

Client/Server Applications with Visual FoxPro and SQL Server


By Chuck Urwiler, Gary DeWitt, Mike Levy and Leslie Koorhan
Technical Editor: Chaim Caron
Copy Editor: Farion Grove

Copyright © 2000 by Chuck Urwiler, Gary DeWitt, Mike Levy and Leslie Koorhan

All other products and services identified throughout this book are trademarks or registered
trademarks of their respective companies. They are used throughout this book in editorial
fashion only and for the benefit of such companies. No such uses, or the use of any trade
name, is intended to convey endorsement or other affiliation with this book.

All rights reserved. No part of this book, or the .CHM Help files available by download from
Hentzenwerke Publishing, may be reproduced or transmitted in any form or by any means,
electronic, mechanical photocopying, recording, or otherwise, without the prior written
permission of the publisher, except that program listings and sample code files may be entered,
stored and executed in a computer system.

The information and material contained in this book are provided “as is,” without warranty of
any kind, express or implied, including without limitation any warranty concerning the
accuracy, adequacy, or completeness of such information or material or the results to be
obtained from using such information or material. Neither Hentzenwerke Publishing nor the
authors or editors shall be responsible for any claims attributable to errors, omissions, or other
inaccuracies in the information or material contained in this book. In no event shall
Hentzenwerke Publishing or the authors or editors be liable for direct, indirect, special,
incidental, or consequential damages arising out of the use of such information or material.

ISBN: 0-930919-01-8

Manufactured in the United States of America.


To my wife, Michelle, for her patience and support.
— Chuck Urwiler

I dedicate this endeavor to Joan and Lou DeWitt.


— Gary DeWitt

To my wife, Heather, and children, Jacob and Megan, for having patience.
— Michael Levy

This is for my wife Sybille, who has put up with the most awful mess that
a home office can be, with the excuse that “once I finish this, then I’ll clean it up.” And I did.
— Leslie Koorhan
v

Our Contract with You,


The Reader
In which we, the folks who make up Hentzenwerke Publishing, describe what you, the
reader, can expect from this book and from us.

Hi there!

I’ve been writing professionally (in other words, eventually getting a paycheck for my
scribbles) since 1974, and writing about software development since 1992. As an author, I’ve
worked with a half-dozen different publishers, and corresponded with thousands of readers
over the years. As a software developer and all-around geek, I’ve also acquired a library of
more than 100 computer and software-related books.
Thus, when I donned the publisher’s cap four years ago to produce the 1997 Developer’s
Guide, I had some pretty good ideas of what I liked (and didn’t like) from publishers, what
readers liked and didn’t like, and what I, as a reader, liked and didn’t like.
Now, with our new titles for the spring and summer of 2000, we’re entering our third
season. (For those keeping track, the ’97 DevGuide was our first, albeit abbreviated, season,
and the batch of six “Essentials” for Visual FoxPro 6.0 in 1999 was our second.)
John Wooden, the famed UCLA basketball coach, had posited that teams aren’t
consistent—they’re always getting better—or worse. We’d like to get better… One of my goals
for this season is to build a closer relationship with you, the reader.
In order to do this, you’ve got to know what you should expect from us.
• You have the right to expect that your order will be processed quickly and correctly,
and that your book will be delivered to you in new condition.
• You have the right to expect that the content of your book is technically accurate and
up to date, that the explanations are clear, and that the layout is easy to read and
follow without a lot of fluff or nonsense.
• You have the right to expect access to source code, errata, FAQs, and other
information that’s relevant to the book via our Web site.
• You have the right to expect an electronic version of your printed book (in compiled
HTML Help format) to be available via our Web site.
• You have the right to expect that, if you report errors to us, your report will be
responded to promptly, and that the appropriate notice will be included in the errata
and/or FAQs for the book.

Naturally, there are some limits that we bump up against. There are humans involved, and
they make mistakes. A book of 500 pages contains, on average, 150,000 words and several
megabytes of source code. It’s not possible to edit and re-edit multiple times to catch every last
vi

misspelling and typo, nor is it possible to test the source code on every permutation of
development environment and operating system—and still price the book affordably.
Once printed, bindings break, ink gets smeared, signatures get missed during binding. On
the delivery side, Web sites go down, packages get lost in the mail.
Nonetheless, we’ll make our best effort to correct these problems—once you let us know
about them.

And, thus, in return, when you have a question or run into a problem, we ask that you first
consult the errata and/or FAQs for your book on our Web site. If you don’t find the answer
there, please e-mail us at books@hentzenwerke.com with as much information and detail as
possible, including (1) the steps to reproduce the problem, (2) what happened, and (3) what you
expected to happen, together with (4) any other relevant information.
I’d like to stress that we need you to communicate questions and problems clearly.
For example…
• “Your downloads don’t work” isn’t enough information for us to help you.
“I get a 404 error when I click on the Download Source Code link on
www.hentzenwerke.com/book/downloads.html” is something we can help you with.
• “The code in Chapter 10 caused an error” again isn’t enough information. “I
performed the following steps to run the source code program DisplayTest.PRG in
Chapter 10, and received an error that said ‘Variable m.liCounter not found’” is
something we can help you with.

We’ll do our best to get back to you within a couple of days either with an answer, or at
least an acknowledgment that we’ve received your inquiry and that we’re working on it.

On behalf of the authors, technical editors, copy editors, layout artists, graphical artists,
indexers, and all the other folks who have worked to put this book in your hands, I’d like to
thank you for purchasing this book, and hope that it will prove to be a valuable addition to your
technical library. Please let us know what you think about this book—we’re looking forward to
hearing from you.
As Groucho Marx once observed, “Outside of a dog, a book is a man’s best friend. Inside
of a dog, it’s too dark to read.”

Whil Hentzen
Hentzenwerke Publishing
August, 2000
vii

Acknowledgements
First of all, I’d like to thank my wonderful wife Michelle for dealing with my extended working
hours while I finished this book. As you all probably know, working a full-time job and then
working on a book on top of it doesn’t leave room for a lot of “quality time” together. She’s
been very supportive of me from the day I met her, and especially during the time I worked on
this book. There were no complaints, only support. Thank you, Michelle... I love you.
Next I’d like to thank Whil for getting me involved with this book. We had talked in the
past about doing some kind of book, so this was a great way for me to whet my appetite, since I
didn’t have to write the whole thing <g>.
Which brings me to my third group of thank yous: Without Gary, Mike and Leslie, I would
have had a lot more late nights of writing. I thank them for their contributions to the book. And
Chaim did a great job of making sure my writing was technically complete as well as clear. I’d
like to thank everyone at Micro Endeavors, both past and present. Without you, I would not
have been able to learn so much about VFP and SQL Server. Thanks, everyone! The drinks
are on me!
I must also thank my family and friends, who are all very excited that I’m finally going to
have my name on the cover of a book. Thanks for your support and encouragement over the
years as I tried to find my place in the world.
And finally, I’d like to thank all of “my” students, as well as those of you who support me
at the DevCons and user groups where I’ve spoken. Without your questions, curiosity, feedback
and willingness to share what you know with me, I wouldn’t be able to write this stuff.
— Chuck

I would like to thank Bonnie Berent and Kim Cameron for personal support and
encouragement; Tamar Granor and Whil Hentzen for supporting and encouraging my writing;
Chaim Caron for his insights and ideas; The Academy for...
— Gary

I don’t think I shall soon forget my experiences while participating in the development of this
book. It has given me a better appreciation for those who give up family and free time to share
their knowledge and experiences with the rest of us.
To Heather, thanks for your support, patience and willingness to proofread the early drafts,
even though you did not understand the topics being discussed.
To Whil Hentzen, thanks for inviting me to participate in this project; to Gary DeWitt,
thanks for heading up the project and taking on the bulk of the work; to my good friend Matt
Tiemeyer, thanks for taking the time to discuss ideas and preview drafts; and to another good
viii

friend, Leslie Koorhan (before you came to participate in the project), my thanks for providing
ideas and the many long discussions.
— Mike

First of all, I thank Whil Hentzen for giving me the opportunity to make my contribution to this
book, and for pushing. There’s someone else who I’d also like to thank for this chance, but
Whil never told me who brought my name up in the first place. Second, there’s Mike Levy,
who offered me a peek at his contributions long before I wrote one word. Mike has been a real
friend as well, before and during this process. Third, there’s Gary DeWitt (I know these are
other authors, but hey, they deserve all the praise that I can muster), who helped me shape my
chapters. He gave me a lot of direction when I was just trying to formulate my thoughts. And
after all, it was his outline of the book that started me. Fourth is Yair Alan Griver, who started
me years ago with his client/server articles, and started me writing with his encouragement and
inspiration. Finally, I also thank Dan Freeman, Paul Bienick, Ken Levy and Markus Egger.
— Leslie
ix

About the Authors


Chuck Urwiler
Chuck Urwiler is a Senior Instructor, Consultant and Developer for Micro Endeavors, Inc., a
Microsoft Solution Provider Partner and Certified Technical Education Center in Upper Darby,
PA. He is a highly respected instructor who has trained thousands of developers nationwide in
all versions of Visual FoxPro and in version 7 of SQL Server. He has authored numerous
seminars and hands-on courses for Micro Endeavors, including introductions to the new
features of SQL Server 7 and Visual FoxPro 6, Migrating FoxPro Applications to Visual
FoxPro, and most recently, Mastering Microsoft Visual FoxPro Development. Chuck was also
a technical contributor and editor for the Microsoft certification exams in Visual FoxPro.
Currently, he holds several certifications: a Microsoft Certified Professional (MCP) in Visual
FoxPro and SQL Server 7.0, a Microsoft Certified Trainer (MCT) and a Certified Technical
Trainer (CTT), as well as being one of the first 1,000 Microsoft Certified Solution Developers
(MCSD) in the world.
Not content to spend all of his time in the classroom, Chuck keeps his knowledge current
by working with the Micro Endeavors development team on a variety of projects, many of
which include both Visual FoxPro and SQL Server. He is a consultant for Micro Endeavors
clients on their software projects, using his expertise and experience to solve business and
technical problems with both SQL Server and Visual FoxPro.
Chuck can be reached at chuck@microendeavors.com.

Gary DeWitt
Gary DeWitt has been a frequent contributor to the FoxTalk and FoxPro Advisor journals for
several years. Gary is the author of Client/Server Applications with Visual FoxPro and the
technical editor of Internet Applications With Visual FoxPro 6.0, from Hentzenwerke
Publishing, and has spoken at regional and national FoxPro conferences including Visual
FoxPro DevCon. He is a Microsoft Certified Professional and a past Microsoft Most Valuable
Professional. In addition to Visual FoxPro, Gary also works with C++, Java and Visual Basic,
and has been in the computing industry in one way or another since 1976. Gary is currently
senior software magician at Sunpro, Inc., the leader in fire service software, where he leads a
team responsible for a large client/server COM application compatible with the National Fire
Incident Reporting System.
Gary can be reached at gdewitt@garydewitt.com.

Michael Levy
Michael Levy is a consultant with G.A. Sullivan, where he specializes in SQL Server and
database technologies. He is a Microsoft Certified Solution Developer (MCSD), Database
Administrator (MCDBA) and Trainer (MCT). As an MCT, more than 400 students in 60
classes have benefited from his 10 years of FoxPro and four years of SQL Server experience.
Mike is a well-known member of the Visual FoxPro community and donates his time helping
others on various Internet newsgroups. In addition, he has spoken at multiple conferences and
user groups and is a frequent contributor to various technical journals. Mike is a University of
x

Cincinnati graduate and lives with his wife, two kids, eight fish and an old dog in a house that’s
still waiting for Mike to discover “landscaping.”
Mike can be reached at ma_levy@hotmail.com.

Leslie Koorhan
Leslie Koorhan is an independent consultant and trainer who specializes in database
applications. He has worked with nearly every version of FoxPro and Visual FoxPro as well as
Microsoft SQL Server since the mid 1990s. He is also a Visual Basic developer. He has written
numerous articles for several publications, most recently a series on Microsoft SQL Server
OLAP Services for FoxTalk. Leslie has also written several training manuals.
Leslie can be reached at lkoorhan@earthlink.net.

Chaim Caron
Chaim Caron is the President of Access Computer Systems, Inc., in New York City, which,
despite its name, specializes in software development using primarily Visual FoxPro. The firm
also provides data integrity testing, data cleansing services, and various other services relating
to data design and software development and support.
Chaim has specialized in FoxPro and Visual FoxPro since 1990. His articles have appeared
in FoxTalk and FoxPro Advisor. He has spoken to technical and industry groups on technical
and business issues for many years.
He spends his free time with his wife and daughter. His third love (after his wife and
daughter) is the mandolin, which he plays with the New York Mandolin Quartet. Major
mandolin influences were provided by Barry Mitterhoff, Carlo Aonzo, Ricky Skaggs, John
Monteleone and, of course, Bill Monroe.
Chaim can be reached at ccaron@earthlink.net.
xi

How to Download the Files


There are two sets of files that accompany this book. The first is the source code
referenced throughout the text, and the second is the e-book version of this book—the
compiled HTML Help (.CHM) file. Here’s how to get them.

Both the source code and the CHM file are available for download from the Hentzenwerke
Web site. In order to obtain them, follow these instructions:
1. Point your Web browser to www.hentzenwerke.com.
2. Look for the link that says “Download Source Code & .CHM Files.” (The text
for this link may change over time—if it does, look for a link that references Books
or Downloads.)
3. A page describing the download process will appear. This page has two sections:
• Section 1: If you were issued a username/password from Hentzenwerke
Publishing, you can enter them into this page.
• Section 2: If you did not receive a username/password from Hentzenwerke
Publishing, don’t worry! Just enter your e-mail alias and look for the question
about your book. Note that you’ll need your book when you answer the question.
4. A page that lists the hyperlinks for the appropriate downloads will appear.

Note that the .CHM file is covered by the same copyright laws as the printed book.
Reproduction and/or distribution of the .CHM file is against the law.

If you have questions or problems, the fastest way to get a response is to e-mail us at
books@hentzenwerke.com.
xiii

List of Chapters
Chapter 1: Introduction to Client/Server 1
Chapter 2: Visual FoxPro for Client/Server Development 19
Chapter 3: Introduction to SQL Server 7.0 27
Chapter 4: Remote Views 57
Chapter 5: Upsizing: Moving from File-Server to Client/Server 75
Chapter 6: Extending Remote Views with SQL Pass Through 95
Chapter 7: Downsizing 125
Chapter 8: Errors and Debugging 145
Chapter 9: Some Design Issues for C/S Systems 159
Chapter 10: Application Distribution and Managing Updates 177
Chapter 11: Transactions 193
Chapter 12: ActiveX Data Objects 209
Appendix A: New Features of SQL Server 2000 225
xv

Table of Contents
Our Contract with You, The Reader v
Acknowledgements vii
About the Authors ix
How to Download the Files xi

Chapter 1: Introduction to Client/Server 1


The PC revolution 1
Client/server to the rescue 2
Features of client/server databases 3
Data access 3
Security 4
Database backup 6
Point-in-time recovery 6
Triggers 7
Referential integrity 8
Indexes 8
Defaults 10
Rules 10
Primary key generation 10
Stored procedures 11
Views 12
User-defined data types 13
Replication 14
Transactions 14
Scalability 14
Reliability 15
Advantages of client/server 15
Performance 16
Cost 16
Security 16
Scalability 17
Summary 17

Chapter 2: Visual FoxPro for Client/Server Development 19


Object-oriented programming (OOP) 19
Support for COM 21
Built-in client/server support 23
Built-in local data engine 23
xvi

Support for other data-access technologies 24


Rapid Application Development (RAD) 25
Summary 25

Chapter 3: Introduction to SQL Server 7.0 27


Why move to SQL Server? 27
Capacity 27
Concurrency 28
Robustness 28
Security 28
Installation 29
SQL Server editions 29
Licensing 30
Character sets 31
Sort order 31
Network libraries 31
Databases, database files and the transaction log 32
Types of databases 32
Database files 33
Creating a database 33
The transaction log 36
How SQL Server allocates storage 36
Transactions and locking 37
Implicit and explicit transactions 37
Locking 37
Database objects 39
SQL Server object names 39
Tables 40
Enforcing data integrity 41
Indexes 46
Views 48
Stored procedures 49
Triggers 52
Summary 55

Chapter 4: Remote Views 57


Connections 57
Remote views 63
Updatable views 65
Buffering 68
Committing and refreshing buffers 69
Other view properties 70
FetchAsNeeded and FetchSize 70
MaxRecords 71
xvii

FetchMemo 71
Tables 72
Field properties 72
DefaultValue 72
RuleExpression 73
UpdateName 73
DataType 73
Summary 74

Chapter 5: Upsizing: Moving from File-Server to Client/Server 75


Why upsize? 75
Using the SQL Server Upsizing Wizard 76
Indexes 81
Defaults 82
Relationships 84
Validation rules 85
Changes made locally 86
Finished at last? Modifying the results of the Upsizing Wizard 87
The local database 88
Summary 93

Chapter 6: Extending Remote Views with SQL Pass Through 95


Connecting to the server 95
The SQLConnect() function 96
The SQLStringConnect() function 96
Handling connection errors 97
Disconnecting 98
Accessing metadata 98
The SQLTables() function 99
The SQLColumns() function 100
Submitting queries 101
Queries that return a result set 101
Retrieving multiple result sets 102
Queries that modify data 105
Parameterized queries 105
Making SQL pass through result sets updatable 108
Calling stored procedures 109
Handling input and output parameters 109
Transaction management 111
Binding connections 113
Asynchronous processing 113
Connection properties revisited 115
Other connection properties 116
xviii

Remote views vs. SQL pass through 118


SQL pass through 118
Remote views 119
Using remote views and SPT together 122
Transactions 122
Stored procedures 122
Filter conditions 123
Summary 123

Chapter 7: Downsizing 125


The case for a single code base 125
Interchangeable back ends 125
Remote views of VFP data 126
Substituting local views for remote views 128
Abstracting data access functionality 130
Microsoft Data Engine (MSDE) 136
What is MSDE? 136
MSDE vs. SQL Server 137
Distributing MSDE applications 141
Migrating MSDE databases to SQL Server 142
Summary 144

Chapter 8: Errors and Debugging 145


Handling errors 145
Trapping errors 145
Reporting errors 146
Conflict resolution 150
View errors 151
Debugging tools 152
SQL Server Profiler 152
The SQL Server Performance Monitor 155
ODBC logs 156
Summary 158

Chapter 9: Some Design Issues for C/S Systems 159


SQL database design issues 159
Data integrity mechanisms 160
VFP developer vs. SQL Server DBA 168
Client/server performance issues 169
Choosing indexes 169
Client/server division of work 171
Data location 173
Security 173
Client application 173
xix

SQL Server logins and permissions 174


Application roles 174
Summary 175

Chapter 10: Application Distribution and Managing Updates 177


Client/server development 177
Development environment 177
Deployment models 179
Traditional 179
Components 180
Server 180
Distributing databases (creating) 181
Existence of SQL Server 181
SQL pass through 182
SQL scripts 183
SQL-DMO 184
Object transfer (DTS) 185
Backup/restore 186
sp_Detach_DB and sp_Attach_DB 186
Managing updates 187
Application changes 187
Database updates 189
Version control coordination between client and server 191
Local lookup data 191
Why 192
Managing updates 192
Summary 192

Chapter 11: Transactions 193


Transaction basics 193
ACID properties 193
Visual FoxPro transactions 194
The missing property 195
SQL Server transactions 196
Implicit transactions 196
SQL Server isolation levels 198
Durable transactions 200
Locking 202
Lock compatibility 203
Blocking 203
Viewing lock activity 204
Deadlocks 207
Transaction gotcha! 208
Summary 208
xx

Chapter 12: ActiveX Data Objects 209


Why ADO? 209
ADO benefits 209
ADO disadvantages 211
Installing and distributing ADO 211
Using ADO within Visual FoxPro 212
The Connection object 212
The RecordSet object 214
The Command object 221
Summary 224

Appendix A: New Features of SQL Server 2000 225


Feature list 225
Installation issues 227
Query Analyzer 227
Debugging stored procedures 228
User-defined functions 229
Referential integrity 231
Trigger enhancements 232
Indexing computed columns 233
New data types 234
Big integers 234
Variants 234
Tables as variables 235
Summary 236
Chapter 1: Introduction to Client/Server 1

Chapter 1
Introduction to Client/Server
Client/server applications differ from file-server applications in many ways, but the
key difference is that client/server applications divide the processing between two or
more applications, a client and a server, which typically run on separate computers.
In this chapter, you will learn a little of the history of client/server computing, as well
as the features of client/server databases in general and Microsoft SQL Server in
particular. You will also learn some of the advantages of client/server databases
over file-server databases.

In the beginning, there were mainframes and minicomputers. All data resided on and was
processed by these often room-filling machines. All bowed down before the mighty MIS
department, as all information was in their hands.
Okay, so that might be an exaggeration, but not by much. In the late 1970s, there were
plenty of data processing centers with raised floors, a sea of disk drives, a wall of tape drives
and an army of operators. In this host-based model, all the processing was done by the
mainframe, while data was entered on dumb terminals. Consider the example of Gary’s first
programming project, in 1979: “The project involved trying to get data from our Data General
minicomputer for a decision support system for our product managers. We wanted to give
our incredibly powerful Apple IIs, with two floppy disk drives, access to the corporate sales
data so the product managers could make decisions on our product lines. We were completely
at the mercy of the MIS department, who controlled all the data. No way were they going to
let us have online access to it. They could be coerced into giving us a monthly dump of sales
data, but that was the best we could do. Our project turned into a primitive data warehouse;
modeling and reporting on the data turned out to be easy compared to getting access to it in
the first place.”
Lest you think that this is merely a history lesson, there are many of these systems still in
place. The most popular American hospital billing system uses an IBM AS/400 minicomputer-
based DB2 database. Many systems like this are unlikely to be replaced in the near future, but
to developers they are mostly a curiosity, as new development on such systems is rare, and the
growth of the mainframe market is pretty flat.

The PC revolution
Then came the personal computer. PCs allowed departments and often entire corporations to
dispense with their expensive, centralized host-based systems and replace them with networks
of PCs sharing files on file servers. The pendulum had swung the opposite way. Rather than
doing all of the processing on the central scrutinizer, it was done on the workstation. Dumb
terminals had been replaced by dumb file servers.
This is where most of us come in. FoxPro, along with other systems like dBase, Paradox
and Access, has a local data engine. All processing is done on the workstation, and the
network is used for file storage only. Throughout this book, this model is referred to as a file-
server database.
2 Client/Server Applications with Visual FoxPro and SQL Server

Because all the processing was performed locally and because the workstation could be a
powerful computer in its own right, we developers were able to give users very sophisticated
user interfaces. But we could not provide them with a secure, fault-tolerant database, and we
used up a tremendous amount of network bandwidth. In fact, application server software such
as Citrix WinFrame or Windows Terminal Server, which reduce network bandwidth by running
applications on a server machine, became popular primarily because of file-server databases
and their need for a big network pipeline. This is because all processing is performed on the
local workstation, while only the files reside on a file server. To perform a query, all
information necessary for finding the result set, such as index keys, must be downloaded in
addition to the result set itself. Rushmore is very efficient about what it brings down, but
whatever it needs still has to come to the local workstation.
Furthermore, improvements in database performance often require upgrades to each
workstation running the application—a potentially expensive proposition when many users
are involved.

Client/server to the rescue


The client/server database is an excellent solution to the problem of delivering sophisticated
applications while maintaining security and fault-tolerance of the database and reducing
network overhead. Client/server databases are so named because a system consists of at least
two applications: a client application and a server application, or service. The client application
typically runs on a workstation and can provide the sophisticated user interface that users have
come to expect. To interact with the database, the client sends requests to the server. The server
application typically runs as a service on an application server machine and manages the
database and responds to requests from clients.
The concept of a client making a request and the server responding to that request is key to
understanding client/server computing. The client has absolutely no control over the data on the
server. It makes a request for everything. The client doesn’t open data files; it asks the server to
log the user in to the database. This is the key to client/server security because there is no back-
door access to data. The client doesn’t download index keys to perform a query; it simply sends
a SELECT statement to the server, and the server sends back only the matching records. This is
the key to reducing network overhead, as only a minimum of network traffic is required.
In a file-server application, performing a query requires many round trips to the server. For
example, opening a table requires sending low-level file access instructions to the server and
returning a handle to the file. Then the workstation sends instructions to access the file
addresses of the index keys, and the server returns those keys. The workstation then processes
the keys to determine the result set and sends to the server the addresses of the records to
retrieve. Finally, the data itself is downloaded. Use your network monitor and a modem
connection sometime to perform a simple query against FoxPro tables, and you’ll get an
excellent demonstration of just how slow and inefficient this process can be. The larger the
database and the lower the network bandwidth, the worse this performance is.
In a client/server application, the client merely sends a SQL statement off to the server:

SELECT * FROM employees WHERE lastname LIKE 'King'


Chapter 1: Introduction to Client/Server 3

The server responds by sending back only the records that match. Not only has the quantity
of transmitted data been reduced, but the number of network round trips has, too.
The problem of improving file-server performance is also partially resolved by
client/server applications because database performance can be improved by upgrading a single
machine, the server, rather than upgrading all the workstations. It is considerably less expensive
to upgrade or replace a single, powerful application server than many lower-level workstations!
There are many client/server databases on the market today. Originally many of them, such
as Oracle, Informix and Sybase, ran only on Unix. Several years ago, Microsoft and Sybase
entered into an agreement whereby Microsoft would develop a version of Sybase SQL Server
for the Windows NT platform, and Microsoft SQL Server was the result. Now many
client/server database vendors, including the leader, Oracle, support Windows NT and/or
Windows 9x.
Client/server databases are frequently referred to as SQL databases because they
commonly support Structured Query Language, or SQL.

Features of client/server databases


This section summarizes the major features of client/server databases and, where appropriate,
makes comparisons to similar features in file-server databases. Features and comparisons are
illustrated using Microsoft SQL Server and Microsoft Visual FoxPro; however, most of these
points apply to other client/server and file-server databases as well.

Data access
The key difference between client/server and file-server databases is in the way data is
accessed. A client/server application always consists of two or more applications: a client
and a server. The database server erects a wall around the physical data, and it can only be
accessed by sending requests to the server application, which processes the requests and
returns the results.
With a Visual FoxPro database, any machine that has VFP or the VFP ODBC driver and
access to the data directory can process that data on the local workstation. All processing is
actually performed on the local workstation, and all information required to perform that
processing must be transmitted from the server to the workstation. After the server data is
copied to memory on the workstation, the user can change the data and the changes are written
directly to the database on the file server.
With a SQL Server database, the client workstation runs one or more applications that
make requests of the database server and accept the results of those requests. The client can
make changes to the data locally, but those changes are not made directly to the database.
Instead, they are packaged as requests, typically a SQL INSERT, UPDATE or DELETE
statement, and sent back to the server. Just as with a request for data, these change requests
are handled by the server, which has the ultimate authority and control over how such requests
are processed.
SQL Server includes a utility called Profiler that provides an excellent demonstration of
just how this works. In Figure 1, you can see a trace in the Profiler. This trace was run while
opening a VFP form that opens a couple dozen views. Each line in the trace shows the actual
SQL statement sent to the server along with details on the number of disk reads and writes,
duration of the processing, and so forth.
4 Client/Server Applications with Visual FoxPro and SQL Server

Figure 1. The SQL Server Profiler in action, demonstrating the request/response


nature of SQL Server.

Security
A Visual FoxPro database has no security. A developer can write procedural code to enforce
security, but this type of security can be circumvented.
By contrast, SQL Server databases are totally secure. All access to the database must be
through the database server application. By default, no user has access to anything in SQL
Server until the administrator has added the user to the system. Even then, the user has no
access until the administrator specifically grants it. This system is called declarative security.
Any attempt to access the data causes the server to check for a user’s login ID and password.
Figure 2 illustrates an attempt to access the Northwind database from Microsoft Visual
InterDev. Note the login dialog.
Code you write in your application to access a SQL Server database will also require
authentication by the server. Attempting to open a remote view of the Northwind employee
table from the VFP Command Window, as shown in Figure 3, will also prompt the user with a
login dialog.
Chapter 1: Introduction to Client/Server 5

Figure 2. An attempt to log in to the SQL Server Northwind database causes the user
to be prompted for a login ID and password.

Figure 3. Attempting to open a remote view of SQL Server data also causes the user
to be prompted for a login ID and password.
6 Client/Server Applications with Visual FoxPro and SQL Server

The preceding illustrations show the SQL Server login dialog, but there are actually many
ways to handle logging in. For example, you can configure your ODBC connections to supply a
login ID and password when connecting so that the login dialog doesn’t appear at all when the
application runs.
SQL Server also offers a feature called Windows NT Integrated Security that can be
used instead of the normal SQL Server authentication. With NT Integrated Security, SQL
Server checks the name of the user logged in to NT rather than requiring a SQL Server user
ID and password.
In addition to authenticating users for access to the database, SQL Server allows
administrators to assign rights to any individual object in the database. For example, some users
might have access to all columns in the employees table, while others might not be allowed to
see addresses or salaries. See Chapter 3, “Introduction to SQL Server 7.0,” for more
information on security in SQL Server.

Database backup
A friend recently described a client’s nightmare with a VFP database. They performed an
automatic tape backup of their network every night. One day, the inevitable happened and the
network went down. No problem, they simply went about restoring from backup. Well, not all
the tables were backed up, as some were open when the backup was performed. So they went
back to the previous night’s backup, but no dice. On and on they went, but no complete backup
had been performed because every night somebody had some files open because they forgot to
shut down their system or a developer was working late. They were in big trouble.
SQL Server eliminates this problem by allowing live backup of a database while it is in
use. An administrator can schedule backups, or an application can periodically send a T-SQL
BACKUP command to the server. The database is dumped to a backup file, which is closed as
soon as the backup is completed, and this backup file is copied to the backup tape. If the server
goes down, the client’s nightmare isn’t a problem. This backup capability permits both 24/7
operation and reliable backup.

Point-in-time recovery
SQL Server records every transaction in a transaction log in memory. Each time a transaction is
completed, it is copied from the log in memory to the log on the disk. At various intervals, the
transactions in the log are written to the physical database on disk. In the case of a crash, the
data can be recovered as long as the transaction log is recoverable. Of course, any updates that
had not yet been written to the physical transaction log would be lost.
The transaction log itself can also be backed up. Normally, the transaction log is not
emptied when the database is backed up. However, when the transaction log itself is backed up,
committed transactions are removed from it to keep the log size to a minimum. So if the
database is backed up on Tuesdays and the transaction log is backed up every day, then the
worst-case scenario even when the transaction log is destroyed is to restore the weekly backup
and then each daily transaction log. Only part of a day’s transactions are lost, which is a
substantial improvement over the aforementioned client’s nightmare.
Backups can be performed more often; however, backups affect performance of
the system. This is one of the trade-off decisions you will have to make when designing a
client/server system.
Chapter 1: Introduction to Client/Server 7

Triggers
Visual FoxPro databases support triggers. A trigger is a stored procedure that is triggered by an
INSERT, UPDATE or DELETE of a record in a table. In VFP databases, triggers are used to
enforce referential integrity and may be used for other purposes as well. One difficulty with
VFP triggers is that the VFP ODBC driver only supports a limited subset of VFP syntax. So
code in a trigger that works fine when running in a Visual FoxPro application may not work
when accessing data via ODBC.
Although SQL Server can use triggers to enforce referential integrity, declarative
referential integrity is the preferred method, simply because declarative integrity performs
substantially better than trigger-based integrity.
Triggers are also frequently used to support business rules and are an excellent way to
provide an audit trail. For example, a trigger might insert a record into an audit table containing
the datetime of the change, the user making the change, and the old and new values.
Here is an example of a very simple auditing trigger. Suppose a fire department wants to
keep track of any changes made to the alarm time, arrival time or cleared time for a fire
incident. Although it is entirely possible that such a change is being made legitimately to reflect
correct times, it is also possible that someone might change these times to make them look
better or to cover up mistakes. Here’s the schema for a simple time logging table:

CREATE TABLE timelog (timelogkey int IDENTITY (1,1),


edittime datetime,
userid varchar(100),
columnname varchar(100),
oldtime datetime,
newtime datetime)

The timelogkey column is an identity column and will automatically enter unique
integers, beginning with one and incrementing by one. Now an update trigger is created for
the incident table:

CREATE TRIGGER utrIncidentUpdate


ON incident
FOR UPDATE
AS
DECLARE @oldtime datetime,
@newtime datetime,
@ikey udtKeyField
IF UPDATE (alarmdate)
BEGIN
SELECT @oldtime = alarmdate, @ikey = incidentkey
FROM deleted
SELECT @newtime = alarmdate
FROM inserted
INSERT timelog
(incidentkey, edittime, userid, columnname, oldtime, newtime)
VALUES
(@ikey, GETDATE(), USER, 'alarmdate', @oldtime, @newtime)
END

This trigger requires a bit of explaining. SQL Server stored procedures use temporary
cursors that are visible only within the stored procedure. In the case of triggers, which are a
8 Client/Server Applications with Visual FoxPro and SQL Server

special type of stored procedure, there are two default cursors: deleted and inserted. In delete
triggers, the deleted cursor holds the values of the row being deleted, while in update triggers it
holds the values of the row prior to the update. In insert triggers, the inserted cursor holds the
values of the row being inserted, while in update triggers it contains the new values of the row
being updated.
The update trigger in the preceding code checks to see whether one of the three critical
times—alarmtime—has been updated. This is done with the UPDATE() function. If so, a row is
inserted into the timelog table. The row includes the current datetime (returned by the SQL
Server GETDATE() function), the user making the change, and the name of the column being
changed. It gets the old and new values from the deleted and inserted cursors, respectively, and
inserts them as well.
By extending this technique, you can see that it is possible to create a complete audit trail
of every change made in the database.

Referential integrity
Visual FoxPro databases support trigger-based referential integrity. When an application or
user attempts to delete, modify or insert a record, the appropriate trigger is fired. The trigger
determines whether the attempted delete, modification or insert can proceed. A deletion trigger
may cause cascaded deletes of child records. Similar processing occurs when an attempt is
made to change a primary key value. The change may be prevented by the trigger, or the
change may be cascaded through the child tables. Although such trigger-based referential
integrity is adequate for some purposes, it becomes less reliable as the schema becomes more
complicated, as thousands of triggers could be firing for a single deletion.
While SQL databases also support the use of triggers for the purposes described in the
previous paragraph, the preferred method is declarative referential integrity. Declarative
referential integrity, supported by SQL Server since version 6.0, enforces referential integrity at
the engine level. Deleting a record when children exist is simply prohibited. Instead of using
triggers to cascade deletes, a stored procedure is typically written to delete records from the
bottom up based on a given primary key for the top-level parent record. This technique is not
only more reliable, but it typically provides better performance, too.
Declarative referential integrity is implemented through the use of foreign key constraints.
Here is an example of how to create a foreign key constraint:

ALTER TABLE EMSAdvanced ADD CONSTRAINT


fk_EMSAdvanced_incident FOREIGN KEY (incidentkey)
REFERENCES incident (incidentkey)

Indexes
Indexes are used in Visual FoxPro databases to display data in a particular order, to improve
query performance through Rushmore optimization, to enforce unique values, and to identify
unique primary keys. SQL Server essentially uses indexes for the same purposes, but SQL
Server does not use Rushmore. Instead, it uses its own optimization techniques designed
specifically for the SQL Server query engine.
Chapter 1: Introduction to Client/Server 9

Clustered indexes
When a new record is added to a VFP table, it is typically appended to the end of the file, as
this is much more efficient than writing a record in the middle of a file. If no index order is set,
then browsing a table will show the records in this native order. Sometimes it makes sense for
performance reasons to occasionally sort a table based on the value of some field, such as a
primary key.
In SQL Server, the physical order of records can be controlled with a clustered index. Each
table may have one clustered index, and a new record will be inserted into the table in the order
determined by the clustered index. Clustered indexes can improve query performance when
queries need to return a range of consecutive records. However, they tend to decrease insert or
update performance, since these operations could force a reorganization of the table.
A clustered index on the customerid column of the Northwind customers table is created
like this:

CREATE CLUSTERED INDEX idxcustomerid


ON customers (customerid)

Unique indexes
In a VFP table, a candidate index is used to enforce the uniqueness of a value in a table. They
are called candidate indexes because the unique value is a likely candidate for a primary key. In
SQL Server, the same thing is accomplished with a unique index. Don’t confuse this with a
unique index in VFP (i.e., INDEX ON…TAG tagname UNIQUE), which is simply an index
containing only a single key even when the table contains multiple records, each of which has a
key of the same value. A unique index in SQL Server, like a candidate index in VFP, prevents
duplication of the value in the table. A unique index on the employeeid column of the
Northwind employees table is created like this:

CREATE UNIQUE INDEX pkemployeeid


ON employees (employeeid)

Primary keys
In a VFP database, you can specify one primary index per table like this:

ALTER TABLE mytable ADD PRIMARY KEY myfield TAG mytag

Behind the scenes, VFP actually creates a candidate tag in the index file and then adds a
special entry in the DBC to indicate that it is the primary key.
Primary keys in SQL Server are very similar, using primary key constraints. This code
creates a primary key constraint and a clustered index on the employeeid column of the
employees table:

ALTER TABLE employee ADD CONSTRAINT pkemployeeid


PRIMARY KEY CLUSTERED (employeeid)
10 Client/Server Applications with Visual FoxPro and SQL Server

Non-clustered indexes
Rushmore optimization in Visual FoxPro is effected with the use of index tags. Query
performance is improved under most circumstances by having an index that matches the filter
expression of a SELECT. Query optimization in SQL Server works in much the same way.
Fields that are likely to be used in filter expressions should have non-clustered indexes. A non-
clustered index on the lastname column of the Northwind employees table is created like this:

CREATE NON-CLUSTERED INDEX idxLastName ON employees (lastname)

Defaults
Both Visual FoxPro and SQL Server databases support defaults. Defaults allow you to specify
a default value for a field. For example, a merchant in the state of Washington might want to
assume that its customers are residents of Washington and automatically insert ‘WA’ in the
state column.

CREATE DEFAULT uddWAState AS 'WA'


EXEC sp_bindefault uddWAState, 'customers.state'

Rules
Both Visual FoxPro and SQL Server databases support rules. Rules allow you to specify field-
level validation in the database. Once specified, rules are enforced by the database engine.
A SQL Server rule is created using a variable, rather than a column name. Here’s a rule
that requires Social Security numbers to consist of nine numeric values. Note the use of the
@social variable (the “@” always denotes a local variable in SQL Server):

CREATE RULE udrSocial AS


@social LIKE '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'

By using a variable, rather than the name of a column, the same rule can be applied to any
number of columns:

EXEC sp_bindrule udrSocial, 'employees.socialsecurity'


EXEC sp_bindrule udrSocial, 'drivers.license'

Primary key generation


In Visual FoxPro, you can generate primary key values through one of two common
techniques. You can call a function or method that returns the next available primary key value,
and then insert this value into the new record before it is committed. Alternatively, you can
write a stored procedure that is invoked by the Default Value of a field, which automatically
places the new value into the field. As with any other VFP stored procedure, you are restricted
outside of a VFP application by the limitations of the VFP ODBC driver. The default values
you so painstakingly create may not be generated at all when a record is inserted via ODBC.
Chapter 1: Introduction to Client/Server 11

SQL Server automates primary key generation by using identity columns. An identity
column may not have a value inserted into it. Instead, it will automatically be set to the next
available value for that column. The initial (or seed) value can be set, as well as the amount by
which the value is incremented. The identity attribute can also be turned off for a column if you
need manual control over the values inserted into the column. The attribute can be turned back
on and set to increment from the highest existing value. This is all handled automatically by the
engine and requires no code on the part of the developer.
However, identity columns are no panacea. Retrieving the last value is normally handled
by checking the value of the @@IDENTITY global variable. But it may be difficult to get the
correct value of this variable, as insert triggers might have caused other identity columns to be
incremented and thus would return the wrong value for @@IDENTITY. Identity columns can
also cause a problem when databases are replicated. If you have the choice, you should avoid
identity columns when you design a database. Learn to use them, though, because you may find
yourself working on databases that use them.

Stored procedures
Visual FoxPro databases support stored procedures. However, stored procedures that run in
Visual FoxPro may not run via the VFP ODBC driver, so it is very difficult to create stored
procedures that are of any value outside of a VFP application. Because SQL Server stored
procedures are run by SQL Server, you never have this incompatibility issue.
One use for stored procedures in SQL Server is to provide parameterized record sets of
data. VFP views support parameters that can be used to filter the rows returned by the view.
For example, the customerorders view can be defined with a parameter to return only those
records matching a particular customer ID:

CREATE SQL VIEW customerorders AS ;


SELECT customers.companyname, orders.orderdate, ;
orderdetails.productid, orderdetails.quantity, orderdetails.unitprice ;
FROM customers JOIN orders ;
ON customers.customerid = orders.customerid ;
JOIN orderdetails ;
ON orders.orderid = orderdetails.orderid ;
WHERE customers.customerid LIKE ?cCustomerID

The parameterized view is opened by setting the value of the parameter and opening the
view with USE:

cCustomerID = 'ALFKI'
USE customerorders

Parameterized views are not supported by SQL Server, but they can be simulated with a
stored procedure like this:

CREATE PROCEDURE usp_customerorders


@cCustomerID nchar(5)
AS SELECT customers.companyname, orders.orderdate,
orderdetails.productid, orderdetails.quantity, orderdetails.unitprice
FROM customers JOIN orders
ON customers.customerid = orders.customerid
12 Client/Server Applications with Visual FoxPro and SQL Server

JOIN orderdetails
ON orders.orderid = orderdetails.orderid
WHERE customers.customerid LIKE @cCustomerID

In T-SQL code, one would access this stored procedure like this:

EXEC usp_customerorders 'ALFKI'

And from a VFP application, one would access this stored procedure like this:

SQLEXEC(lnHandle, "EXEC usp_customerorders 'ALFKI'")

Stored procedures in SQL Server are written using Transact-SQL, also known as T-SQL,
SQL Server’s programming language. Although not as rich a language as Visual FoxPro and
lacking any record-based data navigation (T-SQL is purely set-based), it is nonetheless a
powerful procedural language and can be used for many purposes. It contains syntactical
equivalents to such VFP constructs as IF..ELSE, DO WHILE, RETURN, PARAMETERS
and so on.
One powerful feature of stored procedures in SQL Server is that they can be assigned
security rights just like any other object in a database. One very good use for this is to use
stored procedures for database updates rather than allowing direct access to the tables. The
administrator takes away INSERT, UPDATE and DELETE rights for tables and/or specific
columns, and the only way for a change to be made is to call the appropriate stored procedure
and pass it the values necessary for the change.
Calling SQL Server stored procedures from Visual FoxPro applications will be examined
in greater detail in Chapter 6, “Extending Remote Views with SQL Pass Through.”

Views
Visual FoxPro databases support views. A view is nothing more than a predefined SQL
SELECT. Views can be used to determine which columns to include in a record set or to
perform multi-table joins. A useful view limiting the number of rows and columns returned in a
record set might look like this:

CREATE SQL VIEW customerorders AS ;


SELECT customers.companyname, orders.orderdate, ;
orderdetails.productid, orderdetails.quantity, orderdetails.unitprice ;
FROM customers JOIN orders ;
ON customers.customerid = orders.customerid ;
JOIN orderdetails ;
ON orders.orderid = orderdetails.orderid

This view performs a three-way join and returns only five columns. This type of view may
be ideal for reporting and can also be used for data entry, as views can be made updatable.
Predefining the three-way join simplifies things for users who may otherwise have to write such
a query themselves. However, the VFP ODBC driver only supports the calling of VFP views
that are not parameterized through SQL pass through, so many of your views are only available
in a VFP application.
Chapter 1: Introduction to Client/Server 13

SQL Server also supports views. Here is a T-SQL definition for the same view
defined previously:

CREATE VIEW customerorders AS


SELECT customers.companyname, orders.orderdate,
orderdetails.productid, orderdetails.quantity, orderdetails.unitprice
FROM customers JOIN orders
ON customers.customerid = orders.customerid
JOIN orderdetails
ON orders.orderid = orderdetails.orderid

As with VFP views, SQL Server views can be used to simplify access to the data and can
be made updatable. However, SQL Server views are available to any application that can
access SQL Server tables, making them more flexible than VFP views. Furthermore, security
rights can be assigned to a view in SQL Server. Many developers and DBAs enforce security
by withholding rights to table objects and allowing access to views instead.

User-defined data types


When you define a table in a VFP database, you are limited to the data types that VFP
supports. In SQL Server, you can create a user-defined type, based on an intrinsic (that is,
built-in to SQL Server) type, and use it when you define a table. For example, the US Fire
Administration defines a set of codes, called 901 Codes, that are used for reporting fire
incidents. When reporting a fire incident, there are literally hundreds of fields that could
contain either 901 Codes or some other type of data. Here’s how we create a user-defined
type for 901 Codes:

IF NOT EXISTS (SELECT * FROM systypes WHERE name = 'udtCode901')


EXEC sp_addtype udtCode901, 'char(4)', 'NULL'

Now we have two ways to define a table:

CREATE TABLE mytable (myfield char(4) NULL)

or:

CREATE TABLE mytable (myfield udtCode901)

What’s the difference? What have you gained by using the user-defined type? When you
look at the schema for this table, you now know not only the structure of the column, but also
something about the nature, or business domain, of the data contained in it because you know it
is designed to hold 901 Codes. Many developers spend a lot more time trying to figure out
existing code than they do writing new code, and anything you can do to document your design
will make somebody else’s (or your own future) work easier.
An important restriction on user-defined data types is that they do not provide inheritance.
In other words, if the US Fire Administration changed the codes from four characters to five,
you cannot simply modify the udtCode901 data type and expect the tables to pick up the
change. Instead, you must first unbind or remove the data type from any columns where it is
14 Client/Server Applications with Visual FoxPro and SQL Server

used, and then make the modification to the data type. After the change is made, you can
re-bind the data type to the appropriate columns.

Replication
Replication is the process of synchronizing multiple copies of a database. A company may
have a database in the headquarters and a copy in each regional office. Each night these
databases can be replicated so the headquarters and each regional office will have a copy of
the latest data.
Visual FoxPro databases have no native support for replication. If you want to replicate a
VFP database, you must write the code to do it yourself. While messages occasionally appear
in online forums declaring how “easy” it is to do this, consider a group of conference attendees
who were asked whether they’ve ever attempted this. Only a small percentage said yes, and of
those, almost all gave up before completing the task. Those who completed it usually said they
wouldn’t want to do it again.
SQL Server has built-in replication, which is handled as an administrative function.
However, just because the replication is built in doesn’t mean it’s easy to get it to work. You
still have to ensure that primary keys uniquely identify records, even across multiple copies of
the database.

Transactions
Visual FoxPro supports limited transaction protection with BEGIN TRANSACTION, END
TRANSACTION and ROLLBACK. SQL Server’s transaction protection is far more robust, as
explained in the “Point-in-time recovery” section earlier in this chapter. In addition, by
exposing its transaction process to Microsoft Distributed Transaction Coordinator (MS DTC),
SQL Server can participate in transactions across databases, servers and even database systems.
With MS DTC, multiple databases on multiple servers running SQL Server, Oracle and/or
MSDE can all participate in the same transaction. Transactions are covered in greater detail in
Chapter 11, “Transactions.”

Scalability
The term scalability is in vogue right now. Microsoft uses the word a lot because it’s hell-bent
on overtaking Sun and Oracle in the enterprise market. Scalability is what the press has often
claimed Windows NT and SQL Server lack in comparison to Oracle running on a Sun server.
When an application is described as scaling well, it is typically meant that it can handle very
high usage.
The term scalability can also be applied to applications written for high-usage
environments that can be used in smaller systems as well. Chapter 7, “Downsizing,” addresses
this downward scalability.
Visual FoxPro is capable of handling very large amounts of data, with the engine
supporting tables up to 2GB. However, because the processing is handled by the workstation,
really large tables usually cannot be handled efficiently.
SQL Server can handle terabytes of data. (A terabyte is a trillion bytes, or 1000GB.) To
get an idea of just how big a terabyte of data really is, consider this: The entire 100+ year
history of every transaction ever performed on the New York Stock Exchange is approximately
500GB, or one-half terabyte!
Chapter 1: Introduction to Client/Server 15

SQL Server is a multi-threaded application that supports multiple processors. On a single-


processor system, multi-threading is the key to preemptive multi-tasking. But multiple threads
don’t actually improve performance, as the processor can only do one thing at a time anyway.
In fact, the overhead of thread switching will actually slow performance slightly.
But with a multi-processor system, multi-threaded applications can improve performance
dramatically, as each thread can be assigned to a processor and thus the threads can run
simultaneously. A single-processor server’s performance can be essentially quadrupled by
replacing it with a quad-processor server. SQL Server 7.0, the current version at the time of this
writing, supports up to 32 processors in the Enterprise version, and up to four processors in the
Standard, Desktop and Small Business Server versions or in MSDE.

Technically MSDE isn’t SQL Server, but from an application


‡ development standpoint, it is the same. For more information on
MSDE, see Chapter 7, “Downsizing.”

Versions of SQL Server prior to 7.0 required Windows NT, but starting with version 7.0,
SQL Server is compatible with Windows 95/98, too. This means that the same database that
can service a terabyte of data on a multi-processor Windows 2000 server can also run fine on a
Windows 98 laptop.

Reliability
Visual FoxPro databases are processed on the local workstation. If 100 users are working on a
table simultaneously, then portions of a table and its index exist in the memory of 100 different
computers. The phrase index corruption causes a knowing nod of the head of most every VFP
developer you’ll ever meet.
Such corruption issues are not a problem with SQL Server. For one thing, the data is only
open in one place, not in multiple copies all over a network. Also, the demands of the
enterprise market are such that client/server databases must be absolutely reliable in high-
volume, mission-critical 24/7 applications.

Advantages of client/server
The advantages of client/server systems over file-server systems follow from the main
differences between the two types of systems. This is a book about client/server development,
so this section will primarily deal with advantages of client/server over file-server. But it
should be pointed out that client/server has disadvantages, too, the primary ones being cost
and complexity.
SQL Server is licensed on a per-user basis, with licenses costing roughly $150 to $200
per user. The license fee may seem quite a leap to a VFP developer who’s used to a freely
distributable database engine (but note that there are many SQL databases that cost
considerably more). In addition, there are administrative costs of a client/server solution. Large
client/server databases require almost constant tuning to optimize performance. A changing
user base requires that security be continually updated. For this and other reasons, SQL Server
systems require a database administrator (DBA). For some systems, a part-time DBA is
sufficient, but other systems require one or more full-time DBAs.
16 Client/Server Applications with Visual FoxPro and SQL Server

There is no way to get around the fact that client/server development is more complex and
more expensive than developing file-server systems. If it weren’t, you wouldn’t need this book.
For any given system, you can expect it to take longer and cost more to implement as a
client/server system. But consider the advantages of client/server systems...

Performance
While it is true that Visual FoxPro has a blazingly fast database engine, its performance can
degrade quickly when size and number of users increase and/or network bandwidth decreases.
SQL Server is also blazingly fast. In fact, with identical moderate-sized databases on
identical computers, SQL Server query performance tends to be slightly better than VFP’s
in most situations.
The real performance difference appears when you reduce the size of the network pipe.
Over a slow network, you’ll almost always get significantly better performance from SQL
Server. And with a really low bandwidth connection, like a modem, VFP can’t even compete.
This is because SQL Server only needs to send requests and results over the wire, while VFP
requires the transfer of everything necessary to process the query.
This performance enhancement has a cost. You must carefully tune your queries with the
size of the result set in mind. The point is that reducing the size of the result set with SQL
Server provides the lion’s share of the performance improvements, particularly with low-
bandwidth connections. That’s because only the result set comes down over the wire. But the
result set itself may be only a small part of what VFP needs to perform a query; therefore,
carefully tuning a query for a small result set may not gain you any performance.

Cost
We mentioned that client/server solutions typically cost more than file-server systems, but
under some circumstances, the reverse may be true. A good example of the cost savings
provided by client/server is in large, widely spread fire departments. Most public agencies
simply cannot afford the infrastructure necessary to support high-speed connections between
widely dispersed fire stations and the database server. A modem and a connection to a local ISP
may be the best they can do. Not only are high-speed connections beyond the budgets of many
departments, but those alternatives simply aren’t available outside of metropolitan areas. And
phone service in rural areas is often of poor enough quality that modem connection speeds are
pretty slow compared to most metropolitan areas. So a high-speed solution isn’t affordable, and
a file-server system with low-speed connections is unworkable. That leaves client/server,
which, while typically more expensive than the file-server solution, ends up being cheaper than
a file-server solution of adequate performance.
Another cost factor is that a great deal of performance benefit can be gained by souping up
the server. It may cost a lot less to get one really high-powered server than to have hundreds of
top-of-the-line workstations. One can tune such a system to put a greater burden on the server
and perform less processing on the workstations. With a file-server system, all processing is
performed on the workstation.

Security
A properly managed client/server database can be almost totally secure, no matter how you
access it. File-server databases, on the other hand, have no security at all other than that
Chapter 1: Introduction to Client/Server 17

provided by the network. Anybody with Visual FoxPro and network access rights can do
anything they want to a Visual FoxPro database, no matter how much effort is put into an
application’s security model.

Scalability
Occasionally one hears about Visual FoxPro systems with VFP databases that handle hundreds
of users and millions of records. But these systems are very unusual and are extremely difficult
to implement. SQL Server can handle them with ease, as it can handle thousands of users and
terabytes of data. A client/server architecture is indicated for any system that must support a
very large number of users.

Summary
In this chapter, you learned about the history of database systems, the features of client/server
databases in general and SQL Server in particular, and the benefits of doing client/server
development. In the next chapter, we’ll take a look at Visual FoxPro as a client/server
applications development tool.
18 Client/Server Applications with Visual FoxPro and SQL Server
Chapter 2: Visual FoxPro for Client/Server Development 19

Chapter 2
Visual FoxPro for
Client/Server Development
After reading Chapter 1, you should have a good understanding of what makes
client/server databases different and why you might want to use them. But the $64,000
question is: Why would you use Visual FoxPro to develop your client/server
applications? This chapter will answer that question for you, the developer, and provide
answers that you can give if your clients ask this question.

More than half of all software developers today use a certain variation of Beginner’s All-
purpose Symbolic Instruction Code (BASIC), according to Microsoft. Why? Partly because
of familiarity, partly because of its versatility, and also partly because popularity begets
popularity. But you might recall your parents telling you, “Just because everyone else does it
doesn’t mean you should do it, too.” This chapter discusses six features that make Visual
FoxPro the best client/server rapid application development tool available today: object-
oriented programming, COM support, built-in client/server support, built-in local data
engine, support for other data-access technologies such as ADO, and Rapid Application
Development (RAD). Many of these features could be, or are, the topics of one or more
books of their own. While each feature is discussed separately, keep in mind that it is the
combination of these features that makes VFP such a great client/server development tool.
Not many development tools can offer the combination of features found in FoxPro. Visual
Basic, for example, offers great COM support and support for ADO and is an excellent RAD
environment, but it isn’t object-oriented, while C++ is a great object-oriented programming
language but lacks built-in client/server or local database support and has never been accused
of being good for RAD.

Object-oriented programming (OOP)


The holy grail of software development has long been code reuse. In a procedural-
programming paradigm, we can achieve a certain level of code reuse by writing libraries of
functions/procedures that can be called as needed. Object-oriented programming, or OOP,
makes code more reusable through the use of objects. In the real world, we are surrounded by
objects, each of which has physical characteristics and many of which can do certain things. In
object-oriented programming terminology, an object’s characteristics are called properties, and
the things an object can do are called methods. A person has certain characteristics, such as
name, address, gender, height, weight, bank balance and so forth. A person can also perform
actions, such as writing down all of his or her characteristics. A programming object that
represents a person would have properties for each of his or her characteristics. The person
object can also have a method that would write down—or print out, if you will—these
properties. Representing real-world entities with programming objects in this way, while an
integral part of object-oriented programming, is called object-based programming. Most
20 Client/Server Applications with Visual FoxPro and SQL Server

popular, modern programming languages such as Visual FoxPro, Visual Basic, C++ and Java
support object-based programming. Object-based programming, through its use of abstract
objects to represent real-world objects, is a step in the right direction for improving code reuse.
There are two features common to all object-based programming languages: encapsulation
and polymorphism.
Encapsulation is the combination of data and code into a single entity. The data, in the
form of memory variables belonging to the object, are called properties. The code, called
methods, gives the object its ability to manipulate its data and to perform other actions. To
represent the first name of a person in a procedural language might require a global memory
variable called gcFirstName. The value of the variable would be set like this:

gcFirstName = "Kim"

An object would encapsulate the first name in a property as part of the object:

oPerson.FirstName = "Kim"

Rather than trying to keep track of numerous memvars for each of numerous persons,
objects require that the programmer maintain memvars only for each object.
The person object can also contain code such as the ability to print out its properties. This
code is known as a method and might be called Print(). To print the characteristics of a person
in a procedural program, you might have a function called PrintPerson() to which you would
pass a parameter for each of that person’s characteristics:

PrintPerson(gcFirstName, gcLastName, gcMiddleInitial, gcHeight, etc.)

While such a function is certainly reusable, it isn’t reused easily. An object, on the other
hand, could have a Print() method that contains all the code necessary to print its properties. It
could be called like this:

oPerson.Print()

Which call would you rather make over and over again? More importantly, which call,
when made over and over again, is likely to contain fewer errors and require less debugging?
Polymorphism is the ability of multiple objects to share the same interface. Having the
same interface means that the properties have the same names and data types and the methods
have the same names, parameters and return types. In the real world, it is clear to everyone
that programmers and salesmen are not the same. But despite that, they have the same interface,
as both programmers and salesmen have names, addresses, height, weight and so on.
Furthermore, all programmers and most salesmen can write down their characteristics, though
they might do it differently. A salesman, for instance, would undoubtedly write a much longer
description than a programmer. So the code in a salesman object’s Print() method would be
different from the code in a programmer’s Print() method, but when it comes to using the
objects, they are both manipulated in the same way.
Object-oriented programming goes one step further. Just as certain real-world entities,
such as children, can inherit the characteristics and abilities of their parents, so too can
Chapter 2: Visual FoxPro for Client/Server Development 21

programming objects in an object-oriented language inherit properties and methods from


other objects. A programming language, to be object-oriented, must support not only
encapsulation and polymorphism, but also inheritance. In an object-based language, a
programmer object and a salesman object would each require their own code. But in an
object-oriented language, they could share as much code as appropriate for their common
functionality. Since each is a person, you could create a person object with all the properties
and methods appropriate for all persons. But since they require different Print() methods, you
would then create a programmer object and a salesman object, each of which inherits all the
characteristics of a person object. Then you’d write a programmer.Print() method that prints
concisely and a salesman.Print() method that is long-winded. In pseudo-code, it might look
something like this:

DEFINE CLASS person AS object


DEFINE CLASS programmer AS person
DEFINE CLASS salesman AS person

While object-based programming certainly enhances code reuse by simplifying the way the
code is used, object-oriented programming allows a quantum leap in code reuse because of
inheritance. Of the four languages in Microsoft’s Visual Studio suite—Visual FoxPro, Visual
C++, Visual J++ and Visual Basic—all but Visual Basic are object-oriented. Visual Basic is
“object-based” because it does not support inheritance.
There are two different types of inheritance: single inheritance and multiple inheritance.
Single inheritance means that a child object can inherit the properties and methods of a single
parent, while multiple inheritance means that a child can inherit from multiple parents. With
multiple inheritance, an object normally inherits all the properties and all the methods from a
parent. If an object were created that inherited from both a programmer and a salesman, it
would have a concise Print() method and a long-winded one. While multiple inheritance offers
versatility, it is also more difficult to manage than single inheritance. C++ fully supports
multiple inheritance. To simplify the management of multiple inheritance, some languages,
such as Java, support single class inheritance and multiple interface inheritance. Visual FoxPro
supports single inheritance.

Support for COM


Object-oriented programming is a huge boon to code reuse. However, OOP is a language-
centric solution. To take advantage of OOP, not only must all your objects be written in the
same programming language, but in most cases you must have the source code. This is not a
problem if the objects are written within your company, but wouldn’t it be nice to take
advantage of objects written by others as well? And while we’re at it, wouldn’t it also be nice if
you could place objects on different machines in an organization in order to spread out resource
utilization, simplify distribution or enhance security? Object orientation doesn’t help in either
of these cases. What is needed is an object-brokering technology such as Microsoft’s
Component Object Model, or COM. COM is the key to building applications that can
communicate with one another, take advantage of components or applications from other
vendors, or be distributed among different computers on a network.
COM, the technology, is used to provide four different features:
22 Client/Server Applications with Visual FoxPro and SQL Server

• ActiveX documents allow a user to work in one application while manipulating a


document using another application.
• ActiveX controls allow developers to include additional functionality, typically in the
form of a user interface, within their applications.
• Automation allows one application to control or communicate with another application
or component.
• Remote Automation, or Distributed COM (DCOM) allows components to reside on
different computers, a strategy known as distributed applications.

Visual FoxPro supports all of these different flavors of COM.


Visual FoxPro can be a client for ActiveX documents. With this technology, a VFP
application can allow a user to manipulate linked or embedded documents (ActiveX documents
were once called OLE, or Object Linking and Embedding, documents), such as a Word
document or Excel spreadsheet, using the menu and toolbars of Word or Excel, but without
leaving the VFP application or starting Word or Excel. Visual FoxPro can also act as a server
for ActiveX documents, allowing a VFP application to be hosted by another application like
Word or Excel.
Visual FoxPro applications can use ActiveX controls to provide users with functionality
that is difficult or impossible to implement using VFP’s native controls, or to take advantage
of work already done by others. A great example of this is the Internet Explorer control. With
a huge investment, you could probably figure out how to create a Web browsing form in
Visual FoxPro. But by simply dropping the free IE Browser ActiveX control on a VFP form,
you can include Web browsing and HTML viewing capabilities in an application without
that huge investment.
While most ActiveX controls provide some sort of user interface, not all do. For several
years, there has been a Crystal Reports ActiveX control that allows developers to print, view
and manipulate reports within an application. That control has no user-interface functionality at
all. Seagate is currently replacing it with an Automation component.
While Visual FoxPro can use ActiveX controls, it cannot create them. You must do so with
other tools such as Visual C++ or Visual Basic.
Automation allows a client application to communicate with or control a server component
or application using COM. When Automation support was first introduced in VFP in version
3.0, VFP could be used as a client to control another server application, such as Word or Excel.
As an example, consider an application written in 1995 that public health officials could use to
keep track of certain health issues in Third World countries. Because of budget limitations, it
was not affordable to write a module to attempt to find best-case investment strategies that
would reduce their national burden of disease. Instead, the solution was to use Automation to
control Excel’s Solver component to try to find the best strategy. The total time invested was
less than one day!
Since version 5.0, Visual FoxPro has also had the ability to create server components.
Now, in addition to being able to control other applications, other applications can control your
application or other components you create. Furthermore, multiple applications you or your
company write can make use of components you write. For example, suppose you had a need
for a component that allowed import/export of data in a national standard ASCII format, and
Chapter 2: Visual FoxPro for Client/Server Development 23

two places within your application needed to use this component, and other applications needed
access to it, too. You could create a COM component to encapsulate that functionality. Since it
was built as a COM component, any application, not just your VFP application, could access
the methods that perform the import and export.
The final piece in the COM puzzle is the ability to distribute Automation components on
multiple computers. The first iteration of this was called Remote Automation, but it has mostly
been supplanted by Distributed COM, or DCOM. Why distribute components on different
computers? For the same reasons you separate the application from the database in client/server
computing. In fact, we consider distributed applications to be merely another step beyond
client/server. You separate client applications from server databases for performance,
scalability, security and cost-effectiveness. All the same reasons apply to distributing
components among different network resources.
Regardless of where Automation servers reside—on the local computer or another
computer on the network—their use in a client/server application turns at least part of the
application from a two-tier design into three-tier one. You might create a three-tier application
using stand-alone components running remotely, or you might create components that can run
in some other Automation host environment such as Microsoft Internet Information Server (IIS)
or Microsoft Transaction Server (MTS). Both IIS and MTS are multi-threaded hosts for
improved scalability. Visual FoxPro allows you to create multi-threaded COM components that
scale well in either host, as well as any others that support apartment-model threading.

Built-in client/server support


Visual FoxPro has support for client/server databases built right in. In fact, VFP supports not
just client/server databases, but any ODBC database. It can even connect to VFP data via
ODBC rather than natively, as discussed in Chapter 7, “Downsizing.” Many other popular
client/server development languages, such as Visual Basic or C++, have no built-in support for
data of any kind and require either calling a database server’s API or using data-access libraries
or components. Database applications written with Visual Basic, for example, would provide
data access with DAO (Data Access Objects), RDO (Remote Data Objects) or ADO (ActiveX
Data Objects), depending on what year it was written and what database was in use.
VFP uses ODBC (Open Database Connectivity) to connect to client/server data. ODBC is
currently the most widely used database connectivity technology. Visual FoxPro lets you use its
ODBC features either with remote views, which are pre-defined, updatable queries, or with
SQL pass through, which allows you to send any supported command to the database server.
Remote views are covered in Chapter 4, “Remote Views,” and SQL pass through is covered in
Chapter 6, “Extending Remote Views with SQL Pass Through.”
Record sets created with either remote views or SQL pass through can be manipulated with
all the traditional xBase data navigation and manipulation commands and functions, and can be
bound directly to controls in VFP forms, just as with native VFP data.

Built-in local data engine


Visual FoxPro has its own built-in local data engine that requires no additional components.
Why would you want a local data engine when writing client/server applications? There are
three good reasons: local lookup data, metadata and disconnected data.
24 Client/Server Applications with Visual FoxPro and SQL Server

Some data never or rarely changes. For example, the states in the United States haven’t
changed since 1959. If data is static and does not require security, there is no particular reason
to store it in a server database and no need to send it back and forth across the wire every time
a user needs it. So why not keep some of this static, frequently used data on the local
workstation? Visual FoxPro’s local data engine allows this data to be stored locally, where it
can be accessed quickly and frequently with no drain on the network or the server. Just in case
this data does change, you can keep a master copy on the server and simply check to see
whether it has changed whenever the application starts up. If it has changed, download it from
the server and refresh the local copy; otherwise, just use the local copy. This topic is covered in
greater detail in Chapter 9, “Some Design Issues for C/S Systems.”
Metadata is data that describes other data. Metadata is usually used by the application,
rather than by the user. Using metadata in combination with data-driven (rather than code-
driven) techniques allows you to create more flexible applications more quickly. If the same or
a similar action must be performed on many different items, you can either hard-code the
particulars of each item, or you can write a generic routine and then create a table with a record
for each item. Adding and deleting items is as simple as adding and deleting records in a table,
and reordering items simply requires changing physical or logical record order. Sometimes this
metadata should be available to users, but other times it’s handy for it to be unavailable.
The VFP local data engine also allows metadata to be joined in queries with client/server
data or other local data, even if the metadata is compiled into the EXE. Consider the example
of an application that uses metadata to represent rules the federal government has imposed on
completion of data entry. Users are also allowed to create their own rules. Since the user’s rules
mustn’t clash with the government’s rules, the user is only allowed to apply rules to columns in
the database for which there are no existing government rules. The SQL Server database is
queried for a list of fields and exclude columns with rules in the metadata table.
A final benefit of VFP’s local data engine for client/server development is for disconnected
record sets, such as data on laptop computers that are taken on the road and are not always
connected to the server. A copy of some or all of the server’s data is stored locally. The system
can work on this data even while the laptop is disconnected from the server. With Visual
FoxPro, you can create disconnected record sets either using the offline view feature or by
copying record sets to tables. If local data weren’t supported, then another data engine, such as
MSDE, would have to be installed and used.

Support for other data-access technologies


While this book concentrates on developing client/server applications using Visual FoxPro’s
built-in data access technology, VFP also supports the use of other data access components
such as ADO. Now you might be wondering why, after all this talk about the advantages of
Visual FoxPro’s built-in database support, are we talking about using something else? For a
traditional two-tier client/server application, there’s no particular reason to worry about ADO.
But in a three-tier application, ADO has one very strong feature you might want to look at:
Data can be passed from one component to another as an object. If a front-end component
needs to pass user-modified data to a data-validation component running in Microsoft
Transaction Server on another machine somewhere, you have to get the data there somehow.
It is very easy to send it in an ADO RecordSet object. The front end could either use VFP’s
built-in data support and then convert it to ADO, or VFP could just use ADO in the first place,
Chapter 2: Visual FoxPro for Client/Server Development 25

whichever works best in the specific situation. ADO is covered in more detail in Chapter 12,
“ActiveX Data Objects.”

Rapid Application Development (RAD)


Rapid Application Development, or RAD, means many different things to many people.
Visual FoxPro is a great RAD tool for two reasons: Prototypes can be created quickly and
turned directly into parts of an application, and VFP is just about the fastest way we know
of to build applications.
Prototypes of application components are tremendously useful in the development process.
Both developers and users get an opportunity to see what forms will look like and get a feel for
the work flow. Working prototypes really help improve a design, particularly when users can
work with them. A key word here is working. Looking at screen shots just doesn’t work as well
as working with actual forms, filling in fields and clicking buttons. Many C++ development
shops like to use Visual Basic or Bongo for creating prototypes. But then they have to throw
away their work and do it over again in C++. Yes, it is still faster than trying to prototype in
C++, but wouldn’t it be nice if you could simply put the finished prototypes into a project
and add code to them? You can do this with Visual FoxPro. Visual FoxPro is as good at
prototyping as either VB or Bongo, but unlike VB or Bongo, the finished prototype is fully
usable Visual FoxPro code.
We can’t quantify Visual FoxPro’s development speed, but Gary relates one experience
that demonstrates just how fast it can be: “My company has two development teams, one
working in VFP and one in Java. When I started on a major project, I was a year behind the
three-man Java team. There was just me on the VFP team to develop a similar application.
They had more than 400 SQL Server stored procedures written already. Since my application
had to work on a VFP back end as well as SQL Server, I couldn’t use any of those stored
procedures and had to reproduce all the functionality on my own. For one very complicated
area, I even attempted to duplicate the object strategy used in the Java version in the hope that
it would save me time, as I had quite a bit of Java experience. After working with it for more
than a month, I threw it out completely and did my own from scratch. Despite that lost month,
my project was completed nine months before the Java team’s! I would love to say this
happened because I’m faster than a speeding bullet, but the truth is that Visual FoxPro made
me look like a star. Considering their manpower and time allotment, Visual FoxPro allowed me
to complete my application nine times faster and produce six times the revenue at about one-
tenth the cost.”

Summary
We believe that Visual FoxPro is the finest client/server rapid application development
tool available today. And considering that Visual Basic isn’t object-oriented (well, not yet—
VB 7 promises some level of object-oriented programming), anyone using VFP for
client/server development has an automatic advantage over more than half of all developers.
Hopefully this chapter has given you some ammunition you might need to support your
choice of development tool.
In the next chapter, you’ll learn the basics of Microsoft SQL Server.
26 Client/Server Applications with Visual FoxPro and SQL Server
Chapter 3: Introduction to SQL Server 7.0 27

Chapter 3
Introduction to SQL Server 7.0
The purpose of this chapter is to explore the fundamentals of SQL Server 7.0. We’ll start
by providing an overview of the installation process. We’ll follow that with a discussion
of databases, the transaction log and how SQL Server organizes storage. The remaining
portion of the chapter is devoted to indexes, locking, stored procedures, enforcing data
integrity and the other features of SQL Server that are specific to the implementation of a
database application.

In November 1998, Microsoft announced SQL Server 7.0, a significant new release of SQL
Server that included important improvements in the areas of ease of use, scalability, reliability
and data warehousing.
Microsoft saw a need for a database management system that eliminated the more common
administrative functions and provided a simple programming model for developers. They
wanted to produce a product that would protect the investments made by their customers. They
also wanted a product that had the capability to grow with the customer—a single product that
would offer great performance in the workgroup or enterprise setting and improve reliability.
Finally, Microsoft wanted to provide its customers with a powerful, yet cost-effective data-
warehousing platform.

Why move to SQL Server?


Microsoft SQL Server 7.0 is a scalable, high-performance client/server database. It was
designed to support high-volume transaction processing (Online Transaction Processing, or
OLTP) systems as well as data warehousing and decision support (Online Analytical
Processing, or OLAP) systems.
As many developers know, Visual FoxPro is also capable of astonishing feats of
performance. Why, then, would a Visual FoxPro developer consider using SQL Server as
the data store for an application? Moreover, why would a customer consider the additional
expense of using a client/server back end instead of a VFP data store?

Capacity
Microsoft Visual FoxPro has a maximum capacity of 2GB per table. Though it happens
infrequently, developers facing this limitation have several choices, such as moving older data
to a separate historical table or partitioning data into separate tables by year, region or other
criteria. These compromised designs generally result in systems that are expensive and difficult
to develop and maintain.
Microsoft SQL Server has an almost unlimited capacity. In fact, if you were to stretch SQL
Server to its theoretical limit, you would have roughly one million TB of storage.
28 Client/Server Applications with Visual FoxPro and SQL Server

Concurrency
Concurrency is the ability of multiple users to access data at the same time. The database
engine must be able to serve those users in a timely manner. SQL Server is capable of handling
hundreds (even thousands, depending on hardware) of simultaneous users.

Robustness
SQL Server has many mechanisms that provide a more robust environment than that provided
by Visual FoxPro:
• A new storage structure replaces the frail double-linked chains used by previous
versions of SQL Server.
• SQL Server’s online backup allows the database to be backed up while users
are actively manipulating the data. SQL Server provides a variety of backup
types, allowing the database administrator to create a backup strategy suited for
any environment.
• The transaction log and Autorecovery process ensure that the database will be
restored to a stable state in the event that the server is shut down unexpectedly, such
as by a power failure. We’ll cover the Autorecovery process later when we discuss
the transaction log.

Security
Visual FoxPro does not support security within the database engine. Developers have
implemented application-based security, but this type of security cannot prevent someone from
using Visual FoxPro, Access or even Excel to access Visual FoxPro data directly.
SQL Server provides multiple layers of security that a user must cross before obtaining
access to the data. The first layer controls access to the server itself. Before a user can access
the server, the user must be authenticated. During the authentication process, SQL Server
determines the identity of the user who is attempting to gain access. SQL Server provides two
authentication methods: SQL Server Authentication and NT Authentication.
• SQL Server Authentication: Using this method, SQL Server requires that the user
provide a piece of information that only the user would know: a password. When the
user logs in to the server, he or she provides a login name and password. SQL Server
searches an internal table, and if it finds the login name and password, it permits the
user to access the server.
• NT Authentication: Using this method, SQL Server relies on the Windows NT
Domain to verify the user. In other words, NT is vouching for the user. When a user
tries to connect using an NT Domain account, SQL Server verifies that the user’s
account or the group that he or she is a member of has been granted or denied
permission to access the server.

Which is better? As usual, the answer is not clear-cut. The advantage of NT Authentication
is that users don’t need to remember another password. The downside is that you must have a
Chapter 3: Introduction to SQL Server 7.0 29

Windows NT network and domain in place at the site. The advantage of SQL Server
Authentication is that a user from a non-Windows NT network can access the server. The
downside is that users must remember yet another password.
Gaining access to the server is only the first step. In order to access data in a database, the
user must be mapped to a database user in that database. This type of security allows the
database administrator to grant the user access to specific parts of the data stored on the server,
as opposed to an all-or-nothing arrangement.
The third and final layer of security is within the database itself. Permissions to access and
manipulate database objects (tables, views, stored procedures and so forth) are granted to
database users. When a user submits a query to SQL Server, SQL Server verifies that the user
has been granted permission to execute the query. If the user does not have the proper
permissions, SQL Server returns an error.

Installation
SQL Server is one of the easiest Microsoft BackOffice products to install. Once you have the
hardware set up and an operating system installed, installing SQL Server is nothing more than
inserting the CD and answering a half-dozen questions. Since SQL Server is self-configuring,
there’s very little, if any, post-installation configuration.

SQL Server editions


The first decision you’ll have to make is which edition of SQL Server to install. SQL Server is
available in three editions: Standard, Enterprise and Small Business Server (SBS). Table 1,
originally published in the SQL Server Books Online, summarizes the capabilities of the
different editions.

Table 1. Comparing the capabilities of the Standard, Enterprise and Small Business
Server editions.

Feature Standard Enterprise SBS


Runs on Microsoft BackOffice Yes No Yes
Small Business Server
Runs on Microsoft Windows Yes No No
NT Server
Runs on Microsoft Windows No Yes No
NT Server, Enterprise Edition
Maximum Database Size Unlimited Unlimited 10GB
Number of SMP CPUs 4 32 4
Extended Memory Support No Yes No
SQL Server Failover Support No Yes No
Supports Microsoft Search Yes Yes Yes
Service, full-text catalogs, and
full-text indexes
Supports Microsoft SQL Server Yes (No user-defined Yes (includes user- No
OLAP Services cube partitions) defined cube partitions)
30 Client/Server Applications with Visual FoxPro and SQL Server

There is one more edition of SQL Server not listed in Table 1. If you are covered by a Per-
Seat licensing agreement for any server edition listed in the table, you may choose to install the
Desktop SQL Server edition on any client computer. It is not sold as a separate product; it’s
included on the CD. The Desktop edition was designed for the “Road Warrior” user (the user
who will be disconnected from the main server but will occasionally need to connect and
synchronize). The Desktop edition can be installed on Microsoft Windows NT Server,
Microsoft Windows NT Workstation and Windows 95/98, but it does not provide support for
the following features:
• Parallel queries
• Fiber-mode scheduling
• Read-ahead scans
• Hash and merge joins
• Failover clusters
• Extended memory addressing
• Full-text catalogs and indexes
• Microsoft SQL Server OLAP Services

For more information regarding installation of SQL Server on Windows 95/98, see the
topic “SQL Server 7.0 on Windows 95/98” in the SQL Server Books Online.

Licensing
During the installation process, you’ll be asked to choose between two licensing modes:
Per-Server and Per-Seat.
With Per-Server licensing, the administrator will specify the maximum number of
concurrent users that can connect to the SQL Server at any one time. Concurrent users should
not be confused with connections. A specific workstation can have multiple connections to the
server, but all of those connections still count as only one user. Per-Server licensing is best if
your organization has a single SQL Server or if you have a large number of users but only a few
of them are connected at any one time.
A Per-Seat license allows a specific workstation to connect to an unlimited number of SQL
Servers. If subsequent SQL Servers are installed, the existing user license will cover the new
servers. The only additional licenses necessary are for the new servers.
Your installation can begin with Per-Server licensing. Then, as your organization grows
and more SQL Servers are required, you can take a one-time, one-way upgrade from Per-Server
licensing to Per-Seat.

You will not need a Client Access License (CAL) for the installation of NT
‡ Server that is hosting SQL Server unless you are using file and/or print
services of the NT Server.
Chapter 3: Introduction to SQL Server 7.0 31

Character sets
A character set (or code page) is the list of 256 characters that make up the legal values for
SQL Server character data types (char, varchar, text). The first 128 printable characters are the
same for all character sets.
During installation, you must specify the character set that SQL Server will use to
represent characters within the server. Your choice of a character set is very important. There is
only one character set for the entire server, and it affects all databases on the server. Changing
the character set requires rebuilding the master database (something like a mini-reinstall),
re-creating all user databases, and reloading the data.
It is also important that the client workstations use a code page that is consistent with the
character set that was installed on the server. If not, two workstations may have different
representations for the same bit pattern that is stored within SQL Server.
Code page 1252 is the default and is compatible with the ANSI character set used by the
Microsoft Windows operating systems.

Sort order
The sort order determines how two characters compare to each other during sorting or logical
comparison operations. During installation, you will specify two sort orders. The first is
specific to the selected character set and will be for non-Unicode character data. The second
sort order will be for Unicode character data.
Sort orders fall into three categories: binary, dictionary order–case-sensitive, and
dictionary order–case-insensitive. With binary sorting, each character is sorted and compared
according to its binary representation. If two characters have the same binary representation,
they’re the same. If not, the lower numerical value is sorted higher in the list. A binary sort
order is the fastest sort order because SQL Server does a simple byte-by-byte comparison.
Also, binary sort orders are always case-sensitive because each character has a unique
binary representation.
With the dictionary sort orders, all the letters are sorted case-insensitive. An a will sort into
the same position as the character A. However, for string comparisons, the case sensitivity of
the sort order determines whether the characters are the same. If you install a dictionary order–
case-insensitive sort order (the default), an A will be treated identically to a (A = a). So the
character strings age, Age and AGE are considered identical. If a case-sensitive sort order is
installed, an A is considered different from a (A ≠ a).

Network libraries
Network libraries identify the method that clients will use to communicate with SQL Server.
Each network library represents a different type of Interprocess Communication (IPC)
mechanism that SQL Server will recognize. Network libraries work in pairs; both the client and
server must have the same library. To make communications more flexible, SQL Server is
capable of listening on multiple IPCs simultaneously.
Some of the network libraries support only one type of physical network protocol, while
others are capable of using multiple protocols. For example, TCP/IP sockets requires that the
TCP/IP protocol be installed, whereas the Named Pipes and Multiprotocol network libraries
will support multiple physical network protocols.
32 Client/Server Applications with Visual FoxPro and SQL Server

Across the IPC, SQL Server and the client exchange queries, result sets, error messages,
and status information (see Figure 1).
During the setup, you will be asked for the network libraries to install. The setup will
default to installing Named Pipes, Multiprotocol and TCP/IP sockets. The client will default to
Named Pipes unless configured otherwise.

Figure 1. The architecture involved in the communications between a client application


and SQL Server.

Databases, database files and the transaction log


Logically a SQL Server database is identical to Visual FoxPro’s. Both store a collection of
tables, indexes, views and stored procedures. Physically, though, they’re implemented very
differently. A SQL Server database is implemented as a collection of operating system files
called “database files.”

Types of databases
Microsoft SQL Server supports two types of databases: user and system. User databases are the
ones you’ll create for your applications. Although SQL Server allows a maximum of 32,767
databases on any one server, the typical server contains only one or two. The other type of
database is the system database, which contains the metadata that controls the operation of the
server. Descriptions follow of the four SQL Server system databases.

master
The master database contains the System Catalog, a collection of tables that stores information
about databases, logins, system configurations, locks and processes. It is the most important of
all the system databases.
Chapter 3: Introduction to SQL Server 7.0 33

model
model is both the name and the function of this system database. It is used as a template
whenever a new user database is created. When a new database is created, SQL Server makes a
copy of the model database and then expands it to the size specified by the user.

tempdb
tempdb is SQL Server’s work space, similar to VFP’s work files. When SQL Server needs a
temporary table for solving a query, sorting or implementing cursors, it creates one in tempdb.
In addition, temporary objects created by a user exist in tempdb. Unlike other databases,
tempdb is reinitialized every time SQL Server is started. Operations within tempdb are logged,
but only to support transaction rollbacks.

msdb
msdb contains the metadata that drives SQL Server Agent. SQL Server Agent is the service
that supports scheduling of periodic activities such as backups, and responds to events that
are posted into NT’s Event log. The information for Jobs, Alerts, Operators, and backup
and restore history is held here. You’ll probably have little use for directly accessing the
msdb database.

Database files
A database is physically stored in a collection of database files. A database file is an operating
system file, created and maintained by SQL Server. When you create a database, you specify a
list of files. You can specify three types of files: primary, secondary and log.
• Primary data files: Every database must have one primary database file. In
addition to storing data, this file contains the database catalog as well as references
to the other files that comprise the database. By convention, the primary file has an
.MDF extension.
• Secondary data files: A database may have additional files, called secondary database
files. You might create secondary files if you were running out of space in the primary
file or you wanted to distribute disk activity across multiple physical drives. By
convention, secondary files have an .NDF extension. Note that secondary files require
special consideration, as they complicate the backup and restore process.
• Log files: Every database must have at least one log file. Log files contain the
transaction log. By convention, log files have an .LDF extension.

When you create a database file, you’ll specify several properties including the physical
file name, the path, the initial size, a growth increment, the maximum file size and the logical
name of the file. You’ll use the logical file name whenever you manipulate the file properties
using the SQL Server Enterprise Manager or Transact-SQL.

Creating a database
There are many ways to create a database. The easiest way is to use either the Create Database
Wizard (see Figure 2) or the Database Properties dialog (see Figure 3) from within the SQL
34 Client/Server Applications with Visual FoxPro and SQL Server

Server Enterprise Manager (SEM). Both are graphical wrappers for the CREATE DATABASE
command that does the actual work.

Figure 2. The third page of the Create Database Wizard.

Figure 3. The General page of the Database Properties dialog when creating
a new database. The key symbol to the left of the first file specifies that that file
is the primary.
Chapter 3: Introduction to SQL Server 7.0 35

An alternative way of creating a database is with the Transact-SQL CREATE DATABASE


statement. To create a database this way, you list the database files that SQL Server should
create. For example, the following CREATE DATABASE command will create a database
named Sales that is made up of two database files: saledat.mdf and salelog.ldf. Note that this
command does not create any tables, indexes or any other database objects—it merely creates
the database, just like Visual FoxPro’s CREATE DATABASE command.

CREATE DATABASE Sales


ON
( NAME = Sales_dat,
FILENAME = 'c:\mssql7\data\saledat.mdf',
SIZE = 10,
MAXSIZE = 50,
FILEGROWTH = 5)
LOG ON
( NAME = 'Sales_log',
FILENAME = 'c:\mssql7\data\salelog.ldf',
SIZE = 5MB,
MAXSIZE = 25MB,
FILEGROWTH = 5MB)

The following five properties are used to describe each file:


• NAME: The logical name of the file. The logical name will be used to reference
the database file after the database has been created. For example, if you needed
to increase the size of a database, you would use the Transact-SQL ALTER
DATABASE command and specify the database file to be resized and what the
new size should be.
• FILENAME: The physical operating system name of the file, including path. Although
it is not required, the convention is to use the appropriate extension (.MDF, .NDF or
.LDF) depending on the type of file.
• SIZE: The initial size of the file. If you’re using the SQL Server Enterprise Manager
to create the database, you must specify the initial size in MB. The Transact-SQL
CREATE DATABASE command accepts MB or KB, but the initial size must be
equal to or greater than the size of the Model database.
• FILEGROWTH: New to SQL Server 7.0 is the ability of a database file to increase in
size automatically if it fills. As the database creator, you must specify the
increment to use for the automatic growth. If you’re using the SQL Server Enterprise
Manager to create the database, you’ll specify the increment in MB or as a percentage
of the current file size. The Transact-SQL CREATE DATABASE command also
accepts KB.
• MAXSIZE: The maximum size to which the database file is allowed to grow. If you
use the SQL Server Enterprise Manager, you’ll be able to specify the maximum size in
MB, or you can specify that there is to be no restriction. The Transact-SQL CREATE
DATABASE command will also accept a maximum size in KB. Omit the MAXSIZE
option if you want unrestricted growth.
36 Client/Server Applications with Visual FoxPro and SQL Server

The transaction log


The transaction log is one of the most important pieces of the database. The transaction log
records all changes made to the database. When a transaction is rolled back, the information to
reverse the changes is taken from the transaction log.
The SQL Server transaction log is a Write-ahead log. All data modifications are first made
to the SQL Server buffer cache. A record of these changes is written to the transaction log, and
then the log is written to disk. Later, the data cache will be flushed in a process called
Checkpointing. Any cache pages that have been changed will be written to disk. In the event of
a system failure, the Autorecovery process will use the information in the log to restore the
database to a stable state.

The buffer cache is a place in memory where SQL Server caches data
‡ pages that have been read from disk. It will also contain the execution plan
for stored procedures.

Each time SQL Server starts, every database goes through a recovery phase. During the
recovery phase, SQL Server examines the transaction log looking for transactions that were
committed but not written to disk. SQL Server will reprocess or roll forward those transactions.
In addition, while scanning the transaction log, SQL Server looks for incomplete transactions
that were written to disk. These transactions will be reversed or rolled back.

How SQL Server allocates storage


When you create a database object such as a table, index or stored procedure, SQL Server must
allocate space to store the object in the database. The basic unit of storage is an extent. There
are two types of extents: mixed and uniform. A mixed extent contains data from multiple
objects. In previous versions of SQL Server, each extent was dedicated to exactly one object. A
very small object would use very little space within the extent. Since the extent contained only
that one object, the unused portion would be wasted. Mixed extents permit more efficient space
utilization for small objects.
A uniform extent is an extent that has been reserved for a specific object. SQL Server will
only allocate a uniform extent for larger objects.
Both types of extents are 64K in length and subdivided into eight 8K pieces called pages.
A page is SQL Server’s basic unit of I/O—all database access is done in pages.
When you create a new database object, SQL Server looks for space in an existing mixed
extent. As the object grows in size, SQL Server continues to allocate space in the mixed extent.
However, if the object grows to a size of nine pages, the ninth page (and all pages thereafter)
will be allocated to a uniform extent. From that point forward, all space allocation for the
object will be to uniform extents.

SQL Server’s row size is limited to roughly 8060 bytes because a row
‡ cannot span multiple pages. The rest of the space on the page is taken up
by a 96-byte page header and some overhead for each row.
Chapter 3: Introduction to SQL Server 7.0 37

Transactions and locking


In SQL Server, transactions serve two purposes. First, a transaction ensures that all the
commands within the transaction are performed as a single unit of work regardless of the
number of tables affected. In the event of an error or system failure, all the modifications that
occurred up until the error or system failure would be reversed.
The second purpose of a transaction is to form a unit of recovery. In the event of a system
failure, when the server comes back on line, the Autorecovery process will roll back
transactions that were partially complete at the time of the failure and were partially written to
disk. Also, the Autorecovery process will roll forward (that is, write to disk) transactions that
were committed but not written to disk.

Implicit and explicit transactions


SQL Server supports two types of transactions: implicit (Autocommit) and explicit.
Implicit transactions are independent statements and commit automatically as long as no
errors are encountered.
Each of the following statements are a part of implicit transactions that operate
independently of each other:

UPDATE account SET balance = balance - 100 WHERE ac_num = 14356

UPDATE account SET balance = balance + 100 WHERE ac_num = 45249

If the first statement succeeds but the second fails, there is no mechanism to reverse the
first statement. To correct this problem, both statements need to be treated as a single unit. The
following example uses an explicit transaction to do just that:

BEGIN TRANSACTION
UPDATE account SET balance = balance - 100 WHERE ac_num = 14356
UPDATE account SET balance = balance + 100 WHERE ac_num = 45249
COMMIT TRANSACTION

The BEGIN TRANSACTION statement starts the explicit transaction. In the event of an
error, it is now possible to undo the work done by either statement by issuing the ROLLBACK
TRANSACTION statement. If no error occurs, the transaction must be completed with the
COMMIT TRANSACTION statement.

Locking
All database management systems employ some type of concurrency control to prevent users
from interfering with each others’ updates. SQL Server, like most, uses locks for this purpose.
The query optimizer will determine the best type of lock for a given situation, and the Lock
Manager will handle acquiring and releasing the locks, managing lock compatibilities, and
detecting and resolving deadlocks.
There are three types of locks: shared locks, exclusive locks and update locks.
38 Client/Server Applications with Visual FoxPro and SQL Server

Shared locks
The optimizer acquires shared locks when reading data in order to prevent one process from
changing data that another process is reading. SQL Server normally releases a shared lock once
it is finished reading the data.

Exclusive locks
The optimizer acquires exclusive locks prior to modifying data. The exclusive lock prevents
two processes from attempting to change the same data simultaneously. It also prevents one
process from reading data that is being changed by another process. Unlike shared locks,
exclusive locks are held until the end of the transaction.

Update locks
An update lock contains aspects of both a shared lock and an exclusive lock and is required
to prevent a special kind of deadlock. To understand the reason for update locks, consider
that most data modification operations actually consist of two phases. In the first phase, SQL
Server finds the data to modify. In the second phase, exclusive locks are acquired and the
data is modified.
SQL Server uses an update lock as it’s searching for the data to change. An update lock is
compatible with existing shared locks but not with other update or exclusive locks. After the
update lock has been applied, no other process may acquire a shared, update or exclusive lock
on the same resource. As soon as all the other locks have been released, SQL Server will
promote (that is, change) the update lock to an exclusive lock, make the change, and then
release the lock when the transaction terminates.

Resources
The optimizer determines which resources to lock based on the query that it is trying to solve.
For example, if the optimizer decides that the best way to solve a query is to do a table scan, it
may acquire a lock on the entire table. SQL Server usually prefers to acquire row locks.
The following is a list of the resources that can be locked:
• Database
• Table
• Extent
• Page
• Index Key
• Row

Deadlocks
If two processes have acquired locks on separate resources but also require a lock on the
resource held by the other process, and neither process will continue until it achieves the lock, a
deadlock condition has occurred. Without intervention, both processes will wait forever.
Chapter 3: Introduction to SQL Server 7.0 39

SQL Server detects deadlock conditions automatically and corrects the problem by
choosing one of the processes and making it the deadlock victim. The deadlock victim will be
the process that will break the deadlock and has the least amount of work for SQL Server to
undo. Deadlocks are covered in detail in Chapter 11, “Transactions.”

Database objects
Each SQL Server database consists of a collection of objects such as tables, indexes and
stored procedures. We’ll begin our discovery of database objects with a discussion of
object names.

SQL Server object names


A database object name consists of four components: the server name, the database name, the
name of the object’s owner (the user that created the database object) and the object name.
Database object names are usually written using the following form:

Server.database.owner.name

The server name, database name and owner name are called qualifiers. When all four
components have been supplied, the name is considered fully qualified. You don’t always
have to specify a fully qualified name when referencing an object—all of the qualifiers are
optional. If the server name is omitted, SQL Server defaults to the name of the current server.
If the database name is omitted, SQL Server defaults to the current database. If the owner
name is omitted, SQL Server attempts to access the object using the user’s username. If that
fails, SQL Server will look for an object with the same name but that is owned by dbo. dbo
(“database owner”) is a special database user that is automatically mapped to the creator of
the database.
The following are examples of valid object references:

nts1.northwind.dbo.products
northwind.dbo.products
northwind..products
dbo.products
mlevy.products
Products

The first example is a fully qualified name. The second example omits the server name.
The third omits the owner name but retains the dot delimiters, as they are required. This
notation tells SQL Server that the owner of the object could be either the current user or dbo.
The fourth example omits both the server name and the database name. The fifth uses a specific
database user. The last example shows the most common way to refer to a database object—
just the name. In this case, SQL Server looks for an object owned by the user making the
connection; if one is not found, SQL Server refers to the object of the same name owned by the
database owner.
A legal object name must follow the Rules for Regular Identifiers as follows (see also the
SQL Server Books Online):
40 Client/Server Applications with Visual FoxPro and SQL Server

1. The first character must be one of the following:

• A letter as defined by the Unicode Standard 2.0. The Unicode definition of


letters includes Latin characters a-z and A-Z, in addition to letter characters
from other languages.
• The _ (underscore), @ (at sign) or # (number sign) symbol.

Note that @ and # have special meaning when they are used as the first character
of the identifier. The @ symbol denotes a local variable, while a # symbol denotes
a temporary object.

2. Subsequent characters can be:

• Letters as defined in the Unicode Standard 2.0.


• Decimal numbers from either Basic Latin or other national scripts.
• The @, $, # or _ symbols.

3. The identifier must not be a Transact-SQL reserved word. SQL Server reserves both
the uppercase and lowercase versions of reserved words.
4. Embedded spaces or special characters are not allowed.

If you require an object name that does not conform to these rules, it’s okay. As long as the
identifier is delimited by square brackets, SQL Server will accept it.

Tables
A table is a collection of rows where each row describes a unique entity (for example,
customers, employees or sales orders). A row is a collection of columns, each of which
represents one attribute of the entity (such as name, address and quantity). In SQL Server, a
table is often referred to as a base table. You will see this term used often, especially during
discussions about views.
Theoretically, a database can have a maximum of 2,147,483,647 tables.

Actually, 2,147,483,647 is the maximum number of database objects within


‡ a database. You would only be able to get that many tables if you had no
other database objects.

As with most database objects, there are two ways to create tables in SQL Server. You can
use the SQL Server Enterprise Manager or the Transact-SQL CREATE TABLE command.
To create a table using the SQL Server Enterprise Manager, follow these steps:
1. From within the SQL Server Enterprise Manager, expand a server group and then
expand the server.
2. Expand Databases and then expand the database that will contain the new table.
Chapter 3: Introduction to SQL Server 7.0 41

3. Right-click on Tables and select New Table…


4. In the Choose Name dialog, enter the name for the new table and click OK.
5. Fill in the grid columns to define the columns of the new table. Column names
must follow the same rules for identifiers that were discussed in the section on
object names.
6. Click the Save button to have SQL Server create the table.

To create a table using Transact-SQL, use the CREATE TABLE command. This
is a simplified example of the CREATE TABLE statement that would create the
northwind..employees table:

CREATE TABLE Employees (


EmployeeID int,
LastName nvarchar (20),
FirstName nvarchar (10),
Title nvarchar (30),
TitleOfCourtesy nvarchar (25),
BirthDate datetime,
HireDate datetime,
Address nvarchar (60),
City nvarchar (15),
Region nvarchar (15),
PostalCode nvarchar (10),
Country nvarchar (15),
HomePhone nvarchar (24),
Extension nvarchar (4),
Photo image,
Notes ntext,
ReportsTo int,
PhotoPath nvarchar (255))

Note that your CREATE TABLE statements will probably be more complex.

Enforcing data integrity


One role of the database designer is to create rules that will prevent bad data from getting into
the database. These rules must also prevent the user or application from corrupting good data.
SQL Server provides several excellent tools to assist in this task.
There are three types of data integrity: entity, domain and referential.
Entity integrity requires that no duplicate rows exist. To accomplish this, one or more
columns are marked as unique. Then the database engine assures that every row has a unique
value in this column (or columns). The unique column or columns are designated as a Primary,
Candidate or Alternate Key.
Domain integrity requires that only valid values exist in each column, including whether or
not the column can accept a NULL. For example, if you create a rule that permits only the
characters “M” and “F” for the Gender column, and an application attempts to place any other
value into the Gender column, the database engine will reject the update attempt.
Referential integrity stems from the fact that relationships between tables are a
fundamental concept of the relational database. These relationships are usually implemented
42 Client/Server Applications with Visual FoxPro and SQL Server

by storing matching key values in the child and parent tables. The value in the unique identifier
(or Key) column of the parent table appears in the Foreign Key field of the child table. For
example, the Northwind database contains two tables: Orders and Order Details. Each row in
the Order Details table contains the unique identifier of one of the rows in the Orders table.
You do not want to allow the application to add an order item without specifying a specific
order because every order item must belong to exactly one order.
There are two data integrity enforcement types: procedural and declarative.
Procedural data integrity enforces rules using procedural code stored in triggers and stored
procedures. Procedural data integrity is often used when the database engine has no other
functionality available (not the case for Microsoft SQL Server) or if the rules are too complex
to be handled by declarative integrity.
Declarative data integrity enforces data integrity by checking the rules that are defined
when the tables are created. Declarative data integrity is enforced before any changes are
actually made, and therefore enjoys a performance advantage over the procedural methods.
Table 2 summarizes the constraints and other options that SQL Server provides to enforce
data integrity. Although not listed here, triggers and stored procedures (procedural code) can be
used to enforce all types of data integrity.

Table 2. Different options for enforcing each type of data integrity.

Integrity type Options


Entity PRIMARY KEY constraint
UNIQUE constraint
IDENTITY property
Domain DEFAULT constraint
FOREIGN KEY constraint
CHECK constraints
NOT NULL
Data types
Referential FOREIGN constraints

A discussion of the options listed in Table 2 follows.

Data types
The most basic tool a database implementer has for enforcing domain integrity is the data type.
The data type of a column specifies more than what type of data the column can contain. When
you assign a data type to a column, you are controlling:
• The nature of the data, such as character, numeric or binary.
• The amount of space reserved for the column. For instance, a char(9) will reserve nine
bytes in the row. An int column has a fixed length of four bytes. A varchar(9) column
is a variable length column. In this case, SQL Server will allow a maximum of nine
bytes for the column, but the actual amount used will be determined by the value
stored in the column.
• For numeric data types only, the precision of a numeric column specifies the
maximum number of digits that a column can contain, not including the decimal point.
Chapter 3: Introduction to SQL Server 7.0 43

For instance, a decimal(7,2) column can contain a maximum of seven digits. A tinyint
has a domain of 0 – 255, so the precision is three (but the amount of space reserved
for storage in the row is one byte).
• Also for numeric data types, you can specify the scale. The scale determines the
maximum number of positions to the right of the decimal point. The scale must be
greater than or equal to zero and less than or equal to the precision (0 <= s <= p).
For a column defined as decimal(7,2), SQL Server reserves two places to the right of
the decimal point.

IDENTITY property
Each table may have one column that is an Identity column, and it must be defined using one
of the numeric data types. When a row is inserted into the table, SQL Server automatically
generates a unique sequential numeric value for the column.
As with many column properties, the IDENTITY property can be specified when the table
is initially created, or it can be applied to an existing table using the Transact-SQL ALTER
TABLE command. When you specify the IDENTITY property, you have the option of
specifying a starting value and an increment value. The starting value is called the seed value,
and it will become the value placed into the first row added to the table. From that point
forward, the values will be incremented by the increment value.
You can use the Transact-SQL @@IDENTITY system function to return the last
IDENTITY value assigned. You have to be careful with this system function: It is scoped to the
connection, and it contains the last IDENTITY value assigned regardless of the table.

‡ You cannot specify an explicit value for the IDENTITY column unless you
enable the IDENTITY_INSERT connection option.

Nullability
The nullability property specifies whether or not the column can accept a NULL value.
It is best to specify the nullability property explicitly for each column. If you don’t, SQL
Server makes the decision for you, based on connection and database settings. (See “ANSI
null defaults” and “SET ANSI_NULL_DFLT_ON” in the SQL Server Books Online for
more information.)

Constraints
SQL Server provides constraints as a mechanism to specify data integrity rules. Designers
prefer constraints to procedural mechanisms (triggers and stored procedures) because
constraints are simpler and therefore less vulnerable to designer error. Constraints also enjoy a
performance advantage over procedural mechanisms because SQL Server checks constraints
before updating the data. Procedural mechanisms (i.e., trigger-based integrity solutions) check
the data later in the process—after the data has been updated.
Constraints can be specified when the table is initially defined or added to an existing
table. If a constraint is added to an existing table, SQL Server checks the constraint against the
existing data. If the constraint fails, SQL Server rejects the constraint. To prevent SQL Server
44 Client/Server Applications with Visual FoxPro and SQL Server

from checking existing data, you can include the WITH NOCHECK option. However, WITH
NOCHECK only affects CHECK and FOREIGN KEY constraints.

PRIMARY KEY constraints


The PRIMARY KEY constraint specifies the column or columns that comprise the unique
identifier (key) of the table. A table can have only one primary key. SQL Server enforces
uniqueness for the entire key by creating a unique index on the column or columns that
comprise the primary key (more on unique indexes later). No column that participates in
the primary key may contain a NULL.
You can specify the primary key using Transact-SQL when you create the table
as follows:

CREATE TABLE employee (


Emp_id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Emp_ssn char(9) NOT NULL,

)

You can add a PRIMARY KEY constraint to an existing table using the ALTER
TABLE command:

ALTER TABLE employee


ADD CONSTRAINT PK_employee PRIMARY KEY (emp_id)

To create a PRIMARY KEY constraint using the SQL Server Enterprise Manager,
see the topic “Creating and Modifying PRIMARY KEY Constraints” in the SQL Server
Books Online.

UNIQUE constraints
A table may have multiple unique identifiers (although it can have only one primary key). For
example, suppose that we have a patient table that contains both patient ID and patient Social
Security number. Both columns are unique. If the patient ID is the primary key, we can still
instruct SQL Server to enforce uniqueness of the Social Security number by declaring a
UNIQUE constraint on the Social Security number column.
Just like the PRIMARY KEY constraint, SQL Server will not allow any two rows to
contain the same value in a column marked with a UNIQUE constraint. However, unlike a
PRIMARY KEY constraint, a UNIQUE constraint can be placed on a nullable column.
Creating a UNIQUE constraint using Transact-SQL is very similar to creating a
PRIMARY KEY constraint. The following example shows how you would add a UNIQUE
constraint to an existing employee table:

ALTER TABLE employee


ADD CONSTRAINT UQ_employee UNIQUE (emp_ssn)

To create a UNIQUE constraint using the SQL Server Enterprise Manager, see the topic
“Creating and Modifying UNIQUE Constraints” in the SQL Server Books Online.
Chapter 3: Introduction to SQL Server 7.0 45

CHECK constraints
CHECK constraints enforce domain integrity and are similar to Visual FoxPro’s Field
and Row rules. To create a CHECK constraint, you specify a logical expression involving
the column you wish to check. This expression must not evaluate to False when attempting
to modify the database; otherwise, SQL Server does not permit the modification to occur.
Unlike Visual FoxPro, SQL Server does not allow user-defined functions inside of
CHECK constraints.
You can create a CHECK constraint when you initially define the table or afterwards when
the table already exists.
The following example creates a CHECK constraint on the Gender column that allows
only the character values “M” and “F”:

CREATE TABLE employee (


Emp_id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Emp_ssn char(9) NOT NULL UNIQUE,
Gender char(1) CHECK (Gender IN ('M', 'F')),

)

To create a CHECK constraint using the SQL Server Enterprise Manager, see the topic
“Creating and Modifying CHECK Constraints” in the SQL Server Books Online.

DEFAULT constraints
A DEFAULT constraint specifies a value to place in a column during an insert if a value was
not supplied explicitly. The value specified in the DEFAULT constraint must be compatible
with the data type for the column. Unlike Visual FoxPro, SQL Server DEFAULT constraints
cannot contain user-defined functions.
Here’s the example from the CHECK constraint, but this time a new column has been
added to capture the date and time that the row was created:

CREATE TABLE employee (


Emp_id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Emp_ssn char(9) NOT NULL UNIQUE,
Gender char(1) CHECK (Gender IN ('M', 'F')),

creat_date datetime DEFAULT (GETDATE())
)

In this example, if a specific value is not supplied for the creat_date column, SQL Server
will execute the Transact-SQL GETDATE() function and automatically insert the current date
and time into the column.
To create a DEFAULT constraint using the SQL Server Enterprise Manager, see the topic
“Creating and Modifying DEFAULT Constraints” in the SQL Server Books Online.

FOREIGN KEY constraints


A FOREIGN KEY constraint serves two purposes. It enforces referential integrity by
checking the relationship between the two tables, and it enforces domain integrity on the
46 Client/Server Applications with Visual FoxPro and SQL Server

foreign key column or columns by allowing only valid primary keys from the parent table.
A FOREIGN KEY constraint usually references the parent’s primary key, but it can also
reference any of the parent’s other unique keys (the column or columns that comprise
UNIQUE constraints).
The following ALTER TABLE command defines a FOREIGN KEY constraint on the
Order Details table that references the Orders table:

ALTER TABLE [Order Details]


ADD CONSTRAINT FK_orders_order_details
FOREIGN KEY (Orderid)
REFERENCES Orders(Orderid)

FK_orders_order_details is the name of the constraint. All constraints require a name.


The name may be specified like in the example, or else SQL Server will create one.
FOREIGN KEY (Orderid) identifies the foreign key in the child table: [Order
Details].OrderID.
REFERENCES Orders(Orderid) specifies the primary key in the parent table to which
the foreign key points. This means that the value in the foreign key ([Order Details].OrderID)
must match the value in the parent primary key (Orders.OrderID).
To create FOREIGN KEY constraints using the SQL Server Enterprise Manager,
see the topic “Creating and Modifying FOREIGN KEY Constraints” in the SQL Server
Books Online.

Indexes
Correctly designed indexes are critically important because of their effect on database
performance. (This is true of both SQL Server and VFP databases.)
When SQL Server searches for a specific row or groups of rows, it can check every row of
the table or it can find an appropriate index and use the information in the index to go directly
to the desired rows. The optimizer will decide which method is less expensive (in terms of page
I/O) and choose it.
In addition to speeding up searches, indexes are used to enforce uniqueness. (See the
earlier discussion of PRIMARY KEY and UNIQUE constraints.)
It is generally a good idea to index the following items:
• Columns within a primary key
• Columns within a foreign key
• Columns that frequently appear in WHERE clauses of queries
• Columns that the application uses frequently as the basis for a sort

You should not create indexes on the following items:


• Columns with few distinct values
• Columns that do not appear in the WHERE clauses of queries
Chapter 3: Introduction to SQL Server 7.0 47

You cannot create an index on columns of the following data types:


• Bit
• Image
• Text

Creating indexes
Indexes can be created by using the Transact-SQL CREATE INDEX command or
the SQL Server Enterprise Manager. The partial syntax for the CREATE INDEX
command is:

CREATE [UNIQUE] [CLUSTERED|NONCLUSTERED] INDEX index_name


ON table(column [,…n])

Here’s an example:

CREATE INDEX orders_employeeid


ON orders(employeeid)

This statement creates an index on the employeeid column of the orders table in the
Northwind database.
You can create an index on more than one column. Such an index is called a
composite index.

CREATE INDEX employee_name


ON employees(lastname, firstname)

In contrast to Visual FoxPro, the columns of a composite index need not be of the same
data type. In addition, SQL Server will probably not use a composite index to solve a query
unless the high-order column (in this case, lastname) appears in the WHERE clause of the
query. SQL Server keeps some statistical information about the distribution of the data
within the index. The statistics are used by the optimizer to estimate how useful the index
would be in solving the query. For a composite index, SQL Server keeps statistics only on
the high-order column.
Indexes are stored internally as a “Balanced Tree” (or “B-Tree” for short). In keeping
with the tree metaphor, different parts of the B-Tree are described using terminology similar to
that of a real tree—except upside down (see Figure 4). The Root provides the starting point for
all index searches. Below the root (remember, this tree is upside-down) are the Intermediate
(also known as non-leaf-level) nodes. Large indexes will probably have multiple levels of
intermediate nodes.
At the very bottom of the index are the Leaf nodes. All the keys at the leaf level of the
index are sorted in ascending order based on the key values. The type of index determines the
content of the Leaf nodes.
48 Client/Server Applications with Visual FoxPro and SQL Server

Figure 4. A simple example of a SQL Server B-Tree.

Types of indexes
SQL Server supports two types of indexes: clustered and non-clustered.
Non-clustered indexes are very similar to Visual FoxPro indexes. The leaf level of a non-
clustered index contains one key for every row in the table. In addition, each key has a pointer
back to the row in the table. This pointer is called a bookmark and has two possible forms
depending on whether or not the table has a clustered index (discussed later). If the table does
not have a clustered index, the bookmark is a Row Identifier (RID), which is the actual row
location in the form of file#:page#:slot#. If the table does have a clustered index, the bookmark
contains the key from the clustered index for that row.
You may have up to 249 non-clustered indexes per table, although it is common to have
far less.
The leaf level of a clustered index is the table itself. The clustered index sits on top of the
table. As a result, the table is physically sorted according to the clustered key. For this reason, a
table can have only one clustered index.
SQL Server forces all clustered keys to be unique. If the index was not explicitly
created as UNIQUE, SQL Server adds a four-byte value to the key to make it unique. All
non-clustered indexes on a clustered table (a table with a clustered index) will use the
clustered key as its bookmark.

Views
A view is a virtual table that has no persistent storage or physical presence. It is actually a
definition of a query. Its contents are defined by the results of the query when the query is
executed against base tables (that is, physical or real tables). The view is dynamically produced
whenever it is referenced. To the application, a view looks and behaves just like a base table.
If views look, smell and act like real tables, why bother to use them instead of their base
tables? A view can be used to limit a user’s access to data in a table. Using a view, we can
make only certain columns or rows available. For example, we may want everyone in the
Chapter 3: Introduction to SQL Server 7.0 49

organization to have access to the name, address and phone number information in the
employee table, but only Human Resources personnel should have access to the salary details.
To support this requirement, we would create a view that exposes only the name, address and
phone number. Everyone in the organization would access the employee data through this view
except, of course, Human Resources personnel.
Another use for views is to simplify a complex join situation within the database. The pubs
sample database contains a table of authors and a table of titles. Since there is a many-to-many
relationship between the two tables, a third table, titleauthor, exists that maps authors to titles.
A view could be created that joins the authors, titles and titleauthor tables so that users are
presented with a simpler data structure to use as the basis for queries and reports.
You create (that is, define) a view using the Transact-SQL CREATE VIEW statement. The
CREATE VIEW statement to create the view discussed previously would look like this:

USE pubs
GO
CREATE VIEW titlesandauthors AS
SELECT
Titles.title_id,
Titles.title,
Authors.au_id,
Authors.au_lname,
Authors.au_fname,
Titleauthor.royaltyper AS RoyaltyPercentage
FROM titles INNER JOIN titleauthor INNER JOIN authors
ON authors.au_id = titleauthor.au_id
ON titles.title_id = titleauthor.title_id

Using the view is just a matter of referring to it as you would any real table:

SELECT *
FROM titlesandauthors
ORDER BY title

Stored procedures
A stored procedure is a collection of Transact-SQL statements that is stored in the database.
Stored procedures are similar to procedures in other languages. They can accept parameters,
call other stored procedures (including recursive calls), and return values and status codes
back to the caller. Unlike procedures in other languages, stored procedures cannot be used
in expressions.
Stored procedures are not permanently “compiled” and stored in the database. The only
thing “stored” about a stored procedure is the source code, which is physically stored in the
SYSCOMMENTS system table. When SQL Server needs to execute a stored procedure, it
looks in the cache to see whether there is a compiled version there. If so, SQL Server reuses
the cached version. If not, SQL Server gets the definition from the SYSCOMMENTS table,
parses it, optimizes it, compiles it and places the resulting execution plan in the cache. The
execution plan remains there until it’s paged out (using a “least recently used” algorithm) or
the server is restarted.
50 Client/Server Applications with Visual FoxPro and SQL Server

Stored procedures are a powerful tool in the database implementer’s toolbox. Stored
procedures can be used to encapsulate logic and share it across applications. They can provide
a performance advantage, by allowing SQL Server to reuse execution plans and skip the parse,
optimize and compile steps.
Like views, stored procedures can also be used to limit or control access to data.
Stored procedures are created with the Transact-SQL CREATE PROCEDURE command:

USE pubs
GO
CREATE PROCEDURE getauthors AS
SELECT * FROM authors

The previous example was relatively simple. It simply returns the entire authors table to the
caller. The next example adds the use of a parameter that specifies a filter condition:

USE pubs
GO
CREATE PROCEDURE getauthor
@author_id varchar(11)
AS
SELECT * FROM authors WHERE au_id = @author_id
IF @@ROWCOUNT > 0 RETURN 0
ELSE RETURN -1

This example takes a parameter, the ID of an author, and returns the row from the authors
table that matches it. There’s also some additional logic to check the number of affected rows
using the @@ROWCOUNT system function (similar to Visual FoxPro’s _TALLY system
variable) and return a status code of zero (0) for success or –1 for no matches.
To execute this stored procedure, you would use the EXECUTE statement:

DECLARE @result int -- we need a variable to catch the returned status


EXECUTE @result = getauthor '172-32-1176' – passing the parameters by position

or

DECLARE @result int variable to catch the returned status


EXECUTE @result = getauthor @author_id = '172-32-1176' – passing by name

Note that the RETURN statement can only return an integer value; therefore, it cannot be
used to return character strings or other data types. Fortunately, returning a result set and the
RETURN statement are not the only ways to get data back from a stored procedure. You can
declare specific parameters as OUTPUT parameters. OUTPUT parameters allow a value to be
returned to the calling routine, similar to passing a parameter by reference in Visual FoxPro.
The following example counts the number of books written by the specified author and returns
the count through an OUTPUT parameter:

USE pubs
GO
CREATE PROCEDURE BookCount
@author_id varchar(11),
Chapter 3: Introduction to SQL Server 7.0 51

@bookcnt int OUTPUT


AS
SELECT @bookcnt = COUNT(*)
FROM titleauthor
WHERE au_id = @author_id

Calling this stored procedure looks like this:

DECLARE @lnBookcnt int


EXECUTE BookCount '172-32-1176', @lnBookcnt OUTPUT

The OUTPUT keyword is required in the stored procedure and when the procedure is
called. If the keyword is omitted in either place, SQL Server returns an error.
Here’s a more complex example of a stored procedure that handles errors and manages
a transaction:

-- Create a small database and the necessary tables


USE master
CREATE DATABASE bank

-- The Funds table is the only table that's needed. We're only going to
-- create the columns required by the TransferFunds stored procedure.
USE bank
CREATE TABLE Funds (
Fund_id int IDENTITY(10000,1) PRIMARY KEY,
Amount money)

GO
CREATE PROCEDURE TransferFunds
@SourceFund int = NULL,
@TargetFund int = NULL,
@amount money = NULL
AS
----------------------
-- Parameter checking
----------------------
IF @SourceFund IS NULL
BEGIN
RAISERROR ('You must supply a source fund', 11, 1)
RETURN 1
END
IF NOT EXISTS (SELECT * FROM funds WHERE fund_id = @SourceFund)
BEGIN
RAISERROR ('Source fund not found', 11, 1)
RETURN 1
END
IF @TargetFund IS NULL
BEGIN
RAISERROR ('You must supply a Target fund', 11, 1)
RETURN 1
END
IF NOT EXISTS (SELECT * FROM funds WHERE fund_id = @TargetFund)
BEGIN
RAISERROR ('Target fund not found', 11, 1)
RETURN 1
END
52 Client/Server Applications with Visual FoxPro and SQL Server

IF @amount IS NULL OR @amount < 0


BEGIN
RAISERROR ('Invalid transfer amount', 11, 1)
RETURN 1
END

---------------------
-- Make the transfer
---------------------
BEGIN TRANSACTION Fund_Transfer
UPDATE funds SET amount = amount - @amount WHERE fund_id = @SourceFund
IF @@ERROR <> 0 GOTO AbortTransfer
UPDATE funds SET amount = amount + @amount WHERE fund_id = @TargetFund
IF @@ERROR <> 0 GOTO AbortTransfer
COMMIT TRANSACTION Fund_Transfer
RETURN 0

AbortTransfer:
ROLLBACK TRANSACTION Fund_Transfer
RETURN 1

Triggers
A trigger is a special type of stored procedure. It is tightly coupled to a table and is executed by
SQL Server in response to specific operations against the table. The most common use of
triggers is to enforce rules that are specified procedurally (that is, in procedural code). Triggers
are also used to cascade deletes and updates to child tables and to maintain denormalized data.
When you create a trigger, you specify which operation or operations (INSERT, UPDATE
and/or DELETE) cause the trigger to fire. New in SQL Server 7.0 is the ability to have multiple
triggers for the same operation. For example, you can have multiple update triggers, where each
trigger essentially “watches” for changes in a specific column.

‡ Microsoft has declared that if multiple triggers are defined for the same
operations, their order of operation is unknown.

Unlike a Visual FoxPro trigger, which fires once for each affected row, a SQL Server
trigger fires once no matter how many rows were affected by the query. The trigger always fires
once—even if the query affected no rows. When you write a trigger, you must consider whether
you need additional code to detect and handle the situation where no rows were affected.
Triggers fire after the data has been modified but before the transaction is committed (in
the case of an implicit transaction). Therefore, a trigger can cause a transaction to be aborted by
issuing a ROLLBACK TRANSACTION from within the trigger. Because the trigger fires after
SQL Server modifies the data, the trigger can view the before and after results of the query.
This is accomplished by using two special tables called Inserted and Deleted. The Inserted
and Deleted tables exist in memory and only for the life of the trigger. These tables are not
visible outside the trigger. (For more information on the Inserted and Deleted tables, see the
following sections in this chapter: “The INSERT operation,” “The DELETE operation” and
“The UPDATE operation.”)
You create a trigger using the Transact-SQL CREATE TRIGGER statement. The partial
syntax is shown here:
Chapter 3: Introduction to SQL Server 7.0 53

CREATE TRIGGER trigger_name


ON table_name
FOR [INSERT][,][UPDATE][,][DELETE]
AS
Sql statements

Here’s a simple example that maintains two audit columns, upd_datetime and upd_user.
First we’ll add the two columns to the products table and then create the trigger:

USE northwind
GO
ALTER TABLE Products ADD
upd_datetime datetime NULL,
upd_user varchar(10) NULL
GO

CREATE TRIGGER product_audit


ON products
FOR UPDATE
AS
UPDATE products
SET upd_datetime = GETDATE(),
Upd_user = USER_NAME()
WHERE productid IN (SELECT productid FROM inserted)

The previous example referred to the Inserted table that was mentioned earlier. Let’s
look at the operation of triggers, and their effects on the Inserted and Deleted tables, in a little
more detail.

The INSERT operation


During an INSERT operation, SQL Server inserts the new rows into the table and places a copy
of them into the special Inserted table. This table permits the trigger to detect new rows and act
upon them. The trigger in the following example updates the products.UnitsInStock column
whenever an item is sold:

CREATE TRIGGER maintain_UnitsInStock


ON [Order Details]
FOR insert
AS
UPDATE Products
SET UnitsInStock = UnitsInStock - (
SELECT quantity
FROM inserted
WHERE inserted.productid = products.productid)
WHERE productid IN (SELECT productid FROM inserted)

This example has a flaw: It will work correctly only if rows are inserted into the Order
Details table one at a time. If one INSERT operation manages to produce two Order Details
rows for the same product, the trigger will generate an error since this specific use of a
subquery allows only one row to be returned. Fortunately, this problem is easy to remedy by
replacing the quantity with the SUM aggregate function. The corrected version follows:
54 Client/Server Applications with Visual FoxPro and SQL Server

CREATE TRIGGER maintain_UnitsInStock


ON [Order Details]
FOR insert
AS
UPDATE Products
SET UnitsInStock = UnitsInStock - (
SELECT SUM(quantity)
FROM inserted
WHERE inserted.productid = products.productid)
WHERE productid IN (SELECT productid FROM inserted)

The DELETE operation


During a DELETE operation, SQL Server removes specified rows from the table and places
them in the special Deleted table. Similar to the special Inserted table, the Deleted table permits
the trigger to detect deleted rows.
The following trigger cascades a delete from the Orders table to the Order Details table:

CREATE TRIGGER remove_orderliness


ON orders
FOR DELETE
AS
DELETE FROM [order details]
WHERE orderid IN (SELECT orderid FROM deleted)

This trigger will never fire if you have a FOREIGN KEY constraint defined between the
Order Details and Orders tables. Remember, constraints are checked before any work is
actually done, and triggers fire after changes are made. Before SQL Server executes the
DELETE on Order, the FOREIGN KEY constraint will force it to check for references in the
Order Details table. Finding any foreign keys referencing the row that would be deleted will
cause SQL Server to return a constraint violation error and kill the statement. In order to
implement cascading deletes, you will not be able to use FOREIGN KEY constraints between
the participating tables.

The UPDATE operation


The INSERT and DELETE operations cause the creation of only one of the special tables, but
the UPDATE operation causes the creation of both the Inserted and Deleted tables. This is
understandable if you think of an UPDATE operation as a delete of an existing row followed
by an insert of the modified row. In the course of an UPDATE operation, SQL Server places a
copy of the affected rows into the Deleted table before making the modifications, and then
places a copy of the modified rows into the Inserted table after making the modification.
Thus, the Deleted table has the before image and the Inserted table has the after image of all
modified rows.
The following example prevents any single UPDATE operation from increasing the price
of a product by more that 25 percent:

CREATE TRIGGER price_watcher


ON products
FOR UPDATE
AS
Chapter 3: Introduction to SQL Server 7.0 55

IF UPDATE(unitprice)
BEGIN
If exists (
SELECT *
FROM inserted INNER JOIN deleted
ON inserted.productid = deleted.productid
WHERE inserted.unitprice/deleted.unitprice > 1.25)
RAISERROR(
'No product price may be increased by more than 25%',
10, 1)
ROLLBACK TRANSACTION
END

Summary
Our goal for this chapter was to give you some basic information about SQL Server and
introduce some fundamental concepts and the various database objects that are used to
implement a database design.
In the next chapter, we’ll look at one way to use Visual FoxPro to access SQL Server.
56 Client/Server Applications with Visual FoxPro and SQL Server
Chapter 4: Remote Views 57

Chapter 4
Remote Views
Visual FoxPro provides two built-in mechanisms for working with client/server data:
remote views and SQL pass through. Other data access methods, such as ADO, can
also be used in Visual FoxPro client/server applications. Each technique has its
advantages and disadvantages. Remote views have the advantages of being extremely
easy to use and being bindable to FoxPro controls. A remote view is a SQL SELECT
statement stored in a Visual FoxPro database container (DBC). Remote views use
Open Database Connectivity (ODBC), a widely accepted data-access API, to access
any ODBC-compliant data.
Although the examples in this book use Microsoft SQL Server on the back end,
remote views can also be used with many other back ends such as Oracle, IBM DB2,
Informix, Sybase, Microsoft Access or Excel, or even Visual FoxPro. With a remote
view, you can work with client/server data almost as if it were local Visual FoxPro
data. In this chapter you will learn how to use this terrific tool as the foundation for a
client/server application. In addition, by learning the fundamentals of remote views,
you will be ready to learn about SQL pass through in Chapter 6, “Extending Remote
Views with SQL Pass Through.”

Connections
Before you can create a remote view, you must specify how the view will connect to the back
end. There are several ways to do this, all of which use ODBC. Therefore, both ODBC itself
and the back-end-specific ODBC driver must be installed and configured on the client machine.
For SQL Server development, ODBC installation is done when installing Visual Studio and/or
SQL Server. For an application you distribute, ODBC installation can be done through the
Visual FoxPro Setup Wizard.
Here is a very simple remote view that returns all rows and columns in the Northwind
database’s Customers table:

CREATE SQL VIEW VCustomers ;


REMOTE CONNECTION Northwind ;
AS SELECT * FROM Customers

The second line specifies which connection VFP will use to execute the SELECT—in this
case, one called Northwind. VFP will look for a connection called Northwind in two places:
first in the list of named connections in the current DBC, and then in the client machine’s list of
ODBC Data Source Names, or DSNs.
Named connections, which are stored in the DBC along with the view definitions, offer
greater flexibility than DSNs. Named connections can use a string that defines the server,
database, login name and password for connecting to the back end. A connect string allows you
to define your connection at run time, rather than requiring a DSN, which is especially useful
58 Client/Server Applications with Visual FoxPro and SQL Server

for applications that connect to multiple servers. Alternately, named connections can use an
existing DSN to define the connection.
The quickest way to get rolling with the VCustomers view is to create a DSN to connect to
the SQL Server Northwind database. To create a DSN, start the ODBC Data Sources Control
Panel applet, which, depending on the version of ODBC installed on your machine, looks
something like Figure 1.

Figure 1. The ODBC Data Source Administrator dialog.

There are three types of DSNs: user, system and file. A user DSN can be used only by one
particular user, while a system DSN can be used by any user on the machine. User and system
DSNs are stored in the registry of the client machine, while file DSNs are stored in text files
and can be located anywhere. We typically use system DSNs because we only have to set up
one DSN per machine rather than one per user. Each type of DSN is set up with its own tab in
the dialog.
To create the Northwind system DSN, click on the System DSN tab in the ODBC Data
Source Administrator dialog, click the Add button, select the SQL Server driver, and then
click the Finish button. Now you will see the Create a New Data Source to SQL Server dialog.
Fill in the fields as shown in Figure 2. The DSN name is what you will use when you create
connections, while the description is optional. If SQL Server is running on the local machine,
be sure to put “(local)” in the Server field rather than the machine name. Using the machine
name, particularly on Windows 95 or 98 machines, will frequently cause the connection to fail,
at least with the driver versions available at the time of this writing.
Chapter 4: Remote Views 59

Figure 2. The Create a New Data Source to SQL Server dialog filled in to create a
connection to the Northwind database on the local machine.

When you click the Next button, ODBC will attempt to locate the specified server; if
successful, you’ll be asked to configure the connection as shown in Figure 3 and Figure 4. If
unsuccessful, you may have a problem with a network connection, or you may not have
permission to access the server. Neither of these situations can be rectified here, but require
checking your network or your SQL Server.

Figure 3. Configuring the Northwind connection to use SQL Server security with the
default sa login.
60 Client/Server Applications with Visual FoxPro and SQL Server

Figure 4. Configuring the Northwind DSN to connect to the Northwind database.

Once you’ve created the connection to the Northwind database on SQL Server, create a
Visual FoxPro database by typing the following in the Command Window:

CREATE DATABASE Northwind

Then create a view, open and browse it:

CREATE SQL VIEW VCustomers ;


REMOTE CONNECTION Northwind ;
AS SELECT * FROM Customers
USE VCustomers
BROWSE

The next step is to use a named connection in the VFP database. A named connection is an
object in the VFP database that contains connection information. Why use a named connection
rather than just a DSN? One major reason is that once you have created one, you can share the
connection among multiple views. Each ODBC connection uses resources on both the server
and the client. They take time to establish, they use memory (about 24K per connection on SQL
Server), and having too many of them can seriously degrade performance in some systems.
Although ODBC has a connection pooling feature that allows unused connections to be reused,
you as a developer cannot control this feature from your application.
If the VCustomers view defined previously, and another view, are opened, two ODBC
connections will be established. To demonstrate this, define a view of the Orders table, then
open it and the VCustomers view:
Chapter 4: Remote Views 61

CREATE SQL VIEW VOrders ;


REMOTE CONNECTION Northwind ;
AS SELECT * FROM Orders
USE VCustomers IN 0
USE VOrders IN 0
?CURSORGETPROP("ConnectHandle", "VCustomers")
?CURSORGETPROP("ConnectHandle", "VOrders")

First of all, note that you will be asked for the user ID and password twice. Also, the last
two lines will display two different numbers. By using a named connection, the same ODBC
connection can be used by both views. To create a named connection and a view that can share
it, open the Northwind DBC and type the following in the Command Window:

CREATE CONNECTION Northwind DATASOURCE Northwind


CREATE SQL VIEW VCustomers ;
REMOTE CONNECTION Northwind SHARE ;
AS SELECT * FROM Customers
DBSETPROP('VCustomers', 'View', 'ShareConnection', .T.)

Note the addition of the SHARE keyword to the view definition and the use of the
DBSETPROP() call to set the ShareConnection property. You must do both of these in order
share the connection. Now when you attempt to open the two views, you will only be asked to
log in once, and the connect handle will be the same for both cursors. Note that the SHARE
keyword (in the CREATE SQL VIEW statement) and the ShareConnection property (in the
DBSETPROP statement) have no effect with views using a DSN rather than a named
connection because a DSN connection cannot be shared by multiple views.
In Visual FoxPro, most environment settings are local to a data session. However, it is
important to note that a named connection can be shared by multiple datasessions.
The Northwind named connection we just created uses the Northwind DSN, but you can
also create named connections that use connect strings. A connect string is a string that
contains the server name, login name, password and database:

CREATE CONNECTION Northwind2 ;


CONNSTRING "DSN=northwind;UID=sa;PWD=;DATABASE=northwind"

Each of the four parts of the connect string is delimited by a semicolon. No quote
marks are used for the individual parameters, though you can optionally surround the entire
connect string in quotes. You will need the quotes, however, if you require spaces within
the string.
Named connections can also be created with VFP’s Connection Designer. Right-click in an
open Database Designer and select Connections to open a list of named connections. Click
New and you will see the Connection Designer, which looks like Figure 5.
62 Client/Server Applications with Visual FoxPro and SQL Server

Figure 5. The Visual FoxPro Connection Designer.

The Connection Designer has controls for setting additional properties for named
connections. Each of these properties can also be set using the DBSETPROP() function. The
Visual FoxPro documentation provides a complete listing of properties under the
DBGETPROP() topic. We’ll cover a few of them here:
• Asynchronous. When set to .F., the default, the connection executes commands
synchronously—that is, the next line of code doesn’t execute until the previous
command on the connection has completed. Asynchronous execution allows
commands on the connection to execute in the background while your code continues
to execute. While asynchronous processing may be useful for certain tasks, generally
you want synchronous execution.
• ConnectTimeout. When set to any value other than 0 (the default), VFP will attempt to
acquire the connection for the number of seconds specified. If Visual FoxPro is unable
to connect within this time period, an error occurs.
• IdleTimeout. This is similar to ConnectTimeout, but it doesn’t actually disconnect
when it times out. It merely deactivates the connection. If the connection is used
for a view, VFP will reactivate the connection when you attempt to use it again.
Use with care, as this can cause unclear errors to occur in your application (e.g.,
“Connectivity error: unable to retrieve specific error information. Driver is probably
out of resources.”).
Chapter 4: Remote Views 63

• DispLogin. This property determines whether and how the user is prompted for login
information. The default setting is 1 (DB_PROMPTCOMPLETE from Foxpro.h),
which will only prompt the user if some required login information is missing.
DB_PROMPTALWAYS, or 2, will cause the user to be prompted each time a
connection is made to the server. DB_PROMPTNEVER, or 3, will not prompt the
user, even if no login information is supplied, allowing the connection to fail. This last
setting is required for using remote views or SQL pass through with Microsoft
Transaction Server (MTS).
• DispWarnings. If this property is set to .T. (the default), then non-trappable ODBC
errors will be displayed to the user in a message box. In an application, you’ll typically
set this to .F. and deal with errors yourself. For more about error handling, see Chapter
8, “Errors and Debugging.”

All connection properties can be set persistently in the database by


‡ using DBSETPROP() or, temporarily, for an open connection, by
using SQLSETPROP(). SQLSETPROP() is covered in greater detail
in Chapter 6, “Extending Remote Views with SQL Pass Through.”

Remote views
In the “Connections” section of this chapter, you learned how to create a basic remote view
of the Northwind Customers table. This view is nothing more than a SQL SELECT that gets
all rows and all columns of the Customers table. If you run this on your development machine
with SQL Server running on the same machine, the query will execute quickly, as there are
only 91 records. But on a network with many users—particularly one with a low-bandwidth
connection, and with thousands of customers in the table—this would be a terribly inefficient
query. A more efficient view can be created by adding a WHERE clause to reduce the number
of rows returned. The following view will only return rows where the customerid column
contains ‘ALFKI’:

CREATE SQL VIEW VCustomers ;


REMOTE CONNECTION Northwind SHARE ;
AS SELECT * FROM Customers ;
WHERE customerid LIKE 'ALFKI'

Now the view will only return a single row, but it can only be used for a single customer.
Visual FoxPro allows you to create parameterized views so that you can define the WHERE
clause when the view is executed.

CREATE SQL VIEW VCustomers ;


REMOTE CONNECTION Northwind SHARE ;
AS SELECT * FROM Customers ;
WHERE customerid LIKE ?cCustomerID
64 Client/Server Applications with Visual FoxPro and SQL Server

The cCustomerID “thing” is a “view parameter,” which represents variable data that can be
filled in at run time. By preceding cCustomerID with the question mark operator, you tell VFP
to create the structure of the view in the DBC but to evaluate a memvar called cCustomerID at
run time. If cCustomerID exists when the view is opened or REQUERY() or REFRESH() is
issued, its value will be substituted into the WHERE clause. If the variable cCustomerID does
not exist, the user will be prompted to supply it, as shown in Figure 6. In an application, you
will usually want to specify the values of parameters yourself rather than allowing VFP to
prompt the user like this.

Figure 6. When opening a parameterized view where the parameter does not already
exist, the user is prompted to provide a parameter.

When creating a client/server application, we usually create one SELECT * view per table
in the database and set the parameter to the primary key. We use these views for data entry and
give the view the same name as the table, preceded by the letter “V.” Sometimes it makes sense
to parameterize these views on some foreign key, but generally using the primary key assures
you of views that bring down only a single record.
When views are used to return a range of records for reporting or lookups, it often makes
sense to use parameters other than the primary key. For example, you might want to find all
customers in London:

CREATE SQL VIEW VCustomersByCity ;


REMOTE CONNECTION Northwind SHARE ;
AS SELECT * FROM Customers ;
WHERE city LIKE ?cCity

When you set the variable cCity to the value “London” and open the VCustomersByCity
view, the result set will be only those customers in London.
You can use wildcards in view parameters, too. To find all customers in any city beginning
with the letter “L,” set cCity to a value of “L%” prior to executing the query.

The syntax for wildcards is not the same in SQL Server as it is in FoxPro.
‡ While the % wildcard is the same in both, you cannot use the * wildcard in
SQL Server. Setting cCity to a value of “L*” would not return customers in
cities beginning with “L,” but rather in cities beginning with “L*.” There probably
aren’t any cities in your data with such a name. Instead, use “L%.”

As with any other SQL SELECT statement, you can specify a field list. SELECT * may be
useful in some situations, but it is often more efficient to specify the field list explicitly in order
Chapter 4: Remote Views 65

to bring down only the columns you need. For example, if you only need the customer ID,
company name, city and country for each customer, a more efficient and equally useful view of
customers would look like this:

CREATE SQL VIEW VCustomersByCity ;


REMOTE CONNECTION Northwind SHARE ;
AS SELECT customerid, companyname, city, country FROM Customers ;
WHERE city LIKE ?cCity

Remote views, like other SQL SELECTs, can also join multiple tables. For example, this
view returns all sales territories and the employees responsible for them:

CREATE SQL VIEW VEmployeeTerritories ;


REMOTE CONNECTION Northwind SHARE ;
AS SELECT territories.territoryid, territories.territorydescription, ;
employees.employeeid, employees.lastname, employees.firstname ;
FROM territories LEFT OUTER JOIN employeeterritories ;
ON territories.territoryid = employeeterritories.territoryid ;
LEFT OUTER JOIN employees ;
ON employeeterritories.employeeid = employees.employeeid

You must be certain to use join syntax that is supported by the back end. VFP and SQL
Server 7.0 are pretty similar, but you may encounter back ends that are different.

When creating views, avoid using * for the field list. The view will work
‡ without errors until a field is added to the base table(s) on the SQL
Server. Since the view was defined with the previous version of the
table, Visual FoxPro does not know about the new field(s), and produces the
error “Base table fields have been changed and no longer match view fields”
when executed.

Updatable views
Remote views can be used to update the underlying data. You can append records in views,
delete existing records and update fields. When you are ready to update the data on the back
end, simply issue a call to TABLEUPDATE(), and VFP takes care of sending the changes to
SQL Server.
Remote views can be made updatable in the View Designer, as shown in Figure 7. At a
minimum, you must select one or more primary key columns, determine which columns to
update, and check the “Send SQL updates” check box. Even if you mark every column as
updatable, updates will not be sent unless you also check this check box. In Figure 7, note that
the primary key column has also been marked as updatable. This is because this column’s value
is set by the user, not by the database. If this were an identity column or if its value were set by
an insert trigger, you would not make this column updatable.
66 Client/Server Applications with Visual FoxPro and SQL Server

Figure 7. The Update Criteria page of the Visual FoxPro View Designer can be used
to make remote views updatable.

The Visual FoxPro View Designer is very limited in its ability to modify
‡ remote views. If you have remote views with joins, it’s likely that you won’t
be able to edit them with the View Designer once you have saved them.
Use the View Designer to create your view and mark the updatable fields if you
wish. But when you need to edit the view again, be prepared to do so in code.
(See Chapter 5, “Upsizing: Moving from File-Server to Client/Server.”)

The Update Criteria tab of the View Designer simply provides a convenient user interface
for setting view and field properties in the DBC. The same properties can be set with
DBSETPROP(). The following two lines of code make the CustomerID field updatable and
mark it as a primary key:

DBSETPROP("VCustomers.CustomerID", "Field", "KeyField", .T.)


DBSETPROP("VCustomers.CustomerID", "Field", "Updatable", .T.)

And this line makes the view updatable:

DBSETPROP("VCustomers", "View", "SendUpdates", .T.)

Setting the KeyField and Updatable properties of fields and the SendUpdates property of
the view is critical to updating data. Many a developer has spent a frustrating session trying to
figure out why data isn’t being saved—when it’s because the view isn’t configured to do so.
Figure 7 shows two other properties that are important for updatable views. The first one,
SQL WHERE clause includes, sets the view’s WhereType property, which determines how
Chapter 4: Remote Views 67

collisions are detected. The four option buttons in the View Designer correspond to the four
numeric values that can be set for the WhereType property. Here’s the code that duplicates the
setting shown in Figure 7:

DBSETPROP("VCustomers", "View", "WhereType", 3)

When you set WhereType to 1 (DB_KEY in Foxpro.h), no collisions will be detected


unless there are changes to the primary key. The data in the table could be changed by another
user prior to making your update, and those changes will be ignored. If your user changes the
same column that another user changed, then the change will be wiped out.
If WhereType is set to 2, or DB_KEYANDUPDATABLE, collisions will be detected by
looking for changes only in the columns that have been marked as updatable. If another user
has changed an updatable column, an error is generated, whether or not your user changed that
column, too.
The default setting for WhereType is 3, or DB_KEYANDMODIFIED. With this setting, a
collision is detected anytime another user has changed a column that your user is changing. If
both users changed the lastname column, an error is generated. But if one user changed
lastname and another changed firstname, there is no collision.
The final option for WhereType is 4, or DB_KEYANDTIMESTAMP. SQL Server has a
special data type called timestamp. Timestamp is an eight-byte binary type, not a time or
datetime. If you have a timestamp column in a table, SQL Server will change the value each
time the row is updated. The value is unique in the table and is incremented for each update.
With DB_KEYANDTIMESTAMP, an error will be generated if another user has made any
change to a row. You can create a timestamp column in a table simply by defining a column
called timestamp. This CREATE TABLE statement will create a table with two columns, one
integer and one timestamp:

CREATE TABLE mytable (mycolumn int, timestamp)

SQL Server will automatically assign a timestamp data type to a column named timestamp.
You can also have timestamp columns with other names, in which case you must explicitly
define the data type.
The final option group on the Update Criteria page of the View Designer, Update using,
sets the view’s UpdateType property. The default is 1, or DB_UPDATE (Update in Place), and
is what you will want to use most of the time. To let SQL Server choose the most appropriate
action, leave this setting on DB_UPDATE; otherwise, you will force SQL Server to always
delete and then insert records, causing extra work and slowing performance. The UpdateType
property is set in code like this:

DBSETPROP("VCustomers", "View", "UpdateType", 1)

Properties for a view are set persistently in the DBC with


‡ DBSETPROP(). They can also be set temporarily for an open
view with CURSORSETPROP().
68 Client/Server Applications with Visual FoxPro and SQL Server

Buffering
Because you can’t work directly with a table in a client/server database, the data is
automatically buffered, unlike with VFP tables, where your changes immediately affect the
tables unless you use buffering. As with VFP, there are two ways to buffer a view: row
buffering and table buffering. Row buffering commits changes for one row at a time, while
table buffering commits multiple rows.

‡ Unlike with VFP tables, views can only be buffered optimistically.

There is a popular misconception among VFP developers that row buffering should be
used to buffer one row at a time and table buffering should be used to buffer multiple rows.
While that is true, there’s one additional difference that may override any other considerations
when trying to decide which scheme to use: Row buffering causes changes to be committed
automatically whenever the record pointer moves, while table buffering requires an explicit
call to TABLEUPDATE() to commit changes. Okay, you say, don’t move the record pointer
until you’re ready to commit changes. Sometimes it isn’t that easy, as some VFP commands
will move the record pointer unintentionally, thus causing the changes to be committed
unintentionally. Also, you may want to wrap changes to multiple tables in a transaction. But
if these changes are happening automatically, you won’t be able to combine them into a
transaction. Therefore, we never use row buffering, even if we’re working with only one row
at a time.
When you open a view, it is row buffered by default. You should change it, either by
setting cursor properties in a form’s data environment or by explicitly setting the buffer mode
with CURSORSETPROP(). The following code will change a view’s buffer mode from row
to table:

CURSORSETPROP("Buffering", 5, "myalias")

It’s preferable to open all of your views and then set table buffering for all open work areas
in a method of your form class. Listing 1 shows the code for this method.

Listing 1. This method loops through all open work areas and sets the buffer mode.

PROCEDURE SetBufferMode(tnBufferMode)
IF PCOUNT() = 0
*-- Default to table buffering
tnBufferMode = 5
ENDIF

LOCAL i, lnCount
LOCAL ARRAY laUsed[1]

*-- Get an array of all open work areas


lnCount = AUSED(laUsed)
FOR i = 1 TO lnCount
*-- Set desired buffer mode for each work area
CURSORSETPROP("Buffering", tnBufferMode, laUsed[i,1])
ENDFOR
ENDPROC
Chapter 4: Remote Views 69

Committing and refreshing buffers


When views are table buffered, managing the state of those buffers is entirely up to the
developer. Changes can be sent to the back end with TABLEUPDATE(). To restore the data in
the buffer to its original state, you can use TABLEREVERT(). The data in the buffer can be
refreshed to reflect its current state on the server with REQUERY(). With both
TABLEUPDATE() and TABLEREVERT(), you determine how many rows in the buffer are
updated or reverted with the first parameter. TRUE updates or reverts all records in the buffer,
while FALSE only updates or reverts the current row.
The following line updates the current row in the current work area:

TABLEUPDATE(.F.)

This line reverts all rows in the current work area:

TABLEREVERT(.T.)

The following line updates all rows in the current work area, but stops when the first
collision is detected:

TABLEUPDATE(.T.)

The following line updates all rows in the current work area, but continues after a collision
is detected and attempts to update all the following rows:

TABLEUPDATE(2)

Note that TABLEUPDATE() can take a logical or a numeric first parameter. Numeric 0 is
equivalent to .F., numeric 1 is equivalent to .T.
Collisions occur when two users are attempting to make changes to the same record. The
WhereType property of a view or cursor, as described previously, determines how collisions
are detected. When SQL Server detects a collision, it generates a non-trappable error. If an
automatic commit is made by moving the record pointer, you are not informed of the change. If
you commit changes manually with the TABLEUPDATE() function, then the return value of
the function will inform you whether the update was successful. Collisions will only be detected
if the second parameter to the TABLEUPDATE() function is FALSE, like this:

TABLEUPDATE(.T., .F.)

If a collision occurred, the TABLEUPDATE() function will return FALSE. If you choose
to do so, you can attempt to resolve the collision and then commit the records again, this time
using TRUE for the second parameter:

TABLEUPDATE(.T., .T.)

This will force the changes to be committed. Collision handling is covered in greater detail
in Chapter 8, “Errors and Debugging.”
70 Client/Server Applications with Visual FoxPro and SQL Server

TABLEREVERT(), unlike TABLEUPDATE(), does not cause any changes to be


sent to the back end. It simply discards changes you have made and returns the buffer to the
state it was in when you executed the query. As with TABLEUPDATE(), you can tell it to
revert all rows by passing TRUE as the first parameter, or revert only the current row by
passing FALSE.
REQUERY() loads a new record set into the buffer from data on the server. If any
uncommitted changes have been made to the buffer, REQUERY() will generate a trappable
error, requiring you to call TABLEUPDATE() or TABLEREVERT() before you can
REQUERY() the view. You may want to use REQUERY() when a user changes the value of a
parameter for a view or when you want to refresh the display with the most current data from
the server.

Other view properties


Many properties of views can be set both persistently in the DBC or temporarily for an open
cursor. All of these properties are covered in the VFP documentation; some of the more
important properties are covered here as well.

FetchAsNeeded and FetchSize


The FetchAsNeeded and FetchSize properties work together to determine how multiple
rows are returned from the server. If a view will return many records, you may want some of
them to be displayed in a grid while VFP continues to fetch the rest of the records in the
background. Or you may just want to bring down a grid-sized set of rows and only fetch more
when the user scrolls to the bottom of the grid. If FetchSize is set to a positive number, VFP
will return control to your application as soon as SQL Server returns that number of rows. This
line of code sets the number of rows returned at once to 100:

DBSETPROP("myview", "view", "FetchSize", 100)

If the view brings down 500 records, control is returned to the program as soon as the first
100 are returned. That means either the next line of code will be executed or control of the user
interface will be returned to the user after 100 records. If the FetchAsNeeded property is set to
.T., then no more records will be fetched until the user attempts to scroll to record 101, at
which time the next 100 rows are retrieved:

DBSETPROP("myview", "view", "FetchAsNeeded", .T.)

But if FetchAsNeeded is set to .F., then the remaining 400 rows will be fetched in the
background. In some cases this works great, as a user can be looking at data right away while
more is being fetched in the background. By the time the user gets through all of the first batch
of data, there ought to be at least another batch waiting. However, if there is more code to
execute, you must be cautious of how these properties are set. In the preceding example, if a
following line of code queries another view on the same connection or attempts a SQL pass
through command on the same connection, you will get a “connection is busy” error. To
prevent such errors, you must set the FetchSize property to –1:
Chapter 4: Remote Views 71

DBSETPROP("myview", "view", "FetchSize", -1)

However, if you set it to –1, then all records must be returned before program execution
can continue. This is another good reason to refine your queries so they produce small
result sets.

If you use GENDBC.PRG, which is distributed with Visual FoxPro, to


‡ create a program to recreate a DBC, remember that GENDBC improperly
generates the line for the FetchSize property twice. If you have to change
that property, be sure you change the correct line, or delete the duplicate line!

MaxRecords
The MaxRecords property determines the maximum number of rows in a result set. The main
reason this property exists is to help prevent a non-specific query from sending a large amount
of data to the local workstation. By setting this property to a reasonable value, you prevent the
users from accidentally filling their hard drives with useless data.
Another good example of using the MaxRecords property would be attempting a TOP n
query on a back end that doesn’t support it, such as SQL Server 6.5. This query generates a
syntax error on SQL Server 6.5:

CREATE SQL VIEW myview ;


REMOTE CONNECTION Northwind SHARE ;
AS SELECT TOP 1 lastname FROM employees ORDER by 1

But the same thing could be achieved by limiting the number of records returned by
the view:

CREATE SQL VIEW myview ;


REMOTE CONNECTION Northwind SHARE ;
AS SELECT lastname FROM employees ORDER by 1
DBSETPROP("myview", "View", "MaxRecords", 1)

FetchMemo
The FetchMemo property determines whether the contents of memo fields are brought down
with every record or just when they are needed. When set to .T., like this:

DBSETPROP("myview", "view", "FetchMemo", .T.)

the memo field contents will always be retrieved. This could mean a lot of unnecessary network
traffic. When this property is set to .F., the memo contents will only be retrieved when you or
the user perform some action to cause a MODIFY MEMO to be issued. These include
explicitly issuing MODIFY MEMO or implicitly doing so in a grid, or by navigating to a
record when the memo is bound to an edit field.
72 Client/Server Applications with Visual FoxPro and SQL Server

Tables
The Tables property contains a list of tables included in the view. This property must be set
correctly for TABLEUPDATE() to succeed. Most of the time it works just fine, but
occasionally there are problems with it. Sometimes it works fine when you set the property in
the DBC, like this:

DBSETPROP("VCustomers", "view", "Tables", "customers")

Other times, the Tables property doesn’t make its way to the cursor when the view is
opened, even though the property exists in the DBC. In this case, TABLEUPDATE() returns
an error because it can’t find the Tables property. The following line of code fixes the
problem reliably:

CURSORSETPROP("Tables", "customers")

Other times, the Tables property makes its way to the cursor and yet VFP still gives an
error that no Tables property can be found. Why this happens we don’t know, but we have
occasionally even done this to make it work:

CURSORSETPROP("Tables", CURSORGETPROP("Tables"))

For what it’s worth, we’ve encountered this with both local and remote views. Setting the
property at run time has always fixed it.

Field properties
Earlier in this chapter, you learned about the KeyField and Updatable properties for view
fields. There are a few other important field properties, too. Unlike with connection and view
properties, these can only be set persistently in the database. There is no field-level equivalent
to SQLSETPROP() or CURSORSETPROP().

DefaultValue
The DefaultValue property allows you to set the default value of a field when a record is added.
Some developers believe that default values and rules should exist on the back end so they are
under the control of the database. Others believe that default values should be done on the front
end to provide immediate feedback to users rather than waiting for a round trip to the server.
Still others believe in doing them in both places. If a default value exists in the database, it
should exist in the view, too. That way, your user—and your code—can see it right away.
When you set up a DefaultValue property, it must be delimited in quotes, like this:

DBSETPROP("myview.myfield", "field", "DefaultValue", ".T. ")

If your default value is a string, it must be delimited with two sets of quotes:

DBSETPROP("myview.myfield", "field", "DefaultValue", "'Bob'")


Chapter 4: Remote Views 73

RuleExpression
The RuleExpression property, like the DefaultValue property, can be used to help validate data
up front, rather than waiting for a failed update. Rule expressions work like rules for fields in
VFP tables, and the entire expression is delimited with quotes. This line will prohibit the
postalcode field of the VCustomers view from accepting the value of ‘123’:

DBSETPROP("VCustomers.postalcode", "field", "RuleExpression", ;


"postalcode != '123'")

UpdateName
The UpdateName property is very important for multi-table joins. If a column of the
same name exists in more than one table in a join, it’s critical that the right field in the
view get to the right column in the table. Include both the table and column name in the
UpdateName property:

DBSETPROP("VCustomers.postalcode", "field", "UpdateName", ;


"customers.postalcode")

DataType
The DataType property is one you may find yourself working with a lot because there isn’t an
exact correspondence between data types in VFP and SQL Server. For example, VFP supports
both date and datetime data types. SQL Server doesn’t support a date type, but it has two
datetime types: datetime and smalldatetime, which differ by storage size and precision.
When you create a remote view, VFP will automatically convert SQL Server data types to
FoxPro data types as shown in Table 1.

Table 1. The default data types Visual FoxPro uses for SQL Server data types.

SQL type VFP type


binary, varbinary Memo
bit Logical
char, varchar Character
datetime, smalldatetime Datetime
decimal Numeric
float Double
image General
int, smallint, tinyint Integer
money, smallmoney Currency
numeric Numeric
sysname Character
text Memo
timestamp Memo

You may use DBSETPROP() to change the data type for any field in a view. For example,
if you would rather work with a date type than a datetime type for a birthdate field, you can
change it like this:
74 Client/Server Applications with Visual FoxPro and SQL Server

DBSETPROP("Vemployees.birthdate", "field", "DataType", "D")

You can specify any valid VFP data type just as you would in a VFP CREATE TABLE
statement, including length and precision, as long as the type you specify makes sense. You
can’t convert a datetime to an integer, for example.

Summary
In this chapter, you learned the basics of creating Visual FoxPro remote views of SQL Server
data. You learned about ODBC DSNs, VFP named connections, connection and view
properties, and data type conversions. In the next chapter, you’ll learn about making the
transition from file-server to client/server applications and how to upsize data from VFP to
SQL Server.
Chapter 5: Upsizing: Moving from File-Server to Client/Server 75

Chapter 5
Upsizing: Moving from
File-Server to Client/Server
Surely you’ve heard the question before. In fact, maybe it is why you are reading this
book. If not, the question is inevitable. “What will it take to go client/server?” Long ago
one of the authors was asked this question so many times that finally he sat down with
VFP 3, upsized a VFP database to SQL Server 6.5 and started to get his hands dirty
doing client/server work. This is a great way to learn client/server development. So roll
up your sleeves, make a copy of a project you are familiar with and get ready to move
from file-server development to client/server. In this chapter you will learn how to upsize
a VFP database to SQL Server, how to use the upsized data, and some tips on more
easily transitioning from file-server development to client/server.

We didn’t invent the term “upsizing” and are not really sure we like it. There is no rule that
says a SQL Server database or application is “bigger” than a FoxPro one. But the term has
become so widely used—and is even used in the names of the “Upsizing Wizards”—that we’ll
stick with it here. When we discuss upsizing, we’re referring to converting a Visual FoxPro
database to a client/server database.
Visual FoxPro ships with two Upsizing Wizards: one for SQL Server and one for Oracle.
As elsewhere in this book, all the examples in this chapter will use Microsoft SQL Server.

Why upsize?
If you have an existing file-server application and database that you wish to convert to
client/server, then upsizing the database may be a good way to start the process. If you have
designed your existing application to use local views, then it is possible that the Upsizing
Wizard will do most of the work necessary to make the conversion. If your application accesses
the tables directly rather than using views, then you have a lot more work to do. Even so,
upsizing is still a good place to start, as it gets you an instant copy of the database in SQL
Server so that you can begin working with it quickly.
On the other hand, if you are developing a new client/server application, it is better to use
the tools designed for SQL Server (such as Enterprise Manager, Visual InterDev or Access
2000) to create a new database directly in SQL Server, rather than develop the database first in
VFP and then upsize. This two-step process, called “prototyping locally and deploying
remotely,” was a more reasonable approach with SQL Server 6.x because the 6.x versions were
very difficult to deploy on laptops or small installations for demos or prototypes. By
comparison, the newer versions of SQL Server (7.x and 2000) can easily be installed and run
on laptops and other small machines. Additionally, with MSDE, a prototype can be deployed
royalty-free without the need for a complete SQL Server installation. (For more on MSDE, see
Chapter 7, “Downsizing”).
The best reason for upsizing a VFP database is to learn to use SQL Server with a database
76 Client/Server Applications with Visual FoxPro and SQL Server

with which you are already familiar. After upsizing a database, you will have a VFP version
and a SQL Server version containing the same data. You can easily work with both in order to
get the feel for SQL Server. Look at the data types in the two databases, learn about SQL
Server indexes, see how referential integrity is handled, and compare database tools. It is often
much easier to work with data you know rather than with simplified, sample databases, such as
Pubs or Northwind, which are included with SQL Server.
Despite the preference for working with familiar data rather than sample databases, we will
use the VFP Tastrade database for the examples in this chapter.

The Upsizing Wizard updates your local database container. Therefore,


‡ before upsizing your database, be sure to make a backup of the entire local
database container (x.DBC, x.DCT, x.DCX) and the tables as well. This will
ensure that you can upsize as many times as you like without affecting any current
applications that need to access the database or its tables.

Using the SQL Server Upsizing Wizard


The SQL Server Upsizing Wizard requires a DSN or a named connection to a SQL Server. We
suggest creating a new DSN before even opening the wizard. For the examples in this chapter,
we have created a System DSN named Tastrade, as shown in Figure 1. For more on creating
DSNs, see Chapter 4, “Remote Views.”

Figure 1. The ODBC Data Source Administrator dialog showing a System DSN for
upsizing the Tastrade database.

The Upsizing Wizard must be able to open the database and all of its tables exclusively, so
before running the wizard, close them and ensure they are not in use by anyone else.
To run the SQL Server Upsizing Wizard, select Tools | Wizards | Upsizing on the Visual
FoxPro menu and then, in the resulting dialog, choose the SQL Server Upsizing Wizard. The
Chapter 5: Upsizing: Moving from File-Server to Client/Server 77

first step of the wizard will ask you which database to upsize. Figure 2 shows the first step with
the Tastrade database selected.

Figure 2. Step 1 of the SQL Server Upsizing Wizard showing the Tastrade database
selected for upsizing.

Note here that if the database is already open, you will be warned that the Upsizing Wizard
requires exclusive access to the database. Also note that the VFP database is referred to
throughout the wizard as the “local” database. Clicking the Next button here will take you to
Step 2, shown in Figure 3.

Figure 3. Step 2 of the SQL Server Upsizing Wizard—selecting an ODBC data source
to use for upsizing and for remote views created by the Upsizing Wizard.
78 Client/Server Applications with Visual FoxPro and SQL Server

Step 3 of the Upsizing Wizard allows you to select which tables to upsize. By default, none
are selected, but you may choose any or all of the tables. In Figure 4, you can see that we have
selected to upsize all tables.

Figure 4. Step 3 of the SQL Server Upsizing Wizard. All the tables in the
Tastrade database have been selected for upsizing by moving them to the right-
hand list box.

Simple stuff so far, right? Well, here’s where it starts to get more complicated. Step 4,
illustrated in Figure 5, allows you to map the data type of each column in each VFP table
to a SQL Server data type, to set up timestamp columns, and to use SQL Server’s identity
feature for columns. You select the table in the Table drop-down list, and all the columns
will be shown in the grid below. Note that any table that has a memo field will be marked for
a timestamp column. The timestamp column will not appear in the grid but will be named
timestamp_column when upsized. You have the option at this time to add timestamp columns
to other tables, or to remove the timestamp columns from those that already have them.
However, if you plan to replicate the database, you must remove the timestamp columns, as
they are not supported in replicated databases.
You can choose to use an identity column for the table so that SQL Server can
automatically create unique integer values suitable for use as primary keys by checking
the Identity column check box. The IDENTITY property is described in greater detail
in Chapter 3, “Introduction to SQL Server 7.0.” Identity columns will be named
identity_column, and the seed value and increment will both be set to one. Before using
identity columns, read Chapter 9, “Some Design Issues for C/S Systems,” which describes
some “gotchas.”
Chapter 5: Upsizing: Moving from File-Server to Client/Server 79

Figure 5. Step 4 of the SQL Server Upsizing Wizard showing the data type mapping
from the VFP table to the SQL Server table. The Timestamp column check box is
checked, specifying that a timestamp column will be created for this table, though it
does not appear in the list of column names.

The final task in Step 4 is to set the data types for each column in each table. In most cases,
the default data types will be adequate, but there may be times when you wish to change them.
For example, the VFP numeric data type is mapped by the Upsizing Wizard to the SQL Server
float data type, rather than numeric, and the VFP character type is always mapped to the SQL
Server char, while there may be times when it is preferable to use varchar. Table 1 displays the
default data type conversions used by the Upsizing Wizard.

Table 1. How the Upsizing Wizard maps Visual FoxPro data types to SQL Server
data types.

VFP data type SQL data type


character char
currency money
date datetime
datetime datetime
double float
float float
general image
integer int
logical bit
memo text
memo binary image
character binary binary
numeric float
80 Client/Server Applications with Visual FoxPro and SQL Server

You should be careful when using varchar data types because they can cause performance
problems that can compromise the scalability of your database. Visual FoxPro uses fixed-length
columns, which allow the database engine to make assumptions about where the columns are
stored, thus permitting fast access. In a table that has variable length columns, the database
engine must store additional bytes to describe the length of each variable length column, which
forces the database engine to work harder to retrieve and write data.
Retrieving data does not cause a major performance problem unless there are many
variable length columns in each row and there are a significant number of rows.
However, performance problems are more likely when writing data. When you insert a
row, SQL Server places it in an existing page that has room for the data. (If there is no such
page, SQL Server creates a new page and inserts the row there.) If you update an existing row
and add data to a variable length column, the row is now longer. If the row no longer fits in the
same page, SQL Server moves the row to a page that has enough space, or creates a new page if
necessary. This activity creates a considerable amount of disk I/O. In a high-transaction
environment, this overhead can cause performance problems.
One particular data type to pay attention to is VFP’s date type. Although there are multiple
datetime types, there is no date type in SQL Server! If you use the FoxPro date type, then
whether you like it or not, the SQL Server type will always be a datetime. If you use VFP
remote views, then the best way to deal with this without changing your code is to use
DBSETPROP() to change the data type of the field view to date, as in the following line of
code that changes the order_date field’s data type to date:

DBSETPROP('SALES DETAIL.order_date', 'Field', 'DataType', "D")

In Step 5 of the Upsizing Wizard, you select the database in which the upsized tables will
reside. For some reason, the default is to dump everything into the master database. Be very
careful not to click past this step without changing this. You definitely do not want to put your
data in the master database. In Figure 6, the New option has been selected, and the new
database will be named Tastrade.
Steps 6 and 7 are skipped when you run the wizard against SQL Server 7 databases, as
they only apply to version 6.5. Step 6 enables you to select the database device for a new
database, but devices have disappeared in SQL Server 7. Similarly, Step 7 is used to specify the
device for the transaction log of the new database.
Step 8 is a big one, with lots of important decisions to make. Here you will specify which
table attributes to upsize, whether to upsize data or just structure, how to deal with referential
integrity, and what changes to make to the local DBC. The Upsizing Wizard upsizes indexes,
defaults, relationships, relational integrity and validation rules.
Note that the Upsizing Wizard cannot upsize triggers and stored procedures, which contain
procedural code, because SQL Server does not support VFP procedural code.
Also, because of differences in the way expressions are handled in VFP and SQL Server,
some of the features of your VFP database will not be upsized. The effects of these differences
are described in the following sections.
Chapter 5: Upsizing: Moving from File-Server to Client/Server 81

Figure 6. Step 5 of the SQL Server Upsizing Wizard. A new database named Tastrade
will be created rather than the default, which is to dump all the new tables into the
master database. Don’t overlook this step!

Indexes
Although there are many similarities between indexes in VFP and SQL Server (in both
products, indexes can be used for primary keys, to enforce non-primary-key uniqueness
and to optimize queries), there are numerous differences between indexes in the two
products. Table 2 shows how the Upsizing Wizard maps VFP index types to SQL Server
index types.

Table 2. Visual FoxPro index types and how the Upsizing Wizard maps them to SQL
Server index types.

VFP index SQL Server index


Primary Clustered
Candidate Unique
Unique Non-clustered
Regular Non-clustered

In SQL Server, it is not common to build a clustered index on your


‡ primary key. However, the Upsizing Wizard moves all Visual FoxPro
primary key indexes to SQL Server as clustered indexes! Be aware of this
situation, and be ready to modify your indexes before going into production with
this upsized database.

SQL Server indexes are covered in greater detail in Chapter 3, “Introduction to SQL
Server 7.0,” but here are a few things about them to keep in mind regarding upsizing.
82 Client/Server Applications with Visual FoxPro and SQL Server

First, SQL Server indexes are on columns only, never on expressions or UDFs, and
indexes are always ascending. Therefore, any indexes that contain expressions such as NOT or
UDFs or are descending will not be correctly upsized. Only the column names of the indexes
will be upsized, not the expressions, which is probably not what you expected.
Second, the physical order of a table is determined by the clustered index, only one of
which, for obvious reasons, is allowed per table. This is different from a VFP primary index in
that the physical order of a VFP table is not changed by the value of the column in the key for
that index, but it is changed in a SQL Server table. If a clustered index exists on a table, then a
SELECT with no ORDER BY clause will return records in the clustered index order; if no
clustered index exists, then results are returned in an unpredictable order.
Finally, there are no SQL Server indexes similar to VFP’s so-called UNIQUE indexes.
Though these will be upsized to non-clustered indexes in SQL Server, there is no uniqueness
to them.
By default, the VFP tag names will be retained for the index names when upsized.
However, if a tag name is a reserved word in SQL Server, then an underscore will be appended
to the end of the name. For example, a tag named “level” would become an index named
“level_” after upsizing.

Defaults
Defaults aren’t handled quite the same way in SQL Server and Visual FoxPro. In a VFP
database, a default expression is assigned individually to a field. In SQL Server, defaults are
handled either with constraints or with expressions that are created and then bound to a field. In
this way, fewer expressions need to be created, as it is likely that multiple fields will share a
default expression.
The Upsizing Wizard will create a SQL Server default for every field with a default
expression unless the default expression is zero. If one or more fields have a zero default, then
the Upsizing Wizard will create a default called UW_ZeroDefault and will bind it to each field
that needs it. This default is also used for all VFP logical fields, which are upsized to SQL
Server’s bit data type and bound to the UW_ZeroDefault default unless the logical field in the
local database has a default setting the value to .T., in which case a default is created that sets
the value to 1.
The Upsizing Wizard names defaults by using the prefix Dflt_ plus the table name
and field name separated by an underscore. Therefore, a default for detail.order_date would
be named Dflt_detail_order_date. Names longer than SQL Server’s limit of 30 characters
are truncated.
Expression mapping between VFP and SQL Server is illustrated in Table 3. The following
expressions are the same in both VFP and SQL Server and require no conversion by the
Upsizing Wizard:
• CEILING( )
• LOG( )
• LOWER( )
• LTRIM( )
Chapter 5: Upsizing: Moving from File-Server to Client/Server 83

• RIGHT( )
• RTRIM( )
• SOUNDEX( )
• SPACE( )
• STR( )
• STUFF( )
• UPPER( )

Table 3. Mapping of Visual FoxPro expressions to SQL Server expressions by the


Upsizing Wizard.

VFP expression SQL Server expression


.T. 1
.F. 0
# <>
.AND. AND
.NOT. NOT
.NULL. NULL
.OR. OR
=< <=
=> >=
ASC( ) ASCII( )
AT( ) CHARINDEX( )
CDOW( ) DATENAME(dw, ...)
CHR( ) CHAR( )
CMONTH( ) DATENAME(mm, ...)
CTOD( ) CONVERT(datetime, ...)
CTOT( ) CONVERT(datetime, ...)
DATE( ) GETDATE( )
DATETIME( ) GETDATE( )
DAY( ) DATEPART(dd, ...)
DOW( ) DATEPART(dw, ...)
DTOC( ) CONVERT(varchar, ...)
DTOR( ) RADIANS( )
DTOT( ) CONVERT(datetime, ...)
HOUR( ) DATEPART(hh, ...)
LIKE( ) PATINDEX( )
MINUTE( ) DATEPART(mi, ...)
MONTH( ) DATEPART(mm, ...)
MTON( ) CONVERT(money, ...)
NTOM( ) CONVERT(float, ...)
RTOD( ) DEGREES( )
SUBSTR( ) SUBSTRING( )
TTOC( ) CONVERT(char, ...)
TTOD( ) CONVERT(datetime, ...)
YEAR( ) DATEPART(yy, ...)
84 Client/Server Applications with Visual FoxPro and SQL Server

Relationships
SQL Server 7 has two different ways of handling relationships and referential integrity:
triggers and declarative referential integrity constraints. The Upsizing Wizard can upsize the
referential integrity constraints from a VFP database using either triggers or declarative
referential integrity.
Figure 7 shows the default settings for upsizing, which is to not use declarative referential
integrity. If you choose this option, then the Upsizing Wizard will write triggers that duplicate
the functionality of referential integrity in Visual FoxPro. Table 4 shows how VFP referential
integrity is upsized when you choose this option.

Figure 7. Step 8 of the SQL Server Upsizing Wizard.

Table 4. Mapping by the SQL Server Upsizing Wizard of Visual FoxPro referential
integrity to SQL Server triggers.

Integrity Constraint SQL Server trigger


DELETE Cascade Cascade DELETE trigger
DELETE Restrict Restrict DELETE trigger
INSERT Restrict Restrict INSERT trigger
UPDATE Cascade Cascade UPDATE trigger
UPDATE Restrict Restrict UPDATE trigger

When the Upsizing Wizard creates triggers for referential integrity, it names them by using
the prefix Trig, followed by the letter D for DELETE triggers, I for INSERT triggers or U for
Chapter 5: Upsizing: Moving from File-Server to Client/Server 85

UPDATE triggers, followed by an underscore. The table name follows the underscore. So a
DELETE trigger on the employee table would be named TrigD_Employee.
If you check the Use declarative RI check box in Step 8, then no triggers will be created.
Instead, the Upsizing Wizard will use declarative referential integrity. Declarative referential
integrity, discussed in Chapters 1 and 3, prevents any changes from occurring that would
break the reference and is equivalent to Restrict constraints in VFP. Declarative referential
integrity is a part of the schema rather than a trigger. Without the option of declarative
referential integrity, most SQL Server 7 DBAs would prefer creating a stored procedure for
deleting child records rather than relying on triggers for cascading deletes because the triggers
can create performance issues.

Validation rules
The Upsizing Wizard treats rules much like defaults—a rule object is created and then bound to
a column or data type. This reduces the number of rules if the same rule is required for multiple
columns or types. An example might be the following rule, which prevents entry of values less
than 1,000 or greater than 100,000:

CREATE RULE myrange


AS
@range >= 1000 AND @range <= 100000

Then the rule can be bound to a column by using a system stored procedure called
sp_bindrule:

EXEC sp_bindrule 'myrange', 'mytable.myfield'

The SQL Server Upsizing Wizard does not upsize VFP rules into SQL Server rules,
though. Instead, it writes a trigger for the column with the rule, and the trigger calls a stored
procedure that enforces the rule. For example, in the Tastrade database, the rule for the
order.deliver_by column is converted to the following stored procedure:

CREATE PROCEDURE vrf_orders_deliver_by @status char(10) output AS

IF @status='Failed'
RETURN

IF (SELECT Count(*) FROM orders


WHERE NOT (deliver_by>=order_date)) > 0
BEGIN
RAISERROR 44444 'Cannot be earlier than Order Date'
SELECT @status='Failed'
END
ELSE
BEGIN
SELECT @status='Succeeded'
END

The Upsizing Wizard also creates update and insert triggers TrigI_Orders and
TrugU_orders for the orders table that, in turn, call the vrf_orders_deliver_by stored procedure
86 Client/Server Applications with Visual FoxPro and SQL Server

and pass the appropriate parameter. Although this is a rather unusual way to implement rules, it
certainly works.
The naming convention for the triggers is the same as that defined previously for triggers
created for referential integrity. For the stored procedures for field rules, the prefix vrf_
(validation rule field) is concatenated with the table name and column name, separated by
underscores. Table validation rules begin with vrt_ (validation rule table), followed by the
table name.

Changes made locally


Step 8 also allows you to specify what will happen locally—that is, on the VFP side. The
options (Create upsizing report, Redirect views to remote data, Create remote views on
tables, and Save password with views) are covered in the “Finished at last...” section later in
this chapter.
Step 9 of the Upsizing Wizard allows you to finish the job of upsizing (see Figure 8). You
can simply perform the upsize, which will execute all the choices you have selected in previous
steps, by checking Upsize. After clicking Finish, the changes will actually be made on the
server. If you check Save generated SQL, then the changes will not be made on the server, but
all the SQL code required to create the changes will be written into one of the report tables
described in the next section. The Upsize and save generated SQL option does both.

Figure 8. Step 9 of the SQL Server Upsizing Wizard.


Chapter 5: Upsizing: Moving from File-Server to Client/Server 87

We recommend that you not use the Upsize-only option, because this
‡ option does not provide any opportunity to “tweak” the upsizing process.
Further, the upsize can take a considerable amount of time to execute if
you have not chosen the “structure only, no data” option.

Finished at last? Modifying the results of the Upsizing Wizard


Well, the Upsizing Wizard may be finished at last, but you are not! Now you need to find out
what upsized and what didn’t, what happened in your local DBC and what to do about both.
The Upsizing Wizard creates a project called Report.pjx that contains several tables and
reports for each of those tables. Figure 9 shows the project and all the files contained in it after
upsizing the Tastrade database. Do not ignore this project, which the Upsizing Wizard
automatically creates and opens for you. Table 5 shows each of these reports.

Figure 9. The VFP Project Manager showing the project created by the SQL Server
Upsizing Wizard and the tables and reports associated with it.
88 Client/Server Applications with Visual FoxPro and SQL Server

Table 5. The reports created by the SQL Server Upsizing Wizard.

Report Contents
rpterrs1.frx Errors
rptfiel1.frx Fields
rptinde1.frx Indexes
rptrels1.frx Relations
rpttabl1.frx Tables
rptview1.frx Views

Open the errors report and print it immediately—we guarantee that you will need it. The
fields report can be huge, as it details every field in the database, along with pre- and post-
conversion data types, defaults, rules and so forth. Even with the miniature Tastrade database,
this report runs 24 pages. If some field-level object did not correctly upsize, then the report will
note the errors associated with it. We believe that it is better to start with the errors report in the
first place. If something in the errors report needs further explanation, then open the fields
report for preview and work your way down to the field in question.
It is interesting to read the errors report, as it gives you a good feel for what kinds of things
don’t upsize well. That, in turn, will help you learn more about SQL Server. A good example of
this in the Tastrade database is the failure of many field validation rules to upsize. The most
common reason in this case is that many rules use the VFP EMPTY() function, which cannot
be upsized to SQL Server. Another good example is the orders.order_number default, which
calls a UDF called newid(). The Upsizing Wizard is unaware that the newid() function already
exists in SQL Server 7, and it attempts to upsize an illegal call to that function.
You also may find many errors in views. Typically, these errors are caused by differences
in SQL syntax between SQL Server and VFP and are relatively easy to fix.
The tables in the project are used for creating the reports, with the exception of
sql_uw.dbf. This table contains one row with one column, a memo field containing the T-SQL
script generated by the Upsizing Wizard. This table will exist only if you chose to save the
generated SQL in the last page of the wizard. This script can be quite useful in helping you
learn SQL Server. The script can even be used for deploying a system. See Chapter 10,
“Application Distribution and Managing Updates,” for more information on using scripts to
deploy databases.

The local database


Hopefully you made a copy of the local database, because after upsizing it may have been
changed dramatically. All the VFP tables are still part of the database, as are the original views,
the upsized views and single-table views of each upsized table. However, if you selected the
appropriate options, your local views now point to remote data, new remote views were created
for the upsized tables, and a new connection exists (called “Upsize”). Therefore, if you are
planning to deploy any of this, you have some cleaning up to do first. If you want, you can open
the DBC and clean up the DBC manually. Our preference is to use code, because this approach
is easier and more reliable.
Chapter 5: Upsizing: Moving from File-Server to Client/Server 89

If you have never used GenDBC, then this is a good time to become familiar with it.
GenDBC is a program that is distributed with VFP and can be found in the HOME() +
“tools\gendbc” directory. It creates a PRG that can recreate the structure of a VFP database.
To run it, simply execute the following code in the Command Window:

DO (HOME() + "tools\gendbc\gendbc.prg")

We use GenDBC a lot, and not just because we would rather work with code for views
than the visual tools. Many times, you will have to maintain your views through code, as the
VFP View Designer simply will not allow you to edit many types of complex remote views.
You can visually create the views that you want (and we certainly recommend doing so where
possible), but it is possible that when you try to edit it, you will receive an error.
GenDBC to the rescue! When you run GenDBC, every database object is recreated in
code. It will create a function for each view, table and relation, and another function to generate
the local referential integrity code. Table 6 lists the functions created and their use. If, for
example, your DBC has a view named Category, then a function will be generated named
MakeView_Category.

Table 6. The functions generated by GenDBC.

Function Purpose
MakeView_ Recreate local and remote view.
MakeTable_ Recreate VFP table.
MakeRelation_ Recreate a VFP relation.
MakeRI_ Recreate the relational-integrity code.

At the top of the generated program is a set of calls to each of the functions generated.
Figure 10 shows the VFP Procedures and Functions dialog for the PRG generated by
GenDBC.prg for the VFP database after upsizing Tastrade. Take a close look at the first three.
Table 7 presents descriptions of each of these three functions.

Table 7. The GenDBC-generated functions for Category.

Function Purpose
MakeView_CATEGORY Remote view of the SQL Server table, created by
the Create remote views on tables option in the
Upsizing Wizard.
MakeView_CATEGORY_LISTING Existing local view, redirected to the SQL Server tables,
rather than the VFP tables.
MakeView_CATEGORY_LISTING_LOCAL Existing local view, renamed by appending _LOCAL to
the end.
90 Client/Server Applications with Visual FoxPro and SQL Server

Figure 10. The Procedures and Functions dialog for the VFP editor showing some of
the procedures created by GenDBC for the upsized Tastrade.DBC. Note the grouping
of views in threes: category was created by the Upsizing Wizard from the category
table; category_listing was created by the Upsizing Wizard by converting the local
view; and category_listing_local is the renamed local view.

What to do with these views? Look at the last view in Table 7—though it is helpful to have
the “old” local view available to compare the results to the “new” remote view, you won’t be
deploying the local database. Therefore, you’ll be deleting this one eventually. Regardless, here
is the code for it:

CREATE SQL VIEW "CATEGORY LISTING_LOCAL" ;


AS SELECT category.category_name, category.description, category.picture
FROM tastrade!category

The first view in the list certainly isn’t needed either. However, if you will be reworking
the system, you might want to modify this view a bit and keep it around. You will need to
modify this view because, in its current form, it is not parameterized and will return all records
in the table. Here is the code for the non-parameterized view created by the Upsizing Wizard:

CREATE SQL VIEW "CATEGORY" ;


REMOTE CONNECT "Upsize" ;
AS SELECT * FROM category

This view can be quite useful if it is parameterized to return only a single record:

CREATE SQL VIEW "CATEGORY" ;


REMOTE CONNECT "Upsize" ;
Chapter 5: Upsizing: Moving from File-Server to Client/Server 91

AS SELECT * FROM category WHERE category_id LIKE ?cCategory_id

The second view, CATEGORY LISTING, is the one that most closely matches the original
local view. Here is the original local view:

CREATE SQL VIEW "CATEGORY LISTING" ;


AS SELECT category.category_name, category.description, category.picture ;
FROM tastrade!category

Except for the name, you can see that it is identical to the original local view. Here is the
new view that has been redirected to the remote tables:

CREATE SQL VIEW "CATEGORY LISTING" ;


REMOTE CONNECT "Upsize" ;
AS select category.category_name, category.description, category.picture ;
FROM category

In Figure 10, you can see this pattern of three views per table repeated over and over again.
In Figure 11 you can see that all the local tables are still there, too, but that they’ve been
renamed by appending _LOCAL to the table name.

Figure 11. The Procedures and Functions dialog for the VFP editor showing local
table generation procedures created by GenDBC for the upsized Tastrade.DBC.

So, what is the best way to deal with all this? We suggest opening the PRG and
commenting out the function calls you don’t need. First get rid of all MakeTable, MakeRelation
and MakeRI calls. You aren’t going to be using local data, so why bother keeping those
92 Client/Server Applications with Visual FoxPro and SQL Server

around? Also, comment out all the MakeView_…_LOCAL calls because, again, there is no
local data.
That leaves the MakeView calls that create the remote views. Here we recommend being
selective. Go through each one to decide whether you can use the view. If you’re not sure, keep
the view because it is easier to delete a view later than to recreate it.
Once you’ve commented out all the unnecessary calls, move the PRG to a new, clean
directory, set that directory as your default directory with SET DEFAULT and execute the
modified PRG. Why? Because now it will create a new DBC with only those objects you didn’t
comment out. Then run GenDBC again. Now your generated PRG will be much smaller, as it
no longer contains any of the functions you didn’t call.
For the remainder of your development on the project, this generated file will be your
master for the DBC. You’ll check it, and not the DBC, into your source control program.
You’ll modify it, not the DBC, when you make changes. If you need to modify a view, work on
the code in the PRG and then simply call the function you worked on. Sometimes, when
wholesale changes have been made, you might want to simply delete the DBC from the disk
and run the entire generated PRG to recreate the DBC from scratch.
When you create new views, you should also do so in code in the originally generated
PRG. You might look at the PRG and think that this could be a daunting task. After all, there
are four DBSETPROP() calls for every field in every view! However, not all of those calls are
necessary. Table 8 shows the calls to DBSETPROP() for each view and a brief description of
when it is required.

Table 8. Calls to DBSETPROP() made for each field in a view.

Property When required


KeyField When the field is used as a key for updating.
Updatable When the field must be updated.
UpdateName When the default is incorrect—typically in views with joins where the same
column name exists in more than one source table.
DataType When the default data type is incorrect, such as when you wish to use a
date rather than datetime.

The only property that typically needs to be changed for most fields is Updatable, which,
by default, is set to .F. Instead of setting this property for each field, you can simply let a
procedure set all of them to .T. for you, and then you can set individual fields to .F. if
necessary. Here’s some code that will do that for you:

*-- Open the view to get all the fields


USE (lcView) NODATA ALIAS propgetter
lnCount = AFIELDS(laFields, "propgetter")

*-- Loop through all fields and set properties


FOR i = 1 TO lnCount
lcField = ALLTRIM(lcView) + "." + laFields[i, 1]
DBSETPROP(lcField, "Field", "Updatable", .T.)
ENDFOR

*-- Close view


USE IN propgetter
Chapter 5: Upsizing: Moving from File-Server to Client/Server 93

You will need to set the KeyField property to .T. for at least one column per table involved
in the join. Typically there will be one or two fields in a view that need to be set to .T. As the
default is .F., it is simple to write the one or two lines necessary to do this.
You may find it worthwhile to replace all the property calls that were generated by
GenDBC. Why? If your company’s developers access a source-control database via the Internet
and must sometimes use slow dial-up connections, you’ll find that reducing the size of the file
really helps speed up this process. Also, by using a PRG instead of a DBC, you’ll dramatically
reduce the amount that must be transferred over a slow connection.

Summary
In this chapter you learned about the SQL Server Upsizing Wizard in Visual FoxPro, how it
works, what it does and how to deal with its results. Hopefully, even if you don’t use the
Upsizing Wizard, you’ve picked up some tips that will help you get going in client/server
development. In Chapter 7, you will learn about downsizing.
94 Client/Server Applications with Visual FoxPro and SQL Server
Chapter 6: Extending Remote Views with SQL Pass Through 95

Chapter 6
Extending Remote Views
with SQL Pass Through
As you learned in Chapter 4, remote views make it very easy to access remote data.
Remote views are actually wrappers for SQL pass through, handling the tasks of
managing connections, detecting changes, formatting UPDATE commands and
submitting them to the server. In this chapter, we’re going to take an in-depth look at
SQL pass through. We’ll see how to connect to the server, submit queries, manage
transactions and more.

There is no doubt about it, remote views offer the easiest way to access remote data. However,
with ease of use comes less flexibility. Visual FoxPro contains another powerful mechanism for
manipulating remote data called SQL pass through (SPT). SQL pass through provides all the
functionality of remote views and more.
With SQL pass through you can:
• Execute queries other than SELECT.
• Access back-end-specific functionality.
• Fetch multiple result sets in a single call.
• Execute stored procedures.

However:
• There is no graphical user interface.
• You must manually manage connections.
• Result sets are read-only by default and must be configured to be updatable.

The flexibility that SQL pass through allows makes it a powerful tool. It is important for
client/server developers to understand it thoroughly.

Connecting to the server


In order to use SQL pass through, you must make a connection to the server. Unlike remote
views, Visual FoxPro does not manage the connection. The developer must manually make the
connection, configure its behavior, and then disconnect when the connection is no longer
needed. Connection management is very important because making a connection consumes
substantial time and resources on the client and server.
There are two functions that are used to establish the connection with the remote server:
SQLConnect() and SQLStringConnect().
96 Client/Server Applications with Visual FoxPro and SQL Server

The SQLConnect() function


There are two ways to use the SQLConnect() function to connect to a remote data source. The
first requires that you supply the name of a data source as defined in the ODBC Data Source
Administrator applet of the control panel. The following example creates a connection to a
remote server using the ODBCPubs DSN:

LOCAL hConn
hConn = SQLConnect("ODBCPubs", "sa", "")

The second way to use SQLConnect() is to supply the name of a Visual FoxPro
connection that was created using the CREATE CONNECTION command. As you saw in
Chapter 4, “Remote Views,” the CREATE CONNECTION command stores the metadata that
Visual FoxPro needs to connect to a remote data source. The following example creates a
Visual FoxPro connection named VFPPUBS and then connects to the database described by
the connection:

LOCAL hConn
CREATE DATABASE cstemp
CREATE CONNECTION vfppubs ;
DATASOURCE "ODBCPubs" ;
USERID "sa" ;
PASSWORD ""
hConn = SQLConnect("vfppubs")

The SQLStringConnect() function


The other function that can be used to establish a connection to a remote data source is
SQLStringConnect(). Unlike SQLConnect(), SQLStringConnect() requires a single parameter,
a string of semicolon-delimited options that describes the remote data source and optional
connections settings.
The valid options are determined by the requirements of the ODBC driver. Specific
requirements for each ODBC driver can be found in that ODBC driver’s documentation.
Table 1 lists some commonly used connection string options for SQL Server 7.0.

Table 1. Some common SQL Server 7.0 connection string options.

Option Description
DSN References an ODBC DSN.
Driver Specifies the name of the ODBC driver to use.
Server Specifies the name of the SQL Server to connect to.
UID Specifies the login ID or username.
PWD Specifies the password for the given login ID or username.
Database Specifies the initial database to connect to.
APP Specifies the name of the application making the connection.
WSID The name of the workstation making the connection.
Trusted_Connection Specifies whether the login is being validated by the Windows NT Domain.
Chapter 6: Extending Remote Views with SQL Pass Through 97

Not all of the options listed in Table 1 have to be used for each connection. For
instance, if you specify the Trusted_Connection option and connect to SQL Server using NT
Authentication, there’s no reason to use the UID and PWD options since SQL Server would
invariably ignore them.
The following code demonstrates some examples of using SQLStringConnect().

From this point forward, substitute the name of your server for the string
‡ <MyServer> in code examples.

LOCAL hConn
hConn = SQLStringConnect("Driver=SQL Server;Server=<MyServer>;"+ ;
UID=sa;PWD=;Database=pubs")
hConn = SQLStringConnect("DSN=ODBCPubs;UID=sa;PWD=;Database=pubs")
hConn = SQLStringConnect("DSN=ODBCPubs;Database=pubs;Trusted_Connection=Yes")

Handling connection errors


Both the SQLConnect() and SQLStringConnect() functions return a connection handle. If
the connection is established successfully, the handle will be a positive integer. If Visual
FoxPro failed to make the connection, the handle will contain a negative integer. A simple
call to the AERROR() function can be used to retrieve the error number and message.
The following example traps for a failed connection and displays the error number and
message using the Visual FoxPro MESSAGEBOX() function. Figure 1 shows an example
of the error message.

Visual FoxPro returns error 1526 for all errors against a remote data
‡ source. The fifth element of the array returned by AERROR() contains the
remote data source-specific error.

#define MB_OKBUTTON 0
#define MB_STOPSIGNICON 16

LOCAL hConn
hConn = SQLConnect("ODBCPubs", "bad_user", "")
IF (hConn < 0)
LOCAL ARRAY laError[1]
AERROR(laError)
MESSAGEBOX( ;
laError[2], ;
MB_OKBUTTON + MB_STOPSIGNICON, ;
"Error " + TRANSFORM(laError[5]))
ENDIF
98 Client/Server Applications with Visual FoxPro and SQL Server

Figure 1. The error message returned from SQL Server 7.0 when trying to establish a
connection using an unknown login.

Disconnecting
As mentioned previously, the developer is responsible for connection management when using
SQL pass through. 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.
You break the connection to the remote data source using the SQLDisconnect() function.
SQLDisconnect() takes one parameter, the connection handle created by a call to either
SQLConnect() or SQLStringConnect(). SQLDisconnect() returns a 1 if the connection was
correctly terminated and a negative value if an error occurred.
The following example establishes a connection to SQL Server 7.0 and then drops
the connection:

LOCAL hConn,lnResult
*hConn = SQLStringConnect("Driver=SQL Server;Server=<MyServer>;"+ ;
UID=sa;PWD=;Database=pubs")
hConn = SQLConnect("ODBCPubs", "sa", "")
IF (hConn > 0)
MESSAGEBOX("Connection established")
lnResult = SQLDisconnect(hConn)
IF lnResult < 0
MESSAGEBOX("Disconnect failed")
ENDIF && lnResult < 0
ENDIF && hConn > 0

If the parameter supplied to SQLDisconnect() is not a valid connection handle, Visual


FoxPro will return a run-time error (#1466). Currently there is no way to determine whether a
connection handle is valid without attempting to use it.

To disconnect all SQL pass through connections, you can pass a value of
‡ zero to SQLDisconnect().

Accessing metadata
VFP has two SQL pass through functions that return information about the database you’ve
connected to. The first, SQLTables(), returns a result set containing information about the
tables and views in the database. The second, SQLColumns(), returns a result set containing
information about a specific table or view.
Chapter 6: Extending Remote Views with SQL Pass Through 99

The SQLTables() function


The following example demonstrates using SQLTables() to retrieve information about the user
tables and views within the SQL Server demo database pubs. Figure 2 shows a portion of the
information returned in a VFP Browse window, and Table 2 lists the definitions of the columns
within the result set.

LOCAL hConn, lnResult


hConn = SQLConnect("odbcpubs", "sa", "")
lnResult = SQLTables(hConn, "'TABLE', 'VIEW'")
SQLDisconnect(hConn)

Figure 2. The results of calling SQLTables() on the pubs database.

Table 2. The description of the columns in the result set.

Column Description
Table_cat Object qualifier. However, In SQL Server 7.0, Table_cat contains the
name of the database.
Table_schema Object owner.
Table_name Object name.
Table_type Object type (TABLE, VIEW, SYSTEM TABLE or another data-store-
specific identifier).
Remarks A description of the object. However, SQL Server 7.0 does not return a
value for Remarks.
100 Client/Server Applications with Visual FoxPro and SQL Server

The SQLColumns() function


SQLColumns() returns a result set containing information about each column in the specified
table. This function returns different results depending on a third, optional parameter:
“FOXPRO” or “NATIVE.” The “NATIVE” option formats the result set with information
specific to the remote data source, whereas specifying “FOXPRO” formats the result set with
column information describing the Visual FoxPro cursor that contains the retrieved data. Table
3 lists the columns that are returned by SQLColumns() using the “FOXPRO” option. Table 4
lists the columns returned by SQLColumns() when using “NATIVE” and when attached to a
SQL Server database. Different results are possible depending on the remote data source.

Table 3. A description of the columns returned by the FOXPRO option.

Column Description
Field_name Column name
Field_type Visual FoxPro data type
Field_len Column length
Field_dec Number of decimal places

Table 4. A description of the columns returned from SQL Server using SQLColumns()
and the NATIVE option.

Column Description
Table_cat SQL Server database name
Table_schema Object owner
Table_name Object name
Column_name Column name
Data_type Integer code for the ODBC data type
Type_name SQL Server data type name
Column_size Display requirements (character positions)
Buffer_length Storage requirements (bytes)
Decimal_digits Number of digits to the right of the decimal point
Num_prec_radix Base for numeric data types
Nullable Integer flag for nullability
Remarks SQL Server always returns NULL
Column_def Default value expression
SQL_data_type Same as Data_type column
SQL_datetime_sub Subtype for datetime data types
Character_octet_length Maximum length of a character or integer data type
Ordinal_position Ordinal position of the column (starting at 1)
Is_nullable Nullability indicator as a string (YES | NO)
SS_data_type SQL Server data type code

The following example demonstrates using the SQLColumns() function to retrieve


information about the authors table in the pubs database. Figure 3 shows a Browse window
containing a result set formatted with the FOXPRO option. Figure 4 shows a subset of the
columns returned by the NATIVE option.

LOCAL hConn, lnResult


hConn = SQLConnect("odbcpubs", "sa", "")
Chapter 6: Extending Remote Views with SQL Pass Through 101

lnResult = SQLColumns(hConn, "authors", "FOXPRO")


BROWSE NORMAL && display the results
lnResult = SQLColumns(hConn, "authors", "NATIVE")
BROWSE NORMAL && display the results
SQLDisconnect(hConn)

Figure 3. The results of calling SQLColumns() with the FOXPRO option.

Figure 4. A subset of the columns returned by SQLColumns() with the NATIVE option.
(See Table 4 for a complete list of columns.)

Submitting queries
Most interactions with the remote server will be through the SQLExec() function. SQLExec() is
the workhorse of the SQL pass through functions. You’ll use it to submit SELECT, INSERT,
UPDATE and DELETE queries, as well as calls to stored procedures. If the statement is
successfully executed, SQLExec() will return a value greater than zero that represents the
number of result sets returned by the server (more on multiple result sets later). A negative
return value indicates an error. As discussed previously, you can use AERROR() to retrieve
information about the error. It’s also possible for SQLExec() to return a value of zero (0), but
only if queries are being submitted asynchronously. We’ll look at asynchronous queries in a
later section.

Queries that return a result set


As with the SQLTables() and SQLColumns() functions, result sets returned by a query
submitted using SQLExec() are stored in a Visual FoxPro cursor. Also like the SQLTables()
102 Client/Server Applications with Visual FoxPro and SQL Server

and SQLColumns() functions, the name of the result set will be SQLRESULT unless another
name is specified in the call to SQLExec().
For example, the following call to SQLExec() runs a SELECT query against the authors
table in the pubs database.

From this point forward, examples may not include the code that
‡ establishes the connection.

lnResults = SQLExec(hConn, "SELECT * FROM authors")

To specify the name of the result set rather than accept the default, “SQLRESULT,”
specify the name in the third parameter of the SQLExec statement. The following example uses
the same query but specifies that the resultant cursor should be called authors:

lnResult = SQLExec(hConn, "SELECT * FROM authors", "authors")

Figure 5 shows the Data Session window with the single authors cursor open.

Figure 5. The Data Session window showing the single authors cursor.

Retrieving multiple result sets


As discussed in Chapter 3, “Introduction to SQL Server 7.0,” submitting a query to a remote
server can be a very expensive operation. The server must parse the query and check for syntax
errors, verify that all referenced objects exist, optimize the query to determine the best way to
solve it, and then compile and execute. Luckily for us, Visual FoxPro and ODBC provide a
means to gain an “economy of scale” when submitting queries. It is possible to submit multiple
queries in a single call to SQLExec(), as in the following example:

lcSQL = "SELECT * FROM authors; SELECT * FROM titles"


lnResults = SQLExec(hConn, lcSQL)
Chapter 6: Extending Remote Views with SQL Pass Through 103

SQLExec() returns a value (stored in lnResults in this example) containing the number of
cursors returned.
Now you might be wondering what names Visual FoxPro assigns to each cursor. In the
preceding example, the results of the first query (SELECT * FROM authors) will be placed into
a cursor named Sqlresult. The results from the second query will be placed into a cursor named
Sqlresult1. Figure 6 shows the Data Session window with the two cursors open.

Figure 6. The Data Session window showing the two cursors Sqlresult and Sqlresult1.

Visual FoxPro’s default behavior is to wait until SQL Server has returned all the result sets
and then return the result sets to the application in a single action. Alternately, you can tell
Visual FoxPro to return the result sets one at a time as each one is available. This behavior is
controlled by a Connection property, BatchMode. If BatchMode is True (the default), Visual
FoxPro returns all result sets at once; else, if False, Visual FoxPro returns the result sets one at
a time.
Use the SQLSetProp() function to manipulate connection settings. The following example
changes the BatchMode property to False, causing Visual FoxPro to return results sets one at
a time:

lnResult = SQLSetProp(hConn, 'BatchMode', .F.)

As usual, a positive return result indicates success.


SQLSetProp() has a sibling, SQLGetProp(), that returns the current value of a connection
property. The following code checks that the BatchMode property was set correctly:

llBatch = SQLGetProp(hConn, 'BatchMode')

When BatchMode is False, Visual FoxPro automatically returns only the first result set.
The developer must request that Visual FoxPro return additional result sets by calling the
104 Client/Server Applications with Visual FoxPro and SQL Server

SQLMoreResults() function. SQLMoreResults() returns zero (0) if the next result set is not
ready, one (1) if it is ready, two (2) if there are no more result sets to retrieve, or a negative
number if an error has occurred.
The following example demonstrates the SQLMoreResults() function. In this example,
we’re going to retrieve information about a specific book by submitting queries against the
titles, authors, titleauthor and sales tables.

*-- Get information about the book


lcSQL = "SELECT * FROM titles WHERE title_id = 'TC7777'" + ";"

*-- Retrieve the authors


lcSQL = lcSQL + ;
"SELECT * " + ;
"FROM authors INNER JOIN titleauthor " + ;
"ON authors.au_id = titleauthor.au_id " + ;
"WHERE titleauthor.title_id = 'TC7777'"

*-- Retrieve sales information


lcSQL = lcSQL + "SELECT * FROM sales WHERE title_id = 'TC7777'"

lnResult = SQLSetProp(hConn, "BatchMode", .F.)


lnResult = SQLExec(hConn, lcSQL, 'TitleInfo')

DO WHILE .T.
lnResult = SQLMoreResults(hConn)
DO CASE
CASE lnResult < 0
*-- Error condition

CASE lnResult = 0
*-- No result sets are ready

CASE lnResult = 2
*-- All result sets have been retrieved
EXIT

OTHERWISE
*-- Process retrieved result set
ENDCASE
ENDDO

It is important to realize that SQLMoreResults() must continue being called until it returns
a two (2), meaning no more result sets. If any other SQL pass through function is issued before
SQLMoreResults() returns 2, Visual FoxPro will return the error shown in Figure 7.

The preceding statement is not entirely true. You can issue the
‡ SQLCancel() function to terminate any waiting result sets, but we haven’t
introduced it yet.
Chapter 6: Extending Remote Views with SQL Pass Through 105

Figure 7. The results of trying to issue another SQL pass through function while
processing result sets in non-batch mode.

Queries that modify data


INSERT, UPDATE and DELETE queries are submitted in the same way as SELECT queries.
The following example increases the price of all books in the titles table of the pubs database
by 10 percent:

lnResult = SQLExec(hConn, "UPDATE titles SET price = price * 1.1")

In this example, SQLExec() executes a data modification query rather than a SQL
SELECT statement. Therefore, it returns a success indicator (1 for successful execution or a
negative number in the event of an error), rather than the number of result sets. If the query
successfully updates zero, one, or one million rows, SQLExec() will return a value of one. (A
query is considered successful if the server can parse and execute it.)
To determine the number of rows updated, use the SQL Server global variable
@@ROWCOUNT, which performs the same function as Visual FoxPro’s _TALLY global
variable. After executing a query, @@ROWCOUNT contains the number of rows affected by
the query. The value of @@ROWCOUNT can be retrieved by issuing a SELECT query:

lnResult = SQLExec(hConn, "SELECT @@ROWCOUNT AS AffectedRows", "status")

Note that the value of @@ROWCOUNT is returned as a column named “AffectedRows”


in the first (and only) row of a cursor named “Status,” not to the variable lnResult.
Unlike _TALLY, @@ROWCOUNT is not truly global. It is one of several variables
that are scoped to the connection. Therefore, the value of @@ROWCOUNT must be retrieved
on the same connection that executed the query. If you execute the query on one connection
and retrieve the value of @@ROWCOUNT from another connection, the result will not
be accurate.
Also, @@ROWCOUNT is reset after each statement. If you submit multiple queries,
@@ROWCOUNT will contain the affected row count for the last query executed.

Parameterized queries
You previously read about using parameters in views to filter the query. You can use this same
mechanism with SQL pass through.
106 Client/Server Applications with Visual FoxPro and SQL Server

Using a parameterized query might seem unnecessary at first. After all, since you pass the
query as a string, you have complete control over its creation. Consider the following example:

FUNCTION GetTitleInfo(tcTitleID, tcCursor)


LOCAL lcQuery, hConn
lLcQuery = "SELECT * FROM titles WHERE title_id = '" + tcTitleID + "'"
hConn = SQLConnect("odbcpubs", "sa", "")
lnResult = SQLExec(hConn, lcQuery, tcCursor)
SQLDisconnect(hConn)
RETURN .T.

Creating the query using the technique from the previous example works in most
situations. However, there are situations where using a parameterized query makes more sense.
For example, when different back ends impose different requirements for specifying literal
values, it is easier to allow Visual FoxPro to handle the conversion. Consider dates. Visual
FoxPro requires date literals to be specified in the form {^1999-12-31}. SQL Server, on the
other hand, does not recognize {^1999-12-31} as a date literal. Instead you would have to use a
literal similar to ‘12/31/1999’ or ‘19991231’ (the latter being preferred).
The following code shows how the same query would be formatted for Visual FoxPro and
SQL Server back ends:

*-- If accessing Visual FoxPro using the ODBC driver


lcQuery = ;
"SELECT * " + ;
"FROM titles " + ;
"WHERE pubdate BETWEEN {^1998-01-01} AND {^1998-12-31}"

*-- If accessing SQL Server


lcQuery = ;
"SELECT * " + ;
"FROM titles " + ;
"WHERE pubdate BETWEEN '19980101' AND '19981231'"

In this situation, Visual FoxPro converts the search arguments to the proper format
automatically. The following example demonstrates this:

LOCAL ldStart, ldStop, lcQuery


ldStart = {^1998-01-01}
ldStop = {^1998-12-31}
lcQuery = ;
"SELECT * " + ;
"FROM titles " + ;
"WHERE pubdate BETWEEN ?ldStart AND ?ldStop"

The preceding query would work correctly against both Visual FoxPro and SQL Server.
There are other data types that also benefit from the use of parameterization. Visual
FoxPro’s Logical vs. SQL Server’s Bit is another example. A literal TRUE is represented in
Visual FoxPro as .T., while in Transact-SQL it is 1.
Chapter 6: Extending Remote Views with SQL Pass Through 107

The advantage of parameterization


Parameterized queries provide an additional benefit: Parameterized queries execute more
quickly than non-parameterized queries when the query is called repeatedly with different
parameters. This performance benefit occurs because SQL Server does not have to parse,
optimize and compile the query each time it is called—instead, it can reuse the existing
execution plan with the new parameter values.
To demonstrate, we’ll use the SQL Server Profiler, a utility that ships with SQL Server
7.0. SQL Server Profiler, described in greater detail in Chapter 8, “Errors and Debugging,”
is one of the best tools available for debugging and investigation. It logs events that occur on
the server (such as the submission of a query or calling a stored procedure) and collects
additional information.

‡ SQL Server 6.5 has a similar utility called SQLTrace.

Figure 8 shows the output from the SQL Server Profiler for the following query, and
Figure 9 shows the output for the second one:

LOCAL llTrue,lnResult
lnResult = SQLExec(hConn, "SELECT * FROM authors WHERE contract = 1")
llTrue = .T.
lnResult = SQLExec(hConn, "SELECT * FROM authors WHERE contract = ?llTrue")

Figure 8. The SQL Server Profiler output for a non-parameterized query.

Figure 9. The SQL Server Profiler output for a parameterized query.

There is an important difference between how these two queries were submitted to SQL
Server. As expected, the first query was passed straight through. SQL Server had to parse,
108 Client/Server Applications with Visual FoxPro and SQL Server

optimize, compile and execute the query. The next time the same query is submitted, SQL
Server will have to parse, optimize and compile the query again before executing it.
The second query was handled quite differently. When Visual FoxPro (actually, ODBC)
submitted the query, the sp_executesql stored procedure was used to identify the search
arguments to SQL Server. The following is an excerpt from the SQL Server Books Online:

“sp_executesql can be used instead of stored procedures to execute a Transact-SQL


statement a number of times when the change in parameter values to the statement is
the only variation. Because the Transact-SQL statement itself remains constant and
only the parameter values change, the Microsoft® SQL Server™ query optimizer is
likely to reuse the execution plan it generates for the first execution.”

SQL Server will take advantage of this knowledge (the search arguments) by caching the
execution plan (the result of parsing, optimizing and compiling a query) instead of discarding
the execution plan, which is the normal behavior. The next time the query is submitted, SQL
Server can reuse the existing execution plan, but with a new parameter value.
There is a tradeoff—calling a stored procedure has a cost, so you should not blindly write
all your queries using parameters. However, the cost is worth incurring if the query is executed
repeatedly. There are no magic criteria to base your decision on, but some of the things to
consider are the number of times the query is called and the length of time between calls.

Making SQL pass through result sets updatable


By default, the cursor created to hold the results of a SQL pass through query is read-only.
Actually, changes can be made to the data within the cursor, but Visual FoxPro won’t do
anything with the changes. This may sound familiar, as it’s the same behavior exhibited by a
non-updatable view. As it turns out, the same options that make a view updatable will also
work on a cursor created by a SQL pass through query. The following example retrieves all the
authors from the authors table and makes the au_fname and au_lname columns updatable:

lnResult = SQLExec(hConn, "SELECT * FROM authors", "authors")


CURSORSETPROP("TABLES", "dbo.authors", "authors")
CURSORSETPROP("KeyFieldList", "au_id", "authors")
CURSORSETPROP("UpdatableFieldList", "au_lname, au_fname", "authors")
CURSORSETPROP("UpdateNameList", ;
"au_id dbo.authors.au_id, " + ;
"au_lname dbo.authors.au_lname, " + ;
"au_fname dbo.authors.au_fname, " + ;
"phone dbo.authors.phone, " + ;
"address dbo.authors.address, " + ;
"city dbo.authors.city, " + ;
"state dbo.authors.state, " + ;
"zip dbo.authors.zip, " + ;
"contract dbo.authors.contract", "authors")
CURSORSETPROP("SendUpdates", .T., "authors")

Each property plays an important role in the creation of the commands sent to the server.
Visual FoxPro will create an INSERT, UPDATE or DELETE query based on the operations
performed on the cursor. The UpdatableFieldList property tells Visual FoxPro which columns
Chapter 6: Extending Remote Views with SQL Pass Through 109

it needs to track changes to. The Tables property supplies the name of the remote table, and the
UpdateNameList property has the name of the column in the remote table for each column in
the cursor. KeyFieldList contains a comma-delimited list of the columns that make up the
primary key. Visual FoxPro uses this information to construct the WHERE clause of the query.
The last property, SendUpdates, provides a safety mechanism. Unless SendUpdates is marked
TRUE, Visual FoxPro will not send updates to the server.
There are two other properties that you may want to include when making a cursor
updatable. The first, BatchUpdateCount, controls the number of update queries that are
submitted to the server at once. The default value is one (1), but increasing this property can
improve performance. SQL Server will parse, optimize, compile and execute the entire batch of
queries at the same time. The second parameter, WhereType, controls how Visual FoxPro
constructs the WHERE clause used by the update queries. This also affects how conflicts are
detected. Consult the Visual FoxPro online Help for more information on the WhereType
cursor property.

Calling stored procedures


One of the things you don’t normally do with a remote view is call a stored procedure. For that,
you typically use SQL pass through. Calling a stored procedure via SQL pass through is just
like submitting any other query: The SQLExec() function does all the work.
The following example demonstrates calling a SQL Server stored procedure. The stored
procedure being called, sp_helpdb, returns information about the databases residing on the
attached server. Note that we have the same ability to rename the result set returned by the
query/stored procedure.

lnResult = SQLExec(hConn, "EXECUTE sp_helpdb", "dbinfo")

The preceding example uses the Transact-SQL EXECUTE command to call the stored
procedure. You can also call stored procedures using the ODBC escape syntax, as
demonstrated in the following example:

lnResult = SQLExec(hConn, "{CALL sp_helpdb}")

Using the ODBC escape syntax offers two small advantages. First, ODBC will
automatically convert the statement to the format required by the back end you are working
with—as long as that back end supports directly calling stored procedures. Second, it is an
alternate way to work with OUTPUT parameters.

Handling input and output parameters


Stored procedures, like Visual FoxPro’s procedures and functions, can accept parameters. An
example is sp_helprotect, which returns information about user permissions applied to a
specific database object. The following code calls the sp_helprotect stored procedure to obtain
information about user permissions applied to the authors table. The result set will contain all
the users and roles that have been given explicit permissions on the authors table, and whether
those permissions were granted or denied.
110 Client/Server Applications with Visual FoxPro and SQL Server

lnResult = SQLExec(hConn, "EXECUTE sp_helprotect 'authors'")

Using the ODBC calling convention is slightly different from calling a Visual FoxPro
function, as shown here:

lnResult = SQLExec(hConn, "{CALL sp_helprotect ('authors')}")

Additional parameters are separated by commas:

lnResult = SQLExec(hConn, "EXECUTE sp_helprotect 'authors', 'guest'")


lnResult = SQLExec(hConn, "{CALL sp_helprotect ('authors', 'guest')}")

Output parameters
There is another way to get information from a stored procedure: output parameters. An output
parameter works the same way as a parameter passed to a function by reference in Visual
FoxPro: The stored procedure alters the contents of the parameter, and the new value will be
available to the calling program.
The following Transact-SQL creates a stored procedure in the Northwind database that
counts the quantity sold of a particular product within a specified date range:

LOCAL lcQuery, lnResult


lcQuery = ;
"CREATE PROCEDURE p_productcount " + ;
"@ProductId INT, " + ;
"@StartDate DATETIME, " + ;
"@EndDate DATETIME, " + ;
"@QtySold INT OUTPUT " + ;
"AS " + ;
"SELECT @QtySold = SUM(od.Quantity) " + ;
"FROM Orders o INNER JOIN [Order Details] od " + ;
"ON o.OrderId = od.OrderId " + ;
"WHERE od.ProductId = @ProductId " + ;
"AND o.OrderDate BETWEEN @StartDate AND @EndDate "

*-- hConn must be a connection to the Northwind database


lnResult = SQLExec(hConn, lcQuery)

The stored procedure accepts four parameters: the ID of the product, the start and end
points for a date range, and an output parameter to return the total quantity sold.
The following example shows how to call the stored procedure and pass the parameters:

LOCAL lnTotalCnt, lcQuery


lnTotalCnt = 0
lcQuery = "EXECUTE p_ProductCount 72, '19960701', '19960801', ?@lnTotalCnt"
lnResult = SQLExec(hConn, lcQuery)

You can also call the p_ProductCount procedure using ODBC escape syntax, as in the
following code:
Chapter 6: Extending Remote Views with SQL Pass Through 111

lcQuery = "{CALL p_productcount (72, '19960701', '19960801', ?@lnTotalCnt)}"


lnResult = SQLExec(hConn, lcQuery)

Because SQL Server returns result codes and output parameters in the
‡ last packet sent from the server, output parameters are not guaranteed to
be available until after the last result set is returned from the server—that
is, until SQLExec() returns a one (1) while in Batch mode or SQLMoreResults()
returns a two (2) in Non-batch mode.

Transaction management
A transaction groups a collection of operations into a single unit of work. If any operation
within the transaction fails, the application can cause the data store to undo (that is, reverse) all
the operations that have already been completed, thus keeping the integrity of the data intact.
Transaction management is a powerful tool, and the Visual FoxPro community was
pleased to see its introduction into Visual FoxPro.
In Chapter 3, “Introduction to SQL Server 7.0,” we looked at transactions within SQL
Server and identified two types: implicit (or Autocommit) and explicit. To review, implicit
transactions are individual statements that commit independently of other statements in the
batch. In other words, the changes made by one statement are not affected by the success or
failure of a statement that executes later. The following example demonstrates transferring
funds from a savings account to a checking account:

lnResult = SQLExec(hConn, ;
"UPDATE account " + ;
"SET balance = balance – 100 " + ;
"WHERE ac_num = 14356")

lnResult = SQLExec(hConn, ;
"UPDATE account " + ;
"SET balance = balance + 100 " + ;
"WHERE ac_num = 45249")

Even if the two queries are submitted in the same SQLExec() call, as in the following
example, the two queries commit independently of each other:

lnResult = SQLExec(hConn, ;
"UPDATE account " + ;
"SET balance = balance – 100 " + ;
"WHERE ac_num = 14356" + ;
";" + ;
"UPDATE account " + ;
"SET balance = balance + 100 " + ;
"WHERE ac_num = 45249")

Each query is independent of the other. If the second fails, nothing can be done to undo the
changes made by the first except to submit a correcting query.
112 Client/Server Applications with Visual FoxPro and SQL Server

On the other hand, an explicit transaction groups multiple operations and allows
the developer to undo all changes made by all operations in the transaction if any one
operation fails.
In this section, we’re going to look at the SQL pass through functions that manage
transactions: SQLSetProp(), SQLCommit() and SQLRollback().
SQL pass through doesn’t have a function to start an explicit transaction. Instead,
explicit transactions are started by setting the connection’s Transactions property to a two
(2) or DB_TRANSMANUAL (from Foxpro.h). The following example shows how to use
the SQLSetProp() function to start a manual (Visual FoxPro term) or explicit (SQL Server
term) transaction:

#include FOXPRO.h
lnResult = SQLSetProp(hConn, "TRANSACTIONS", DB_TRANSMANUAL)

Enabling manual transaction does not actually start a transaction. The transaction actually
starts only when the first query is submitted. After that, all queries submitted on the connection
will participate in the transaction until the transaction is terminated. You will see exactly how
this works in Chapter 11, “Transactions.” Regardless, if everything goes well and no errors
occur, you can commit the transaction with the SQLCommit() function:

lnResult = SQLCommit(hConn)

If something did go wrong, the transaction can be rolled back and all operations reversed
with the SQLRollback() function:

lnResult = SQLRollback(hConn)

Manual transactions can only be disabled by calling SQLSetProp() to set the Transactions
property back to 1. If you do not reset the Transactions property to 1, the next query submitted
on the connection automatically causes another explicit transaction to be started.
Taking all that into account, the original example can be rewritten as follows:

#include FOXPRO.h

lnResult = SQLSetProp(hConn, "TRANSACTIONS", DB_TRANSMANUAL)
lnResult = SQLExec(hConn, ;
"UPDATE account " + ;
"SET balance = balance – 100 " + ;
"WHERE ac_num = 14356" + ;
";" + ;
"UPDATE account " + ;
"SET balance = balance + 100 " + ;
"WHERE ac_num = 45249")

IF (lnResult != 1)
SQLRollback(hConn)

*-- Relay error message to the user


ELSE
SQLCommit(hConn)
Chapter 6: Extending Remote Views with SQL Pass Through 113

ENDIF

SQLSetProp(hConn, "TRANSACTIONS", 1)
RETURN (lnResult = 1)

The code in the preceding example wraps the UPDATE queries within the explicit
transaction and handles an error by rolling back any changes that may have occurred.

Binding connections
Sometimes it’s necessary for two or more connections to participate in the same transaction.
This scenario can occur when dealing with components in a non-MTS environment. To
accommodate this need, SQL Server provides the ability to bind two or more connections
together. Once bound, the connections will participate in the same transaction.
If multiple connections participate in one transaction, any of the participating connections
can begin the transaction, and any participating connection can end the transaction.
Connection binding is accomplished by using two stored procedures: sp_getbindtoken and
sp_bindsession. First, execute sp_getbindtoken against the first connection to obtain a unique
identifier (the bind token, as a string) for the connection. Next, pass the bind token to
sp_bindsession, which is executed against another connection. The second function binds the
two connections. The following example demonstrates the entire process:

LOCAL hConn1, hConn2, hConn3, lnResult, lcToken


lcToken = ""
hConn1 = SQLConnect("odbcpubs", "sa", "")
hConn2 = SQLConnect("odbcpubs", "sa", "")
hConn3 = SQLConnect("odbcpubs", "sa", "")

lnResult = SQLExec(hConn1, "EXECUTE sp_getbindtoken ?@lcToken")


lnResult = SQLExec(hConn2, "EXECUTE sp_bindsession ?lcToken")
lnResult = SQLExec(hConn3, "EXECUTE sp_bindsession ?lcToken")

SQLDisconnect(hConn1)
SQLDisconnect(hConn2)
SQLDisconnect(hConn3)

In the example, three connections are established to the server. In the first call to
sp_getbindtoken, you get the bind token. You must use the ? and @ symbols with the lcToken
variable because the binding token returns through an OUTPUT parameter. You then pass the
bind token to the second and third connections by calling sp_bindsession.

Asynchronous processing
So far, every query that we’ve sent to the server was sent synchronously. Visual FoxPro paused
until the server finished processing the query and returned the result set to Visual FoxPro.
There are times, however, when you may not want Visual FoxPro to pause while the query is
running. For example, you may want to provide some feedback to the user to indicate that the
application is running and has not locked up, or you may want to provide the ability to cancel a
query mid-stream. To prevent Visual FoxPro from pausing, submit the query asynchronously.
Just remember that this approach makes the developer responsible for determining when the
query processing is finished.
114 Client/Server Applications with Visual FoxPro and SQL Server

Switching to asynchronous processing is not complicated. There is a connection property,


Asynchronous, that determines the mode. If Asynchronous is set to FALSE (the default), all
queries will be sent synchronously. Note that Asynchronous is a Visual FoxPro connection
property and therefore is scoped to the connection.
The following example demonstrates making a connection and then using the
SQLSetProp() function to change to asynchronous mode:

hConn = SQLConnect("odbcpubs", "sa", "")


lnResult = SQLSetProp(hConn, "ASYNCHRONOUS", .T.)

There is absolutely no difference between submitting a query in synchronous mode and


submitting a query in asynchronous mode. There is, however, a difference in the way you detect
that the query has completed. If a query is submitted in synchronous mode, SQLExec() will
return a positive value indicating the number of result sets returned or a negative value
indicating an error. In asynchronous mode, that still holds true, but SQLExec() can return a
zero (0) indicating that the query is still being processed. It is up to the developer to poll the
server to determine when the query has been completed.
Polling the server is quite easy. It requires resubmitting the query again. ODBC and the
server realize that the query is not being resubmitted, that this is merely a status check. In fact,
you can simply pass an empty string for the subsequent SQLExec() calls. Regardless, the
following example shows one way to structure this process:

LOCAL llDone, lnResult


llDone = .F.
DO WHILE !llDone
lnResult = SQLExec(hConn, "EXECUTE LongQuery")
llDone = (lnResult != 0)
ENDDO

The loop will stay engaged as long as SQLExec() returns a zero (0) identifying the query
as still being processed.
In the preceding example, the stored procedure being called does not actually run any
queries. The stored procedure LongQuery simply uses the Transact-SQL WAITFOR command
with the DELAY option to pause for a specific period of time (two minutes in this case) before
proceeding. The code for LongQuery is shown here:

lcQuery = ;
[CREATE PROCEDURE longquery AS ] + ;
[WAITFOR DELAY '00:02:00']
*-- hConn should be a connection to the pubs database
lnResult = SQLExec(hConn, lcQuery)

The following program demonstrates calling the LongQuery stored procedure in


asynchronous mode and trapping the Escape key, which is the mechanism provided to
terminate the query:

LOCAL hConn, lcQuery, llCancel, lnResult


lcQuery = "EXECUTE LongQuery"
Chapter 6: Extending Remote Views with SQL Pass Through 115

hConn = SQLConnect("odbcpubs", "sa", "")


SQLSetProp(hConn, "ASYNCHRONOUS", .T.)

SET ESCAPE ON
ON ESCAPE llCancel = .T.

WAIT WINDOW "Press Esc to cancel the query" NOWAIT NOCLEAR

llCancel = .F.
lnResult = 0
DO WHILE (!llCancel AND lnResult = 0)
lnResult = SQLExec(hConn, lcQuery)
DOEVENTS
ENDDO

WAIT CLEAR

IF (llCancel AND lnResult = 0)


WAIT WINDOW "Query being cancelled..." NOWAIT NOCLEAR
SQLCancel(hConn)
WAIT WINDOW "Query cancelled by user"
ELSE
IF (lnResult > 0)
WAIT WINDOW "Query completed successfully!"
ELSE
WAIT WINDOW "Query aborted by error"
ENDIF
ENDIF

SQLDisconnect(hConn)

If the user presses the Escape key, the ON ESCAPE mechanism sets the local variable
llCancel to TRUE, terminating the WHILE loop. The next IF statement tests whether the query
was canceled before the results were returned to Visual FoxPro. If so, the SQLCancel()
function is used to terminate the query on the server.
Asynchronous queries are a bit more complicated to code, but they permit the user to
cancel a query, which adds polish to your applications.

Connection properties revisited


Throughout this chapter you have seen connection properties used to configure the behavior of
SQL pass through. Visual FoxPro has two ways to configure default values for connection
properties. The first and perhaps easiest to use is the Remote Data tab of the Options dialog
(see Figure 10). Note that unless the defaults are written to the registry using the Set As
Default button, changes made using the Remote Data tab will affect all new connections but
will only persist until the current Visual FoxPro session is terminated.
SQLSetProp() can also be used to configure connection defaults through the special
connection handle zero (0)—the environment handle. Unlike the Remote Data tab of the
Options dialog, the changes made to the environment handle using SQLSetProp() cannot be
made to persist beyond the current session. However, you can configure connection properties
explicitly using SQLSetProp() as part of your application startup routine.
116 Client/Server Applications with Visual FoxPro and SQL Server

Figure 10. The Remote Data tab of the Options dialog.

Other connection properties


Earlier in this chapter, we explored several connection properties: Batch Mode, Asynchronous
and Transactions. There are many more connection properties. The Help for SQLSetProp() lists
17 different properties, some of which are seldom, if ever, used (for example, ODBChdbc).
However, some are worthy of mention.

The DispLogin property


The DispLogin connection property controls whether Visual FoxPro prompts the user with the
ODBC login dialog. Table 5 lists the possible values and their descriptions.

Table 5. The legal values for the DispLogin connection property.

Numeric value Constant from Foxpro.h Description


1 (Default) DB_PROMPTCOMPLETE Visual FoxPro will display the ODBC
connection dialog if any required connection
information is missing.
2 DB_PROMPTALWAYS Client is always prompted with the ODBC
connection dialog.
3 DB_PROMPTNEVER Client is never prompted.
Chapter 6: Extending Remote Views with SQL Pass Through 117

The following example demonstrates the effect of DispLogin.

The following example will not work correctly if your ODBC DSN is
‡ configured for NT Authentication.

lnResult = SQLSetProp(0, 'DISPLOGIN', 1) && Set to the default value


hConn = SQLConnect("odbcpubs", "bad_user", "")

You should be prompted with the ODBC connection dialog similar to Figure 11.

Figure 11. Visual FoxPro prompting with the ODBC connection dialog due to missing
login information.

If you execute the following code, you’ll get a different result. Visual FoxPro will not
prompt with the ODBC connection dialog, and SQLConnect() will return a –1.

lnResult = SQLSetProp(0, 'DISPLOGIN', 3) && never prompt


hConn = SQLConnect("odbcpubs", "bad_user", "")

It is highly recommended that you always use the “never prompt” option for this property,
as you should handle all logins to the server through your own code instead of this dialog.

The ConnectionTimeOut property


The ConnectionTimeOut property specifies the amount of time (in seconds) that Visual FoxPro
waits while trying to establish a connection with a remote data source. Legal values are 0 to
600. The default is 15 seconds. You may want to adjust this value upward when connecting to a
server across a slow connection.

The QueryTimeOut property


The QueryTimeOut property specifies the amount of time (in seconds) that Visual FoxPro
waits for a query to be processed. Legal values are 0 to 600. The default is 0 seconds (wait
indefinitely). This property could be used as a governor to terminate long-running queries
before they tie up important server resources.
118 Client/Server Applications with Visual FoxPro and SQL Server

The IdleTimeOut property


The IdleTimeOut property specifies the amount of time (in seconds) that Visual FoxPro will
allow a previously active connection to sit idle before being automatically disconnected. The
default is 0 seconds (wait indefinitely).

Remote views vs. SQL pass through


Developers have strong opinions about the usefulness of remote views. Many members of the
Visual FoxPro community feel that remote views carry too much overhead and that the best
performance is achieved by using SQL pass through. In this section, we’ll again use the SQL
Profiler to capture and evaluate the commands submitted to SQL Server.

Refer to the topic “Monitoring with SQL Server Profiler” in the SQL Server
‡ Books Online for more information on using the SQL Server Profiler.

SQL pass through


Figure 12 shows the commands sent to SQL Server for the following query:

lnResult = SQLExec(hConn, ;
"UPDATE authors " + ;
"SET au_lname = 'White' " + ;
"WHERE au_id = '172-32-1176'")

Figure 12. The commands captured by SQL Profiler for a simple UPDATE query.

Notice that nothing special was sent to the server—the query was truly “passed through”
without any intervention by Visual FoxPro. When SQL Server received the query, it proceeded
with the normal process: parse, name resolution, optimize, compile and execute.
Figure 13 shows the commands sent to SQL Server for a query that uses parameters. This
is the same query as before, except this time we’re using parameters in place of the literals
‘White’ and ‘172-32-1176.’

LOCAL lcNewName, lcAuID


lcNewName = "White"
lcAuID = "172-32-1176"
lnResult = SQLExec(hConn, ;
"UPDATE authors " + ;
"SET au_lname = ?lcNewName " + ;
"WHERE au_id = ?lcAuID")
Chapter 6: Extending Remote Views with SQL Pass Through 119

Figure 13. The commands captured by SQL Profiler for the UPDATE query
with parameters.

This time we see that Visual FoxPro (and ODBC) has chosen to pass the query to SQL
Server using the sp_executesql stored procedure.
There is a benefit to using sp_executesql only if the query will be submitted multiple
times and the only variation will be the values of the parameters/search arguments (refer
back to the section “The advantage of parameterization” for a review of the sp_executesql
stored procedure).
Figure 14 shows the commands sent to SQL Server when multiple queries are submitted in
a single call to SQLExec().

lnResult = SQLExec(hConn, ;
"SELECT * FROM authors; " + ;
"SELECT * FROM titles")

Figure 14. The commands captured by SQL Profiler when multiple queries are
submitted in a single call to SQLExec().

As we hoped, Visual FoxPro (and ODBC) has submitted both queries to SQL Server in a
single submission (batch). For the price of one trip to the server, we’ve submitted two queries,
and the only drawback is the funny name that will be given to the results of the second query
(you may want to review the section “Retrieving multiple result sets” for a refresher).

Remote views
Now that we’ve examined SQL pass through, let’s perform the same exercise using remote
views. The code to create the remote view was generated by GENDBC.PRG and is shown here:

CREATE DATABASE MyDBC && A database must be open

CREATE SQL VIEW "V_AUTHORS" ;


REMOTE CONNECT "ODBCPubs" ;
AS SELECT * FROM dbo.authors Authors

DBSetProp('V_AUTHORS', 'View', 'UpdateType', 1)


DBSetProp('V_AUTHORS', 'View', 'WhereType', 3)
DBSetProp('V_AUTHORS', 'View', 'FetchMemo', .T.)
DBSetProp('V_AUTHORS', 'View', 'SendUpdates', .T.)
120 Client/Server Applications with Visual FoxPro and SQL Server

DBSetProp('V_AUTHORS', 'View', 'UseMemoSize', 255)


DBSetProp('V_AUTHORS', 'View', 'FetchSize', 100)
DBSetProp('V_AUTHORS', 'View', 'MaxRecords', -1)
DBSetProp('V_AUTHORS', 'View', 'Tables', 'dbo.authors')
DBSetProp('V_AUTHORS', 'View', 'Prepared', .F.)
DBSetProp('V_AUTHORS', 'View', 'CompareMemo', .T.)
DBSetProp('V_AUTHORS', 'View', 'FetchAsNeeded', .F.)
DBSetProp('V_AUTHORS', 'View', 'FetchSize', 100)
DBSetProp('V_AUTHORS', 'View', 'Comment', "")
DBSetProp('V_AUTHORS', 'View', 'BatchUpdateCount', 1)
DBSetProp('V_AUTHORS', 'View', 'ShareConnection', .F.)

*!* Field Level Properties for V_AUTHORS


* Props for the V_AUTHORS.au_id field.
DBSetProp('V_AUTHORS.au_id', 'Field', 'KeyField', .T.)
DBSetProp('V_AUTHORS.au_id', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.au_id', 'Field', 'UpdateName', 'dbo.authors.au_id')
DBSetProp('V_AUTHORS.au_id', 'Field', 'DataType', "C(11)")
* Props for the V_AUTHORS.au_lname field.
DBSetProp('V_AUTHORS.au_lname', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.au_lname', 'Field', 'Updatable', .T.)
DBSetProp('V_AUTHORS.au_lname', 'Field', 'UpdateName', 'dbo.authors.au_lname')
DBSetProp('V_AUTHORS.au_lname', 'Field', 'DataType', "C(40)")
* Props for the V_AUTHORS.au_fname field.
DBSetProp('V_AUTHORS.au_fname', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.au_fname', 'Field', 'Updatable', .T.)
DBSetProp('V_AUTHORS.au_fname', 'Field', 'UpdateName', 'dbo.authors.au_fname')
DBSetProp('V_AUTHORS.au_fname', 'Field', 'DataType', "C(20)")
* Props for the V_AUTHORS.phone field.
DBSetProp('V_AUTHORS.phone', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.phone', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.phone', 'Field', 'UpdateName', 'dbo.authors.phone')
DBSetProp('V_AUTHORS.phone', 'Field', 'DataType', "C(12)")
* Props for the V_AUTHORS.address field.
DBSetProp('V_AUTHORS.address', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.address', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.address', 'Field', 'UpdateName', 'dbo.authors.address')
DBSetProp('V_AUTHORS.address', 'Field', 'DataType', "C(40)")
* Props for the V_AUTHORS.city field.
DBSetProp('V_AUTHORS.city', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.city', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.city', 'Field', 'UpdateName', 'dbo.authors.city')
DBSetProp('V_AUTHORS.city', 'Field', 'DataType', "C(20)")
* Props for the V_AUTHORS.state field.
DBSetProp('V_AUTHORS.state', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.state', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.state', 'Field', 'UpdateName', 'dbo.authors.state')
DBSetProp('V_AUTHORS.state', 'Field', 'DataType', "C(2)")
* Props for the V_AUTHORS.zip field.
DBSetProp('V_AUTHORS.zip', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.zip', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.zip', 'Field', 'UpdateName', 'dbo.authors.zip')
DBSetProp('V_AUTHORS.zip', 'Field', 'DataType', "C(5)")
* Props for the V_AUTHORS.contract field.
DBSetProp('V_AUTHORS.contract', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.contract', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.contract', 'Field', 'UpdateName', 'dbo.authors.contract')
DBSetProp('V_AUTHORS.contract', 'Field', 'DataType', "L")
Chapter 6: Extending Remote Views with SQL Pass Through 121

Figure 15 shows the commands captured by SQL Profiler when the remote view
was opened.

Figure 15. Opening the remote view.

As with the simple SQL pass through query, nothing special is sent to the server.
Figure 16 shows the results of changing a single row and issuing a TABLEUPDATE().

Figure 16. After changing one row and calling TABLEUDPATE().

Just like the parameterized SQL pass through, Visual FoxPro (and ODBC) uses
sp_executesql to make the updates. In fact, modifying multiple rows and issuing a
TABLEUPDATE() results in multiple calls to sp_executesql (see Figure 17).

Figure 17. The commands sent to SQL Server when multiple rows of a remote view
are modified and sent to the server with a single call to TABLEUPDATE(.T.).

This is exactly the situation for which sp_executesql was created. The au_lname column of
the first two rows was modified. SQL Server will be able to reuse the execution plan from the
first query when making the changes for the next, eliminating the work that would have been
done to prepare the execution plan (parse, resolve, optimize and compile) for the second query.
What have we learned? Overall, remote views and SQL pass through cause the same
commands to be sent to the server for roughly the same situations, so the performance should
be similar. Given these facts, the decision to use one over the other must be made based on
other criteria.
Remote views are a wrapper for SQL pass through and, hence, a handholding mechanism
that handles the detection of changes and the generation of the commands to write those
changes back to the data store. Anything that can be done with a remote view can also be done
using SQL pass through—although it may require more work on the part of the developer.
However, the converse is not true. There are commands that can only be submitted using SQL
pass through. Returning multiple result sets is the most obvious example.
Remote views require the presence of a Visual FoxPro database, which might be a piece
of baggage not wanted in a middle-tier component. On the other hand, the simplicity of
122 Client/Server Applications with Visual FoxPro and SQL Server

remote views makes them a very powerful tool, especially when the query is static or has
consistent parameters.

Using remote views and SPT together


In most cases, you don’t have to choose between using remote views vs. SQL pass through.
Combining the two in a single application is a very powerful technique. All the SQL pass
through functions, including SQLExec(), SQLCommit(), SQLRollback(), SQLGetProp() and
SQLSetProp(), can be called for existing connections. So if a connection to the server is
established by a remote view, then you can use the same connection for SQL pass through.
To determine the ODBC connection handle for any remote cursor, use
CURSORGETPROP():

hConn = CURSORGETPROP("ConnectHandle")

In the following example, the previously described v_authors view is opened, and then its
connection is used to query the titles table:

USE v_authors
hConn = CURSORGETPROP("ConnectHandle", "v_authors")
lnResult = SQLExec(hConn, "SELECT * FROM titles")

If your application uses remote views with a shared connection, then by using this
technique you can use a single ODBC connection throughout the application for views and
SQL pass through. The following sections give some brief examples of how combining remote
views with SQL pass through can enhance your applications.

It is impossible to allow views to use a connection handle that was acquired


‡ by a SQL pass through statement. Therefore, to share connections
between views and SQL pass through statements, you must open a view,
acquire its connection, and then share it with your SQL pass through commands.

Transactions
Even if remote views suffice for all your data entry and reporting needs, you will need
SQL pass through for transactions. Transactions are covered in greater detail in Chapter
11, “Transactions.”

Stored procedures
Consider the example of a form that firefighters use to report their activity on fire incidents.
It uses 45 different views, all of which share a single connection, for data entry. However,
determining which firefighters are on duty when the alarm sounds is too complicated for a
view. A stored procedure is executed with SQLExec() to return the primary keys of the
firefighters who are on duty for a particular unit at a specific date and time. The result set is
scanned and the keys are used with a parameterized view that returns necessary data about
each firefighter.
Chapter 6: Extending Remote Views with SQL Pass Through 123

Filter conditions
Suppose you give a user the ability to filter the data being presented in a grid or report. You can
either bring down all the data and then filter the result set, or let the server filter the data by
sending it a WHERE clause specifying the results the user wants. The latter is more efficient at
run time, but how do you implement it? Do you write different parameterized views for each
possible filter condition? Perhaps, if there are only a few. But what if there are 10, 20 or 100
possibilities? Your view DBC would quickly become unmanageable.
We solved this problem by creating a single view that defines the columns in the result set,
but does not include a WHERE clause. The user enters all of his or her filter conditions in a
form, and when the OK button is clicked, all the filter conditions are concatenated into a single,
giant WHERE clause. This WHERE clause is tacked onto the end of the view’s SQL SELECT,
and the resulting query is sent to the back end with SQLExec(). Here’s an example with a
simple WHERE clause looking for a specific datetime:

*-- Open a view and use it as a template


USE myview IN 0 ALIAS template
lnHandle = CURSORGETPROP("ConnectHandle", "template")

*-- Get the SQL SELECT of the template


lcSelect = CURSORGETPROP("SQL", "template")

*-- Create a WHERE clause and add it to the SQL


lcWhere = " WHERE alarmtime = '" + lcSomeDatetime + "'"
lcSelect = lcSelect + lcWhere

*-- Execute the new query


lnSuccess = SQLExec(lnHandle, lcSelect, "mycursor")

You can even make the new result set updatable by simply copying some of the properties
from the updatable view used as a template:

*-- Copy update properties to the new cursor


SELECT mycursor
CURSORSETPROP("Buffering", 5)
CURSORSETPROP("Tables", CURSORGETPROP("Tables", "template"))
CURSORSETPROP("UpdateNameList", CURSORGETPROP("UpdateNameList", "template"))
CURSORSETPROP("UpdatableFieldList", CURSORGETPROP("UpdatableFieldList",
"template"))
CURSORSETPROP("SendUpdates", .T.)

Summary
In this chapter, we explored the capabilities of SQL pass through and how to use it effectively.
The next chapter takes a look at building client/server applications that can scale down as well
as up, allowing you to use a single code base for all your customers.
124 Client/Server Applications with Visual FoxPro and SQL Server
Chapter 7: Downsizing 125

Chapter 7
Downsizing
Scalability is a popular term these days and is commonly used to mean that something
small can be made big. But what about the opposite situation? What about taking
something big and making it small? What about taking client/server systems and
downsizing them so they work where SQL Server might not be appropriate or cost-
effective? This chapter addresses how developers can maximize their efficiency by
creating systems that can use either client/server or file-server back ends with a single
code base. In this chapter, you’ll learn how to write applications for either VFP or SQL
Server back ends, and how to downsize them by using either remote SQL Server views
and local VFP views or remote SQL Server and remote VFP views. You’ll also learn how
the Microsoft Data Engine (MSDE) lets you deploy true client/server applications on any
computer—for free.

The case for a single code base


Let’s consider the example of a client/server application that was written years ago for a
company with an installed base of 1,800 file-server systems. The dealers naturally wanted to
work on accounts that paid big commissions, and many of those accounts wanted client/server.
The app in question was a 10-year-old FoxPro application that had been converted to VFP 3.0
(during the VFP 3.0 beta, a long story in itself) and had undergone extensive updates and
improvements. It didn’t take long to realize that using the existing code might yield a
client/server application quickly, but it would be a terrible product. Lacking the resources to
write two products, the solution was to write a single new application that could switch between
a SQL Server or VFP back end at any time.
Some of us write shrink-wrapped software, but most VFP developers are consultants
who are working on a different application for each client. If each client gets a different
application, how can you have a single code base? Hopefully this is a rhetorical question, and
you reuse as much code as necessary. If your application framework and common classes are
designed with multiple back ends in mind, you can use them for any application, file-server
or client/server.

Interchangeable back ends


Hopefully you’ve already read the previous chapters and learned about client/server-style
development. The first step to ensure the success of any application or components that are
back-end-independent is to make sure you design client/server-style. It is much better to have a
client/server-style file-server application than to try to do it the other way around.
Any object-oriented programmer can tell you that the key to code reusability is abstraction.
The same is true if you want to reuse your code with different back ends. You need to abstract
your data access functionality into a few distinct components. The obvious place to start is with
the data access mechanism itself. As explained earlier, this book deals primarily with VFP
126 Client/Server Applications with Visual FoxPro and SQL Server

views for data access, but other abstract mechanisms, such as ADO, will work as well. For
details on using ADO, see Chapter 12, “ActiveX Data Objects.”
A client/server-style application using Visual FoxPro data can be designed using two types
of views: local or remote.

Remote views of VFP data


While it may not seem obvious, a VFP application can access VFP data via remote views by
using the VFP ODBC driver. This means that the actual opening and closing of tables,
processing of queries and so forth are handled by the VFP ODBC driver instead of VFP itself.
As you learned in Chapter 2, “Visual FoxPro for Client/Server Development,” ODBC adds
another layer of overhead to your application. You might think this is unnecessary overhead
that will degrade performance. Under some circumstances, you would be correct. But other
times, you would be surprised by performance improvements. In informal tests using remote
views of VFP data where the database resided on the local workstation, performance via ODBC
was approximately 80 percent slower than using local views of the same tables. In other words,
it took 80 percent longer to perform a query via ODBC than with native VFP access. But when
the data was moved to a network file server, the performance equation reversed itself! In fact,
in many cases ODBC performance was observed to be up to 250 percent better than native.
What could possibly explain ODBC performance that is better than native performance?
Nobody at Microsoft is talking, but here’s a theory. If you create two views of a VFP table, one
local and one remote, and then USE them NODATA and observe the results with Network
Monitor, you’ll see a surprising difference in network traffic. The local view produces several
times the network traffic of the remote view. The remote view seems to bring down the schema
only; we suspect that the local view causes VFP to bring down a bunch of stuff that it may need
for Rushmore optimization. When a query is executed, VFP may or may not have downloaded
what it actually needed to optimize the query. If it downloaded the right stuff, then the query is
faster. But if it was wrong, then not only is the query slower, but a bunch of unnecessary stuff
came down over the wire. The ODBC driver doesn’t seem to make such guesses. That means it
needs to bring down everything necessary for each query, but it doesn’t bring down anything it
doesn’t need, either. Remember, this is just a theory. We’re not privy to the inside workings of
Visual FoxPro’s database engine.
All this isn’t to say that you should jump on this technique in order to improve
performance, but that ODBC performance may be better than you think.

Advantages of remote VFP data


The biggest reason to use remote views of VFP data is that branching logic for the different
back ends is almost totally eliminated. Here are the main advantages of using remote VFP data
(that is, VFP data via ODBC):
• Since both back ends are connected via ODBC, the mechanics of establishing a
connection is the same.
• Remote views can have any name, including the name of the table on the back end.
But local views must have a different name than the table, as VFP will open both the
view and the table when using local views.
Chapter 7: Downsizing 127

• On-the-fly queries can be created anywhere in an application using SQL pass


through, as it is compatible with both back ends. Although there are limitations in
SQL syntax compatibility, by and large a SELECT can be passed through to either
back end.
• Cursors created on the fly can be made updatable with remote data, with either back
end. Local cursors created with VFP’s native SELECT are not updatable. See Chapter
6, “Extending Remote Views with SQL Pass Through,” for more information on
updatable cursors.
• Transactions can be handled the same way with either back end by using ODBC
transactions via SQLSETPROP().

Later in this chapter, you will learn details of how to create components that keep back-
end-dependent branching code in a limited number of places. If your application uses remote
views of VFP data, almost no branching logic is necessary.

Disadvantages of remote VFP data


The first time we used this technique to create an application, it seemed almost too good to be
true. It was. The disadvantages of remote VFP views are primarily related to the limitations of
the VFP ODBC driver:
• Only a limited subset of VFP syntax is supported by the ODBC driver. This makes it
extremely difficult to write stored procedures that work via ODBC.
• The VFP ODBC driver documentation doesn’t offer much help in figuring out the
previous point.
• The VFP ODBC driver doesn’t have a debugger.
• Field defaults that use stored procedures don’t work. The procedure executes, but the
value doesn’t end up in the field.
• Calling stored procedures via ODBC isn’t supported.

Another disadvantage is unrelated to the ODBC driver, per se, but rather has to do with the
normal differences among any two back ends: The SQL syntax differs between VFP and SQL
Server or between any two back ends. A SQL pass through command that works for one back
end may not work for the other. However, as long as you pass through simple SQL syntax that
is compatible with both databases, this isn’t a problem.

Creating remote views of VFP data


Most of what you need to know to use VFP data remotely is the same as what you learned
about remote views of SQL Server data in Chapter 4, “Remote Views.” The main difference is
in creating an ODBC DSN. To do so, use the same steps as in Chapter 4, but use the Microsoft
Visual FoxPro driver instead of the SQL Server driver. Figure 1 shows the Setup dialog for a
VFP ODBC connection.
128 Client/Server Applications with Visual FoxPro and SQL Server

Figure 1. The ODBC Visual FoxPro Setup dialog, expanded to show all options.

If you are connecting to a VFP database, you set the path to be the full path and file
name of the DBC. When connecting to free tables, you specify the directory only. Note that
UNC paths are supported. The lower portion of the dialog, below the path, is only visible
after the Options button is clicked. The settings there correspond to SET COLLATE, SET
EXCLUSIVE, SET NULL, SET DELETED and FetchAsNeeded.

Substituting local views for remote views


The obvious way to write a client/server-style application against a VFP back end is with
VFP’s local views. This is the best solution in most situations, but it does require that you write
more code to abstract data-handling functionality. Abstracting data-handling functionality is
covered in more detail later in this chapter.
Local views differ from remote views in that no connection is used. The database and table
are specified in the local view definition.

CREATE SQL VIEW myview AS ;


SELECT * FROM mydatabase!mytable

As long as the VFP database is in the search path, this syntax works fine. Put your local
views in a separate DBC from your VFP database and give a copy to each user, just as you
would with remote views. In other words, your application uses two DBCs: one with views and
one with tables. This makes your application more modular and reliable and makes it easier to
use the data environment of forms.
Chapter 7: Downsizing 129

One nice thing about local views is that the VFP View Designer doesn’t hiccup nearly as
often as with remote views. Many remote views with joins cannot be edited in the View
Designer and require all work to be done in code. But this is much less often the case with
local views.
When you open a local view, VFP actually uses at least two work areas. If the view is
based on a single table and no view of that table has been opened yet in the current data
session, then after you USE the view you will see one work area for the view, and one for the
table itself. When the view joins multiple tables, one work area will be used for the view and
one for each table in the join. Figure 2 shows the three work areas opened for the following
local view of data in the VFP TasTrade sample:

CREATE SQL VIEW VcustOrders AS ;


SELECT * ;
FROM tastrade!customer JOIN tastrade!orders ;
ON customer.customer_id = orders.customer_id

Figure 2. The VFP Data Session window showing three work areas opened for a
single local view.

Another nice feature of local views is that if you create a multi-table join on tables for
which relations are defined, the View Designer will automatically detect those relations and
create join conditions to match, as shown in Figure 3.
130 Client/Server Applications with Visual FoxPro and SQL Server

Figure 3. The VFP View Designer will automatically detect persistent relations
between tables and create join conditions that match.

Abstracting data access functionality


Using two different back ends with a single application could easily turn into a nightmare if you
aren’t careful. After all, there are numerous places where different actions are required for the
different back ends. You could end up with lots of branching logic like this:

IF VFP back end


Do VFP stuff
ELSE
Do SQL Server stuff
ENDIF

Yet your application must be able to provide both types of functionality. The way to
prevent unmanageable spaghetti is to pull the branching code out into a few abstract
components that are then used by various parts of the application when working with data.
There are three main areas where you should perform this abstraction:
• Application-level data-handling class(es)
• Form-level data-handling class(es)
• Views DBC
Chapter 7: Downsizing 131

Application-level data handler


Application startup is a good time to handle various differences in the back ends, such as
connecting to the database. Connecting with SQL Server might entail opening a remote view to
establish an ODBC connection, along with login security. With VFP you might open each table
up front so that index keys are downloaded over a slow connection at startup, not when each
view is opened in individual forms.
A good way to handle this is to have an application object determine which back end is
being used and then instantiate the desired application-level data handler. Base all data-
handling classes on the same abstract class so they’ll share the same interface. Then they can be
used interchangeably. This technique of selectively instantiating a particular class based on run-
time conditions is called a class factory.
Following is a list of some of the data-handling functionality you might want to put into an
application-level data handler:
• Establishing the connection to the database. With a SQL back end, this is done by
opening a remote view with a shared connection in order to load the ODBC DLLs and
open an ODBC connection on the server. With a VFP back end, it may be as simple as
setting the search path to the location of the VFP database.
• Handling security. With SQL Server, this may be as simple as letting the connection
handle the login or as elaborate as applying roles to the connection. With VFP, as
there is no built-in security, you’ll have to do it all in your application code.
• Ensuring that application and database versions are synchronized. We consider it
good practice to put a version number on a database and include metadata identifying
the version in the application. This helps ensure that the two are in synch. The
application-level data handler checks that they are in synch and, if not, either warns
the user and shuts down the application or runs whatever routines are necessary to
re-synchronize them.
• Doing the slow stuff at startup. For example, over a low-bandwidth connection to a
VFP database, opening tables for the first time can be quite slow. Once a table has
been opened, however, queries against it are generally of acceptable speed because the
table doesn’t have to be reopened. Rather than slow down application performance
each time a table must be queried, you might consider looping through each table in a
DBC and opening it. This technique slows down application startup, but it makes
other actions the user takes later appear pretty zippy. Opening all the tables in a DBC
is pretty easy, as you can use the ADBOBJECTS() function or even USE the DBC
itself as a table to get the name of every table in it:

SELECT DISTINCT objectname ;


FROM mydatabase.dbc ;
WHERE objecttype LIKE 'Table'

You also might want to let the application-level data handler provide other database-
specific services to the remainder of the application. Although we prefer to put non-startup
functionality in the form-level data handler, we do use a method in the application-level data
handler that returns which type of back end is being used.
132 Client/Server Applications with Visual FoxPro and SQL Server

Form-level data handler


Forms generally require data-handling services. How you implement these services depends on
which back end you use. Because forms with private data sessions are isolated from one
another, each form requires some of its own data-handling functionality. As with application-
level data handling, we use a class factory to instantiate the correct data-handling class for each
form. The form asks the application-level data handler which back end is being used and
instantiates the appropriate form-level data handler. These data-handling classes, as with the
application-level data-handling classes, are subclassed from a single class to ensure they share
the same interface. If the data handler is instantiated from the form’s Load event, it will exist
before any objects on the form are instantiated and before the form’s DataEnvironment object
is instantiated.
The first thing to have the data handler do is open the correct views DBC. In order to
simplify multi-programmer development, it’s preferable to use views DBCs with different
names for the different back ends. So the data handler simply opens the appropriate DBC, the
name of which is stored in a property of the data-handling class. Once the views DBC is
opened, you won’t have to refer to it again, as each view can be accessed simply with USE and
without passing the DBC name to it.
However, note that we don’t use the form’s DataEnvironment object, preferring instead to
open all views with good old-fashioned procedural code. Furthermore, since we prefer to keep
form definitions in class libraries (VCXs), rather than “form” files (SCXs), we don’t have a
DataEnvironment object to work with anyway. But if you do use the DataEnvironment, note
that it stores the name of the DBC. So you’ll either have to change the name of the DBC for
each cursor object in the DataEnvironment’s BeforeOpenTables event or be sure that your
different views DBCs have the same name. The latter is definitely simpler for coding but a little
more difficult to maintain, as it is so easy for developers to make changes to the wrong versions
of like-named files.
We create five methods on the data handler and use these methods to replace five native
VFP functions and/or SQL pass through commands, as shown in Table 1.

Table 1. Five form-level data handler methods and the VFP functions they replace.

Method VFP function


UpdateTable() TABLEUPDATE()
RevertTable() TABLEREVERT()
BeginTransaction() BEGIN TRANSACTION or SQLEXEC(nHandle, “Transactions”, 2)
CommitTransaction() END TRANSACTION or SQLCOMMIT()
RollbackTransaction() ROLLBACK or SQLROLLBACK()

We’d never write code like Listing 1 in a form.

Listing 1. A simple snippet that begins a transaction and attempts to update two
views. If either update fails, the transaction is rolled back; otherwise, it is committed.

BEGIN TRANSACTION
DO CASE
CASE ! TABLEUPDATE("view1")
ROLLBACK
Chapter 7: Downsizing 133

CASE ! TABLEUPDATE("view2")
ROLLBACK
OTHERWISE
END TRANSACTION
ENDCASE

Instead, we would use the form-level data handler as shown in Listing 2.

Listing 2. A snippet that does the same thing as the code in Listing 1, but calls the
form-level data handler instead of making the calls directly.

WITH THISFORM.oDataHandler
.BeginTransaction()
DO CASE
CASE ! .UpdateTable("view1")
.RollbackTransaction()
CASE ! .UpdateTable("view2")
.RollbackTransaction()
OTHERWISE
.CommitTransaction()
ENDCASE
ENDWITH

In Chapter 6, “Extending Remote Views with SQL Pass Through,” you learned about
transaction handling with remote data, which explained why the transaction-handling
methods should be different between the two data handlers. The VFP data handler’s
BeginTransaction() method simply needs to pass through BEGIN TRANSACTION,
something like this:

BEGIN TRANSACTION

while the SQL Server handler sets the connection’s Transactions property to manual:

SQLSETPROP(lnHandle, "Transactions", DB_TRANSMANUAL)

Naturally, each method needs to check existing settings and so forth, but the preceding
code shows the primary functionality.
The meat of the CommitTransaction() method for VFP looks like this:

END TRANSACTION

while the SQL version looks like this:

SQLCOMMIT(lnHandle)
SQLSETPROP(lnHandle, "Transactions", DB_TRANSAUTO)

Note that the VFP command END TRANSACTION both commits and ends a
transaction, but that the SQL Server version must set the Transactions property back
to automatic. The RollbackTransaction() methods are essentially the same as
134 Client/Server Applications with Visual FoxPro and SQL Server

CommitTransaction(), but ROLLBACK is substituted for END TRANSACTION and


SQLROLLBACK() for SQLCOMMIT().
The UpdateTable() and RevertTable() methods simply pass parameters to VFP’s
TABLEUPDATE() and TABLEREVERT() functions, respectively. We abstract them in case
we want to add back-end-specific functionality here. We’d sure hate to suddenly have to find
and replace thousands of calls to TABLEUPDATE()because a need for back-end-specific
functionality arose that wasn’t foreseen earlier in the project.

Views DBC
If you’ve worked with lots of local data in VFP applications, you may be in the habit of calling
DBCs “databases.” If you do, wash your mouth out with soap right now and don’t do it again.
A DBC is not truly a database. A database is a collection of tables, while a DBC is nothing
more than a metadata table containing information about tables, views and/or connections. You
wouldn’t give every user his or her own copy of a database, but you can and should give every
user his or her own copy of the views DBC. The DBC contains nothing more than a bunch of
code and properties defining the views and connections. If each user has a copy, you don’t have
to worry about pathing, and you can temporarily store all kinds of useful, user-specific data in
the DBC. This technique is covered in more detail in Chapter 4, “Remote Views.”
Since a view is nothing more than a collection of code and properties, it can be used to
abstract data access functionality. A view of the same name in two different DBCs can be
defined differently for different back ends. Stored procedures with the same interface can do
different things. Properties for objects in the DBCs can be set differently. In fact, these are the
main things to do differently in your views DBCs.
Each view definition must be written using the SQL syntax supported by the appropriate
back end, as back end requirements and capabilities vary. For example, VFP and SQL Server
7.0 both support TOP n queries, but don’t try this with SQL 6.5. You’ll have to leave that
clause out of your SELECT and use the view’s MaxRecords property instead.
Different back ends also support different functions. For example, to produce a query
returning rows for a particular date in VFP, you would use the YEAR() function, but in SQL
Server, you would use the DATEPART(year) function. Different back ends also have different
keywords, so a view that works fine in VFP might fail in SQL Server because you attempted to
use a SQL Server keyword.
Just be sure that you create views that look the same to the front end. This may take
some trial and error, and you should work with both back ends at the same time. Consider
one author’s experience of working on a module and testing it with a VFP back end, only
to come back some time later and discover that a SQL Server keyword had been used in a
view definition, making it necessary to go back and change a bunch of code where the view
was used.
Some functions just seem to fit best as stored procedures in the views DBC. As long as
you’re only opening one DBC, you can call the stored procedure at any time with a simple
function or procedure call. One excellent use for DBC stored procedures is to generate primary
keys. With SQL Server, you may call a SQL stored procedure or use an identity column, while
with VFP you might get a primary key directly from the VFP database. Either way, if you
create a stored procedure in the views DBC, it can contain the logic that is appropriate for its
back end; all you do is call the procedure. If you put this sort of function in your application-
Chapter 7: Downsizing 135

level data handler, you might find yourself writing lightweight COM components where you
need this functionality and don’t want the overhead of a data-handling class. You can simply
move the code to the views DBCs and rewrite the data handler to pass the call through to the
stored procedure.
Finally, you can set properties for each object in the DBC. One good example of the need
for this is to make back-end-specific data type conversions, as with date fields in VFP tables
and datetime fields in SQL Server tables. As SQL Server has no date data type, you must use
datetimes. This is no problem if you also use datetimes in the VFP schema, but if you used
dates in VFP, then you simply change the DataType property of the view’s field. A SELECT of
a date field in a VFP table will automatically set the data type to date. You can easily change it
like this:

DBSETPROP("myview.mydatefield", "Field", "DataType", "T")

So hard to get a date


There’s one final major gotcha when you write against both VFP and SQL Server back ends:
empty and null dates.
When you insert a row directly to a VFP table and you don’t specify a value for a nullable
column, that column will be stored as a NULL unless the column has a default value. When you
use a remote view to add a row, you get the same results. But if you add rows with local views,
instead of a NULL, the column will contain an empty value. This isn’t a problem with most
data types—you simply check for EMPTY() and/or ISNULL():

IF EMPTY(myfield) OR ISNULL(myfield)
Do something

But datetimes are different. EMPTY() will return FALSE in a remote view containing an
empty datetime field. If the empty datetime field is in SQL Server, then in a remote view it will
appear as 01/01/1900 12:00:00 AM (with SET(“DATE”) = “MDY”). To complicate matters
further, an empty datetime field in a remote view of VFP data will be 12/30/1899 12:00:00
AM. So every time you test for an empty or null datetime, you also have to test for it being
equal to one of these datetime values.
This is an excellent argument for writing your own function to test for EMPTY() or
ISNULL(). If the VARTYPE() of the value being tested is “T,” be sure to test for the “empty”
datetime values.
To make matters worse, you may have to deal with actual dates that are the same as the
“empty” ones. There are people alive today who were born on those days, though admittedly
not many. Fortunately, you most likely don’t have to deal with the time for those dates. So to
ensure that you are dealing with an actual datetime, rather than a phantom one, set the time
component to something other than 12:00:00 AM. We use 12:00:01 AM instead.
By the way, you can’t ensure against empty dates in your user interface because somebody
can always get to a field some other way. Remember, your application isn’t the only way
people can get to data.
136 Client/Server Applications with Visual FoxPro and SQL Server

Microsoft Data Engine (MSDE)


For years we’ve been writing applications that can use either VFP or SQL Server back ends, all
the while thinking that there’s got to be a better way. Wouldn’t it be nice if you could serve all
users with a single back end? In June 1999, Microsoft released the Microsoft Data Engine. It
just might be the better way we’ve been looking for. A developer can now create an application
for SQL Server and distribute it for smaller systems using MSDE.

What is MSDE?
MSDE is a client/server database that’s 100 percent compatible with SQL Server 7.0. It
is included with Microsoft Office 2000 Premium and Developer editions, and a royalty-free
run-time distribution version is available for licensed users of any of the following
Microsoft products:
• Visual FoxPro 6.0, Professional edition
• Visual Basic 6.0, Professional and Enterprise editions
• Visual C++, Professional and Enterprise editions
• Visual InterDev 6.0, Professional edition
• Visual J++ 6.0, Professional edition
• Visual Studio 6.0, Professional and Enterprise editions

Following are some of the key features of MSDE.

Free run-time distribution and licensing


This is one of the best parts: MSDE is free. At the time of this writing, the royalty-free run-time
engine can be downloaded at http://msdn.microsoft.com/vstudio/msde. Better yet, visit the
same site and order a free CD-ROM that contains the run-time engine as well as a free copy of
SQL Server 7.0 Developer edition, which is licensed for developing MSDE solutions. This
same CD is also included in the MSDN Universal Subscription.

SQL Server compatibility


MSDE is fully compatible with SQL Server 7.0:
• They use the same ODBC driver and are both fully accessible with SQL
Database Management Objects (SQL-DMO).
• They support exactly the same set of SQL commands and the same version of the
T-SQL language.
• They support the same files. A database can be migrated from MSDE to SQL Server
or vice versa simply by detaching the database from one, moving the files and
attaching to the other.
Chapter 7: Downsizing 137

• MSDE, like SQL Server, provides multi-processor support with Windows NT


and 2000.
• They are both compatible with Microsoft Distributed Transaction Coordinator
(MS DTC), which is covered in Chapter 1, “Introduction to Client/Server.”
This allows separate databases in MSDE and SQL Server to participate in the
same transaction.
• MSDE databases can be replicated to SQL Server and vice versa.

‡ Replication of MSDE databases requires a SQL Server client access


license to replicate with other SQL Server databases.

Operating system compatibility


MSDE runs on Windows 95, 98, NT and 2000. Windows NT and 2000 are supported on both
Intel and DEC Alpha platforms.

Microsoft Office 2000


As mentioned earlier, MSDE is now a feature of Office 2000 Premium and Developer editions.
Access 2000 must already be installed in order to install MSDE from the Office 2000 CD,
which can be found in the Sql\X86\Setup folder. Once installed, Access 2000 “Data Projects”
can be created and maintained using either the Jet engine or MSDE. Makes one wonder what
Microsoft’s plans are for the future of Jet.

MSDE vs. SQL Server


If MSDE sounds sort of like a free version of SQL Server, it isn’t. There are many differences
between the two engines, the two biggest of which are: MSDE is tuned for five concurrent
users and has no user interface at all. Following are some details of the differences between
SQL Server 7.0 and MSDE.

User limitations
Microsoft says MSDE is tuned for five or fewer concurrent users. What does this really mean?
Theoretically it means the server is likely to be actively handling requests from five users at the
same time. But it doesn’t mean an MSDE system is limited to five users.
In order to explore the limits of this feature, we did some tests to attempt to determine how
many users could be logged into MSDE at the same time and how performance was affected as
the number of users went up. We were able to connect more than 100 users without a problem.
However, there was a severe performance penalty as the number of users increased. With 15 or
fewer connections, there seemed to be no difference in performance between MSDE and SQL
Server 7.0 on similarly configured systems. But as soon as a sixteenth user was connected,
everything slowed down dramatically. When 16 users were connected, everything was slower
than with 15, even when only one user was actually doing anything.
138 Client/Server Applications with Visual FoxPro and SQL Server

Capacity limitations
Each MSDE database is limited to 2GB. Bigger databases require SQL Server.

No user interface
SQL Server ships with Enterprise Manager, Performance Monitor, Profiler, Query Analyzer
and other great tools for administering SQL Server and designing/modifying databases. None
of these tools are included in MSDE. However, if those tools exist on a network, they can be
used to manage a MSDE server. We’ll discuss three other possible tools here: Microsoft
Access 2000, Microsoft Visual InterDev 6.0 and in-house tools.

Access 2000
Microsoft Access 2000 can be used to manage most aspects of an MSDE server, including
database schema, triggers, stored procedures, views, security, backup/restore and
replication. Some things that aren’t particularly easy to work with from Access are user-
defined data types, defaults and rules. Many of the individual administrative tasks are
performed almost identically in Access and Enterprise Manager. For example, creating
views uses tools that differ in the two products only in their toolbars. Figure 4 shows the
design surface for views in Access, and Figure 5 shows the Enterprise Manager version,
which offers a toolbar.

Figure 4. Designing the Northwind database’s Quarterly Orders view in


Access 2000.
Chapter 7: Downsizing 139

Figure 5. The Northwind database’s Quarterly Orders view in Enterprise Manager.


Note the addition of a toolbar.

Visual InterDev 6.0


Visual InterDev offers another alternative for managing MSDE databases. In comparison to
Access, you have the ability to automatically generate T-SQL scripts for schema creation or
changes. Figure 6 shows part of a change script generated in Visual InterDev.

Figure 6. A dialog containing a change script created by saving a schema change in


Visual InterDev 6.0.
140 Client/Server Applications with Visual FoxPro and SQL Server

As with Access 2000, you’ll find many of the design surfaces in Visual InterDev to be
similar to those in Enterprise Manager. An example is the table design view, shown in Figure
7, which is identical to the one in Enterprise Manager.
However, as with Access, you’ll find that Visual InterDev isn’t a complete replacement for
the Enterprise Manager. For example, Visual InterDev doesn’t offer an easy way to manage
users or security.

Figure 7. The table design view in Visual InterDev 6.0 is identical to the one in
Enterprise Manager for SQL Server 6.0.

In-house tools
Chapter 10, “Application Distribution and Managing Updates,” discusses creating tools that
allow users to perform various management functions for SQL Server databases. After all, you
may need to make schema changes during the life of a project and, though you could require
users to run scripts in the Query Analyzer, providing the user with an application specifically
for managing your database may give you better control. MSDE makes such a tool/application
even more important. Perhaps your users don’t have Access or Visual InterDev. Maybe that’s
good, as users can do a fair amount of damage with such tools. But they may need to perform
simple tasks such as changing the administrators password or adding new users. These tasks are
fairly simple to perform and are discussed in greater detail in Chapter 10.
Chapter 7: Downsizing 141

Distributing MSDE applications


MSDE can be distributed royalty-free by using the MSDE for Visual Studio installation
program, which can be found on the MSDE for Visual Studio CD in the \MSDE directory. The
Intel version is MSDEx86.exe, and the Alpha version is MSDEAlpha.exe. Only the Intel
version will be discussed here. Although the \MSDE directory contains documentation on how
to use MSDEx86.exe, at the time of this writing the documentation on both the CD and the
Microsoft Web site is incorrect in some ways. Unfortunately, if you do something wrong in the
installation process, the process simply fails without any warnings or error messages. However,
a log file, setup.log, will be written to the Windows directory and can be checked for result
codes, which will be listed later in this chapter.
The MSDE installation program is an InstallShield self-extracting command-line program
and relies on a response file (.iss) for installation options. A default version of this file, called
unattend.iss, is in the \MSDE directory on the CD. You can use this file as-is for a default
installation. To perform the default installation, use this syntax:

c:\temp\msdex86.exe –a –f1 c:\temp\unattend.iss

The path in the command line must be the fully qualified path to the two files. You may
rename the .iss file, but you must pass the fully qualified path, not a relative path, or installation
will fail. If you use InstallShield or other commercial installers for your install program, you
can specify the path in an installation script with a memvar for the location the user selected
for installation.
Microsoft says in its documentation to surround the .iss file name and path with double
quotes, but we’ve found that doing so causes frequent installation failures. You must also use
the –a and –f1 switches or your installation will fail. Installation will also fail if there are any
spaces in either path on the command line.
The documentation also says to use a –s switch to cause a “silent mode” install, and that
omitting the switch will provide a user interface during the install. In tests, the switch does
nothing and you get a silent install whether you want it or not. Because you’re stuck with a
silent install that takes several minutes with no feedback to the user, be sure to warn them in
some way prior to performing the MSDE install.
The .iss file contains numerous installation settings, including the installation directory.
This path is hard-coded into the file, and you cannot use relative paths. This means the user has
no choice of destination directory for MSDE. You could programmatically change the file at
install time to substitute a user-defined path, but this would be quite a bit of work with most
installation programs.
The MSDE installation will also fail when certain registry keys exist on the target machine.
MSDE cannot be installed on a computer that has had SQL Server on it unless
SQL Server has been completely uninstalled. We found this out the hard way by attempting
to put MSDE on a machine that had once had SQL Server 6.5 on it. The 6.5 installation had
been upgraded to 7.0; 6.5 was later uninstalled. Unfortunately, numerous registry keys
remained behind.
The MSDE installation program writes a file called setup.log in your Windows directory.
This file looks just like an INI file, and there are four lines to look for to help debug the
installation. If everything went fine, it will look like this:
142 Client/Server Applications with Visual FoxPro and SQL Server

[Status]
Completed=1
[ResponseResult]
ResultCode=0

If the Completed value is anything other than 1, the installation failed. If the ResultCode
value is anything other than 0 or –1, the installation also failed. Even though the ResultCode
value of –1 is technically an error, if Competed is 1 and ResultCode is –1, then the installation
simply requires a reboot. Other ResultCode values are shown in Table 2.

Table 2. MSDEx86.exe installation ResultCode values.

Value Meaning
0 Success.
-1 General error, or requires reboot.
-2 Invalid mode.
-3 Required data not found in the .iss file.
-4 Not enough memory available.
-5 File does not exist.
-6 Cannot write to the response file.
-7 Unable to write to the log file (don’t know how you’d find this one out).
-8 Invalid path to the InstallShield silent response file.
-9 Not a valid list type (string or number).
-10 Data type is invalid.
-11 Unknown error occurred during setup.
-12 Dialog boxes are out of order.
-51 Cannot create the specified folder.
-52 Cannot access the specified file or folder.
-53 Invalid option selected.

After a successful installation, the Startup folder on the Start button will contain a shortcut
called “Service Manager.” This is the only user interface for MSDE. When the system is
booted, the Service Manager will start. However, by default, the MSDE service itself will not
be started. Start the Service Manager to start MSDE, and also check the “Auto-start service
when OS starts” check box so that MSDE will automatically start when the computer is booted.
If the user has a license for Microsoft Office 2000 Premium or Developer edition, then
MSDE can also be installed from the Office 2000 CD. Run \Sql\X86\Setup\Sqlsetup.exe from
the Office 2000 CD. This version will install Microsoft DTS (Data Transformation Service) in
addition to MSDE. This version of MSDE can only be installed on machines where Access
2000 is already installed.

Migrating MSDE databases to SQL Server


Because MSDE is file-compatible with SQL Server 7.0, you can move an MSDE database to a
SQL Server, or vice versa, at any time simply by copying the files from one server to another
and attaching them to the SQL Server.
Because the server locks the files open when it is running, you need to detach the database
from the server, which can only be performed when there are no connections into the database.
Here is the T-SQL command to detach the Northwind database in MSDE, which you can either
send to SQL Server with VFP SQL pass through or by using SQL Server’s Query Analyzer:
Chapter 7: Downsizing 143

EXEC sp_detach_db 'Northwind'

After copying all the appropriate MDF and LDF files to the new server, you attach
them like this:

EXEC sp_attach_db @dbname = N'Northwind',


@filename1 = 'd:\mssql7\data\northwnd.mdf',
@filename2 = 'd:\mssql7\data\northwnd.ldf'

There’s one minor catch. Although a list of users is stored in the sysusers table of each
database, the Security IDs (SIDs) required by SQL Server are actually stored in the
master..sysxlogins table. A user won’t be able to get into the database after the move
because the SIDs don’t match. This is easily corrected by running the SQL Server
sp_change_users_login stored procedure for every user. Listing 3 shows a VFP procedure that
calls sp_change_users_login for every user and application role in the database. If the ODBC
DSN you use to connect already specifies the database, then you don’t need to call this
procedure with the name of the database. This works only for normal SQL Server users and
application roles; it does not work for NT-integrated users. You might encounter situations
where this step isn’t required, such as when no logins are defined in a database because all
access is through the administrative login.

Listing 3. This code will reset the internal Security ID (SID) for a SQL database after it
has been moved from one server to another or from MSDE to SQL Server.

LPARAMETERS tcDatabase
LOCAL lnHandle, lcSQL, lnResult

*-- By connecting without parms VFP asks for DSN, then login and password
lnHandle = SQLCONNECT()

*-- Connect to a database if the tcDatabase parameter was received


* Otherwise, if a database is specified in the DSN, this parm is not needed
IF !EMPTY(tcDatabase)
lnResult = SQLEXEC(lnHandle, "USE " + tcDatabase)
IF lnResult < 0
RETURN .F.
ENDIF
ENDIF

*-- Must be for SQL Users and/or Application Roles only


* Does not work for NT Users, dbo, guest, or INFORMATION_SCHEMA
lcSQL = "SELECT name FROM sysusers WHERE (issqluser = 1 OR isapprole = 1) " + ;
"AND name NOT LIKE 'dbo' " + ;
"AND name NOT LIKE 'guest' " + ;
"AND name NOT LIKE 'INFORMATION_SCHEMA'"
lnResult = SQLEXEC(lnHandle, lcSQL, "sqlusers")
IF lnResult < 0
RETURN .F.
ENDIF

SELECT sqlusers
SCAN
144 Client/Server Applications with Visual FoxPro and SQL Server

*-- Call SQL Server stored procedure to fix logins


SQLEXEC(lnHandle, "EXEC sp_change_users_login 'Auto_Fix'," + ;
"'" + ALLTRIM(sqlusers.name) + "'")
ENDSCAN

USE IN sqlusers
RETURN

You aren’t limited to migrating only from MSDE to SQL Server. You
‡ can migrate the other way, too. However, be aware that MSDE has
capacity limitations that might prevent a large database from being
migrated to MSDE.

Summary
In this chapter, you learned a couple of approaches to using the same application code with
either a VFP or SQL Server back end: using remote views with SQL Server and local views
with VFP, and using remote views with both SQL Server and VFP. You also learned about
Microsoft Data Engine, a SQL-Server-compatible client/server database that just might
eliminate the need to code for more than one back end, as it allows you to deploy your SQL
Server application from laptops to the enterprise. In the next chapter, you’ll learn about error
handling in client/server applications.
Chapter 8: Errors and Debugging 145

Chapter 8
Errors and Debugging
Error handling and debugging in traditional Visual FoxPro file server applications is
relatively straightforward because traditional Visual FoxPro applications use a single
technology that controls the user interface, data access, and the actual storage of data.
Client/server applications use three separate technologies (the Visual FoxPro user
interface, ODBC and a SQL Server database), which must communicate with each other.
Because this architecture is more complex, the process of handling errors in the
application and debugging is also more complex. In this chapter, you will learn some of
the secrets of handling client/server errors. You are also introduced to some debugging
tools that will make debugging easier for you.

Handling errors
After reading this far into the book, you may have gathered that handling data errors in a
client/server application is not very different from a traditional Visual FoxPro application. This
is simply because most data updates are handled through a TABLEUPDATE() or SQLExec()
call, and you can use the AERROR() function to determine the cause of any failures. Any other
type of failure (such as an application error) is trapped with either the Error event of the object,
or through your global ON ERROR handler.
Unfortunately, it is not that simple: Handling client/server errors from Visual FoxPro
can get tricky, particularly when SQL Server is used in a way that is not friendly to the
client application.

Trapping errors
The first lesson to learn is how to trap the errors you receive during a TABLEUPDATE() call.
You probably know that this function does not report errors through the ON ERROR handler.
Instead, you must use the AERROR() function to capture the reasons for TABLEUPDATE()
failures. In file-server applications, this array has a single row containing the details of the
failure. For any Visual FoxPro error, the array contains the data shown in Table 1.

Table 1. The elements returned by AERROR() for Visual FoxPro errors.

Element Data type Contains Description


1 Numeric Number Visual FoxPro error number.
2 Character String Visual FoxPro error message.
3 Character NULL or string Optional error parameter (same as SYS(2018)).
4 Numeric NULL or number Work area where error occurred.
5 Numeric NULL or number Trigger that caused the failure (1=insert,
2=update, 3=delete).
6 NULL Always null.
7 NULL Always null.
146 Client/Server Applications with Visual FoxPro and SQL Server

However, for errors that occur through an ODBC update, such as when updating data on a
SQL Server with a remote view, the array will always have the same value, 1526, in the first
column. This is because all ODBC errors trigger the same Visual FoxPro error (1526). The
remaining elements of the array contain data that differs from a traditional Visual FoxPro error,
since ODBC errors are reported differently. The contents of the array from an ODBC error are
shown in Table 2.

Table 2. The elements returned by AERROR() for an ODBC error.

Element Data type Contains Description


1 Numeric 1526 Error number, always 1526.
2 Character Error message text. Visual FoxPro error message.
3 Character ODBC error The error parameter—same info as column 2,
message text but less of it
4 Character ODBC SQL state If the error is directly related to ODBC, this is an
error number that describes the error. These
codes can be found in the ODBC Programmer’s
Reference.
5 Numeric ODBC data source If the error is a SQL Server error, this element
error number contains the SQL Server error number, which
can be found in the SQL Server Books Online.
6 Numeric ODBC connection Shows the connection handle across which the
handle error occurred.
7 NULL Always null.

ODBC errors can create multiple rows in the array created by AERROR().
‡ Normally, you are primarily interested in the data of the first row, but the
other rows could contain important information that is related to the error
reported in the first row.

Reporting errors
By analyzing the array, you can quickly determine the cause of any update errors by reading the
fifth column and comparing the value there with error numbers from SQL Server. For example,
imagine that you are working with the sample pubs database on SQL Server. In this database,
there is an authors table, which contains information about the authors of the books in the
database. The sample CREATE TABLE statement shown here defines the first column of the
authors table, named au_id:

CREATE TABLE authors


(au_id varchar(11) NOT NULL
CHECK (au_id like '[0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]'),
--other columns defined here…

From this statement, you can see that the au_id column has a CHECK constraint
(equivalent to a field validation rule in Visual FoxPro), restricting the data to the format
Chapter 8: Errors and Debugging 147

999-99-9999, where 9 represents any valid digit. Therefore, if you attempt to place any data
into this column that does not meet this CHECK constraint, the operation fails.

The CHECK constraint created here does not specify a name for the
‡ constraint. This causes SQL Server to generate a name for the constraint
in the format CK__tablename__fieldname__xxxxxxxx, where xxxxxxxx is a
unique character string. To avoid this, you should always provide a name for all
constraints, for reasons that should become clear in the next few paragraphs.

Now imagine that you have created a view that retrieves data from the authors table and
allows the user to update that data. Suppose that after your user has pulled down a record to
edit, he or she decides to change the au_id column of an author. In doing so, the CHECK
constraint on the column is violated, which causes TABLEUPDATE() to fail. After invoking
the AERROR() function, the resultant array will contain the following information:

LAERROR Pub A
( 1, 1) N 1526 ( 1526.00000000)
( 1, 2) C "Connectivity error: [Microsoft][ODBC SQL Server
Driver][SQL Server]UPDATE statement conflicted with
COLUMN CHECK constraint 'CK__authors__au_id__08EA5793'.
The conflict occurred in database 'pubs', table
'authors', column 'au_id'."
( 1, 3) C "[Microsoft][ODBC SQL Server Driver][SQL Server]UPDATE
statement conflicted with COLUMN CHECK constraint
'CK__authors__au_id__08EA5793'. The conflict occurred in
database 'pubs', table 'authors', column 'au_id'."
( 1, 4) C "23000"
( 1, 5) N 547 ( 547.00000000)
( 1, 6) N 1 ( 1.00000000)
( 1, 7) C .NULL.
( 2, 1) N 1526 ( 1526.00000000)
( 2, 2) C "Connectivity error: [Microsoft][ODBC SQL Server
Driver][SQL Server]UPDATE statement conflicted with
COLUMN CHECK constraint 'CK__authors__au_id__08EA5793'.
The conflict occurred in database 'pubs', table
'authors', column 'au_id'."
( 2, 3) C "[Microsoft][ODBC SQL Server Driver][SQL Server]The
statement has been terminated."
( 2, 4) C "01000"
( 2, 5) N 3621 ( 3621.00000000)
( 2, 6) N 1 ( 1.00000000)
( 2, 7) C .NULL.

As you can see from this output, the array contains two rows. The first reports the
violation of the CHECK constraint, and the second tells you that SQL Server has terminated
the statement.
While you are developing your application, you can show the contents of the second or
third columns of this array, since only you (or other developers) would ever see the message.
However, if your end users are anything like the folks that we’ve encountered, they will surely
react with panic to a message like that one! Therefore, you will eventually want to capture an
error like this and “translate” it into something more friendly before putting the application in
front of them.
148 Client/Server Applications with Visual FoxPro and SQL Server

This is where the trouble begins, as there is no easy way to uniquely identify the errors
you receive back from SQL Server. For example, notice that the fifth column of the first row
of the array contains the SQL Server error number 547. If you check this error number in
the Help system of SQL Server, you will find that this error is reported for any kind of
constraint violation, not only CHECK constraints. This means that the only other way to
determine the exact cause of the error is to parse the character string in the second or third
column of the array.
In deciding which column to use, notice that the first and second rows of the array have the
same exact error message in the second column, but the third column reports a different error
for each row.
Regardless, using the contents of the error array to create a user-friendly message poses a
small problem that can really only be solved in one of two ways. The first is to search for the
constraint name (in this case, CK__authors__au_id__08EA5793) and present a friendly
message for each. While this seems easy enough, it requires that you always have an up-to-date
list of constraints that are on the server and details about their meaning. If the constraint is
modified at any time, you will have to update your client-side code to match. Also, refer to the
earlier note about how this constraint name came to exist. If someone simply regenerates the
schema of the database or the table, the name change could break all of your code.
A second approach to error handling is a radical change over this first approach: Do not
update the data on the server through views, but use stored procedures instead. The first step to
making this approach work is to define a stored procedure for handling the updates of author
records, as in the following T-SQL code:

/* Add the message */


EXECUTE sp_AddMessage
@Msgnum = 50001,
@Severity = 16,
@Msgtext = 'Invalid author ID specified. Use the format
999-99-9999 for author IDs.'
GO

/* Create the stored procedure */


CREATE PROCEDURE UpdateAuthors
@Key Varchar(11) = NULL,
@Au_ID Varchar(11) = NULL
AS
IF @Key IS NULL
RAISERROR ('You must provide the @Key parameter when
calling UpdateAuthors',16,1)
ELSE
IF @Au_ID IS NOT NULL
IF @Au_ID LIKE '[0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]'
UPDATE Authors
SET Au_ID = @Au_ID
WHERE Au_ID = @Key
ELSE
RAISERROR (50001,16,1)
ELSE
RAISERROR('No update occurred since @au_id was not provided',16,1)
go
Chapter 8: Errors and Debugging 149

This code starts by creating a system message that is stored in the master database of SQL
Server. This makes the message available to all databases, and it can be easily invoked by the
RAISERROR function. After the message is created, the stored procedure code can reference
the message through its unique error number (in this case, 50001). Note that this code merely
creates the system message and the stored procedure—it does not handle any kind of update of
an author record, nor does it actually execute the stored procedure.
With this procedure in place in the pubs database, instead of posting the update directly
through a Visual FoxPro view, you can now use a SQL pass through call:

*--turn off the non-trappable warnings


SQLSetProp(lhConn,"DispWarnings",.F.)
*--execute the stored proc to try the update
lnResult = SQLExec(lhConn,"EXECUTE UpdateAuthors "+ ;
"@key = 'keyval', @au_id = 'new value'")
IF lnResult < 0
*--something failed, check it out
lnRows = AERROR(laError)
*--more code normally follows, but is omitted here

The SQL pass through statement produces an error that can be captured from within Visual
FoxPro with the AERROR array. In this case, the error array looks like this:

LA Pub A
( 1, 1) N 1526 ( 1526.00000000)
( 1, 2) C "Connectivity error: [Microsoft][ODBC SQL Server
Driver][SQL Server] 'Invalid author ID specified. Use
the format 999-99-9999 for author IDs."
( 1, 3) C "[Microsoft][ODBC SQL Server Driver][SQL
Server] 'Invalid author ID specified. Use the format
999-99-9999 for author IDs."
( 1, 4) C "37000"
( 1, 5) N 50001 ( 50001.00000000)
( 1, 6) N 1 ( 1.00000000)
( 1, 7) C .NULL.

Therefore, if the stored procedure is programmed with these “friendly” error messages, you
can simply display these messages directly from the AERROR array. Furthermore, you can
trap for each specific error number (in this case, 50001) and translate the error message if
desired. However, to get this kind of information, you will need to use stored procedures and
forego the convenience of using views.
A third alternative is to not use any data validations on the server at all, and instead handle
all of them from within the client application. This approach performs well, but only if you are
writing the sole application that will ever touch the data on the server. Leaving the database
open with no validation whatsoever is typically not a good idea, as it makes it very easy for bad
data to enter the system.
Decisions, decisions… The choice is up to you. One major factor in your decision may
depend on how much access you have to the server. If you are the entire development
department, and you take over the SQL Server and the client-side application development,
then you can choose to either design your own custom stored procedures for the updates, or
monitor the naming of constraints and other rules so you can capture them when using views.
150 Client/Server Applications with Visual FoxPro and SQL Server

However, you may work in an environment where you do not “own” all of the pieces of
the application. For example, the SQL Server may already be in use by another department.
This department claims ownership of the server and the associated database that you need to
access in order to write your Visual FoxPro application. Since the database belongs to the
other department, you may have political problems in acquiring the necessary access to the
database(s) on that server. Without proper access, you will have a tough time determining the
current status of any rules, stored procedures and so on. This can definitely complicate matters,
and may force you to resort to error-handling techniques that otherwise would not be your
first choice.

Conflict resolution
No, this is not a section on how to deal with difficult employees or your “significant other.” It
is, however, meant to introduce another big difference between Visual FoxPro and SQL Server:
how they handle update conflict resolution.
When using native Visual FoxPro data, you have some choices when deciding how to deal
with update conflicts. Recall that this error occurs when two users have pending changes on the
same record and then attempt to commit their changes. Only one user can update at a time, so
when the first user finishes his or her update, the second user is left with a problem because the
data on disk is not the same as it was when the edit began. You can control Visual FoxPro’s
behavior through the second parameter of TABLEUPDATE(), which allows you to modify
how the second user experiences this problem.
If you set the second parameter to False, the second user’s update fails with error 1585,
“Update conflict,” as Visual FoxPro will detect that the first user updated the disk values while
the second user was waiting with pending changes. On the other hand, if you set the second
parameter to True, the second user is permitted to overwrite the first user’s changes without
questions or errors. Informally known as “last one in, wins,” this avoids the update conflict
problem entirely. This is a great choice if it is not common for two users to edit the same
records concurrently, and it reduces the amount of error-handling code that you have to write.
When working with remote data, the same options are available and operate similarly.
What changes is how you handle an update conflict error (i.e., use False for the second
parameter). When a conflict occurs against Visual FoxPro data, you can use the OLDVAL()
and CURVAL() functions to view the state of a field before it was edited and the actual value
on disk, respectively. This allows you to go so far as to show the user what changes were made
by the other user and let them evaluate how to proceed.
However, when dealing with remote data, CURVAL() is worthless, as it always returns
the same value as OLDVAL(). Therefore, you have to use a completely different technique to
resolve conflicts when working with remote data. Since CURVAL() does not work, you have
to find a different way to get at the actual data on the server. You may first think that
REQUERY() is the answer, but this cannot be done on a view that has pending changes.
The only technique that seems to work is to open a separate copy of the data in its own
work area, either with SQL pass through or with the view. You still have to use the
REQUERY() function after opening this second copy to ensure that you’re looking at the latest
values. This is due to the way that Visual FoxPro caches the view’s contents. But once you
have opened the view and executed REQUERY(), you can retrieve the current values and use
them as part of your conflict resolution.
Chapter 8: Errors and Debugging 151

Before we continue, we should point out another subtle difference that exists between
updates against native data and updates against remote data. Recall that TABLEUPDATE() can
detect update conflicts with its second parameter set to False. When applied to native data, an
update conflict occurs even if the two users edited different fields. This is due to the way Visual
FoxPro implements data buffering—a comparison of the entire row takes place, instead of only
checking the modified fields. To check whether there is truly an update conflict, you must write
a handful of code that employs the use of the OLDVAL() and CURVAL() functions. However,
when using views (either local or remote, actually), you can choose a WhereType that ensures
only the modified fields are checked against the back end for update conflicts.
For example, when your WhereType is set to the default of 3 (key and modified fields),
Visual FoxPro will submit an UPDATE statement that only includes the fields that were
modified by the current user. As long as the other user did not edit any of these same fields, no
conflict exists. However, if you use the WhereType of 2 (key and updatable fields), you are
bound to hit update conflicts more readily, as this will include any fields marked as updatable.
Note that choosing a WhereType of 4 (key and timestamp) is going to catch any update
conflict in the entire record, as the timestamp will be updated regardless of which field or fields
were changed. However, if you need to detect changes in any field (particularly memo fields),
this proves to be the most efficient option.
Finally, if you wish to avoid update conflicts entirely, you can choose a WhereType
of 1 (key fields only), so that Visual FoxPro only cares about matching the key fields before
posting the update. This has the same effect as specifying True for the second parameter of
TABLEUPDATE().

View errors
There are many possible errors that ODBC can return when a view update fails. There are
three main categories of errors: Something is wrong with your view, something is wrong
with the data, or something is wrong with the server. You’ve already seen how data errors
happen, and with server errors, a Visual FoxPro error occurs that you can trap with traditional
error handling.
During development, you are most likely to run into errors in your views. Most of these
errors will be among those listed in Table 3.

Table 3. Common ODBC error messages indicating errors in views.

Error message Action


No update table(s) specified. Use the Tables Use the Tables property to specify at least one server
cursor property. table, or modify the Tables property.
No key column(s) specified for the update Set the KeyField property to .T. for at least one field,
table table_name. Use the KeyFieldList or use the KeyFieldList property of the view.
cursor property.
No valid update table specified for column Set the table qualifier for the UpdateName property
column_name. Use the UpdateNameList and for the column or for the UpdateNameList property for
Tables cursor properties. the view.
The KeyField List cursor property doesn’t Set the KeyField property to .T. for at least one field,
define a unique key. or use the KeyFieldList property of the view.
152 Client/Server Applications with Visual FoxPro and SQL Server

One frustrating issue with some of these is the use of the ownership qualifier dbo for
tables. Sometimes you’ll get an error message such as the first one in Table 3—“No update
table(s) specified. Use the Tables cursor property.” So you check the value of the property,
expecting it to be empty, but it says something like this:

dbo.category

This is exactly what it should be for the category table. We don’t know why, but
sometimes VFP chokes when dbo is used and vice versa. So, if you get this message and you
used dbo, then change the property to remove dbo. If the property doesn’t contain dbo, then
change the property to add it. This error doesn’t happen very often, but you can’t guess how
many hours it will cost you to find it the first time! We have had the error occur with both SQL
Server 6.5 and 7.0, but not yet with SQL Server 2000.

Debugging tools
Now that you have the general idea of how to handle and report errors in your client/server
application, you need to learn how to debug errors that originate in any part of the application.
On the client side, you can continue to use your familiar Visual FoxPro debugging tools:
the Debugger, Event Tracker, Coverage Profiler and, of course, your collection of past
experience. But the client side is only one of three places where problems can crop up. The
other two are in the ODBC driver and on the SQL Server. These two pieces have their own
ways of debugging: ODBC logs (or trace) and some SQL Server tools.

SQL Server Profiler


One of the most useful tools for debugging tricky client/server problems is the Profiler, which
is installed with SQL Server. You can use the Profiler to monitor events on a SQL Server. The
Profiler will capture data about each event and allow you to either view that data in the
Profiler’s graphical environment or save the data in a file or a SQL Server table for later
viewing. You might be surprised by how many events take place on a SQL Server; fortunately,
the Profiler also allows you to filter for certain types of events.
The Profiler can trace numerous types of events. Here is a partial list:
• Login connections, failures and disconnections
• Transact-SQL statements
• Lock acquisition and release
• Beginning or end of a stored procedure
• Beginning or end of statements in stored procedures
• Errors
• Beginning or end of SQL batches
• Beginning or end of transactions
• Remote procedure calls
Chapter 8: Errors and Debugging 153

By using the Profiler, you can examine the commands that VFP sends to SQL Server,
evaluate the performance of individual commands or stored procedures, and even debug T-SQL
stored procedures one line at a time.
The Profiler can be found in the Microsoft SQL Server file group. To create a trace,
open it and select File | New | Trace. Figure 1 shows the resulting Trace Properties dialog.
All you need to do here is name the trace. In the example shown in Figure 1, the trace is
called MyTrace.

Figure 1. The SQL Profiler Trace Properties dialog.

Figure 2 shows a simple trace created by opening a view of the Tastrade category table
created by the SQL Server Upsizing Wizard in Chapter 5, “Upsizing: Moving from File-Server
to Client/Server.” This view shows some of the interesting information that can be supplied by
the Profiler. The Event Class column describes the server event. When VFP sends a query, the
class is SQL:BatchCompleted, and the Text column next to it will show the actual SQL
statement that was sent in the batch. Note that the SQL User Name column displays the default
administrative login. Since this system is using NT security, the NT User Name appears as
well. The CPU column will show which CPU in a multi-processor server handled the request.
SQL Server is multi-threaded, so each thread can be sent to a different CPU. In this case,
however, it is running on a single-processor system, so only CPU 0 is used. The next three
154 Client/Server Applications with Visual FoxPro and SQL Server

columns are particularly useful, as they can be used to troubleshoot performance problems.
These show the number of reads, the number of writes, and the duration in milliseconds of the
event. Partially hidden is the Connection column, and further to the right, off the screen, is the
datetime of the event. The datetime column isn’t particularly useful, as we would rather use the
duration column to troubleshoot performance.

Figure 2. A simple trace in the SQL Server Profiler.

The Profiler is a great tool for tracking down all kinds of issues. Consider the following
example of using the Profiler to track down a particularly nasty performance problem in the
way the SQL Server 6.5 product handled certain messages from the ODBC driver. In this
situation, Visual FoxPro was used as the client application for a SQL 6.5 box that was quite
powerful: multiple processors, high-throughput RAID array, and all the other toys.
Nevertheless, for some reason, when an insert occurred to a SQL table with a Text field, the
application appeared to freeze.
The client tried letting it run its course to determine if the insert would eventually complete
or if it was a lock-up situation. After waiting two hours with no response, they asked for help.
When it had been determined that the code was implemented correctly and that the database
schema was valid and efficient, the Profiler provided the answer—it supplied the pertinent
information and explained why performance was so awful.
The table was rather large, with more than 200,000 records, and each record had a
Text field. SQL Server 6.5 was quite wasteful with Text fields, as it allocated a single 2K
page for each Text field, regardless of whether the field contained data or not (SQL Server
7 fixed this problem by allowing its 8K pages to be shared among multiple Text fields).
Therefore, searches through Text fields were to be avoided at all costs, since each search
would require moving and searching through 400MB of data. Even on this heavy-duty machine,
searching through that much data would be slow, particularly since there are no indexes on
Text fields.
What was happening was that SQL 6.5 received the update request and converted it into
three statements. The first inserted all of the data, except the contents of the Text field. Instead
Chapter 8: Errors and Debugging 155

of using the actual Text data, the statement provided a unique value for the Text field.
Then, the next statement was the death knell: it was performing a SELECT statement with a
WHERE clause to find the unique value in the Text field! (Wouldn’t you at least think it
would try to find the record with the primary key of the table?) It did this because it would
use another function called UPDATETEXT to place the data into the Text field—used in
order to avoid other kinds of problems with sending large amounts of Text data across an
ODBC connection.
Once this performance problem was discovered, it was easy to rewrite their update routine
to solve the problem. Without the Profiler, there would have been no clue as to why the server
would choke on something as apparently simple as an INSERT statement.
Another great use for the Profiler is as a learning tool. You can find out all sorts of neat
things by trying a function in the Enterprise Manager or from Visual FoxPro, and then looking
at the Profiler to see what happened. For example, imagine that you are not sure how to build a
CREATE DATABASE statement in T-SQL. You can use the Enterprise Manager to create a
new database, and then switch over to the Profiler to see what command(s) the Enterprise
Manager submitted to the server. Here’s the output from trying this little test:

CREATE DATABASE [test]


ON PRIMARY
(NAME = N'test_Data',
FILENAME = N'C:\MSSQL7\data\test_Data.MDF' ,
SIZE = 1,
FILEGROWTH = 10%)
LOG ON
(NAME = N'test_Log',
FILENAME = N'C:\MSSQL7\data\test_Log.LDF' ,
SIZE = 1,
FILEGROWTH = 10%)

You may also uncover some undocumented features or discover how to do something
programmatically that you thought could only be done through the Enterprise Manager. For
example, we once used the Profiler to help troubleshoot a problem with the Upsizing Wizard
before the source code for the wizard was available.

The SQL Server Performance Monitor


Another useful tool for tracking down performance problems on the server is the Performance
Monitor, shown in Figure 3. This tool is used to view details about a wide variety of
“counters” available through SQL Server. For example, you can see if your processors are
always running at maximum throughput, if SQL Server is constantly swapping data in and out
of the buffer cache, if you are using large amounts of locks or other resources, or even how
many users are logged into the system at any given time.
As you can see, the Performance Monitor is best at telling you whether your SQL
Server hardware and software are configured properly for the typical load it needs to handle.
You can log the activity as well, so it can be reviewed over time to help discover any
possible degradation.
156 Client/Server Applications with Visual FoxPro and SQL Server

Figure 3. The SQL Server Performance Monitor utility.

ODBC logs
Although not our favorite tool for debugging a client/server application, ODBC trace is
sometimes the only way to determine whether the problem lies between Visual FoxPro and
SQL Server. For example, perhaps you believe that SQL Server is receiving an ill-formed
statement that is causing an error that you just cannot seem to track down within either Visual
FoxPro or SQL Server.
We don’t jump at the chance to use ODBC logs, because they are tremendously large due
to the detailed information stored therein, and it is quite tedious to wade through them. Because
C++ programmers, not Visual FoxPro developers, develop ODBC drivers, the logs they
produce are full of hex values, but you can also view function calls and their success codes.
This permits you to view the exact steps that ODBC takes to get data from the server and to put
data back onto it. Armed with this information, you may be able to determine the cause of the
problem you are having and perhaps come up with a workaround for it.
Depending on how you are accessing the server, there are two places where you can
request creation of an ODBC log. First, if you are using a DSN, there is a check box where you
can enable tracing (see Figure 4). Even if you are not using a DSN, you can also use the
Tracing tab to enable an ODBC trace (see Figure 5).
Chapter 8: Errors and Debugging 157

Once you turn on ODBC logging, it will log every statement that occurs across the ODBC
connection until it is disabled. Since this generates a large amount of information, you will want
to turn it off as soon as you have logged the desired functions.

Figure 4. Enabling ODBC logging in an ODBC Data Source.

Figure 5. Enabling general ODBC logging.


158 Client/Server Applications with Visual FoxPro and SQL Server

One big problem with using an ODBC trace is the amount of time it takes to create the file.
For example, what if you were to trace the opening of a view that retrieves data from the
authors table in the pubs database? This table has a “whopping” 23 records, and contains
records that are no more than 151 bytes wide. The table uses mostly varchar fields, so the
record width is variable and is usually much smaller than 151 bytes. Worst-case scenario means
that the table is 3473 bytes (or 3.4K) at its largest possible size.
When this view is opened under normal circumstances, it opens almost instantaneously.
However, when the ODBC trace is enabled, opening the view will take 28 seconds. Clearly, the
additional time is required to report the wealth of information produced by the ODBC trace to
the log—the resulting log file is 178K!
This log file contains several useful bits of information. The first part of the file shows how
the ODBC driver connects to the server, changes the database context, and sets a variety of
options, including the timeout values set by the Visual FoxPro connection definition used by
the view. Once the connection is complete, the log shows that the SQL statement “SELECT *
FROM dbo.Authors Authors” was executed. The rest of the log contains the steps used to
retrieve the column data types, and their values for each record.
Therefore, you can use the ODBC log to produce a high level of detail on how Visual
FoxPro is communicating through the ODBC driver to talk to SQL Server. However, in our
experience, the ODBC log has been a “last resort” debugging tool. In most cases, if the
problem is not something easily traced within the Visual FoxPro debugger, the next step is to
use the SQL Server Profiler.

Summary
In this chapter, you have seen the details of handling errors and how to go about debugging
problems. Errors are handled differently in a client/server application, and regardless of the
development product you choose, you must decide where to actually handle errors. In Visual
FoxPro, you can perform all error handling in the client application and only send clean data to
the server, you can use views and capture TABLEUPDATE() errors, or you can discard views
and use stored procedures on the server and SQLExec() statements.
Of course, you can still use the Visual FoxPro debugging tools to track down any Visual
FoxPro errors. However, when the problems seem to be outside of Visual FoxPro, SQL Server
and ODBC both provide some good tools for watching all of the activity generated by your
Visual FoxPro application.
In the next chapter, you will see more information on how to design a client/server
application in order to keep it flexible, scalable and maintainable.
Chapter 9: Some Design Issues for C/S Systems 159

Chapter 9
Some Design Issues
for C/S Systems
In the first part of this book, you learned about the capabilities of Visual FoxPro as a
client/server tool and the capabilities and features of Microsoft SQL Server. We also
demonstrated why client/server applications can be better than file-server applications.
But before you begin with a client/server application, you’ll want to know about the
choices in design that have to be made. This chapter covers the issues raised by moving
an application from file-server to client/server and the options for design, performance
and security.

Microsoft Visual FoxPro is a full-featured product with a fully developed language, visual tools
for handling forms and reports, object orientation features, a great database engine, and many
other tools that make the development process run smoothly. Microsoft SQL Server is a
database and query engine with quite a few administration tools to make things like security
and backups a snap to perform. But SQL Server lacks a front end, and although it has a
language (Transact-SQL), it is not designed to handle everything that a product like VFP can.
The lack of a front end is not a detriment; it’s simply a design decision to allow developers to
use products with which they are already familiar.
But as the saying goes, “Familiarity breeds contempt.” In this case, the contempt is not for
the devil you know, but rather for the devil you don’t know: SQL Server. In this situation,
familiarity with a known product leads you to feel uncomfortable with the capabilities of the
new product. That’s not necessarily a bad thing, but with client/server, you may have to rethink
your programming habits. You have to remember that client/server performance is better than
file-server when the pipeline gets smaller. But that is only true when you treat the pipeline
gingerly, restricting access across the network to messages between client and server rather
than massive data transfers.
The question that arises is, just how should you design a client/server application? Where
should the messages be restricted and how? Should the server do everything that has to do with
the data? When should you take advantage of Visual FoxPro’s local data speed and language
capabilities? How do you reconcile VFP’s database container against SQL Server’s database?

SQL database design issues


When designing a client/server database, the main questions are database integrity and
indexing. The answers to these questions are easy. If you’re a Visual FoxPro developer, you’re
going to want to use the power of VFP to handle integrity and validation. If you’re a SQL
Server database administrator, you’re going to want to use the built-in capabilities of the SQL
database. Unfortunately, regardless of what role you play, you have to remember that the other
players are not all wrong.
160 Client/Server Applications with Visual FoxPro and SQL Server

Data integrity mechanisms


SQL Server or Visual FoxPro? Database design or language and forms? Why should SQL
Server be used? Because the integrity is in force no matter what front-end product is used—
there is no way to circumvent database design. This choice was the one that was promoted
when the database container was introduced into Visual FoxPro 3.0. Why use Visual FoxPro
for integrity? Because of its object-oriented capabilities, its event-driven form design and its
strong language. The important aspect of client/server that is being discussed is whether
integrity should be handled before the data is sent to the server, or at the server.
Data integrity takes four forms: entity integrity, domain integrity, referential integrity and
user-defined integrity, sometimes called business rules. Entity integrity guarantees that no two
records in a table can be exactly alike. Domain integrity enforces the values that can be entered
into a field, and referential integrity ensures that the relationship between two tables cannot be
broken. User-defined integrity is usually defined as those rules that a business sets up that do
not fall into the other categories. Domain integrity can sometimes be thought of as a form of
this, but since the mechanisms for enforcing domain integrity can be built into the structure of a
table, it is considered along with the other forms.
In this section, the various mechanisms for enforcing integrity will be explored, both in
Visual FoxPro and in SQL Server.

Data types, NULLs and defaults


These are forms of domain integrity—that is, what values are allowed into a particular field.
Data types restrict the types of values, not allowing a Null value is also restrictive, and defaults
are values to be used in lieu of any input value.

Data types
No one can possibly argue that a database needs a data type defined for each field in a table.
The decision is whether the front-end program should also know about the data type. When
using a remote view in Visual FoxPro, the type becomes known when the view is used, so there
is no question that the proper data type restriction will be handled—except when certain SQL
Server types are used that do not have a correct mapping to the types in Visual FoxPro. These
are the types that allow designers to avoid wasting database space because of the defined
ranges of allowable values. Smallmoney is only four bytes, as opposed to Money’s eight bytes.
But both types are converted to Currency (eight bytes) in VFP. The same thing goes for
Smalldatetime. Besides Int, SQL Server also supports Smallint and Tinyint, which are two
bytes and one byte, respectively. Both get converted into Integer in VFP. Binary and Varbinary
are converted into Memo (binary) types in VFP and are virtually uneditable directly (code can
be easily written to convert back and forth), and Uniqueidentifier (which is a 16-byte binary
value presented as 32 hexadecimal characters) is treated as Character data in VFP. Because of
these conversions, it is very easy to forget and allow values using Visual FoxPro’s restrictions
that will fail when the view sends the data back to SQL Server.
The issue is: Where should the problem be handled? Since the remote view changes some
data types in conversion, you might choose to use Visual FoxPro code to enforce the tighter
restraints of the SQL Server data types. This can be done at the view level via the View Field
Properties dialog by setting a Field Validation Rule where needed (as in Figure 1), or in the
forms used for data entry, or even in classes that you create for the fields in the forms. When
Chapter 9: Some Design Issues for C/S Systems 161

using forms or classes, you would put the test into the LostFocus (or perhaps Valid) event of
the control or class.

Figure 1. The View Field Properties dialog showing a Validation Rule entered for the
UnitPrice field.

The complete expression cannot be seen, and there is a comment that explains the
validation.Otherwise, without any type of code on the client side, you would have to
process any errors returned by the server. This means that you would have to learn the
various error codes that might be returned from the server and then write the code in VFP
to handle those errors.
The downside of coding data type validations on the client side is that since a data type
is defined in the database, any changes in the data type on the server would require changes
in the code on the client. The issue here would be one of deployment. Although the server
change only necessitates a change in one place, the client changes mean recompiling the
software and then distributing it on every computer where it’s used. By using error processing
on the client side, every change in structure would not require a rewrite because the errors
would still be valid.
Another less favorable way to handle this problem is to restrict the database design on the
server to using only those data types that are directly transferable to the VFP data types, thus
avoiding the range errors that might result.

Nulls
Both SQL Server and Visual FoxPro recognize Null values. A problem only surfaces when a
field that accepts Null values on the server is filled with spaces (for Character fields—for other
data types, there are other values to watch for) on the client. Instead of getting the Null value,
which is what may have been intended, the server will receive the field with the spaces. When
162 Client/Server Applications with Visual FoxPro and SQL Server

binding a form’s controls directly to the remote view, that would normally not be a problem. If
the user does not fill in a value for the field, then the view would place a Null value in the field
on the server. But if the user actually types in a blank of any kind, then the spaces will be sent
to the server.
To handle this on the front end may require special coding so that if a field were left blank,
then a Null value would be inserted into the field. The problem here is that a bound control is
more difficult to handle. The other problem is once again how to know the structure of the
database on the client side. In order for you to use VFP to process the situation, you have to
know the database structure of the server, and if there are any changes, then those changes must
migrate to the client application.
The flip side of Null is Not Null, where a Null value is not allowed. This means that a
value must be returned to the server, or errors occur. When doing an insert using a remote view,
if a field is left blank, then VFP tries to insert a Null value into that field, causing the insert to
fail. For any field that does not allow Null values, either validation has to be done on the client
side, or the error code must be processed from the server.
There is no easy way of handling Null’s on the client side. In any case where you will be
using remote views, you will need to know the Null property of the field on the server. Then it
is up to you as to whether to put the requirements of the field in the view, the form or a class.

Defaults
Defaults on the server can be very handy. They provide the necessary value for a field when no
value is given for that field when a record is inserted from the client. Defaults override the
blank on the client side. (Actually, when a record is added through a remote view, Visual
FoxPro generates an INSERT statement that will only include values for the fields that have
been modified in the new record. That means any fields that are left blank are omitted from the
INSERT statement.) If a field on the server allows Null values and has a default, the default is
used whenever the field is left blank in a new record. In this way a default covers the situation
better than the Null property.
The question is whether or not to put the default value on the client side of the application
as well. The rationale for this is to let the data entry people see the value that would be used.
On the other hand, this requires that the client application be kept informed of any changes on
the server.

Rules and check constraints


As explained in Chapter 3, “Introduction to SQL Server 7.0,” a rule is an object containing a
simple logical test of a field’s value, and if the test returns False, then the modification or insert
is rejected. A CHECK constraint is similar, except it is a part of the table structure itself. A rule
must be bound to a field after it is created. A field can only have one rule bound to it. A
CHECK constraint can test multiple fields from the same record, and there can be many
CHECK constraints that impact a single field.
Rules and CHECK constraints both enforce domain integrity. They are used to check the
range of possible values for a field, or to perhaps perform a pattern check on a Character data
type. Besides being enforced at the server level, they can be enforced at the client level via field
validation rules. These can be set at the client, by using the View Field Properties dialog or by
Chapter 9: Some Design Issues for C/S Systems 163

using the DBSETPROP() function to set a Row Rule Expression. You can also process these
rules via events in forms or classes, or by using VFP code when saving the changes to a record.

Primary keys
Primary keys are used to enforce entity integrity. By definition, the primary key is a value that
cannot be duplicated throughout an entire table. No two records can have the same primary key
value. Therefore, no two records can be exactly alike.
For all practical purposes, primary keys are created and behave the same in both Visual
FoxPro and Microsoft SQL Server. When you designate a primary key, both products create an
index (called candidate in VFP, unique in SQL Server) that enforces the primary key rule.
Primary keys are especially important to have on the server, because a remote view needs at a
minimum the primary key value in order to update existing records.
The source values for primary keys can either come from the data itself (natural) or can be
artificial, generated by your application or the system. Some designers choose to use natural
data as a primary key so that no extra data need be created for that purpose. This may come out
of a natural way of uniquely identifying individual entities within a table, such as name (by
using a combination of the name parts), Social Security number, badge number, part number,
invoice number, or any of several ways that are natural to the data itself.
Other designers prefer to create new fields for a primary key because of compactness or to
prevent changes to the key itself. An artificial or surrogate key is usually a single field that is
either an integer value or a short string. Since primary keys are generally used as foreign keys
when establishing relationships between tables, keeping them short is important as both a disk
space saver and a performance enhancement for quick joins between tables.

Generating keys
There is no intent here to say that one way of generating primary keys is better than another,
but rather simply to explore the issues when generating them. If the key is being created by the
application, or perhaps the usage of the application, then there is not really a problem. The code
for creating that key will be in the VFP program, so that when a new record is created, then the
new value for the primary key will be inserted along with the rest of the data. The only
important note is to remember that SQL Server may also be set to enforce uniqueness, so if
there is a possibility of error, it will have to be handled by the application code as well. You
will also have to be sure to set the primary key as updatable in the view definition.
On the other hand, if you use some mechanism on the server, then there are definite
repercussions. The first is that you will not know the value of the new primary key until after
the insert has completed. In fact, using a remote view may necessitate a REQUERY() of the
view after the insert has completed. This can cause quite a problem in terms of performance,
and there is no shortcut around it. The performance issue is that the insert is handled in the
background by VFP, and since the new primary key is only on the server, it cannot be seen on
the client. Normally a refresh of the current record would show any changes to a record in a
view, but since the REFRESH() function requires the primary key on the client, it will fail to
show the new record.
There are ways of handling this situation, but they all require multiple trips between the
client and server. If you are using the IDENTITY property (described in Chapter 3,
“Introduction to SQL Server 7.0”) for creating primary keys in a table, you could use the
164 Client/Server Applications with Visual FoxPro and SQL Server

following method. When you add a record to a table with the IDENTITY property, the
@@IDENTITY function will return the last value generated by an insert for that connection.
(This was also described in Chapter 3.)
Regardless of how you implement this technique, you must first find out the connection
handle being used by the cursor where the results of the view are stored. Next, generate an
insert command and use SQL pass through to send the command to the server. Finally, use SQL
pass through to send a command that will return the @@IDENTITY value in another cursor.
The following code demonstrates this technique:

lhConnection = CURSORGETPROP("ConnectHandle")
lnOK = SQLExec( lhConnection, "INSERT INTO TableName (col1, col2, col3) " + ;
VALUES (12,'First', 'Last')")
IF lnOK = 1
lnOK = SQLExec( lhConnection, "SELECT @@IDENTITY AS PriKey", IdentTable)
IF lnOK = 1
SELECT IdentTable
lnNewRec = PriKey
USE
ENDIF
ENDIF

After this, lnNewRec will have the new primary key value generated by the IDENTITY
property on the server. You can place the value into the primary key field of the view, but you
will not be able to edit that record until after a REQUERY() of the view has been done.
Note that you can also use the TABLEUPDATE() function after you INSERT data into a
view to provide the same INSERT INTO statement as described previously. However, you
must still determine the connection handle and use a SQLExec() call to grab the
@@IDENTITY value, and you will still need to REQUERY() before being able to edit the
record in the VFP cursor.

This technique works even if you request the IDENTITY value before
‡ committing the changes to a transaction. Therefore, if you insert a
parent record after beginning a transaction, you can still use SELECT
@@IDENTITY to get the foreign key value that you need for any child
records. However, if you ROLLBACK the transaction, the IDENTITY value
is essentially “lost.”

Referential integrity
There are various ways to enforce referential integrity on the server and a couple of ways in
Visual FoxPro. The difference is that although SQL Server supports what is known as
Declarative Referential Integrity (DRI) where the integrity is built into the database structure,
VFP does not. Both server and client can use triggers for referential integrity.
Regardless of which method is used on the server, there is virtually nothing that can be
done on the client to prevent a referential integrity violation. Since these problems occur
because of referencing another table in the database, the data modification must pass through to
the server in order to get the error. The only thing that you can do is program for the error, and
handle it after the server returns the result.
Chapter 9: Some Design Issues for C/S Systems 165

DRI/foreign keys
Just like Visual FoxPro, Microsoft SQL Server supports the creation of relationships via the
CREATE TABLE or ALTER TABLE commands. The difference is that in VFP, the options
only create a defined relationship that is a precursor to using the Referential Integrity (RI)
Builder, whereas in SQL Server, the relationship becomes the referential integrity.
You can establish a relationship in both products by creating a foreign key in a child table
that references the primary key in a parent table. In SQL Server, this is called DRI, and it
establishes a restrictive relationship. DRI will cause an error when an attempt is made to delete
a record in the parent table that has records in a child table, or when the primary key of the
record in the parent table is modified and that record has related records in a child table. This
means that there is no way to institute cascading deletes or updates when using DRI.
One way that you could do cascading deletes is by deleting the child records first via some
client code. This is not easily performed when using remote views, but it is one way of handling
it from VFP.

Triggers
Both client and server support the use of triggers for handling referential integrity. In Visual
FoxPro it is possible to use the RI Builder to make the job of code generation easier, but there
is no such mechanism in SQL Server. There you would have to write Transact-SQL code to
handle the situation. That means you will write code for the Update and Insert triggers on the
child table, and Update and Delete triggers on the parent table. Note that the Upsizing Wizard
(covered in Chapter 5, “Upsizing: Moving from File-Server to Client/Server”) will write basic
RI code into the triggers for you, but only if you have selected the correct options and
employed the RI Builder in your VFP database.

Review of data integrity


At this point, you have seen the various types of integrity and the options available via the
built-in tools of Microsoft SQL Server, as well as their impact on the use of remote views in
Visual FoxPro. There are many cases when a database design on the server can be mimicked on
the client via one of several tools, either through field properties of a view, a form’s events or
the events of a class. Along with those options, you can also handle the problems through
trapping errors.
When possible, transferring restrictions from the server to the client can prevent
unnecessary network traffic by allowing the client to verify and validate data before the
modification is sent to the server. The downside to this technique is that changes on the server
then have to be made to the client application as well. The application has to be recompiled and
then deployed out to all of the client workstations where it was first distributed.
With error handling, the good thing is that changes to the server do not have to be
promulgated to the client application. But then a round trip to the server is made in order to
find out that a mistake has been made. The bad thing about error handling is that you need to
find out the error codes of SQL Server and how to process the error messages, which was
covered in Chapter 8, “Errors and Debugging.” To refresh your memory, if you attempt to
delete a record from a parent table that has related records in child table, then you will get a
VFP error 1526 indicating an ODBC error. By using the AERROR() function, you can discover
through the fifth element of the array created that SQL Server error number 547 occurred,
166 Client/Server Applications with Visual FoxPro and SQL Server

which is a constraint violation. By examining the message in element 2, you will find out what
statement caused the error, which constraint, by name, was violated, and the database, table and
column names that are involved.
Even though you may find the first method preferable, there is the fact that transferring the
rules cannot cover every situation. Sometimes the modification has to go to the server anyway
in order to find out the error, as with referential integrity. Sometimes the problem is simply that
the tools discussed just don’t do the job by themselves. Something else is going to be needed.
That something should also handle errors at the place they happen, the server.

Stored procedures
You can write code on the server that can handle all of the operations that you want to do for
the client/server application, or you can write code just to handle some of the operations, such
as data modification. But when you start to use code on the server, you will lose the ability to
use all aspects of certain tools on the client, specifically remote views.
Remote views are SELECT statements that return data from the server to the client, and
when they are used for updating as well, they will also generate the data modification
statements automatically. These statements are the INSERT, UPDATE and DELETE
commands that are common to most SQL implementations. The problem is that you have very
little flexibility in dealing with the data (it’s automatic) and a lot of headaches wrapped around
error handling. One technique that you might have already thought of is that the remote view
does not have to be updatable. Instead, you could use it for returning the data from the server,
and then manually create the data modification statements, which are sent to the server via SQL
pass through. But that does not eliminate the error handling or the explicit knowledge needed of
the server’s data integrity mechanisms.
This is simply a choice. The alternative is to use code on the server in the form of stored
procedures. Just as in Visual FoxPro, stored procedures are stored in the database. That way,
they are available for use by any client application. Stored procedures on the server can handle
all aspects of data retrieval and data modification.
Within a stored procedure can be all of the error handling that you would need when using
SQL Server. This way, the nature of the errors is known at the place where the errors occur
instead of at the client. The stored procedure can then either handle the error or return an error
code to the client application that would be defined so that you would know how to handle the
error perfectly. Although this might seem to be the same thing as programming the validation
code in the client, the difference is that changes can sometimes be isolated at the server without
having to rewrite any code on the client. The documentation for the stored procedure would
indicate what inputs were required and what the return codes would mean.
Stored procedures also create a solution to the insert problem that was previously
described when allowing the server to generate a primary key. By using stored procedures,
all of the issues are handled at the server, and the procedure returns the new key to the client
as part of the way it works. This avoids the extra trip to the server to find out what key
was generated.
Basically, there are two ways that you can use stored procedures: either through SQL pass
through commands or via ActiveX Data Objects (ADO). With SQL pass through, you are only
slightly limited in what the stored procedures can do for you. ADO, on the other hand, provides
Chapter 9: Some Design Issues for C/S Systems 167

more flexibility with stored procedures, but forces you to add some code to handle the relative
lack of integration between ADO and VFP.

SQL pass though


If you choose to use SQL pass through queries (specifically the SQLExec() function), then one
format for return codes would be via a cursor that would be returned by the stored procedure.
That is, the stored procedure would have code similar to this:

SELECT @RetCode AS ReturnCode, @RetMessage AS Message

This would create a one-record cursor with two fields, one with the code indicating
success, failure or whatever, and the other with an explanatory message. There might be other
fields for special cases, such as stored procedures that would handle INSERT operations, and
would return a generated primary key value for the new record.
By definition, the SQLExec() function in Visual FoxPro creates a cursor with the alias of
Sqlresult, but the third parameter allows you to specify the name of the alias to use for any
cursors generated by the command. If the stored procedure generates multiple cursors, then the
aliases will be the name selected followed by numbers, where “1” would be the second set of
data returned from the server.
When using SQL pass through, you must create the entire stored procedure command, as in
the following code:

lcCommand = "EXECUTE procDelCust 123"


SQLExec( lhConnection, lcCommand, "ResultSet")

In this example, the command to run the stored procedure, the name of the procedure and
the parameters all became one string variable that was then passed to the server. As you can
see, any parameters must be a part of the string itself. When passing character values, you will
need to enclose them with single quotes within the command. SQL Server supports optional
parameters. That means that not all parameters have to be defined when executing the stored
procedure. But if you are passing the parameters by position, as in the preceding code, you
cannot skip any parameters. In that case, or in every situation, you can use named parameters so
that order does not matter.

lcCommand = "EXECUTE procSalesByCategory @CategoryName='software'"

This example assumes that there is a parameter with the name of @CategoryName. All
parameters in SQL Server start with an @.
Just as views can be parameterized in Visual FoxPro, so too can SQL pass through
commands. This is done simply by placing a ? in front of a variable name. If the variable name
does not exist when the SQLExec() function is executed, an input dialog box will appear
prompting the user to enter the value. Normally, you would create the variable just before
execution based on some other form of input.

lcCatName = THISFORM.txtCategory.Value
lcCommand = "EXECUTE procSalesByCategory @CategoryName=?lcCatName"
SQLExec( lhConnection, lcCommand, "SalesCat")
168 Client/Server Applications with Visual FoxPro and SQL Server

ADO
A little more flexibility results when ADO is used instead of pass through queries. The problem
with SQL pass through is that return codes must be handled through a cursor or result set. SQL
pass through does allow OUTPUT parameters, but does not allow for return codes from
procedures. (Yes, procedures can return a single, integer value.) This is somewhat limiting, but
by using ADO, that problem is overcome.
Without getting into an entire discussion of ADO (as it is covered more completely in
Chapter 12, “ActiveX Data Objects”), you should understand how the ADO command object
handles stored procedures. In ADO, the command object has the ability to handle stored
procedures and their parameters in a more object-oriented way. Command objects have a
property for storing the name of the stored procedure (CommandText) and another property to
indicate that the command is a stored procedure (CommandType). The command object also
has a parameters collection, used in place of passing variables from Visual FoxPro.
The parameters collection contains a parameter object for every parameter passed into a
stored procedure, and even one for the return value. The advantage of this is that if a parameter
of a stored procedure is defined as an OUTPUT parameter, then after the command object has
been executed, that parameter object will have the output value. This way, stored procedures
can be designed to return information in a more natural way than a cursor. It makes sense that a
procedure that modifies data should not return data, and by using ADO, you can avoid that
sticky situation.
The downside to ActiveX Data Objects is that they cannot be used in the same fashion as
remote views. Visual FoxPro does not have an ADO tool to make development with this latest
technology easy. As a result, you would have to do a lot more coding than with remote views.
ADO does have a recordset object that is a little like the cursors in Visual FoxPro, and they can
even be updatable. Unfortunately, VFP cannot use the recordset cursor (they’re called cursors
also) in the same way as the temporary data returned via ODBC. Instead, a recordset cursor
exists in the memory of the client workstation, and extra code would be needed to populate a
local cursor. Also, although recordset cursors can be updatable, they work differently from
remote views, usually causing a higher resource drain on the server.
ADO will be covered in more detail in Chapter 12, “ActiveX Data Objects.”

VFP developer vs. SQL Server DBA


Throughout this section, you have seen the various data integrity issues that must be dealt with
in any database. You have also seen the choices that have to be made when designing a
client/server application. The last question is the choice to be made for your application. The
answer depends on exactly what your role is in designing the application.
A Visual FoxPro developer has had it all for many years. One of the fastest database
engines on a desktop, a language geared for processing data, a complete programming language
with all the functions that go into most language-only products, object-oriented language,
design tools that have gotten better with every version—these are the features of VFP that have
made development easy, fast and comfortable. What that means when it comes time to design
integrity into the client/server database is that VFP seems like the best place to take care of
these needs.
A Microsoft SQL Server database administrator has been working with a database product
that is robust, secure and capable of handling thousands of users. The administrative tools for
Chapter 9: Some Design Issues for C/S Systems 169

SQL Server, especially the Enterprise Manager, have made the day-to-day tasks easier and
easier to handle. The DBA feels that SQL Server is strong enough and has enough built-in
features to make data integrity the province of the server. What that means is that the DBA
feels that all data integrity needs are met at the server.
This is the biggest issue in a client/server design, balancing the experience of the
different parties to achieve the goal of a well-designed, robust and secure database. One
of the things that you may encounter when moving to a client/server design is that the
server needs more administering than the typical desktop database solution. Therefore it is
important to understand the needs of other parties who will be or are already involved in a
client/server database.
The answer to how integrity should be handled is not a black and white decision. It is not
all or nothing, but rather a balance where the strengths of the client are balanced with the
strengths of the server. By now you should realize that it is impractical to try to design the
client side without some knowledge of the server design. So even though you would like to
have all data integrity handled at the server so that the client application can be designed once,
there will be modifications on the server that impact the client.
In the rest of this chapter, we will examine some other issues regarding performance and
security that are also part of the database design, and then in the next chapter, we will present
choices and recommendations that will help you decide.

Client/server performance issues


After designing the database structure and making sure that the data retains consistency and
accuracy, performance is the next important decision. Performance can be broken down into
three areas: server, client and network. You have to be concerned with all three in the database
design to make sure that everything runs well.

Choosing indexes
The biggest impact on the server in terms of performance is indexing. If there are no indexes,
the server must read all records of a table to search for data. This also goes for data
modifications, in order to find the record to modify. Although you’re happy to have the server
handle data retrieval, without indexes, any server will be brought to its knees in no time, and all
the presumed benefits of client/server will be lost.
Choosing indexes in SQL Server is a complex issue because there will many different ways
of querying data. The two different types of indexes, clustered and non-clustered, were
explained in Chapter 3, “Introduction to SQL Server 7.0.” What you need to know now is how
to choose your indexes.
The brute-force way to find out which indexes are useful is through trial and testing. First
you create one or more indexes, and then you find out whether your queries will use any of
them. If there are any unused indexes, then you should drop them because they will just add
overhead to maintenance without giving you any benefit.
Rather than the trial-and-error approach, Microsoft SQL Server provides some tools that
can help you find good indexes. The tools all reside within the administrative applications
installed with SQL Server. (You can also install these tools on any client system as well.)
In the Query Analyzer, there are two tools that can help you. The first one is the Graphical
Estimated Execution Plan. This can be selected from the Query menu or from the toolbar. You
170 Client/Server Applications with Visual FoxPro and SQL Server

can also specify that the execution plan be displayed along with the execution of a statement.
By examining the graphical plan, you will discover which indexes, if any, are being used for a
statement or group of statements (a batch). Figure 2 shows a sample execution plan.

Figure 2. Graphical Estimated Execution Plan.

This plan shows the steps that the query engine of SQL Server will perform in order to
carry out the command. When you position the mouse pointer over any of the icons, a box will
appear explaining the nature of the step, various statistics on that step and the argument used to
perform it. Through this tool, you will begin to understand the “thinking” behind SQL Server’s
optimizer and then be able to choose good indexes.
Furthermore, the estimated execution plan will actually flag steps where you have no
statistics in red and offer a suggestion for creating those statistics. Statistics are what the SQL
Server optimizer uses in determining whether an index is useful to optimize a statement. When
you right-click on any icon, the context menu that appears will allow you to build and examine
indexes on the server.
Another tool in the Query Analyzer is found in the Query menu: Perform Index Analysis.
When you use this option, SQL Server will offer suggestions for indexes that would improve
the performance of the statement being analyzed. After suggestions are offered, there will be an
opportunity for you to create those indexes by accepting the commands that the Index Analysis
shows (see Figure 3).

Figure 3. The Query Analyzer Index Analysis dialog.


Chapter 9: Some Design Issues for C/S Systems 171

The problem with the previous two ways of analyzing queries is that they generally do not
take into account the overall usage of the data. Determining which index should be the
clustered index, or none, and how you should build your non-clustered indexes (composite,
unique and so forth) is very difficult because there is no way to predict exactly all of the ways
that the data will be queried. To that end, the Enterprise Manager has a tool called the Index
Tuning Wizard, which can be a great help.
Before you use this tool, you should either create a script (text file with a .SQL extension)
with all of the queries that are being used, or a workload file from the SQL Server Profiler. The
first method is almost the same as using the Query Analyzer Index Analysis tool, so the real
benefit comes from the second method.
The SQL Server Profiler is a tool that can be used to trace the commands that come into
SQL Server. When creating a trace, you can specify what to include or exclude from the trace.
By specifying a particular table in a database, the trace will only include statements that
reference that table. By saving the results into a workload file (which can be either a table in the
database or a file on the disk), you will have a picture of how that table is being used. This
workload table or file can then be used as the input for the Index Tuning Wizard. This way, you
will get recommendations based on the actual usage of your data.

Client/server division of work


One of the aspects of client/server that has to be addressed is where to do the work. As
discussed in the section on data integrity, true client/server involves a division of responsibility.
Not everything should be done on the server, and certainly not all validation should be done,
even when it can be, on the client.
The power of the server is that it can greatly reduce disk I/O. Microsoft SQL Server is
designed to reduce the amount of information read from and written to the disk. That is, it
attempts to do these things as little as possible, and with the greatest degree of efficiency. SQL
Server will leave as much data as it can in memory, as well as storing the query plans of stored
procedures and even some ad hoc queries. This capability means that a typical server will have
much more memory installed than the client computers.
Some tasks are better handled on the client systems. Data entry, by necessity will be done
there, but also formatting for reports is better handled on the client side. Although there are
formatting functions, data conversion functions, and even some statistical and mathematical
functions in Transact-SQL, it is not a language that is well suited for those types of
calculations. Selecting and sorting the data is what the server is good for, while formatting and
calculating is what the client does well.
Another area of concern is record-by-record processing. Although SQL Server can do this,
via server-side cursors, that kind of processing should be done in Visual FoxPro, as it is better
suited to this kind of processing. An alternative is to use Transact-SQL statements that do set
processing. There are many times when it is assumed that the only way to do something is
record-by-record, but a careful examination of the activity will reveal many times when more
advanced SQL statements will do the job. But if all else fails, try to come up with a way to
handle it on the client.
Not all of these decisions are so simple, but as a rule, it is very easy to remember that the
server does data retrieval and the client does data reporting. As for data entry and validation, a
middle ground does have to be found. The forms are on the client, and the data validations will
172 Client/Server Applications with Visual FoxPro and SQL Server

be on the server, but the business rules—the user-defined integrity—could be on either, or both.
It is better to try and centralize the business rules, so that they can more easily be changed, but
there are other performance issues to consider.

Bandwidth
Along with managing the server and client, you also have to be concerned with the network as
well. Bandwidth refers to the capacity of a network, which can be impacted by many factors.
These factors include the physical layout, the type of cabling used, the distance from clients to
servers, and the number of users on the network. But one of the more important factors is the
design of the client/server layout.
Although there are many physical characteristics, the one thing that is known is that
bandwidth is a finite resource and should not be abused. You want to keep the amount of
information passing across the network to a minimum. That way, any network concerns will
stay with the physical. In order to keep network use down, try not to download data
unnecessarily, and try to keep trips between client and server to as few as possible.
To help limit the amount of downloaded data, only specify the fields that are absolutely
needed on the client. Furthermore, limit the number of records returned through the use of the
WHERE clause, and make sure users are required to specify what it is they are trying to find.
Reducing the number of trips between the client and server is a bit trickier. As you’ve
already seen in the data integrity section, sometimes you have to go to the server to validate
data entry, which means that if an error occurs, you end up with two extra trips. One way of
helping with that is to keep the number of error trips to a minimum. This can be done by not
reporting just one error at a time. It is more efficient to gather up all the errors committed by
the data entry person and then report them back so that multiple discovery trips are eliminated.
Stored procedures can do this as well as the multiple rows returned by AERROR(). However,
as mentioned earlier, stored procedures also minimize the amount of information that needs to
be sent to the server in the first place, so you can see that stored procedures can be an even
bigger help in reducing trips as well.

Scalability
Scalability refers to the ability of an application to handle larger and larger numbers of users.
This goes hand-in-hand with bandwidth reduction, as the more that you design to protect
bandwidth, the more users the system will be able to handle. But it is also a part of the design
as well.
In the past, using only Visual FoxPro, it was easy to create forms with controls that were
bound to the data itself. For the size of the applications and the way that VFP actually takes the
information into each client system’s memory, this works well. But when all of the data is being
shared among many different client systems and the data stays in the memory of the server, then
binding forms impacts scalability.
Now it is acknowledged that using remote views hardly involves binding client forms to
server data, but the potential for abuse is there and it should be avoided at all costs. One way to
look at it is to ask yourself if the design that you’re using will work well for two users, 10 users,
100 users, 500 users, 1,000 users and more. By keeping the number of potential users in mind,
your designs will be more efficient.
Chapter 9: Some Design Issues for C/S Systems 173

Data location
This last area of performance is the tricky one, and you’ll soon see why it is important. There
are times when the data should be stored on the client instead of the server. That’s right, even
though the server is where the data is kept, there are times when you want to store data on the
client. This is done not as a permanent solution, but rather to reduce network traffic. For
example, suppose the server had a table of state and province postal codes. These are not likely
to change; therefore, it’s a waste of network bandwidth to download this table more often than
it’s modified.
The same is true to some degree for any table that is relatively stable. We don’t mean that
it has to be completely stable, just that it has to be data that is modified infrequently. This way,
the data can be stored on the client and only downloaded when necessary. This enables you to
move a little bit more of the validation to the client system, but this time, rather than it being
hard-coded, it is data-driven validation, based on the data stored on the client.
The only question then is when should the data be downloaded, and how will you know
when that data has been modified. There are several options for the first question. It can be
done every time the application is launched, the first time the application is run during a
calendar period, or when the data is modified. There are many ways that modification can be
detected, such as a special smaller table that can hold datetime values for when the data is
changed, or sending it automatically through replication.

Security
The last design issue is security—making sure that only authorized users have access to the
data. Although you may have used application-level security in the past, Microsoft SQL
Server is so good at handling this that you’ll definitely want to use the built-in tools to
administer security.

Client application
Just as in the days of writing VFP-only applications, the client program may still require a user
to supply a login ID and password. This time, however, the client side will merely pass that
information on to the server, where the user will be authenticated.
If you are using SQL Server in a Windows NT network, then by using NT authentication,
the user will not even have to worry about a login procedure. NT authentication means that the
user was already authenticated when they logged onto the network. So if their network user ID
was registered with SQL Server, or if any Windows NT group they are a member of is
registered with SQL Server, they’ll have access.
If you cannot use NT authentication, you can handle logins by storing the necessary
information in memory and using that to connect to the server in one of several ways. The first
is by creating a data source through the ODBC Data Source Administrator. Then, when
creating the connection, specify the data source name. The data source can actually specify the
login information, so if everyone using the application has been validated through some other
means, then perhaps the user need not specify any login information. But this would be very
risky, as anyone who can access the client computer would be able to gain access to the server.
Another way is directly through the ODBC driver, using what is known as a DSN-less
connection. Login information must be specified using this technique. Finally, if you are using
174 Client/Server Applications with Visual FoxPro and SQL Server

ADO, then you could use the OLE DB provider for SQL Server. Using this technique, you’ll
need the login information as well.

SQL Server logins and permissions


Administering logins in Microsoft SQL Server is a big job. All the users of the client/server
application have to be set up on the server along with passwords. It’s a little easier if you are
using Windows NT, for then you can either set up the users or the NT groups of which they are
members. The latter makes administration easier and more efficient.
Anyone who needs access to SQL Server must have some sort of valid login, or they
cannot do anything with the data on the server. The data is accessed through SQL Server only,
so no one has the ability to get to the data files and open them without using SQL Server.
After a user has gained access to the server, they need permissions to get at the data.
Permissions are set on tables and stored procedures. With a table, a user could be granted or
denied the ability to execute SELECT, INSERT, UPDATE or DELETE statements. As for
stored procedures, security is controlled by the user’s ability to submit the EXECUTE
command. Regardless, without the proper permissions, the users will be unable to either read
or modify the data.
Security is another area where stored procedures have an advantage. A user can be granted
EXECUTE permission on a stored procedure, but be denied permissions to modify and extract
the data from the tables accessed in the stored procedure. This means that you can have tight
control over how users get to the data. This happens due to an efficiency built into Microsoft
SQL Server. If the stored procedure and the objects it uses have the same owner, then the
permissions on the underlying objects are not checked. Handled correctly, all objects in a
database are automatically owned by dbo (the database owner), allowing you to easily take
advantage of this feature.
Another way to make administration of permissions easier is to use roles within a database
on SQL Server. A standard role is very much akin to a Windows NT group. You can set any
number of users in a database as members of a role. Then you can assign permissions to the
roles, rather than to the individual users. This way, as various people leave their jobs and new
people replace them, you won’t have to do any more than drop the old login and create a new
one. Then assign the new login to the database, make it a member of the correct role, and your
job is done.
Even if a user is a member of multiple roles, the various permissions are combined so that
SQL Server will determine exactly what it is that that user can do. Keep in mind that in SQL
Server, a DENY permission always overrides a GRANT permission.

Application roles
There’s a new way to manage security in Microsoft SQL Server 7 through a feature called
application roles. Unlike server and database roles, application roles do not have members.
Instead, an application role has a password. Since almost all database activity, other than
administration, will be handled through your client/server application, there is no need to
manage standard roles and their memberships. Instead, have the client application login to the
server and then set the application role. Once the application role is set, the user will be unable
to do anything that is not allowed to the application role.
Chapter 9: Some Design Issues for C/S Systems 175

Even if a user has permissions that the role does not have, they will be unavailable during
the application. The user’s membership in any standard roles will not have any impact on the
application role because it overrides the connection’s permissions.
The Transact-SQL statement that sets the application role is the following:

EXECUTE sp_setapprole <AppRoleName>, <Password>

The sp_setapprole is a system stored procedure that activates the application role and
is submitted through a SQLExec() call. There is an option to encrypt the password as it is
sent to the server. The users will still need to be able to authenticate or log on to the server,
but they will need absolutely no database permissions. All the permission setting will be
with the application role, and those should be set to match up with the activity of the
client/server application.

Summary
In this chapter, you learned about the issues around client/server database design. You learned
what the various options are when planning out an application such as this, with special
attention being paid to those areas where there are conflicts between client design and server
design. When planning out the data integrity issues, keep in mind the pros and cons of where
validation is done. When validation is performed on the client, the issue is deployment and
recompiling when changes are made. When validation is done on the server, the issue is
network traffic and server overload.
You learned that stored procedures aid in handling validation, security and error
processing, as well as cutting down on network traffic. You also saw the use of stored
procedures through ADO and the advantages that ADO brings.
Client/server design is not just choosing between client and server; it’s also making
database decisions that will impact performance and security.
In this chapter, the options were presented so that you can make informed decisions. In the
next chapter, you will learn about the care and feeding of a client/server database.
176 Client/Server Applications with Visual FoxPro and SQL Server
Chapter 10: Application Distribution and Managing Updates 177

Chapter 10
Application Distribution
and Managing Updates
In Chapter 9, you learned about the options available in designing a client/server
application. In this chapter, you’ll learn how to plan development so as to make the
deployment as easy as possible, the options for deploying, and the management of
changing the application, whether changing the server side, the client side or both.

Planning for distribution and modification is no less important than any other aspect of the
development process. You have to provide your users with the ability to install your finished
project anywhere they choose, and you must provide a usable mechanism for the distribution
of all parts of the application. Then you have to make sure that you have plans in place to
handle changes to any or all parts of the product. Finally, during this process you must also
devise a method of version control, which encompasses not only the code but also the server
side of things. So planning for change is clearly a much bigger job in client/server than in
one-tier applications.
In this chapter we will look at the planning stages of development, and then examine the
various ways of deploying the client side of the application. We’ll explore the actual
distribution of a database design for the server side, and wrap up with a discussion of updates
and version control.

Client/server development
What creates the challenge in planning for distribution and change is that you are working with
a client/server application. This may sound simple, but it means that one part of the application
resides on the client and the other part resides on the server. (Just think what happens when you
start designing n-tier applications, with three or more parts!)
Again, this might seem obvious, but there are a lot of issues that can make the development
process a true headache if the obvious is overlooked. Among other things, if you develop the
entire application on one computer, remember that eventually the parts will be separated—no
part of your code should assume anything regarding locations. In the following sections,
we’ll examine the challenges of planning for relocation in both Visual FoxPro and Microsoft
SQL Server.

Development environment
Before beginning the development process, it’s a good idea to examine the environment in
which the development will be done. Everyone who is a part of the development process will
be working on his or her own computer, so it’s important that the code and the database be
shared in some fashion during this time. For this reason, it is important to have some kind of
source control software. Even if you are the only developer involved, source control software
can be a wise investment if it also supports version control.
178 Client/Server Applications with Visual FoxPro and SQL Server

You should also never make the mistake of assuming that the locations being used for
development will remain the same or even have the same relationship once the application is
set up for production.

SQL Server and Visual FoxPro


Visual FoxPro acts as if it is a single-user system. Although files, such as tables, classes, and
program code, can be shared through rights granted in the operating system, the actual software
runs in the memory space of the workstation. This issue by itself is always a problem, whether
or not you are developing a client/server application. Since various parts of the client side can
be edited at the same time, it is imperative that you use some sort of source control. Source
control software, such as Microsoft Visual SourceSafe (VSS), can help maintain control over
the various parts of the development process. VSS works by controlling the various files, only
allowing people to check out and edit the components if they have the correct rights, and then
locking the files so that no one else can edit them. (There is a feature that allows multiple users
to edit the same file at the same time, but this is generally not recommended.)
SQL Server runs on a server, so although development can be done from any workstation,
all of the changes are right there on the server. Everything is shared, as long as users have the
correct permissions within SQL Server. The issue is that all work done on the server, such as
creating tables, views, stored procedures and triggers, is stored right there in the database and
its files. There is no direct way to maintain source control over what’s inside a SQL database,
but you can maintain source control over files that are not in the database. However, you can
create script files by using SQL Server’s Generate Script Files menu option, from within the
Enterprise Manager. This way, whatever is done can be placed under source control. The
difference is that a developer could still edit the actual objects in the database without checking
the script out first. This means that it is up to the development staff to devise a workable plan
for implementing source and version control.

Programming for deployment


It’s important to be aware of the repercussions associated with writing programs that hard-code
computer names for the production systems, as they tend to be different from the development
systems—not only the computer names, but also the database names can be different. The trap
here is that the server name must be known in order for the client application to make a
connection. Although it is remotely possible that the production and development systems will
have the same name, it’s highly unlikely. This will not always be an issue, and in some cases it
may only be something to worry about during installation, but the client-side program should
derive the server name from somewhere so that it is not compiled into the code.
This situation presents various levels of difficulty. In the simplest of client/server
applications, there is only one server being used and no duplicate databases. In this case, the
only thing to program for is the actual connection information. The connection is how the client
application establishes which target server to use. This connection can take many forms in
today’s modern applications. First, you can use Visual FoxPro’s Connection object that is
saved in a VFP database container. Remote views in a database container can easily take
advantage of a Connection object, making them ideal for quick view design. However,
Connection objects and remote views can also use predefined ODBC Data Source Names
Chapter 10: Application Distribution and Managing Updates 179

(DSNs). Furthermore, you can use either Connection objects or DSNs when using the
SQLConnect() VFP function.
The problem with using ODBC DSNs is that they are defined on the computer that is doing
the connecting. There are three types of ODBC DSNs: user, system and file. Both user and
system DSNs reside on the client computer itself, and even though a file DSN can be relocated
to other computers, the server name is still definitely a part of it. This means that the DSN used
during development has to be reset at installation in order to point to the production server.
If you use a file DSN, then you can modify it at installation with the server name. That’s
because a file DSN is a text file with one line that reads “SERVER=<name of server>.” By
using the low-level file I/O functions in Visual FoxPro or the Windows Scripting Host object
library, you can create or duplicate a file DSN during the installation process. There are other
issues that might have to be addressed in a file DSN; these are covered in the next section.
However, the biggest problem in using file DSNs is that, by default, Visual FoxPro does not
see them in the database container tools.
Another issue to consider in the development process is the aforementioned possibility that
the database might have a different name. This is also addressed by using a file DSN or SQL
pass through functions. But it’s not always that simple. There are applications where there are
multiple identical databases—that is, they have the exact same set of tables and stored
procedures and everything else. This type of application is used in cases where a business
might have to handle the exact same type of data for many clients, such as accountants.
You can avoid using DSNs by instead providing a complete connect string. To create
remote views that use DSN-less connections, you must create a Connection object in a Visual
FoxPro database container, and then provide the connect string in the properties of the object.
With SQL pass through, you can specify a connect string with the SQLStringConnect()
function, allowing you to avoid the need for a VFP DBC. Of course, all of your server
communication would then have to occur through SQL pass through.
Hopefully, you now appreciate the necessity of avoiding hard-coded references to servers
and databases, and see the wisdom in devising alternatives for creating such references. One of
the simplest techniques is to make the whole connection process data-driven. For example, you
could create and use a local table that stores server and/or database names. When the
application is launched, the table values are read and stored in variable names, or properties,
that are used throughout the code. During installation, a procedure captures the server name,
the database names and their locations, and stores them in the local table. The fact that this
table may be duplicated is not a problem, as long as it is accessible to the user who did the
installation, or other users if the app is run from a network.

Deployment models
Once you’ve written and tested the application, how do you deliver it? Before exploring the
various options available for distributing the server side of the application, let’s look at the
various methods of sending the applications out to the users.

Traditional
In the old days—before Windows, before components, before multi-tier and, most of all, before
the Internet—applications were deployed very simply: You copied them. You’d develop the
program using whatever development platform you wrote in, copy the files to a floppy disk and
180 Client/Server Applications with Visual FoxPro and SQL Server

send or carry it to the target system, and then copy them from the floppy to the user’s hard disk
or network drive.
The only problem was that in order for this to work, the program had to be compiled into a
stand-alone executable. With products like FoxPro, you had to make sure that the user had a
copy of the same package that was used for programming. It did get simpler, in a way, once you
could distribute a run-time version of your development package, with no extra cost! But
whether it was a run-time or a development version of the language, you started down a
slippery slope. Soon, with Visual FoxPro, it isn’t enough just to install the code and a package;
other programs are also needed. That requires you to be very careful about matching up the
development platform with the users’ platforms.

Components
Once programming no longer involved just one development product, it became necessary to
find a way to break the application apart, and a way to make sure all the parts would find their
way into the delivered application.
Components came to the rescue. Components are just a way to break a program into parts.
Each part performs various tasks so that no part needs any particular set of parts (i.e., they have
low dependency on each other). Of course, after years of trying to write “reusable” code, Visual
FoxPro came along and introduced you to classes, and suddenly, reusable code became the
rule. So in using VFP, you started to break down the parts of an application into components,
such as user interface, business rules and data handling.
Having done all that, VFP also was brought into the Microsoft component fold, allowing
you to take advantage of third-party components, even components not written with Visual
FoxPro. How this all came about is a part of the never-ending saga of finding better ways to
write and maintain programs, while making them match up with the users rather than forcing
users to match the code.
Microsoft’s answer to components is the Component Object Model (COM). COM works
by registering the details of every component that’s installed in your Windows operating
system. This is handled through the Windows registry automatically whenever any component
software is installed on a computer. Then, when another product needs to use that component,
Windows looks up a code known as the class ID (or CLSID) in the registry to determine where
the actual software is for that component.
When you create an application, you can take advantage of these components and even
create your own. Then, when the project is turned into an application, you must collect all
of the components you’ve used and make sure that they get installed and registered on the
target computer.
The advantage of using components is that they can be modified and reinstalled without
your having to recompile the entire program. In this way, minor modifications to these parts
can be made independent of all other parts. Using the old way, everything was written into one
big program, requiring you to redistribute that big program every time a change was made.

Server
The new element in today’s environment is that the server side also has to be deployed. This
includes the database design, the seed data for the database, script files that are used with the
application, and a plan for importing data from other sources to start the database. Unlike the
Chapter 10: Application Distribution and Managing Updates 181

client side of the application, there is no automatic way of packaging the parts of the server
side. The server can consist of many separate files that have no real connection to each other.
This means that you have to use a technique similar to the old way of doing things. That is,
some files may have to be transported (copied) from the development platform to the server’s
production platform.
In the next section, we will explore the various challenges of deploying the server, and the
different ways of actually accomplishing this.

Distributing databases (creating)


Before you begin to plan for the distribution of new databases, you need to determine whether
SQL Server 7.0 is already installed at the production site. After this issue has been determined
and handled, the database must be created and initialized at the site. In this section, you will see
the challenges of handling installations and database creation.

Existence of SQL Server


As stated earlier, you should not hard-code the server name, because the server name may be
different after installation. But there are other issues as well. The big question is whether or not
SQL Server already exists on the target server. If it is already there, then you must determine
whether it is a full-fledged multi-use server, whether there might be multiple databases for
different applications, and the availability of a database administrator.

First installation
If there has never been a SQL Server installation, then you may be responsible for setting it up.
That means obtaining the Microsoft SQL Server software and the correct number and type of
licenses. Yes, unlike the client side of the application, which in many cases can be distributed
royalty-free, Microsoft SQL Server requires licenses. For information on licensing, see Chapter
3, “Introduction to SQL Server 7.0.”
There are a number of other issues that you must consider when installing the server, such
as the character set, the sort order, the Unicode collation sequence, the account to use for the
SQLServer service and SQLServerAgent service, and which components need to be installed.
These are the same sorts of things that you should have encountered when you installed
Microsoft SQL Server in the development environment.
The recommended method of handling a new installation for a client system is to use a
batch file and an initialization file. The initialization file will contain all the instructions needed
by SQL Server’s setup program so that the install can be done unattended. There are several
sample initialization files that ship with SQL Server, but if you need to create one of your own,
you can launch the setup program with an option that will save your choices into an
initialization file. You can even cancel the install at the last step and the file will still be
created. The last step will be to include two extra sections needed for the install process. These
are the [SdStartCopy-0] and [SdFinish-0] sections. You can find examples of these sections at
the beginning and end of the sample files.
The following command creates the initialization file:

setupsql.exe k=Rc
182 Client/Server Applications with Visual FoxPro and SQL Server

This will create a file called setup.iss in the \Windows or \WinNT directory. To use the
initialization file that was just created, use the following code:

start /wait setupsql.exe –f1 C:\WinNT\setup.iss –SMS –s

The “start /wait” together with the –SMS switch forces the installation to complete without
returning to the command prompt until the setup is finished. The –f1 switch must specify the
full path and name of the initialization file that you create. The –s switch causes it to run in
silent mode and never present any screen to the user. You must include a full path to the
setupsql program.
If you are controlling the installation through a batch, then you’ll also need to allow the
user to choose the location of the SQL Server software and databases. That choice will be
for both the computer where SQL Server will reside as well as the folders where the program
and databases will be placed.
Once the Microsoft SQL Server installation is done, your server-side database can
be installed.

Prior existence
If SQL Server is already in place at the location of your client/server application, then the entire
process can be as simple as having the user point to the server, and then choosing the location
of your database. The consideration here is whether or not there is already an active SQL
Server installation at the target site.
If there is an active server for other databases, then security is an issue. In order for your
installation to be successful, the person or job doing the installation must have the rights to
create a database, or be a system administrator. If there is already a database administrator at
that site, then it’s possible that the installation would have to be in conjunction with that person
or department. There are SQL Server facilities where security is maintained very tightly and
permissions to act as a system administrator are severely limited. This means that you will have
to coordinate the software installation with the database administration staff.

SQL pass through


With the SQL pass through method, the entire installation can be done entirely from within
Visual FoxPro. It can also be data-driven, with all of the components of the database stored in a
table, and then the SQL commands can be created dynamically and passed into the server.
The first step in using this method is to establish a connection to the server. There are three
ways in which this can be accomplished:
• The SQLConnect() function
• The SQLStringConnect() function
• A remote view, followed by the CURSORGETPROP() function

With the first method, there are two options. You can use a predefined Connection object
from a database container, or you can supply the ODBC data source name, followed by a login
ID and password. The second method uses a string that contains all of the information needed
Chapter 10: Application Distribution and Managing Updates 183

by the server in order to connect. Both of these connect functions will return a connection
handle that will be used in future SQL pass through functions.
The last method first connects via a remote view; then, by querying the resulting cursor’s
ConnectHandle property through the CursorGetProp() function, the same result is achieved.
Here are samples of these three methods:

hSQL = SQLConnect("MyServer","Installer","password")

hSQL = SQLStringConnect("DSN=MyServer;UserID=Installer;Password=password")

USE MyRemoteView
hSQL = CURSORGETPROP("ConnectHandle")

The connection handle is then used in subsequent calls to the server as the first argument in
all of the SQL pass through functions. The only function that can be used with data definition
language is the SQLExec() function. This function takes two required arguments and one
optional argument. The first argument is the handle; the second is the SQL statement to be
executed on the server. This statement can be a parameterized query, similar to parameterized
views, so that values for the statement can be supplied through other variables. The third
optional argument is the alias name for the result set(s) returned from the command, if any. By
default the alias used is SQLResult, but you can specify any name.
After adding some tables, you could use the SQLTABLES() function to download the
names of the tables and/or views that you’ve created in the database. Your program might do
this to check that all of the objects were created as desired.
There are other SQL pass through functions that can also be used to look at columns, start
and complete transactions, and set properties for the connection itself. One thing to remember
is that the SQLExec() function allows multiple statements to be sent to the server.
The advantage of using SQL pass through is that all of the setup code is done through a
routine written in Visual FoxPro. The disadvantage is the same thing. If you’ve built a SQL
Server database for development and testing, then you already have the format needed for the
installation. In order to create a VFP pass through program, you’ll also have to write a program
to break the database down into its component parts and objects.

SQL scripts
SQL scripts allow the entire SQL Server database to be created through text files containing
Transact-SQL commands. Using this technique, you will need to have the script files (usually
text files with the extension of .SQL) sent to the target system during installation, and then
launch them either by loading them into the Query Analyzer tool or through the use of the
command utility osql.
The advantage of script files is that they can be generated automatically through a menu
option in the Enterprise Manager. This option takes you to a dialog where you choose which
objects to script, as well as other options such as scripting the permissions, making sure the
appropriate logins are in the script, and ensuring that indexes, triggers and constraints are part
of the script as well.
The disadvantage of this option is that it does not automatically script the addition of any
data that may have to be in the database before the application begins. This requires additional
scripts to run in order for the data to be inserted.
184 Client/Server Applications with Visual FoxPro and SQL Server

SQL-DMO
SQL Server’s Distributed Management Objects (SQL-DMO) is the framework upon which the
Enterprise Manager is built. By using the same object model, you can write Visual FoxPro
programs that can do the same things as the graphical user tool that ships with SQL Server. In
fact, you could design your own graphical tool for the installation of your application.
As with SQL pass through, SQL-DMO affords you the client-side programming option of
installation. SQL-DMO is more specific than pass through. In this method, you instantiate
objects to create, set their properties, and then execute the methods that will do the job. The
following example creates a new database:

oSQLServer = CREATEOBJECT("SQLDMO.SQLServer")
oSQLServer.Connect("MyServer","sa","")

oDatabase = CREATEOBJECT("SQLDMO.Database")
oDataFile = CREATEOBJECT("SQLDMO.DBFile")
oLogFile = CREATEOBJECT("SQLDMO.LogFile")

oDatabase.Name = "CSExample"

* Define the primary data file


oDataFile.Name = "CSData1"
oDataFile.PhysicalName = "C:\MSSQL7\DATA\CSData1.MDF"
oDataFile.PrimaryFile = .T.
oDataFile.FileGrowthType = 0 && growth in MB
oDataFile.FileGrowth = 1 && 1 MB

oDatabase.FileGroups("PRIMARY").DBFiles.Add(oDataFile)

* Define the transaction log file


oLogFile.Name = "CSLog1"
oLogFile.PhysicalName = "C:\MSSQL7\DATA\CSLog1.LDF"

oDatabase.TransactionLog.LogFiles.Add(oLogFile)
oSQLServer.Databases.Add(oDatabase)

Within SQL-DMO, there are objects defined for everything in SQL Server, including
databases, files, file groups, tables, columns, triggers, stored procedures, views, users and
logins. Everything that has to do with administration of a SQL Server database is covered in
this object model.
The advantage of using SQL-DMO is that, like SQL pass through, everything can be
handled from a Visual FoxPro program. And the control is much tighter and more exact than
with pass through. Just as with the other method, the entire setup of the database can be data-
driven, with all of the object definitions stored in a VFP table, or database. Another advantage
is that SQL-DMO has events that your program could be set up to handle.
The disadvantage of SQL-DMO is that the database used for development and testing has
to be broken down into its component parts; this can be handled with a SQL-DMO program.
This method also requires extensive knowledge of the SQL-DMO object model. Just remember
that once you have written routines to use SQL-DMO, they can be used again and again in
future installations as well as for database maintenance.
You might think that since SQL-DMO is designed for administration, data modifications
would require another object library, such as ADO. But actually, there are several Execute…
Chapter 10: Application Distribution and Managing Updates 185

methods available with the Database and SQLServer objects, allowing you to actually pass
through any command, even those commands that return results.

Object transfer (DTS)


If the development system is on a network connected to the target system, then this method may
be the easiest way to transfer the structures and data. Within Data Transformation Services
(DTS), there is a task that transfers objects from one SQL Server 7.0 system to another SQL
Server 7.0 system. You can create this type of DTS package by using the Import Wizard in the
Enterprise Manager.
To start the Import Wizard, right-click on the Data Transformation Services folder and
choose All Tasks | Import Data… The first two screens of the wizard prompt you for the server
names, database names and login information. When the third screen asks about the type of
data transfer, you should choose “Transfer objects and data between SQL Server 7.0
databases.” The fourth screen is where you set up the object and data transfer. The default
options are shown in Figure 1.

Figure 1. The DTS Import Wizard “Select Objects to Transfer” screen.

This screen is where you set up which objects to transfer, whether to create the objects on
the destination server, whether to drop any existing objects, and whether to transfer the data. By
clearing the check box for Transfer all objects, you can choose the Select Objects… button and
decide exactly what goes and what doesn’t. By clearing the Use default options check box, you
can select the Options… button to set such things as transferring users, logins, indexes, triggers,
keys and permissions.
In many ways, transferring objects is very similar to generating scripts. In fact, note that in
the screen shown in Figure 1, there’s also a place to specify where the scripts are stored. That’s
because the transfer objects task does the transfer via scripts that are just about the same as the
scripts that would be generated by the SQL scripts option discussed earlier.
186 Client/Server Applications with Visual FoxPro and SQL Server

The advantage of this method is that the transfer of objects is automated to a high degree,
and once the DTS package has been created, it can be run over and over again until everything
is set up just the way it should be. The other big advantage of object transfer is that any data
already set up before production can also be transferred in the same process.
The disadvantage of this method is that it only works when the source and destination are
connected. Another problem is that different techniques will be required when doing server
upgrades because the transfers are of whole objects, and this method does not allow for
modification of objects—at least, not without losing any existing data.
This method is also attractive if you have data in non-relational sources that will need to be
transferred into the client/server system. This is because DTS is designed to pick up data from
anywhere through OLE DB, and transform it and copy it to any destination. Since you would
already be using DTS for the object transfer, then it would require just a little more work to
incorporate the transfer of the data within the same package. All it would need is the addition of
some other tasks to the object transfer package.

Backup/restore
Another method that is also simple and straightforward is to create a backup of your
development database and restore it on your production server. If the setups for the
development and production systems are identical, then you can use the SQL Server backup.
Simply back up the database to tape, disk or across the network, and then use the SQL Server
restore process to put the database into the target system. In order for this to work, both SQL
Server installations must have used the same installation options, including character set, sort
order and Unicode collation sequence.
Since the files that make up the database must be the same number and size, the relative
capacities of both systems must be the same. That is, if you have a 20GB data file when you
back up the database, then you’ll have to restore to a 20GB file. You cannot split it into
separate files when you do the restore. On the other hand, you can put the files that make up a
database in different relative locations when you do the restore.
Probably the most common reason for using this method over others is that the backup
files can be transferred easily. The only reason not to use this technique is that your
development version of the database may have objects that are only for development and
should not be moved to the production system.

sp_Detach_DB and sp_Attach_DB


Yet another way of transferring the files that comprise the database is to use two system stored
procedures called sp_Detach_DB and sp_Attach_DB. This method is actually preferable to the
backup/restore method in some ways.
By using the system stored procedure sp_Detach_DB, you can remove a database from the
server where it is located without dropping the files. Once this is done, you can simply copy the
data and log files to the target system and then use the sp_Attach_DB system stored procedure.
In fact, if the source SQL Server is not running, you don’t have to use the sp_Detach_DB
procedure. Instead, you can simply copy the files while the server is not running.
The advantage of this method is that the transfer is very simple and straightforward, with
just one caveat: The sp_Attach_DB procedure requires the name of the database and the
Chapter 10: Application Distribution and Managing Updates 187

physical names (locations) of all of the files. There is also a limit of 16 files per database in
one command.
There are a couple of disadvantages to this method. First, if you detach the database, then
you’ll have to run the sp_Attach_DB stored procedure to reattach the database to the
originating server. However, this could also be an advantage, since you’ll be testing your
command for the production system. Another disadvantage is that, like the backup/restore
method, the installation options for both target and destination must be the same, and you will
want to make sure that there are no development objects in the database.
Table 1 summarizes the various installation methods.

Table 1. Summary of installation methods and their respective advantages


and disadvantages.

Installation method Advantages Disadvantages


SQL pass through Client-side programming. Everything is done through T-SQL by
functions using the SQLExec() function.
All objects must be turned into code.
SQL script files Can be generated via menu No data inserts are handled
option in Enterprise Manager. automatically.
SQL-DMO programs Client-side programming. Requires careful coding, covering
Object model code with properties, all objects.
methods and events. Normally does not handle data inserts.
Tends to perform more slowly than
other methods.
DTS Object Transfer Uses built-in tool. Transfer only works when systems are
Can handle database objects on the same network.
and data.
Backup/restore Uses built-in tool, no coding Source database cannot have any
necessary. development objects in it.
Installations must be identical.
sp_Detach_DB and Uses built-in system stored Source database cannot have any
sp_Attach_DB procedures. development objects in it.
Installations must be identical.

In choosing a method for installation, you must also consider how you will be handling
updates or modifications to the database. The next section covers this topic.

Managing updates
Updates are very difficult to manage, especially in a client/server application. That’s because
you might be updating the client-side programming, or the server-side database, or both. This
requires you to devise the update strategy before you even begin the installation.
In this section we will look at the various elements of the update process, and conclude
with the challenge of coordinating between client and server.

Application changes
On the client side, you have the program or application to consider. The issues here are that no
project is ever done, and you have to make sure that the application can handle change easily
and that the system for managing change keeps the program consistent. Changes can take the
188 Client/Server Applications with Visual FoxPro and SQL Server

form of minor bug fixes, major changes to the way the program works, and upgrades to the
system that result in the product being vastly different than it was before.
Managing code changes is not new to client/server, and many of the issues surrounding
this task are the same as with other application architectures. The difference with client/server
is that a program can be split up into different parts that do not have to be modified at the
same time.

Version control
The first step in handling updates is to have source control software that will help you manage
the process. It’s also helpful if you can do version control, either in the source control software
or in the application itself.
A common technique today is to add a version number to your executables, where the
numbers represent major, minor and revision changes. The major number is reserved for big
changes in the application, such as new features that were not in the original program. Also, a
change in the user interface screens may be regarded as a major modification. Minor numbers
are used for changes that are less dramatic but demonstrate some sort of visible change. For
example, a new option could have been added to a menu or a new screen incorporated
seamlessly into the application. Revision numbers typically represent a new compilation of the
code that corrects application bugs.
Figure 2 shows the screen in Visual FoxPro where version numbers can be set. This screen
is accessed by choosing the Build button on the Project screen, and then choosing the Version
button in the Build dialog. The Auto Increment check box causes the Revision number to be
increased by one with every build. You can only use the Version screen by creating a COM
server or Win32 executable.

Figure 2. Version dialog showing where to indicate version numbers.


Chapter 10: Application Distribution and Managing Updates 189

When you are checking a copy of an existing program and trying to determine the version
number, you can use the AGETFILEVERSION() function to build an array of information from
this dialog. Element number 11 will have the product version value.
By using version numbers, it’s easy to track down bugs that have already been fixed. First
you determine the version of the software that the reporting party is using, and then see whether
that bug was fixed in subsequent versions.
The most important aspect of version control is managing the distributed versions. If there
are multiple copies of the client code, then every user has to be tracked and updated. You may
need to maintain a table of this information to track exactly who has which version. You must
also have methods in place to make sure that you do not overwrite a newer version with an
older update.

Traditional
With a monolithic application, a new version is the entire application. This means that the
entire project has to be rebuilt and then distributed in some manner to the customers or users.
No matter how small an adjustment was made to the code, you must distribute the entire
program file.
In using this method, you will need to create an update program that reads the version
information from the target site, so that the version being installed is assured of being
more recent.

Component-based
The big advantage of components is that the entire application does not have to be distributed
for every change. Rather, components are separate parts of the whole that have an interface
that’s unlikely to change. Here the word interface refers to the way one piece of code “talks” to
another piece of code. For example, you might have a form that gathers information from the
user and then passes the data on to another piece of code that validates that data. Since the
validation routines are not in the form, they can be updated separately from the form itself. The
only thing to make sure of is that the names of the methods and properties of the validation
component are not changed.
The major disadvantage of component-based applications is that each component will
have its own version number. This is where the minor numbers can make a big difference. A
minor number change can indicate a change in the interface of a component, so that you could
have a system where components always match up as long as the major and minor numbers are
the same.
In spite of any versioning problems, components are currently the preferred mechanism
for creating applications. In this way, each component of an application can be modified and
improved with little impact on other components. In fact, it’s possible for applications to be
developed using different software products as long as they are component-enabled.

Database updates
Database changes are a little more difficult than simply modifying code. For one thing, some
updates are changes to the database schema (i.e., the structure of tables), which impacts data
already in the database. Other changes could involve improving performance by modifying,
adding or dropping indexes. New stored procedures could be created in conjunction with added
190 Client/Server Applications with Visual FoxPro and SQL Server

features of the client-side application. Existing stored procedure code could be modified to
handle changes to the schema, or to take advantage of changes that improve performance.

Version control
Version control is much harder to manage on the server side because there are no built-in
version numbers in a SQL Server database. In addition to the aforementioned problems with
source control, you will also have to manage your own version numbers.
There are fewer options available for updating SQL Server than there are for installation.
For example, it’s not possible to back up the development database, or even a sample
production database, because the restore process would overwrite any data that the user of the
application had added to the database. Transferring objects would only work for new objects,
or non-data-containing objects such as views and stored procedures. And there’s no way to use
the sp_Attach_DB system stored procedure without encountering the same problems associated
with the backup/restore method.

SQL pass through


Just as with installation, this method allows you to do updates via Visual FoxPro code. SQL
Server supports ALTER commands for modifying the schema of tables, so it is a valid
approach. The biggest advantage of this method is that the update code can be a COM server
or executable that would have a version number matching up with the changes to the server.
This would be just one way for you to manage the version control of the server side of
the application.
The disadvantage is the same as for installation. You must break down the objects from the
database into either data for a control table, or the actual ALTER statements. This also makes
the maintenance of the database difficult. Should objects be stored as they were originally
created, and then include information about their changes? Or should they be stored as the
result of the changes?

SQL scripts
For changes to a SQL Server database, SQL scripts are probably the best method. That’s
because as you make changes to the test database, you can save the changes as scripts. Nearly
every tool in the Enterprise Manager that can make schema changes has a “Save change script”
button that allows you to save the commands the Enterprise Manager generates. This way, you
will be able to know exactly what was done and when. It also helps with source control because
source control systems can monitor new files just as easily as old files.
If you are not using the Enterprise Manager, whatever commands you create to modify the
database can be saved in script files and then used to update the production systems. The
disadvantage that was stated earlier for this method does not apply for updating the schema. So
for all practical purposes, this is the preferred method, but it still does not solve all of the
version control issues.

SQL-DMO
This method is similar to SQL pass through, but instead of just using Transact-SQL commands,
you would use the objects of the management object model to set properties and run methods.
The advantages of using this technique are that the code is much more precise than using the
Chapter 10: Application Distribution and Managing Updates 191

SQLExec() function, and that it can also be used for querying the current structure of
the database.
Since a VFP program would be used to handle the update, you could do the same thing
as with the pass through option and set versioning information in the update code. You would
do this by using COM servers or executable programs and storing the version information for
the database in the program. This method would still not solve the problem of creating version
number information within the database itself.
The disadvantage of this method is that you would have to know exactly what the changes
are, and document as well as program them. Otherwise, there would be no native way of
knowing exactly what changes are made.

Version control coordination between client and server


Version control has been discussed previously in regard to knowing what version a user has and
what bugs occurred in that version. Here we’ll address the issue of matching versions of clients
and servers.
This is the most difficult area to manage because a change to one side of the application
has no direct impact on the other in terms of version numbers. As an example, suppose you
make a change in a table’s schema. This may necessitate either a change in stored procedures
that access that table or in client-side components that access that table. If the changes are
required in both client and server, then the changes in one are incompatible with earlier
versions of the other. But there is no guarantee, given the nature of client/server, that the
changes will be applied at the same time. If there are no safeguards, then this could cause a
crash of your application or, worse, corruption of the data within the database.
In order to avoid this, you will need to create a versioning mechanism on the database, so
that the version can be checked against the version of the client-side code. Your program has
to know, or find, its own version numbers and then match them against the version numbers on
the server.
This version control situation can get quite complicated. For example, you might have
made updates to the client application that do not affect the server. Or perhaps you’ve made
updates to the server that do not impact the client. When a modification requires both to
change, it can create a strange situation in which a client version would work with certain
server versions, but not with others.
It is imperative that you manage this carefully. Create a table in the database that stores
the major, minor and revision numbers, and perhaps even use a fourth value for purposes of
coordination. Then your client code would have to query that table and match up the values
to the revision values internal to the code. If there is no match, then the application should
end with a message about the upgrades needed, or automatically begin a process that performs
the upgrade.

Local lookup data


As mentioned earlier, it may be beneficial to have data stored in local Visual FoxPro tables
instead of on the SQL Server. Let’s review why you might use them, and then we’ll offer some
ideas about how to ensure that they are up to date.
192 Client/Server Applications with Visual FoxPro and SQL Server

Why
Sometimes a client/server application has data that rarely changes. Every development project
will define “rarely” differently, but this could be data that is changed monthly, yearly, daily,
once per session or never. But however you define what constitutes a “rare” change, once
you’ve made that decision, you can improve the performance of your application by
downloading rarely changed data to local tables on the client machine. Once the data is local,
then lookups into that data will go much faster, improving overall speed. How much data can
be handled in this way depends on the capacity of the client hard disk and the amount of time
needed to download the data.
Of course, you will also have to determine when to download the data for periodic
refreshes. As stated earlier, it might be done just on the basis of the date, or upon startup of the
client application. In any case, the client application must check for the data locally so that if
it’s not there, it can be downloaded. The most important thing on a day-to-day basis is to
balance the latency factor against the performance factor. That is, how important is it to have
the absolute latest information vs. the fastest performing application.

Managing updates
Local lookup tables present a special concern when updating. The biggest issue is when those
tables are modified in any fashion. The updating program itself cannot be aware of all the client
locations of the lookup tables. Therefore, when you create updates that impact those local
tables, something in your application should be made aware of the change and then download
the latest table (which will include all of the changes).
The challenge is that local lookup tables are part of the client side of the application, but
changes to the server are where those tables are modified. In order to handle this special
situation, updates to locally stored information will need modifications to both sides of the
application: the database and the Visual FoxPro code.

Summary
In this chapter, you have learned about the special challenges of installing and updating a
client/server application. Along with the challenges, you have seen several ways of handling
these processes. You have also witnessed the special planning that’s required to ensure a
successful client/server setup.
You should now understand why it is so important to use some sort of version control, so
that when updates are performed, you’ll know exactly how the pieces of the overall puzzle are
put together. And that’s just what this is—a kind of jigsaw puzzle, where the individual pieces
have to be cut just right to fit. The ever-present problem is that the shapes of the pieces keep
changing, making the puzzle that much tougher to solve.
Chapter 11: Transactions 193

Chapter 11
Transactions
One of the main benefits of using SQL Server is its ability to handle transactions. This
benefit comes with a learning curve, as SQL Server handles transactions differently than
VFP does. SQL Server provides a much greater level of control over how transactions
occur. As usual, having more control means there is more to know. This chapter covers
the details of SQL Server’s transaction handling as well as how to design and write
Visual FoxPro code to manage transactions properly.

Transaction basics
A transaction is a sequence of data modifications that is performed as a single logical unit of
work. This logical unit of work is typically specified by an explicit set of commands, such as
the BEGIN TRANSACTION and END TRANSACTION commands from Visual FoxPro.
Through the operation of a transaction, the database is essentially “transformed” from one state
to another, ideally uninterrupted by any other activity that might be running concurrently.
The classic example of a transaction is the bank customer who uses an ATM to transfer
some money from a checking account to a savings account. This requires a logical unit of work
that consists of two distinct steps: Deduct the amount from the checking account, and then
credit the savings account by the same amount.
If the logical unit of work is broken, either the bank is unhappy (i.e., the money was
applied to the savings account but not removed from checking) or the customer is unhappy (i.e.,
the money was removed but never deposited). This could happen for a variety of reasons, many
of which have been quantified in a set of properties known by the acronym ACID.

ACID properties
The ACID acronym was coined sometime in the early 1980s. It first appeared in a paper
presented to the ACM. Since then, databases have the ACID test applied to them in order to
discover whether they have “real” transactions. The ACID properties are Atomicity,
Consistency, Isolation and Durability.
• Atomicity: A transaction must support a unit of work, which is either committed in
whole or discarded in whole. For example, in the ATM example, either both accounts
must be updated or neither account can be updated.
• Consistency: This property means that a transaction must leave the data in a consistent
state at the completion of the transaction—that is, the transaction can only commit
legal results to the database. Updates must conform to any rules programmed into
the system .
• Isolation: Every transaction must be isolated from all other transactions. That is, one
transaction cannot read or use data from uncommitted data in another transaction. Any
194 Client/Server Applications with Visual FoxPro and SQL Server

SQL-92 compliant database (like SQL Server) supports a choice of four different
levels of isolation. These levels are discussed later in this chapter.
• Durability: This property requires that a complete transaction be stored permanently,
regardless of any type of system failure. That is, a transaction must maintain the unit
of work either by permanently storing the changes that it says are committed to the
database, or by rolling back the incomplete transaction, even if power fails to the
computer at any time
SQL Server fully supports all four properties, but Visual FoxPro supports only the first
three—it falls short in the Durability test. The following sections document how both Visual
FoxPro and SQL Server fare against the ACID test.

Visual FoxPro transactions


In Visual FoxPro, the BEGIN TRANSACTION, ROLLBACK and END TRANSACTION
commands specify the logical unit of work for a transaction, which meets the criteria for the
Atomicity transaction property. For example, the following Visual FoxPro code uses a
transaction to ensure that both the checking account withdrawal and the savings account
deposit from the earlier transfer perform as a single unit of work:

*--Code to open checking and savings tables


*--with table buffering happens somewhere up here…
*--Now find the right account records
=SEEK(lnAcctCheck,"Checking","acctid")
=SEEK(lnAcctSave,"Savings","acctid")
*--and update their balances
REPLACE balance WITH balance – 100 IN checking
REPLACE balance WITH balance + 100 IN savings
*--start a transaction
BEGIN TRANSACTION
IF NOT TABLEUPDATE(.T.,.F.,"checking")
*--whoops, bad things happened
ROLLBACK
lcErrMsg = "Unable to update checking account"
ELSE
IF NOT TABLEUPDATE(.T.,.F.,"savings")
*--something went wrong
ROLLBACK
lcErrMsg = "Unable to update savings account"
ELSE
*--all is well, so commit
END TRANSACTION
ENDIF
ENDIF

Remember that Visual FoxPro transactions can only be applied against


‡ tables that are associated with a DBC. If the tables are not in a DBC, the
transaction has no effect, and changes are applied to the tables regardless
of the ROLLBACK or END TRANSACTION statements.
Chapter 11: Transactions 195

Through this code, you can also see how the Visual FoxPro transaction qualifies as
Consistent, as the TABLEUPDATE() functions fail if any data integrity rules fail, such as field
validation rules or referential integrity.
What may not be so obvious is how Visual FoxPro handles the Isolation property of
the transaction. Like other database systems, locks are used to maintain the isolation
between Visual FoxPro transactions in the form of record locks, header locks and perhaps
even file locks.
During each of the TABLEUPDATE() calls in the previous code, Visual FoxPro will
implicitly attempt to lock the records that were modified by the REPLACE commands. In this
case, only two record locks are required to ensure the complete isolation of this transaction,
which will ensure that someone else doesn’t write to either record while this transaction is in
progress. In addition, if another process tries to read the data before this transaction executes its
END TRANSACTION statement, the other process will read the original, unchanged data.
Inside of a transaction, Visual FoxPro will hold these row locks until either the
ROLLBACK or END TRANSACTION statements are issued. This means that the transaction
overrides the native TABLEUPDATE() behavior where the locks would normally be released
as soon as the modifications were written to disk.

As you may already know, either a ROLLBACK or END TRANSACTION


‡ statement completes the logical unit of work, even though the syntax
causes one to believe that only the END TRANSACTION statement
completes a Visual FoxPro transaction.

Visual FoxPro’s transaction isolation has performance consequences that are not obvious
with this example. Imagine instead that the modifications made during the transaction included
not only some record updates, but also the addition of new records. In these kinds of updates, it
is likely that either a header lock or file lock will be required, which is held throughout the
transaction. Since there is only one header or file lock, this can quickly cause lock contention
between active transactions, as each must battle to acquire the single header or file lock.
You should also understand that Visual FoxPro implicitly acquires the locks necessary to
update an index or memo file, which are stored in a table’s associated CDX and FPT files,
respectively. Therefore, if your transaction performs a modification on a field that is part of an
index key, or inserts a new record, that table’s CDX file must be locked. There are no record
locks in a CDX file—Visual FoxPro locks the entire CDX file. This is also true for FPT files,
which means that only one person can lock either of these files at any given time.
This is one of the main reasons that you must keep your transactions as short as possible.
If one transaction performs an update that requires an index or memo file lock, all other
transactions will be unable to perform similar operations until the locks are released.

The missing property


Visual FoxPro does not support the Durability property of transactions because it cannot ensure
the persistence of the modifications made during a transaction. In the preceding example, if the
computer hardware, the operating system or the Visual FoxPro application fails before the
END TRANSACTION statement is executed, the data in either table is left untouched, keeping
the logical unit of work intact.
196 Client/Server Applications with Visual FoxPro and SQL Server

However, if such a failure occurs during the execution of the END TRANSACTION
statement, there is no mechanism to recover from this failure. Granted, this should be a small
window of opportunity, but it does exist, and it grows as more changes are made during the
transaction. If this type of failure occurs, you may end up (at best) with a partially committed
transaction where the checking account is debited but the savings account is not credited. In the
worst-case scenario, the two tables in the transaction may end up corrupted beyond repair.

SQL Server transactions


Now that you are familiar with how Visual FoxPro supports the ACID properties, you may be
wondering how SQL Server stacks up. In this competition, the hands-down winner is SQL
Server, as it fully supports the critical Durability property as well as providing four distinct
levels of Isolation.
The logical unit of work is specified in SQL Server by using the BEGIN TRANSACTION,
ROLLBACK TRANSACTION and COMMIT TRANSACTION statements. When you specify
these commands, you are using an explicit or user-defined transaction, which we covered in
Chapter 3, “Introduction to SQL Server 7.0.” In that chapter, you were also introduced to the
idea of Autocommit transactions, in which SQL Server automatically wraps each SQL
statement into its own transaction. To illustrate this, imagine that you execute the following
SQL statement to change all phone numbers in the 408 area code to the 987 area code:

USE Pubs
UPDATE Authors SET Phone = '987 '+Right(Phone,8) WHERE Phone like '408 %'

This statement has the potential to affect multiple records, since it’s unlikely that the
WHERE clause will match only one record. If any of the records cannot be updated for any
reason, it is important that the entire UPDATE statement fail. This is what Autocommit
transactions provide—they automatically wrap any SQL statement into its own implied
transaction, as if the following code was written:

USE Pubs
BEGIN TRANSACTION
UPDATE Authors SET Phone = '987 '+Right(Phone,8) WHERE Phone like '408 %'
IF @@Error <> 0
ROLLBACK TRANSACTION
ELSE
COMMIT TRANSACTION

Implicit transactions
There is a third type of transaction in SQL Server, which is different from an Autocommit or an
explicit transaction, known as an implicit transaction. Visual FoxPro uses implicit transactions
behind the scenes when talking to SQL Server.
Implicit transactions are activated with the SET IMPLICIT_TRANSACTIONS ON
T-SQL statement. The SQL Server ODBC driver issues this statement when you activate the
Manual Transactions property setting of a connection. The following code performs this
activation, which was first demonstrated in Chapter 6, “Extending Remote Views with SQL
Pass Through”:
Chapter 11: Transactions 197

#INCLUDE FoxPro.h
lnResult = SQLSetProp(lhConn, "TRANSACTIONS", DB_TRANSMANUAL)

You could also set this permanently through the Connection Designer by clearing the
“Automatic Transactions” option, but this is not recommended. In either case, once activated,
any of the following statements will implicitly begin a transaction on SQL Server. Note that if a
transaction is already in effect, these commands do not start a new transaction, as nested
transactions are not possible while implicit transactions are in effect.
• ALTER TABLE
• FETCH
• REVOKE
• CREATE
• GRANT
• SELECT
• DELETE
• INSERT
• TRUNCATE TABLE
• DROP
• OPEN
• UPDATE

When using implicit transactions, you must explicitly issue either the ROLLBACK
TRANSACTION or COMMIT TRANSACTION command to complete the transaction,
even though you did not issue a BEGIN TRANSACTION command. Otherwise, the
transaction does not complete, potentially blocking other users from accessing the data
needed by their transactions. From Visual FoxPro, you can complete the transaction by
using the SQLRollback() or SQLCommit() SQL pass through functions. The following
example demonstrates how these functions are employed:

*--open and buffer the views (not shown)


*--grab either view's connection handle
*--assuming they are sharing it
lhConn = CURSORGETPROP("ConnectHandle","Checking")
*--assign data to the view parameters
vnAcctCheck = 10001 && checking acct num
vnAcctSaving = 10002 && savings acct num
*--get the records
REQUERY('Checking')
REQUERY('Savings')
*--make modifications
REPLACE balance WITH balance – 100 IN checking
REPLACE balance WITH balance + 100 IN savings
198 Client/Server Applications with Visual FoxPro and SQL Server

*--turn on manual transactions


lnResult = SQLSetProp(lhConn,"TRANSACTIONS",DB_TRANSMANUAL)
*--perform updates and finish transaction
IF NOT TABLEUPDATE(.F.,.F.,"Checking")
SQLRollback(lhConn)
lcError = "Unable to update Checking account"
ELSE
IF NOT TABLEUPDATE(.F.,.F.,"Savings")
SQLRollback(lhConn)
lcError = "Unable to update Savings account"
ELSE
SQLCommit(lhConn)
ENDIF
ENDIF
*--restore automatic transactions property
lnResult = SQLSetProp(lhConn,"TRANSACTIONS",DB_TRANSAUTO)

In this example, the CURSORGETPROP() function is used to acquire the connection


handle of a view, allowing the SQL pass through statements to share the same connection as
the views.
When the SQLSetProp() function is issued to start the manual transaction, the ODBC
driver submits the SET IMPLICIT_TRANSACTIONS ON statement, forcing the UPDATE
statement (implicitly issued during the TABLEUPDATE() function call) to start a transaction.
If either TABLEUPDATE() fails, then the SQLRollback() function is invoked to discard
and complete the transaction; otherwise, the SQLCommit() function commits and completes
the transaction.
It is important that you issue the final SQLSetProp() function call to restore the automatic
transactions setting. If you skip this step, any of the statements listed previously will start a new
implicit transaction, which will prevent other sessions from updating data, since this type of
transaction must be explicitly completed.
Now that you have seen the three different transaction types, you know how to maintain an
atomic unit of work for any SQL Server transaction.

SQL Server isolation levels


Another major difference in the way SQL Server handles transactions is in the way transactions
are isolated from one another. Visual FoxPro supports only one isolation level, which is more
or less handled internally by Visual FoxPro. SQL Server gives you the choice of four different
isolation levels, and you can select a different isolation level at any time, even during a session.
The isolation levels that SQL Server supports derive from the SQL-92 standard. There are
four different levels: read uncommitted, read committed, repeatable read, and serializable. To
fully understand how these levels are implemented, you may wish to review the section on
locking in Chapter 3, “Introduction to SQL Server 7.0,” before continuing.
In considering the different isolation levels, you should know how each level allows you to
control the typical anomalies associated with concurrent transactions: uncommitted
dependencies, inconsistent analysis and phantom reads.
• An uncommitted dependency is a fancy term for the “dirty read” problem. This
problem occurs when one session is allowed to read the uncommitted data from
another transaction before it has completed.
Chapter 11: Transactions 199

• Inconsistent analysis describes a problem also known as a non-repeatable read. This


condition occurs when a transaction reads the same data twice, but when it reads the
data for the second time, the data has changed. This means that during the first
transaction, a second concurrently executing transaction has changed the data that was
previously read by the first transaction.
• Phantom reads also occur when a transaction reads the same data more than once. For
example, imagine a query that retrieves all of the customers within a certain ZIP code.
When it is initially executed, only 100 customers are returned. However, when it is
executed the second time within the same transaction, 102 customers match the
criteria. These two extra customers are known as phantoms.

Note that all of these anomalies can be avoided if locks are employed at the right resource
(i.e., row, page, table or index) and of the right type (i.e., shared, exclusive and so forth).
Having described these anomalies, it is now easier to define how each isolation level prevents
one or more of these problems.
• Read uncommitted is the lowest level of isolation, and it provides the greatest amount
of throughput, as it virtually eliminates locking between sessions. By enabling this
level, you allow transactions to read dirty or uncommitted data. This occurs because
the session that is set to read uncommitted will “read through” any exclusive locks
held by other sessions. Furthermore, shared locks are not used when reading data at
this level. All of the previously specified anomalies can occur at this level.
• Read committed is the default isolation level of SQL Server transactions. It ensures
that the session respects any exclusive locks held by other sessions, preventing the
dirty read problem described earlier. However, this isolation level releases any shared
locks immediately after the data is read. Therefore, non-repeatable reads and phantom
reads can occur at this level.
• Repeatable read is the next highest isolation level available. SQL Server enforces
repeatable reads by allowing a transaction to hold the shared locks used to read data
until the transaction completes. This prevents other sessions from modifying this data,
as data modifications require an exclusive lock, which is incompatible with shared
locks. This isolation level still allows phantom reads to occur.
• Serializable is the highest isolation level available, and therefore it also has the
potential for the highest amount of lock contention (read: slowest performance). In
serializable transactions, dirty reads, non-repeatable reads and phantoms are
eliminated. However, instead of using the expected table locks, SQL Server uses
something known as a key-range lock. For example, in a query that asks for all
customers in a certain ZIP code, the pages of the index that contain the keys that point
to the matching records are locked. This prevents other sessions from inserting
customer records into the table that would become part of the selected ZIP code.

It is important to understand this level of detail, as it is the only way that you can control
how locking is handled by SQL Server. There are no equivalents to Visual FoxPro’s RLOCK()
or FLOCK() functions in T-SQL.
200 Client/Server Applications with Visual FoxPro and SQL Server

Setting isolation levels


At this point, you may be wondering how these levels are implemented. One way to set them is
through the SET TRANSACTION ISOLATION LEVEL command, and the other is through
the table hints section of any standard SQL statement.
At the session level, the default is to use the read committed isolation level. However, if
you desire a particular connection to work at the serializable level, you can issue the following
command from Visual FoxPro:

lnResult = SQLExec(lhConn, "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")

Any statements issued across this connection (the one specified by the connection handle
lhConn) will now work under the serializable isolation level. This isolation level remains in
effect until the connection is dropped, another SET TRANSACTION ISOLATION LEVEL
statement is issued, or if any specific SQL statement uses any locking hints to override the
locking behavior.
Note that this setting is only for the current connection—other connections are unchanged
by this setting, and will operate at their own isolation levels. This means that you can have
separate connections that work at different isolation levels, but this is typically not a good idea,
particularly from a debugging standpoint.
The alternative method of setting the isolation level is to use the table hints portion of the
FROM clause of a SELECT, UPDATE or DELETE T-SQL statement. With the right table
hint, you can override the default connection-level setting of the isolation level for an
individual statement. For example, the following query will override the default connection
setting and resort to a level of uncommitted read for the duration of the query:

SELECT COUNT(*) FROM authors WITH (NOLOCK)

Another possible hint is the READPAST hint. When using this locking hint, SQL Server
will skip any locked rows. This statement could prevent you from seeing a complete result set,
but the benefit is that your transactions will not wait until locks are released before reading the
data. This is a useful tool for determining whether blocking is a problem in specific queries.
If you have a specific query that should operate as serializable, but the session is set at
the read committed level (the default), then use the SERIALIZABLE or HOLDLOCK hints
(they are interchangeable hints). This will force only the current statement to hold locks;
when used inside of a transaction, this could help prevent unnecessary lock contention, as only
the specific statement will use serializable isolation, instead of every table that participates in
the transaction.

Durable transactions
The last ACID property to test against SQL Server is Durability, which specifies that a
complete transaction must be stored permanently, regardless of any type of system failure. As
mentioned earlier, this is the only ACID property where Visual FoxPro is lacking. However,
SQL Server transactions qualify as durable. SQL Server implements durability via its
transaction log.
As was briefly discussed in Chapter 3, SQL Server uses a technique known as write-ahead
logging to ensure the durability of its transactions. This means that any data modifications are
Chapter 11: Transactions 201

first written synchronously to the transaction log (i.e., your application will wait until it
completes this operation before control returns) before they are committed to disk. In fact,
there may be a long period of time between the moment when SQL Server receives your
modification and the moment SQL Server commits the data to disk.
When you submit a data modification statement to SQL Server, the statement is logged.
Actually, either the statement itself is logged, or the before and after versions of the data are
logged. In either case, enough information is placed into the log so that SQL Server can
reconstruct the data modification at any time. Logging occurs for any SQL statement that
modifies data (e.g., UPDATE, DELETE or INSERT) as well as statements that create indexes
and modify structures (yes, you can roll back a CREATE INDEX statement in SQL Server!).
Once this logging is complete, SQL Server loads the proper extent(s) into a part of
memory known as the buffer cache, but only if the data does not already reside there. This is
where the data modification is made, allowing any other processes to see the change. In other
words, SQL Server, much like Visual FoxPro, tries to read data from memory or write
modifications to memory, not directly from disk. Therefore, what resides on disk may not
actually be what’s currently held in memory. This is usually referred to as a “dirty” buffer, and
it must eventually get committed to disk.
On an as-needed basis, SQL Server will write these “dirty” buffers to the actual database
files with something known as the Lazy Writer process. When this occurs, SQL Server marks
the transaction log so that it understands that logged changes have been committed to disk. This
mark is known as a checkpoint, and any logged transactions that occur before the checkpoint
are known to be on disk, while those appearing after the checkpoint are not.
The checkpoint process occurs only after a certain amount of activity has occurred in the
database. If it is a slow day in the database, checkpoints will happen less frequently, while on a
particularly busy day, checkpoints may occur much more often. This is controlled by something
known as the recovery interval, a system-level setting that controls just how much dirty data is
allowed to exist at any given time. In advanced situations, you can manipulate this setting to
limit the occurrence of checkpoints, but for nearly all situations, the default setting is
appropriate (read: don’t mess with this setting!).
The durability of a SQL Server database becomes apparent when the server fails for
some reason (hardware failure, operating system crash and so on). Since SQL Server has
logged all data modifications, it can use the log to determine how to recover the database after
a failure. For example, imagine that several sessions were updating tables when the power
failed on a server. If the workstations received notification that their updates were successful,
then their updates have been written to the transaction log successfully. If a workstation did
not complete an update successfully, then the transaction log may not contain the COMMIT
TRANSACTION statement for that workstation’s transaction.
When the server is restarted, SQL Server starts the recovery process. This process reads
the transaction log, starting with the last checkpoint in the log, as this marks the last complete
transaction that is known to be written to disk. Any complete transactions that appear after the
checkpoint are now committed through a roll forward process, while incomplete transactions
are automatically rolled back, as some of their changes may have been written to disk.
Note that SQL Server may write the pending changes to disk at any time to make room in
the buffer cache for extents that contain pages for other operations. This is why the recovery
202 Client/Server Applications with Visual FoxPro and SQL Server

operation must roll back incomplete transactions, as some or all of the data could have been
committed to disk, even though a checkpoint has not occurred.
Hopefully you now understand why SQL Server’s transactions are superior to those of
Visual FoxPro, as SQL Server passes the ACID test with excellence.

Do not use a write-caching controller with a SQL Server. This will


‡ completely destroy the ability of SQL Server to know when changes
have actually been written to disk, which eliminates the durability of
the data that the write-ahead logging provides. The exception to this rule is
a battery-backed disk controller.

Locking
An important consideration in using transactions is how users are affected by the locking
that occurs during transactions. First, let’s review the basic locking strategy employed by
SQL Server.
There are several resources that can be locked, such as a row, an index key (i.e., a row in
an index), a page (either a data or index page), an extent, a table, or the database. In normal use
of a database, data modifications will only require locks at the row, page or table level. An
extent is only locked briefly when SQL Server allocates or drops an extent, while the database
lock is used by SQL Server to determine whether someone is “in” a database.
SQL Server usually acquires record locks in favor of page or table locks in most situations.
The actual lock that is acquired is based upon the determination of the query optimizer, which
uses a cost-based algorithm to determine how locks are placed. Therefore, it is possible that
SQL Server may use a series of page locks instead of row locks, as page locks will consume
fewer resources than a large number of row locks.
Regardless, for each of these resources, SQL Server can use one of several different lock
modes. These include shared, exclusive and update locks, as well as a set of intent locks.
Shared locks (S) are used when reading data, while exclusive locks (X) are used in data
modifications. Update locks (U), as discussed in Chapter 3, are used when SQL Server must
read the data before modifying it. Update locks are put in place while it reads the data and
then “promotes” these locks to exclusive locks when finished reading but prior to making
the modifications.
Intent locks are used to prevent possible performance problems with locks at the row or
page levels. When a process intends to lock a row or a page, the higher-level resource in the
object hierarchy is first locked with an intent lock. For example, if an S lock is required on a
specific row of a table, SQL Server first attempts to get an intent shared (IS) lock on the table
and, upon success, attempts an IS lock on the page for that row. If both succeed, then the
shared row lock is acquired. By employing intent locks, SQL Server avoids having to scan
every row in a page or every page in a table before determining whether the page or table can
be locked.
These intent locks include intent shared (IS), intent exclusive (IX) and shared with intent
exclusive (SIX) locks. These acronyms (IS, IX, SIX and so forth) are visible when viewing
lock activity on the server.
Chapter 11: Transactions 203

Lock compatibility
In Visual FoxPro, locks are simple, as there is only one lock mode for all of the available
resources (record, header or file). That one lock mode, which compares somewhere between a
shared lock and an exclusive lock in SQL Server, can be acquired on any of these resources. If
one user holds a lock, then other users are unable to modify that locked resource until they
acquire that lock, but they can read the locked resource.
In SQL Server, the complexity of the various lock modes raises the question of which
locks can be acquired concurrently with other locks. The answer to this question can be found
in the SQL Server Books Online, in a Help page titled “Lock Compatibility.” This page
contains a compatibility matrix, which is reproduced in Table 1.
As you can see from Table 1, shared locks are compatible with other shared locks,
allowing more than one person to read data concurrently. However, exclusive locks are not
compatible with any other lock, preventing any kind of concurrent update or reading of data
while it is being modified.

Table 1. SQL Server lock compatibility matrix (from Books Online).

Existing granted mode


Requested mode IS S U IX SIX X
Intent shared (IS) Yes Yes Yes Yes Yes No
Shared (S) Yes Yes Yes No No No
Update (U) Yes Yes No No No No
Intent exclusive (IX) Yes No No Yes No No
Shared with intent exclusive (SIX) Yes No No No No No
Exclusive (X) No No No No No No

Blocking
The result of any lock incompatibility is called blocking in the SQL Server documentation. To
relate this to your knowledge of Visual FoxPro, blocking occurs when one user is holding a
record lock that another user requires. Since the second user cannot acquire a lock until the first
is released, the first user blocks the second user. By default, Visual FoxPro will force the
blocked user to wait indefinitely for the blocking user to release the lock. However, also by
default, the blocked user can press Esc when he or she grows tired of waiting. This behavior
can be changed in Visual FoxPro through the SET REPROCESS command; however, this
command has no effect on how SQL Server locks data.
In SQL Server, when blocking occurs, the session being blocked will wait indefinitely for
the blocking session to release the conflicting lock. If you wish to modify this behavior, you
can use the SET LOCK_TIMEOUT command, which changes the amount of time the session
will wait before automatically timing out a lock attempt. When a lock timeout occurs, your
TABLEUPDATE or SQLExec call will fail, so use AERROR() to determine the cause of the
error. In the returned array, the fifth element will contain error number 1222, which
corresponds to a SQL Server Timeout error.
204 Client/Server Applications with Visual FoxPro and SQL Server

‡ If you change the default lock timeout period, you will need to add code
that tests for error 1222. If this error occurs, you must roll back and restart
any transactions that are in progress, as a lock timeout error does not
cause SQL Server to abort any transactions.

The lock timeout setting is specified in milliseconds; therefore, to set the timeout to three
seconds, issue the following Visual FoxPro command:

lnResult = SQLExec(lhConn,"SET LOCK_TIMEOUT 3000")

You can use @@LOCK_TIMEOUT to query the session’s current lock timeout setting.
The default setting is –1, which conveniently corresponds to SET REPROCESS TO –1 in
Visual FoxPro:

lnResult = SQLExec(lhConn,"SELECT @@LOCK_TIMEOUT","cTimeout")

If you would like to test blocking, you can do this easily from Visual FoxPro with two
views, each of which uses a separate connection. If you share connections between them, you
will not be able to test the effects of blocking unless you load two separate instances of Visual
FoxPro. To test blocking, start a transaction, and then issue a statement that changes data on the
back end. Do not finish the transaction, so that any locks will stay in effect while you use the
second connection to attempt access to the same data that is locked in the first connection.
The following code snippet demonstrates how this is accomplished:

*--code to open view with buffering


*--grab connect handle from view
lhConn = CURSORGETPROP("ConnectHandle","view1")
*--now start a transaction
SQLSetProp(lhConn,"Transactions","DB_TRANSMANUAL")
*--then issue some kind of change
REPLACE field1 WITH newvalue IN view1
llResult = TABLEUPDATE(.F.,.F.,"view1")
*--only issue next statement after testing
SQLRollback(lhConn)

Obviously, it would be quite beneficial to determine the reason for blocking at any given
time. This is accomplished by viewing the current lock activity, either through SQL Server
Enterprise Manager or by executing one of SQL Server’s stored procedures from within
Visual FoxPro.

Viewing lock activity


The simplest way to view the current lock activity in SQL Server is to fire up the Enterprise
Manager (SQL EM). Expand your server in the tree, and then open the management folder to
view the management sub-tree. Once you’ve drilled down to this level, you can see the Current
Activity node—expand the node by clicking the plus sign. When you expand the node, SQL
EM will take a snapshot of the activity on the server at that moment and fill the sub-nodes with
this information.
Chapter 11: Transactions 205

To view general information about all processes and the possibility of blocking, click on
the Process Info node. This will display the screen pictured in Figure 1, which shows one row
for each connection to the server. The first six SPIDs are system processes that have nothing to
do with Visual FoxPro connections to the server. However, SPIDs 9 and 10 are two Visual
FoxPro connections, both of which have open transactions.

Figure 1. Viewing process information in the Enterprise Manager.

From this view, you can see that SPID 10 is waiting for a locked resource from another
process. To see which process is doing the blocking, you would have to scroll and view the last
two columns, titled “Blocked By” and “Blocking,” respectively. Figure 2 shows these last two
columns, and shows clearly that SPID 9 is blocking SPID 10. Note that you have to scroll back
to the left to see which rows correspond to which SPID.

Figure 2. Viewing blocking information.


206 Client/Server Applications with Visual FoxPro and SQL Server

Of course, this dialog only gives you a single clue as to what is causing the blocking
between these two processes. The Wait Resource column shows that a KEY lock is preventing
SPID 10 from progressing. To get more detail, you can expand the Locks/Process node, and
then select the desired SPID from the list. This will display a list of the locks that the process
has acquired, as well as any locks that it is waiting to acquire. Take a peek at Figure 3 for the
output that is seen for the current situation where SPID 9 blocks SPID 10.
In this figure, you can clearly see that the process is waiting for a KEY lock in the authors
table, while it has already acquired an intent shared (IS) lock on the page and the table.

Figure 3. Viewing process locking details.

While you are in the process list, you can double-click any of the SPID’s icons to get a
dialog that displays the Process Details, which includes a very handy display of the last T-SQL
command batch issued by the process. Since SPID 10 is blocked by another process, this allows
you to see what commands the blocked process issued, which could help you determine the
cause of the blockage.
Of course, all of this information is great, but what if you do not have access to SQL EM?
Fortunately, you can access most of this information from Visual FoxPro, but it must be from
an unblocked process! The system stored procedures sp_who, sp_who2 and sp_lock return
cursors of data about current activity on the SQL Server. These stored procedures can be
executed with Visual FoxPro’s SQLExec SQL pass through function.
Note that sp_who2 is an undocumented procedure that returns additional information over
that of sp_who. Both of these procedures return information about each SPID, including the
information viewed in the current activity window (in fact, it seems sp_who2 is called by SQL
EM for the current activity window). The sp_lock procedure returns locking information
about all processes, and returns the same information as the Locks windows under the current
activity tree.
All of these procedures accept a parameter for the number of the desired SPID. For
example, the following Visual FoxPro code calls the sp_who2 procedure to retrieve
Chapter 11: Transactions 207

information about the SPID for the current connection, accessed by using the @@spid system
function, and places the result into a cursor called cWho2:

lnResult = SQLExec(lhConn,"EXECUTE sp_who2 @@spid","cWho2")

You may invoke these procedures without specifying any parameter in order to retrieve
information about all processes.

Deadlocks
Deadlocks are a different concept than blocking and should be treated as a completely different
problem. While blocking is usually only temporary, a deadlock will last indefinitely if not
explicitly handled. To understand a deadlock, imagine the following scenario: Two users are
accessing the same database. Within a transaction, the first user accesses the customer table
and attempts to change information in the record for Micro Endeavors, Inc. in that table.
Meanwhile, also within a transaction, the second user accesses the contact table and attempts to
change the phone number for Mr. Hentzen. For some reason, the first user now attempts, within
the same transaction, to change the same record in the contact table, and the second user
attempts to change the same record in the customer table.
Since the first user holds an exclusive lock on the customer record, the second user is
waiting for that lock to be released in order to continue. However, the second user is holding a
lock on the contact record, forcing the first user to wait for that lock to be released before
continuing. There you have it: a deadlock, also known as a circular chain of lock requests.
If this situation were to happen in Visual FoxPro, with any luck you will have changed
SET REPROCESS from its default so that at least one of the two processes would
automatically fail in its attempt to get the second lock. When the user’s lock attempt fails, they
would be given the chance to try their transaction again, and most likely would succeed.
In SQL Server, this situation is automatically handled by an internal scheme for detecting
deadlocks. When SQL Server detects a deadlock, one process is chosen as the deadlock victim
and that transaction is automatically rolled back. SQL Server chooses the process that has the
least amount of activity as the victim, and when the transaction is canceled, an error is returned
to the application. This means that your application must always detect error number 1205
when issuing a TABLEUPDATE or SQLExec call. This error can occur before a transaction
has completed. When error 1205 is detected, you must restart your transaction, since the server
has already rolled back the transaction for the deadlock victim.
Deadlocks can be avoided by ensuring that all of your stored procedures and Visual
FoxPro code access resources in the same order at all times. In other words, if both of the
aforementioned users attempted to access the customer table first, and then the contact table,
the deadlock would not have occurred. However, since this requirement cannot always be met
in the real world, you will need to add code to detect when the user is the victim of a deadlock
and handle it accordingly.
Occasionally, it is necessary to set one process at a lower priority level than
another for the purpose of resolving deadlocks. If this is the case, you can use the SET
DEADLOCK_PRIORITY command to establish the sessions that should be the preferred
victims of deadlocks.
208 Client/Server Applications with Visual FoxPro and SQL Server

Transaction gotcha!
After reading all of this information about how SQL Server and Visual FoxPro handle
transactions, you still may not be aware of the fact that Visual FoxPro transactions do nothing
to start, end or commit transactions on the back-end database. Consider the following Visual
FoxPro code that attempts to update two remote views:

*--views opened with buffering


BEGIN TRANSACTION
IF NOT TABLEUPDATE(.T.,.F,"view1")
ROLLBACK
ELSE
IF NOT TABLEUPDATE(.T.,.F.,"view2")
ROLLBACK
ELSE
END TRANSACTION
ENDIF
ENDIF

In this example, a Visual FoxPro transaction is started and wraps the updates of two
different views. Unfortunately, in client/server applications, these updates are applied to
remote tables, not Visual FoxPro tables. Therefore, the TABLEUPDATE() statements are not
affected by the Visual FoxPro transaction, thereby writing their changes immediately to the
source tables.
In other words, if the first TABLEUPDATE() succeeds but the second one fails, the
ROLLBACK command has no effect whatsoever. The solution? Look earlier in this chapter for
the code that starts a transaction by setting the Transactions property of the connection to
manual and submits the SQLRollback() or SQLCommit() SQL pass through functions.
No matter what, do not use Visual FoxPro transactions against remote views.

Summary
In this chapter, you have seen a comparison of Visual FoxPro and SQL Server transactions.
The ACID properties are used to test the quality of transactions by a database system. Visual
FoxPro falls a bit short, but SQL Server transactions are fully compliant with the ACID
standard. You also learned how blocking and deadlocks occur and how to retrieve the
information that SQL Server provides on locks that each process is holding.
In the next chapter, you will switch gears entirely and see how to use the basics of ADO in
a Visual FoxPro application.
Chapter 12: ActiveX Data Objects 209

Chapter 12
ActiveX Data Objects
ActiveX Data Objects (ADO) has been mentioned in previous chapters as a possible
alternative to communicating with SQL Server via ODBC. This chapter introduces you to
ADO, presents the pros and cons of using ADO, and explains the mechanics of using
ADO in Visual FoxPro applications.

Why ADO?
What purpose does ADO serve in Visual FoxPro development? Why use ADO when views and
SQL pass through seem to provide all the necessary functionality?

ADO benefits
ADO provides several benefits over native Visual FoxPro data access:
• ADO is the best technology for passing data between tiers of an n-tier application.
• ADO can access non-relational data sources such as text files and CSV files.
According to Microsoft’s Web site, “OLE DB provides high-performance access to
any data source, including relational and non-relational databases, email and file
systems, text and graphics, custom business objects, and more.”
• ADO permits return values to be retrieved from stored procedures.
• ADO can be used as a workaround for several bugs or deficiencies in VFP.

Passing data between tiers of an n-tier application


In n-tier applications, data must be passed between the tiers. Consider a three-tier architecture
consisting of a back end (database), a middle tier (business objects) and a front-end tier (user
interface). (Since Visual FoxPro is often used for the middle tier, a practice recommended by
Microsoft, assume VFP-based middle tiers for this discussion.) In a three-tier architecture,
passing data between the business object tier and the front end can be problematic if the two
tiers are constructed using different technologies. For example, if the front end is constructed
using Visual Basic and the middle tier is constructed using Visual FoxPro, sending data back
and forth will be complicated because Visual Basic cannot understand the cursor or DBF that
Visual FoxPro understands. Since one of the main benefits of a three-tier system is the ability to
use multiple technologies for the front end, the middle tier must be capable of passing data to
and from front-end tiers built from different technologies. Using ADO to send data back and
forth solves this problem.
ADO is an excellent choice for a communications or data access technology because it is
universally understood (at least within the world of Microsoft products). It can be used with
210 Client/Server Applications with Visual FoxPro and SQL Server

any Visual Studio product as well as within Active Server Pages. Other non-Microsoft
products, such as Java, can also use ADO with varying levels of compatibility.
Alternatives to ADO are not satisfactory. These include:
• Accept the limitation that all front ends will be constructed in the same technology
as the middle tier. However, this limitation eliminates one of the main benefits of
n-tier architecture.
• Pass an array to the front end. This seems like a good idea until you realize that
different products handle arrays differently, forcing you to write custom array-
handling code for each client. While Visual FoxPro does provide some help in this
arena, there are issues with handling the different data types, validation of the data,
and ensuring that the clients can return any necessary information as a compatible
array. In addition, passing data back from the front end to the middle tier is more
complicated and requires extensive custom coding.

We agree with Microsoft’s suggestion that ADO is the best choice for passing data
between the front and middle tiers of a multi-tiered application.

The ability to access non-relational data (OLE DB vs. ODBC)


ADO uses OLE DB rather than ODBC. OLE DB provides one major capability that ODBC
lacks: the ability to access non-relational data.
ODBC can access only relational databases that understand basic SQL commands. OLE
DB, on the other hand, can access relational as well as non-relational data sources such as
text and CSV files. Therefore, ADO permits Visual FoxPro (or other host technologies) to
access data sources that were previously unavailable or were available but required importing
and converting.

Stored procedures
In Chapter 6, “Extending Remote Views with SQL Pass Through,” you learned how to call
SQL Server stored procedures with the SQLExec() function. Through SQLExec(), you can pass
parameters as inputs and accept return values through OUTPUT parameters. However, there is
no mechanism for capturing the return value from a SQL Server stored procedure (i.e., when
the T-SQL RETURN command is used to return an integer value).
ADO provides the ability to invoke stored procedures, and to capture any type of
returned value.

VFP deficiencies—ADO to the rescue


Since ADO is a completely different data access technology than a remote view or SQL pass
through statement, it can be an alternative tool that you can use to work around any bugs or
problems with the native Visual FoxPro data access technologies.
For example, SQL Server 7 introduced a few data types to support Unicode character sets,
but VFP does not handle these new data types correctly in some situations. If you create a
remote view or a SQL pass through statement to retrieve data from an nText column (one of
the new SQL Server data types), Visual FoxPro does not place the data into a Memo field, as
it should. Instead, it incorrectly places the data into a character field of 255 characters, which
Chapter 12: ActiveX Data Objects 211

can result in truncation. (See article Q234070 in the Microsoft Knowledge Base for more
details on this topic.) Since this is a bug in Visual FoxPro, you need a workaround. One
approach is to use ADO instead of a view or SQL pass through. ADO properly retrieves data
from nText columns.
Note: The new data types that support Unicode are nChar, nVarchar and nText. These
work similarly to their non-Unicode counterparts, except that they consume two bytes per
displayed character. These data types are important when creating a database that must store
characters from other languages, since a language like Japanese has well over 255 distinct
characters. With Unicode, more than 65,000 distinct characters are available, allowing the full
Japanese character set to be stored in a Unicode field.

ADO disadvantages
There are some disadvantages to using ADO with Visual FoxPro:
• ADO data cannot be handled in the same way as data retrieved through Visual FoxPro
remote views or SQL pass through. Instead, you must access the data through the
properties and methods of the ADO object.
• Native Visual FoxPro form controls cannot be bound to ADO data. (However,
ActiveX controls exist that can be bound to ADO data sources.)
• ADO data cannot be manipulated using powerful Visual FoxPro technology.
(However, ADO data can be converted to cursors, which can be manipulated directly
by native Visual FoxPro.)

As you can see, there are advantages and disadvantages to using ADO within a Visual
FoxPro application. Many of the disadvantages could be reduced or eliminated by changes to
Visual FoxPro. It is widely hoped that future versions of Visual FoxPro will provide better
support for ADO.

Installing and distributing ADO


Before you can use ADO, you will need to install it. If you are using Windows 2000, you
need not do anything, as all of the components of ADO 2.5 are installed with the operating
system and updated through Windows Update. However, if you are running an earlier
operating system, you will need to download the latest and greatest version of ADO.
This can be downloaded for free from Microsoft’s Web site at
http://www.microsoft.com/data/download.htm. Follow the instructions to download ADO,
which is included in the self-extracting archive file MDAC_TYP.EXE. This file contains all of
the components of ADO, several OLE DB providers, several ODBC drivers, and the core
ODBC components. Therefore, to install the latest version of these components, simply execute
this file from Explorer.
This process also explains how you can distribute ADO to your client machines for
deployment purposes. Simply copy the MDAC_TYP.EXE file to a directory accessible to the
client workstations and execute it to install the necessary files.
212 Client/Server Applications with Visual FoxPro and SQL Server

Using ADO within Visual FoxPro


Using ADO within Visual FoxPro is straightforward, requiring only the use of the
CREATEOBJECT() function and knowledge of the properties and methods of the ADO
object model.
The CREATEOBJECT() function is used to instantiate the objects of the ADO object
model: Connection, RecordSet and Command. Each object has its own set of properties, events,
methods and collections, which provide all the features necessary to access, manipulate and
update data from any accessible data source.
You can find extensive help for ADO in the MSDN library that ships with Visual FoxPro.
A good place to look after reading this chapter would be the topics “ADO, Basics” and “ADO
Jumpstart for Microsoft Visual FoxPro Developers.”

The Connection object


The Connection object is used to connect to a data source, and it also handles transactions and
reporting errors. ADO uses OLE DB to connect to data sources, but since there is an OLE DB
provider for ODBC drivers, OLE DB can connect to a wide variety of databases including
Visual FoxPro or SQL Server.
To create a Connection object, you must instantiate a COM server with the ProgID of
ADODB.Connection. This is done with the CREATEOBJECT() function as follows:

loConn = CREATEOBJECT("ADODB.Connection")

Note that creating a Connection object does not connect to any data source. To connect
to a data source, you must specify values for one or more properties of the Connection object,
and then invoke a method of the object to initialize the connection. The following code
shows how to connect to a SQL Server called MySQLSvr by invoking the Open method of a
Connection object:

loConn = CREATEOBJECT("ADODB.Connection")
IF VARTYPE(loConn) = "O"
lcConnStr = "Driver=SQL Server;Server=MySQLSvr;Database=pubs;" + ;
"uid=User;pwd=Password"
loConn.Open(lcConnStr)
ENDIF

On the other hand, you can populate the ConnectionString property before invoking the
Open method, like this:

loConn = CREATEOBJECT("ADODB.Connection")
IF VARTYPE(loConn) = "O"
loConn.ConnectionString = "Driver=SQL Server;Server=MySQLSvr;" + ;
"Database=pubs;uid=User;pwd=Password"
loConn.Open()
ENDIF

To test whether the connection was successful, query the value of the State property on the
Connection object. If the State property is one, the connection is open; otherwise, if it is zero,
Chapter 12: ActiveX Data Objects 213

the connection failed and is closed. An unsuccessful connection attempt triggers a Visual
FoxPro error, which can be trapped by an object’s Error event or any ON ERROR routine.
The preceding examples used an ODBC driver to connect to SQL Server. However, an
OLE DB provider also exists for SQL Server. Using the OLE DB provider improves
performance, since ODBC is bypassed.
To use the SQL Server OLE DB provider, use a different connection string as follows:

loConn = CREATEOBJECT("ADODB.Connection")
IF VARTYPE(loConn) = "O"
lcConnStr = "Provider=SQLOLEDB;User ID=User;Password=Password;" + ;
"Initial Catalog=Pubs;Data Source=MySQLSvr"
loConn.Open(lcConnStr)
ENDIF

This code connects to the pubs database on a SQL Server called MySQLSvr using the SQL
Server OLE DB provider called SQLOLEDB. Since a connection string is rather cryptic, you
might prefer to create this connection string with a Microsoft Data Link file. To start, simply
create a new, empty file with a UDL extension. Then, through Windows Explorer, double-click
the file, which will open the dialog shown in Figure 1.

Figure 1. The Microsoft Data Link Properties dialog.


214 Client/Server Applications with Visual FoxPro and SQL Server

Once the dialog is open, you can use the Provider and Connection pages to provide the
details of the desired connection (such as the provider, server name, login credentials, and the
initial database to select). Use the Test Connection button on the Connection page to verify
your selections, and then press the OK button.
Next, open the UDL file with Notepad. The UDL file should appear like the example
shown in Figure 2. It contains the full connection string that corresponds to the options you
selected in the UDL dialog. Simply copy the connection string into your Visual FoxPro code
for use with an ADO Connection object.

Figure 2. A UDL file that has been opened with Notepad.

After you connect to a data source, you will probably want to retrieve data from that
source. Retrieving data requires another ADO object called the RecordSet object.

The RecordSet object


When data is downloaded to an ADO object, it is held in a RecordSet object. To create a
RecordSet, use the ADODB.RecordSet ProgID with the CREATEOBJECT() function,
as follows:

loRS = CREATEOBJECT("ADODB.RecordSet")

As with the Connection object, creating the object does not populate the object with data.
Retrieving data requires a call to the Open method of the RecordSet object. The following code
example retrieves all of the records from the authors table in the pubs database on SQL Server
and places the records into a RecordSet object:

loRS = CREATEOBJECT("ADODB.RecordSet")
IF VARTYPE(loRS) = "O"
lcSQL = "SELECT * FROM Authors"
lcConnStr = "Provider=SQLOLEDB;User ID=User;Password=Password;" + ;
"Initial Catalog=Pubs;Data Source=MySQLSvr"
loRS.Open(lcSQL, lcConnStr)
ENDIF

Note that behind the scenes, the Open method created its own Connection object with
attributes specified in the connection string, which was passed as the second parameter. You
can confirm that the RecordSet object stored the Connection object specification with the
following code:
Chapter 12: ActiveX Data Objects 215

loCn = loRS.ActiveConnection
Activate Screen
?loCn.ConnectionString
?loCn.State

The ActiveConnection property is an object reference to the Connection object that the
RecordSet object uses to connect to a data source. By checking the connection’s
ConnectionString property, you can see which data source has been opened by the RecordSet.
It is not common to allow the Connection object to be created implicitly, since it is harder
to share an implicitly created connection with other ADO objects. A better practice is to create
a Connection object explicitly, and then use that connection for one or more RecordSet objects
as follows:

loCn = CREATEOBJECT("ADODB.Connection")
loRS = CREATEOBJECT("ADODB.RecordSet")
IF VARTYPE(loCn)="O" AND VARTYPE(loRS) = "O"
loCn.ConnectionString = "Provider=SQLOLEDB;User ID=sa;Password=;" + ;
"Initial Catalog=Pubs;Data Source=MySQLSvr"
loCn.Open()
IF loCn.State = 1
WITH loRS
.ActiveConnection = loCn
.Open("SELECT * FROM Authors")
ENDWITH
ENDIF
ENDIF

In this example, the Connection and RecordSet objects are created with the
CREATEOBJECT() function. The Connection object’s ConnectionString property is specified,
and then the Open method is invoked. If the connection is successfully opened, an object
reference to the Connection object is stored in the ActiveConnection property of the RecordSet
object. This property tells the Open method where to send the SQL SELECT statement (which
is specified as a parameter to the Open method). The Open method then executes the SQL
SELECT statement that, in this example, retrieves all records from the authors table.

Displaying RecordSets with code


The next step is to figure out how to view the data within the RecordSet. The following block
of code will print on the Visual FoxPro “desktop” all of the columns of each record in the
RecordSet referenced by loRS:

ACTIVATE SCREEN
CLEAR
DO WHILE NOT loRS.EOF
FOR EACH loField IN loRs.Fields
??TRANSFORM(loField.Value)+CHR(9)
ENDFOR
? && Move to next line
loRS.MoveNext()
ENDDO
216 Client/Server Applications with Visual FoxPro and SQL Server

The RecordSet’s EOF property will be True if you have moved past the last record of
the RecordSet, similar to the way a Visual FoxPro cursor works. In addition, only one record
can be “seen” at any time—the RecordSet will initially position the record pointer on the
first record after retrieving the data. Inside of each record, you can access each field with the
Fields collection.
Each field has numerous properties, such as the Value property, which was referenced in
the preceding code. You can also get each field’s Name, DefinedSize, NumericScale or
Precision through properties of the same names.
The MoveNext method works just like the SKIP command in Visual FoxPro, moving the
record pointer to the next record. You can also MoveFirst, MovePrevious or MoveLast,
corresponding to the GO TOP, SKIP –1 and GO BOTTOM Visual FoxPro commands.
It is interesting to note that the similarity between a RecordSet and a Visual FoxPro
cursor is not an accident: The RecordSet is based on the Visual FoxPro cursor engine. This
similarity will become more apparent as you explore other methods and properties of the
RecordSet object.

Displaying RecordSets with ActiveX controls


One limitation of the ADO RecordSet in a Visual FoxPro environment is the inability to bind
the fields of a RecordSet to native VFP form controls. However, you can view the data of a
RecordSet in a Visual FoxPro form, but it requires ActiveX controls.
By using the same ActiveX controls used by a Visual Basic developer, you can display the
contents of a RecordSet in a grid on a Visual FoxPro form. To try this, start by creating a
Visual FoxPro form that contains code for retrieving a RecordSet in its Load method. Make
sure that you use form-level properties for the object references; otherwise, you won’t be able
to “see” the RecordSet from the form’s controls:

WITH ThisForm
.oCn = CREATEOBJECT("ADODB.Connection")
.oRS = CREATEOBJECT("ADODB.RecordSet")
*--Other code goes here to open record set
ENDWITH

The default cursor type for an ADO RecordSet is known as a forward-only static cursor.
This means that you can only use the MoveNext method of the RecordSet (i.e., forward-only),
and that any changes on the data source are not reflected in the cursor (i.e., static). Before you
can display a RecordSet in an ActiveX control on a Visual FoxPro form, you must change the
cursor type of the RecordSet to allow movement in any direction. However, a static cursor is
preferred for performance reasons, as it will not maintain communication with the server to
detect changes made by other users. The CursorType property is used to specify the type of
cursor used by the RecordSet, and must be specified before opening it. To create the static
cursor required by the ActiveX grid control, use 3 for the CursorType, as in the following code:

.oRS.ActiveConnction = .oCn
.oRS.CursorType = 3 && adOpenStatic
.oRS.Open("SELECT * FROM Authors")
Chapter 12: ActiveX Data Objects 217

The next step is to place an instance of the Microsoft ADO Data control onto your form
and give it a name like oleDataCtrl. This control is needed to provide the proper interface so
the ActiveX grid can bind to the RecordSet. You can place this control anywhere within the
form, as the control is invisible at run time.
Now place a Microsoft DataGrid control on your form and set its Name property to
oleGrid. Once these controls are on your form, your form will look similar to Figure 3. To
make it all work, write the following code in the Init method of the form—this will cause the
DataGrid to display the contents of the RecordSet you created earlier:

WITH ThisForm
.oleDataCtrl.Object.RecordSet = .oRS
.oleDataCtrl.Object.Refresh()
.oleGrid.Object.DataSource = .oleDataCtrl.Object
.oleGrid.Object.Refresh()
ENDWITH

Figure 3. The ADO Test form loaded in the Form Designer.

When you execute the form, the data will appear in the grid control as shown in Figure 4.
However, the data will be read-only, as the RecordSet also defaults to a read-only cursor
type. To modify this, change the LockType property of the RecordSet object so it will use
optimistic locking:

.oRS.LockType = 3 && adLockOptimistic


.oRS.Open("SELECT * FROM Authors")
218 Client/Server Applications with Visual FoxPro and SQL Server

Figure 4. Viewing an ADO RecordSet at run time in a Visual FoxPro form with
ActiveX controls.

ADO constants
If you check the Help system on the ADO RecordSet and Connection objects, you will see
many references to constants that begin with the letters “ad.” You saw some of these constants
referenced in the previous code snippets, such as adLockOptimistic and adOpenStatic. While a
Visual Basic program intrinsically recognizes these constants, Visual FoxPro does not;
therefore, you must either explicitly reference their values or create “constants” with the
#DEFINE preprocessor directive:

#DEFINE adLockOptimistic 3
#DEFINE adOpenStatic 3
...
.oRS.CursorType = adOpenStatic
.oRS.LockTpe = adLockOptimistic

To make this easier, Microsoft now distributes an include file, adovfp.h, that you can use
in your applications. This file contains all the constants recognized by ADO, including those
mentioned in the previous code snippets. To get this file, visit the Visual FoxPro home page at
http://msdn.microsoft.com/vfoxpro and search for a utility called VFPCOM. This file is self-
extracting and expands into several files, including the adovfp.h file.
Chapter 12: ActiveX Data Objects 219

Displaying RecordSets with the VFPCOM utility


The VFPCOM download includes other files, all of which comprise the VFPCOM utility, a
tool created by the Visual FoxPro team at Microsoft primarily for use with ADO. (However,
VFPCOM could be used with any COM server, not just ADO.)
Another limitation of ADO in a Visual FoxPro development environment is the inability to
use native Visual FoxPro commands and functions against the data in the ADO RecordSet.
Instead, you have to work with the Fields collection to manipulate the data, which requires a lot
more code than if you were working with a Visual FoxPro cursor.
VFPCOM alleviates this limitation by providing methods for converting ADO RecordSet
objects into Visual FoxPro cursors and vice versa. The following code shows how to convert a
RecordSet to a Visual FoxPro cursor called cAuthors with the VFPCOM utility:

loCn = CREATEOBJECT("ADODB.Connection")
loRS = CREATEOBJECT("ADODB.RecordSet")
loVFPCOM = CREATEOBJECT("VFPCOM.COMUtil")

IF VARTYPE(loCn)="O" AND VARTYPE(loRS) = "O"


loCn.ConnectionString = "Provider=SQLOLEDB;User ID=sa;Password=;" + ;
"Initial Catalog=Pubs;Data Source=MARS2000"
loCn.Open()
IF loCn.State = 1
loRS.ActiveConnection = loCn
loRS.Open("SELECT * FROM Authors")
lnError = loVFPCOM.RSToCursor(loRS,"cAuthors")
IF lnError <> 0
MESSAGEBOX("Unable to create Visual FoxPro cursor")
ELSE
SELECT cAuthors
BROWSE NOWAIT
ENDIF
ENDIF
ENDIF

Unfortunately, the cAuthors cursor created in the preceding code is never updatable, so
you must create code that writes any changes back to the data source, either through the
RecordSet or with native Visual FoxPro techniques.

More on VFPCOM
One exciting feature of the VFPCOM utility is the ability to bind Visual FoxPro code to the
events of any COM server, including any object from ADO. For example, the RecordSet
object has events that occur when data is changed in the current record. These events are
WillChangeRecord, WillChangeField, FieldChangeComplete and RecordChangeComplete.
Native Visual FoxPro cannot handle these events, as it does not contain functionality to receive
and process events from a COM server like ADO. Armed with the VFPCOM utility, you can
have Visual FoxPro code respond to these events as they happen.
To handle the events triggered from an ADO RecordSet, you first create a Visual FoxPro
class that “hooks” to the events of the RecordSet. This is easy to do using VFPCOM’s
ExportEvents method as follows:
220 Client/Server Applications with Visual FoxPro and SQL Server

loVFPCOM = CREATEOBJECT("VFPCOM.COMUtil")
loRS = CREATEOBJECT("ADODB.RecordSet")
lnStat = loVFPCOM.ExportEvents(loRS,"sample.prg")
IF lnStat = 0
MESSAGEBOX("program created successfully")
ENDIF

The previous code creates a program called sample.prg, which contains the definition of a
custom class with methods for each possible event of the RecordSet:

DEFINE CLASS RecordsetEvents AS custom


PROCEDURE EndOfRecordset(fMoreData,adStatus,pRecordset)
* Add user code here
ENDPROC

PROCEDURE FetchComplete(pError,adStatus,pRecordset)
* Add user code here
ENDPROC

PROCEDURE FetchProgress(Progress,MaxProgress,adStatus,pRecordset)
* Add user code here
ENDPROC

PROCEDURE FieldChangeComplete(cFields,Fields,pError,adStatus,pRecordset)
* Add user code here
ENDPROC

PROCEDURE MoveComplete(adReason,pError,adStatus,pRecordset)
* Add user code here
ENDPROC

PROCEDURE RecordChangeComplete(adReason,cRecords,pError,adStatus,pRecordset)
* Add user code here
ENDPROC

PROCEDURE RecordsetChangeComplete(adReason,pError,adStatus,pRecordset)
* Add user code here
ENDPROC

PROCEDURE WillChangeField(cFields,Fields,adStatus,pRecordset)
* Add user code here
ENDPROC

PROCEDURE WillChangeRecord(adReason,cRecords,adStatus,pRecordset)
* Add user code here
ENDPROC

PROCEDURE WillChangeRecordset(adReason,adStatus,pRecordset)
* Add user code here
ENDPROC

PROCEDURE WillMove(adReason,adStatus,pRecordset)
* Add user code here
ENDPROC

ENDDEFINE
Chapter 12: ActiveX Data Objects 221

Of course, you will want to add user code in these methods to make them respond
appropriately. Once you make your modifications, you can bind a RecordSetEvents object to a
RecordSet with the BindEvents method of the VFPCOM utility, as follows:

*--Create object that will hook to ADORS events


SET PROCEDURE TO Sample.prg
loADORSHook = CREATEOBJECT("RecordsetEvents")
*--Bind to events of ADO RecordSet object
loVFPCOM.BindEvents(loRS,loADORSHook)

*--Do stuff in the RS to trigger ADO events


loRS.MoveFirst && GO TOP
loRS.MoveNext && SKIP
loRS.Fields(0).Value = "999-99-9999" && REPLACE…

Now you can detect an event that occurs when a user modifies data in a RecordSet, moves
a record pointer, or performs other operations that trigger events.
Note that the Connection object and the yet-to-be-introduced Command object also have
events that may be of interest. Be sure to check the ADO documentation in the MSDN library
for more details.

The Command object


You have seen how to connect to data, read and manipulate data, and respond to events. Next,
you will see how ADO executes stored procedures or other commands on a data source. These
features are encapsulated within the ADO Command object.
Like the RecordSet object, a Command object requires the services of a Connection object.
ADO will create a connection implicitly if one is not specified. As previously mentioned, it is
usually better to create a Connection object explicitly that can be shared among multiple
RecordSet and Command objects.
The main method of a Connection object is its Execute method, which executes the code
that exists in the object’s CommandText property. This command can be a stored procedure
call or any other SQL statement that the data source understands.
In other words, the Command object is used for everything that SQL pass through can do
from within Visual FoxPro.
The following code shows how to create a Command object that connects to a SQL Server
and executes the byroyalty stored procedure from the pubs database:

#INCLUDE adovfp.h
LOCAL loCn, loCmd, loRS, loFld

loCn = CREATEOBJECT("ADODB.Connection")
loCn.ConnectionString="Driver=SQL Server;"+ ;
"Server=MySQLSvr;uid=sa;pwd=;Database=Pubs"
loCn.Open()

IF loCn.State = 1

loCmd = CREATEOBJECT("ADODB.Command")
loRS = CREATEOBJECT("ADODB.RecordSet")

loCmd.ActiveConnection = loCn
222 Client/Server Applications with Visual FoxPro and SQL Server

loCmd.CommandText = "EXECUTE byroyalty 40"


loCmd.CommandType = adCmdText

loRS = loCmd.Execute()

ACTIVATE SCREEN
DO WHILE NOT loRS.EOF
FOR EACH loFld IN loRS.Fields
??TRANSFORM(loFld.Value)+" "
ENDFOR
loRS.MoveNext
?
ENDDO
ENDIF

You can see that the Command object has an ActiveConnection property, just like the
RecordSet object. The actual command is specified by the CommandText property, and the
CommandType property is used to signify that the contents of CommandText are literal text for
the server. In this example, since the command includes the EXECUTE keyword as well as a
parameter, it was necessary to use adCmdText instead of the expected adCmdStoredProc.
Finally, since the byroyalty procedure returns a result set, a RecordSet object is used to capture
the result.
You will not be able to call a stored procedure with output parameters or a return value
with the preceding code. Instead, you must take advantage of the Command object’s
Parameters collection. This collection is designed to handle the variations in the number and
type of parameters used by the range of available stored procedures.
To use the Parameters collection, you must add Parameter objects to the collection. When
adding parameters, you also specify the attributes of each parameter, such as whether it is an
input or output parameter, the parameter’s data type and, for input parameters, the input value.
The following code modifies the previous example to use the Parameters collection:

loCmd.ActiveConnection = loCn
loCmd.CommandText = "byroyalty"
loCmd.CommandType = adCmdStoredProc

loParam = loCmd.CreateParameter("Percentage",adInteger,adParamInput,0,40)
loCmd.Parameters.Append(loParam)

loRS = loCmd.Execute()

As you can see, the CreateParameter method builds the actual Parameter object separately
from the Command object. Once the Parameter object is created, you use the Append method
to add it to the Parameters collection of the Command object. This parameter object is then
automatically passed to SQL Server when the stored procedure is invoked.
As you read in Chapter 6, “Extending Remote Views with SQL Pass Through,” SQL pass
through can receive output parameters from SQL Server but cannot handle return values from
stored procedures. Fortunately, the ADO Command object can handle return values by adding
the appropriate Parameter object to the Parameters collection. To demonstrate, first consider
the following sample SQL Server stored procedure that accepts an input parameter and returns
a RecordSet, an output parameter and a return value:
Chapter 12: ActiveX Data Objects 223

CREATE PROCEDURE myProc


@inparm int,
@outparm int OUTPUT
AS
SELECT name FROM sysusers WHERE uid < @inparm
SELECT @outparm = 88
RETURN 99

To invoke this procedure properly, it must be called with an input parameter as well as an
output parameter. Further, it returns a RecordSet (from the SELECT statement) and an integer
value (from the RETURN statement). The following code illustrates how to invoke this
procedure properly with a Command object so you can display the contents of the returned
RecordSet, return value and output parameter:

loCmd.CommandText = "myproc"
loCmd.CommandType = adCmdStoredProc

* Set up parameters
loParam = loCmd.CreateParameter("Return", adInteger, adParamReturnValue,0, 0)
loCmd.Parameters.Append(loParam)

loParam = loCmd.CreateParameter("InParm", adInteger, adParamInput,0, 2)


loCmd.Parameters.Append(loParam)

loParam = loCmd.CreateParameter("OutParm", adInteger, adParamOutput,0, 0)


loCmd.Parameters.Append(loParam)

loRS = loCmd.Execute()

* Print contents of RecordSet


ACTI SCRE
DO WHILE NOT loRS.EOF
FOR EACH loFld IN loRS.FIELDS
??TRANS(loFld.VALUE)+" "
ENDFOR
loRS.MoveNext
?
ENDDO
* Must close RecordSet before you can get return values
loRS.Close()
* All collections are zero based
?"Return value: " + TRANSFORM(loCmd.Parameters(0).Value)
?"Output Parameter: " + TRANSFORM(loCmd.Parameters(2).Value)

Notice how the parameters are created and appended to the Command object. The order of
the parameters is significant: You must declare the return value first, followed by each
parameter in the order required by the stored procedure. Rearranging the parameter causes the
Execute method to fail.
Also note that you must close the returned RecordSet object before you can query the
returned values; otherwise, the parameters will contain empty values. This requirement
exists because of the way that ADO retrieves the results from a data source—first the
RecordSet is passed, then the Output parameters and return values. Therefore, you must
224 Client/Server Applications with Visual FoxPro and SQL Server

always close any RecordSets before you are able to retrieve the actual values returned from a
stored procedure call.

Summary
This chapter has shown you the basics of using ADO within a Visual FoxPro application. The
advantages and disadvantages of ADO were described and compared to using the native Visual
FoxPro tools for accessing data. You then saw examples of the Connection, RecordSet and
Command objects, which showed the purpose of each type of object. Hopefully, you now have
enough information to incorporate ADO technology into your client/server applications.
Appendix A: New Features of SQL Server 2000 225

Appendix A
New Features of
SQL Server 2000
This book was written about SQL Server 7 and its set of features. However, at press
time, Microsoft had just finalized SQL Server 2000, with a scheduled launch date of late
September 2000. Obviously, you might have questions about how the new version
affects your Visual FoxPro client/server applications and, in particular, if any of SQL
Server 2000’s new features are worth exploring. This appendix serves as a short
comparison of the two products, describing some of what Microsoft did to make SQL
Server 2000 superior to SQL Server 7.

For a greater level of detail on any features of SQL Server 2000, you can check Microsoft’s
Web site at http://www.microsoft.com/sql/, or install and read SQL Server 2000 Books Online,
particularly the topic “What’s New in Microsoft SQL Server 2000.”

Feature list
Table 1 shows an abridged high-level feature list for SQL Server 2000. The complete version
of this table is available from Microsoft at www.microsoft.com/sql/productinfo/sql2ktec.htm.
In the Editions column, “E” stands for the Enterprise edition, “S” for Standard and “P” for
Personal. Each edition has its own hardware and operating system requirements, which are
detailed on Microsoft’s Web site as well as in Books Online.

Table 1. Some of the new features of SQL Server 2000.

Feature Benefits Editions


URL and From a browser, use SQL, XML templates or XPath in the URL E,S,P
HTTP Access line for querying.
OpenXML Access, manipulate and update XML documents as if they were E,S,P
tables using Transaction SQL (T-SQL) and stored procedures.
Full-Text Search Enable Full-Text Search across the Web and intranets for E,S,P
formatted documents, such as Microsoft Word, Microsoft Excel
and HTML. Track changes automatically.
English Query Allow all users to access data through natural language queries. E,S,P
Graphically author queries with wizards in the included Microsoft
Visual Studio environment. Generate Multi-Dimensional
Expressions (MDX) to query cubes.
Multi-Instance Run reliably in hosted scenarios with separate database E,S,P
Support instances per customer or application.
Security Protect data with higher default security on installation. Includes E,S,P
support for Secure Sockets Layer (SSL) connections and
Kerberos. C2-level certification underway.
Installation Disk Create standard or default databases for server farms from any E,S,P
Imaging machine on the network with built-in cloning technology.
226 Client/Server Applications with Visual FoxPro and SQL Server

Table 1, continued

Feature Benefits Editions


Distributed Partition Achieve software scale-out on the data tier by partitioning E
Views workload across servers. Add additional servers for greater
scalability.
Log Shipping Automatically keep databases synchronized for warm standby on E
multiple backup servers to share load—no matter how physically
far apart.
Parallel Index Take full advantage of symmetric multiprocessing (SMP) E
Creation hardware to speed up index creation, easing the load on
frequently updated systems.
Failover Clustering Install failover-ready databases directly from setup. Use E
active/passive failover with standby hardware or active/active
failover in hardware-constrained environments. Databases can
failover to any surviving node with four-node failover.
32 CPU SMP Scale up SQL Server 2000 databases to SMP systems with as E
System Support3 many as 32 processors.
64GB RAM Handle the largest data sets and transactional loads with up to E
Support3 64GB of RAM for SQL Server 2000.
Indexed Views Create indexes on views to improve performance of existing E
queries without recoding. Speed up analysis and reporting that
rely on complex views.
Online Index Keep the server up and running while reorganizing indexes to E,S,P
Reorganization improve performance.
Microsoft Active Manage databases centrally alongside other enterprise E,S,P
Directory™ resources. View and search for servers, replication publications,
Integration5 cubes and more.
SQL Query Debug stored procedures. Set breakpoints, define watches, view E,S,P
Analyzer variables and step through code. Trace executing code on the
server or the client. Easily write T-SQL based on templates.
User-Defined Achieve code reuse by creating T-SQL functions. Incorporate E,S,P
Functions routinely used logic to simplify development.
Cascading Control how changes propagate through tables when keys E,S,P
Referential Integrity are updated.
Constraints
Instead of and After Execute code flexibly by specifying what happens in lieu of an E,S,P
Triggers operation or after it.
Indexes on Define indexes on column types even when the data in the E,S,P
Computed Columns column is computed from other columns.
New Data Types Store and reference data flexibly with bigint, sql_variant, and table E,S,P
data types.
Column Level Store objects that have different collations in the same database. E,S,P
Collations Collations can be specified at the database level or at the
column level.

As you can see from Table 1, there are plenty of new features—and this table is not a
complete list! To save space, the features dealing specifically with data warehousing and XML
were left out of the table; however, if you are working with either of these technologies, be sure
to check out SQL Server 2000. It contains numerous features over those provided by SQL
Server 7 for data warehousing, while providing support for XML—support that did not exist at
all in SQL Server 7.
Appendix A: New Features of SQL Server 2000 227

Since there are so many new features, this appendix will only cover those that are related
to the topics covered in this book.

Installation issues
Before you attempt to install SQL Server 2000, you should take the time to read the
setup/upgrade Help provided on the installation CD. It provides valuable information for
upgrading an existing SQL Server 7 to 2000, as well as the new installation options available
to you.
One installation issue with SQL Server 2000 is that it now allows multiple instances of the
server on the same computer. This is useful if you need to run databases for different clients or
applications, but cannot afford the additional expense of multiple servers. Under SQL Server 7,
if you needed a different sort order or code page for two different databases, you were forced to
install the product on two different machines. This is because these features can only be set at
the server level.
Once you have installed SQL Server 2000, you will find that you have the same tools that
were available under SQL Server 7, such as Enterprise Manager, Service Manager, Books
Online, and the Query Analyzer. However, all of these tools have been enhanced for use in
SQL Server 2000.

Query Analyzer
The SQL Server 2000 Query Analyzer has significant enhancements over the version included
with SQL Server 7. One very nice feature is the Object Browser, displayed on the left side of
the Query Analyzer window (shown in Figure 1). It contains a list of all the available objects,
as well as a hierarchical list of available functions and variables. Any member of the object
browser can be dragged to the query window, where text is automatically entered based upon
the dragged object.
For example, you can grab a table from the Object Browser and right-mouse-drag it to the
query window. When you release the right mouse button, a shortcut menu appears, allowing
you to insert code for any of the following commands:

CREATE TABLE table …


ALTER TABLE table …
DROP TABLE table …
SELECT…FROM table
INSERT INTO table …
UPDATE table SET…
DELETE FROM table…

This is extremely powerful, as it frees the developer from having to remember all of a
table’s column names or the syntax of these commands. The CREATE TABLE command even
includes any constraints on the selected table.
The Object Browser also contains templates for many T-SQL commands. These can also
be dragged and dropped into the query window, allowing you to quickly build scripts. The
templates insert any necessary parameters enclosed in less than/greater than symbols (i.e.,
<parameter>) so that you can easily find and replace the parameters for your particular needs.
228 Client/Server Applications with Visual FoxPro and SQL Server

Figure 1. The SQL Server 2000 Query Analyzer showing the new Object Browser.

Debugging stored procedures


The SQL Server 2000 Query Analyzer also contains a highly anticipated feature: a source-
level debugger for T-SQL stored procedures. The debugger, shown in Figure 2, can only run
within the Query Analyzer and can only debug stored procedures that have been saved in a
database. This means that you cannot debug a script that you have saved to a SQL file or that
only exists in the query window.
To use the debugger, simply right-click the desired stored procedure in the Object
Browser and choose Debug. This will load the debugger into Query Analyzer, allowing
you to set breakpoints, step through the T-SQL source, view and change the value of any
local variables, view global variables, and check the procedure nesting level with a call
stack window.
Appendix A: New Features of SQL Server 2000 229

Figure 2. The SQL Server 2000 T-SQL debugger in break mode.

User-defined functions
Another feature that Visual FoxPro developers greatly missed in SQL Server 7 was the ability
to create and incorporate user-defined functions nearly anywhere in code. SQL Server 2000
changes things by allowing you to write user-defined functions, store them in a database, and
use them inside commands or even as column definitions.
The following example demonstrates how to create a simple user-defined function, and
then shows its use within a CREATE TABLE statement:

CREATE FUNCTION MyFraction (


@Val1 Decimal(4,1),
@Val2 Decimal(4,1) )
RETURNS Decimal(9,7)
AS
BEGIN
RETURN (@val1/@val2)
END
GO
CREATE TABLE sqltest (
numerator Decimal(4,1),
denominator Decimal(4,1),
result AS (
230 Client/Server Applications with Visual FoxPro and SQL Server

MyFraction(numerator,denominator) ) )
GO

To test this functionality, insert some values into the first two fields of the table. When you
query the table afterwards, you can see that SQL Server 2000 has automatically populated the
third column in each inserted record with the result of the user-defined function:

INSERT INTO sqltest (numerator,denominator) VALUES (5,4)


INSERT INTO sqltest (numerator,denominator) VALUES (3,2)
INSERT INTO sqltest (numerator,denominator) VALUES (1,7)
INSERT INTO sqltest (numerator,denominator) VALUES (2,3)

SELECT * FROM sqltest

numerator denominator result


--------- ----------- ----------
5.0 4.0 1.2500000
3.0 2.0 1.5000000
1.0 7.0 .1428570
2.0 3.0 .6666660

(4 row(s) affected)

Other than this special feature, user-defined functions in SQL Server 2000 can be used
in the same fashion as in Visual FoxPro. For example, you can use this same function within
a query:

SELECT job_desc, min_lvl, max_lvl, MyFraction(max_lvl,min_lvl) as ratio


FROM jobs

job_desc min_lvl max_lvl ratio


----------------------------- ------- ------- -----------
New Hire - Job not specified 10 10 1.0000000
Chief Executive Officer 200 250 .8000000
Business Operations Manager 175 225 .7777770
Chief Financial Officer 175 250 .7000000
Publisher 150 250 .6000000
Managing Editor 140 225 .6222220
Marketing Manager 120 200 .6000000
Public Relations Manager 100 175 .5714280
Acquisitions Manager 75 175 .4285710
Productions Manager 75 165 .4545450
Operations Manager 75 150 .5000000
Editor 25 100 .2500000
Sales Representative 25 100 .2500000
Designer 25 100 .2500000

(14 row(s) affected)

SQL Server 2000 user-defined functions can return any data type except text, ntext, image,
cursor or timestamp. This means that you can use the new data types (detailed in the “New Data
Types” section later in this appendix) as return values from user-defined functions, providing
plenty of flexibility for your database implementation needs.
Appendix A: New Features of SQL Server 2000 231

Referential integrity
One of the biggest “gotchas” in SQL Server 7 is that declarative referential integrity only
supports restrictive relationships. This is because of the way the FOREIGN KEY and
REFERENCES constraints were designed in SQL Server 7—they can only handle the restrict
rules. If you need to cascade a Delete or Update, you must work with stored procedures, or
remove the constraints and implement the cascade with T-SQL code in the appropriate trigger.
SQL Server 2000 now allows cascading referential integrity constraints. This means that
you no longer have to write code to implement a cascading Delete or Update. Furthermore,
since constraints are defined at the table level without code, this new feature will perform more
efficiently over the trigger- or stored-procedure-based techniques necessary under version 7.
To implement cascading RI, you can use the Table Properties dialog (shown in Figure 3),
which is part of the table designer in Enterprise Manager. By clicking the Cascade check boxes,
you will set the appropriate cascading constraint in the table.

Figure 3. The Relationships page of the Table Properties dialog.


232 Client/Server Applications with Visual FoxPro and SQL Server

Alternatively, you can use SQL Server 2000’s CREATE TABLE or ALTER TABLE
T-SQL commands, which now support the ON DELETE, ON UPDATE and CASCADE
keywords. The following example creates a relationship between an existing State code table
and a new Customer table, basing the relationship upon the state code field in both tables:

CREATE TABLE dbo.customer (


cu_id int NOT NULL IDENTITY (1, 1),
cu_last char(30) NULL,
cu_first char(30) NULL,
cu_company char(30) NULL,
cu_addr char(30) NULL,
cu_city char(30) NULL,
cu_stcode char(2) NULL,
cu_zip char(10) NULL )
GO

ALTER TABLE dbo.customer ADD CONSTRAINT


FK_customer_state FOREIGN KEY (cu_stcode)
REFERENCES dbo.state (st_code)
ON UPDATE CASCADE
ON DELETE CASCADE
GO

Trigger enhancements
In SQL Server 7, constraints are fired before the data is modified in a table. Therefore, if a
constraint fails, SQL Server fails the modification, leaving the data untouched and preventing
the firing of triggers. Triggers can only fire after a data modification has taken place, which can
only happen after all constraints have passed successfully. However, since all triggers finish
with an implied COMMIT TRANSACTION, triggers that need to discard changes must do so
by issuing a ROLLBACK TRANSACTION statement. This reverts changes that were already
made to the data.
SQL Server 2000 still supports these types of triggers—they are now called AFTER
triggers. In addition, a new type of trigger exists in SQL Server 2000 called an INSTEAD
OF trigger. These triggers fire instead of the triggering action (i.e., INSERT, UPDATE or
DELETE), execute before any constraints, and can be used on tables or views. Therefore, when
a data modification is made in SQL Server 2000, any INSTEAD OF triggers fire first, then the
constraints and, finally, any AFTER triggers.
The best place to use INSTEAD OF triggers is on views, particularly when the view
contains more than one base table. This allows any insertion of records into a view to work
properly and permits the view to be fully updatable. Without INSTEAD OF triggers, views can
only modify data in one table at a time.
Another trigger feature that was new for SQL Server 7 has been enhanced in SQL Server
2000. In version 7, it became possible to define multiple triggers for a single operation. For
example, you can create multiple UPDATE triggers, where each trigger essentially “watches”
for changes in a particular column. The only problem with multiple triggers is that SQL Server
7 did not provide any mechanism for specifying the order in which these multiple triggers
would fire.
Appendix A: New Features of SQL Server 2000 233

This shortcoming of SQL Server 7 forced developers to write a single trigger that
encapsulated the functionality of the desired multiple triggers. With a single trigger, calls could
be made in the desired sequence.
In SQL Server 2000, you can now specify which trigger fires first and which fires last with
the sp_SetTriggerOrder system stored procedure. For example, if you have three update
triggers named Upd_Trig1, Upd_Trig2 and Upd_Trig3, you can force them to fire in numerical
order with the following T-SQL code:

EXECUTE sp_SetTriggerOrder
@TriggerName='Upd_Trig1',
@Order='first',
@stmttype='UPDATE'

EXECUTE sp_SetTriggerOrder
@TriggerName='Upd_Trig3',
@Order='last',
@stmttype='UPDATE'

All triggers have an order of ‘None’ by default, which means that their order has not
been specified. If you need to determine whether a trigger is first or last, you must use the
OBJECTPROPERTY() function with one of the following properties: ExecIsFirstInsertTrigger,
ExecIsFirstUpdateTrigger, ExecIsFirstDeleteTrigger, ExecIsLastInsertTrigger,
ExecIsLastUpdateTrigger or ExecIsLastDeleteTrigger.

DECLARE @objID int, @IsFirst tinyint


SET @objID = OBJECT_ID('Upd_Trig1')
SET @IsFirst = OBJECTPROPERTY(id,'ExecIsFirstUpdateTrigger')
PRINT @IsFirst

In the preceding code, if the @IsFirst variable contains zero, the trigger is not the first
update trigger. If the variable contains one, then the trigger has been specified as the first
update trigger for the table.

Indexing computed columns


SQL Server 7 only allowed you to build indexes on the columns of a table. While not a
limitation in transactional systems, this does prevent the best performance possible in analytical
systems, because data marts and warehouses typically require lots of aggregation and
calculation against table data. Wouldn’t it be great if you could index columns that already
contained these calculations? SQL Server 2000 now permits you to index computed columns,
and even allows indexes on views.
Transactional systems will not benefit from indexed computed columns or views, as they
will tend to slow data entry speed. This is simply because SQL Server will need to update the
indexes as new data is added or existing data is modified. The more indexes and the more
complex those indexes are, the slower inserts and updates will become. The same issue exists in
Visual FoxPro and should be familiar.
However, for analytical (i.e., OLAP or data warehousing) systems, this feature can provide
a huge performance benefit. By indexing the appropriate computed columns, the data can be
retrieved more quickly, as it will already contain the necessary calculations and be accessible
234 Client/Server Applications with Visual FoxPro and SQL Server

through an index instead of a table scan. The restriction here is that the calculated column
function must be deterministic. This means that the function must always return the same result
set when provided with the same set of input values.
Another restriction can be demonstrated with the table definition created in the preceding
“User-defined functions” section, where a function was used as a column definition. While this
column could be indexed, SQL Server 2000 will not yet allow it. To enable the indexing of this
column, you must use the SCHEMABINDING function. A schema-bound function will prevent
the associated object from being altered or dropped, ensuring that any dependencies on the
function do not accidentally “disappear” and break the function.

New data types


SQL Server 2000 introduces three new data types called bigint, sql_variant and table. These
new data types were added to provide greater flexibility when writing scripts or when the “old”
data types of SQL Server 7 do not meet storage needs.

Big integers
The bigint data type is an eight-byte (64-bit) integer value with a range of
±9,223,372,036,854,775,808. This can be used in IDENTITY columns where the number
of records will exceed the “limited” range of the int data type (a four-byte integer with a
range of ±2,147,483,648). However, as this new data type is incompatible with the current
integer functions, SQL Server 2000 also added a COUNT_BIG() function and the
ROWCOUNT_BIG() function. These are functionally equivalent to the COUNT() function
and @@ROWCOUNT variable, but the returned data type is a bigint instead of an int.

Variants
The sql_variant data type is very similar to what we’re used to in Visual FoxPro—it’s a variant
data type, and it can hold any data type at any time. It cannot store BLOB data (text, ntext or
image data) or timestamp data, but it can be used as the data type for any column! Therefore, it
is possible now in SQL Server 2000 to have a column that stores different types of data in each
row. For example, here is a test SQL script to verify that this really works:

CREATE TABLE s2ktest (


Field1 Sql_variant,
Field2 Int,
Field3 Char(30) )
GO

INSERT INTO s2ktest VALUES ('test',1,'Char')


INSERT INTO s2ktest VALUES (getdate(),2,'Date')
INSERT INTO s2ktest VALUES (3.1453,3,'Real')
INSERT INTO s2ktest VALUES (49,4,'Int')

SELECT * FROM s2ktest

Note how the INSERT INTO statements put a different data type into each row of the
table. None of these statements fail because the first field is defined with the sql_variant data
type. Of course, once the data has been stored, you will want to retrieve the data, and the
Appendix A: New Features of SQL Server 2000 235

SELECT statement handles this with no problems. However, if you desire to know the data
type of the actual data in the column, you can use the SQL_VARIANT_PROPERTY() function
to get the data type, similar to how the TYPE() function works in Visual FoxPro.
For example, if you wanted to add a column that displays the data type of the first field,
you could use this SELECT statement to produce the following output:

SELECT s2kTest.*,SQL_VARIANT_PROPERTY(Field1,'BaseType') FROM s2kTest

Field1 Field2 Field3 Field4


------------------------ ------ ------ --------
test 1 char varchar
2000-08-24 21:29:53.520 2 date datetime
3.1453 3 real numeric
49 4 int int

(4 row(s) affected)

Tables as variables
The new table data type permits result sets from queries to be stored in a variable on the server.
This means that you cannot use the table data type for a column definition, but it can be used
within server-side code. This data type is a clear advantage over using temporary tables, since
these tables always consume some amount of space in the tempdb database. On the other hand,
data stored in a table data type exists entirely in memory, eliminating the performance problems
and storage requirements of temporary tables.
Defining a table data type requires use of the DECLARE command in T-SQL, with
additional text that specifies the structure of the table. Once the table has been declared, you
can work with it as any other table: Insert data, delete records or modify data that you’ve placed
into it. The following is an example of how to do this:

--Create table in memory


DECLARE @MyTable Table (Field1 int, Field2 Varchar(10), Field3 DateTime)
--Switch to northwind database
USE Northwind
--Throw some records into the table
INSERT INTO @MyTable
SELECT OrderID,CustomerID,OrderDate
FROM Orders
WHERE orderDate < '1996-07-10'
--Select data from the table
SELECT * FROM @MyTable

While this is not a tremendously useful example, it at least shows how a table variable
works like a temporary cursor in Visual FoxPro. It is important to note here that the @MyTable
variable goes out of scope when the procedure ends, so any data stored in the @MyTable
table will be released at that point. This is true for any variable declared within a SQL Server
stored procedure.
236 Client/Server Applications with Visual FoxPro and SQL Server

Summary
With only the features covered in this appendix, it’s easy to see how SQL Server 2000 offers a
tremendous amount of benefit over SQL Server 7. However, this is only a small part of what
has changed for the newest version of SQL Server. The features mentioned here relate to
databases that are built for use in transactional, non-Internet-based applications. For Internet
applications, SQL Server 2000 provides numerous XML features to sweeten the pot over SQL
Server 7. Additionally, for OLAP applications, there are plenty of enhancements to further
improve on the performance of retrieving data from your data warehouse.
In any case, upgrading from SQL Server 7 to SQL Server 2000 seems like a win-win
situation, no matter what type of application you are planning to build with it.
Index 237

Index
cache, buffer, 36,
,CAL (Client Access License), 30
@, 10
Calling stored procedures, 109
Abstracting data access functionality, 130
Candidate index, 9
Access 2000, 138
Capacity, 27
Accessing metadata, 98
Capacity limitations, 138
ACID properties, 193
Changes made locally, 86
ActiveX controls, Displaying RecordSets
char, 73
with, 217
Character sets, 31
ActiveX Data Objects, 209
CHECK constraints, 45
ADO, 168
Checkpointing, 36
ADO benefits, 209
Choosing indexes, 169
ADO constants, 219
Client Access License, 30
ADO disadvantages, 211
Client application, 173
Advantages of client/server, 15
Client/server database, 2
Advantages of remote VFP data, 126
Client/server development, 177
AERROR(), 97
Client/server division of work, 171
Agent, SQL Server, 33
Client/server performance issues, 169
An uncommitted dependency, 199
Client/server to the rescue, 2
Application changes, 188
Clustered index, 9
Application Distribution, 177
Code page, 31
Application roles, 175
COM, 21
Application-level data handler, 131
Command object, 222
Asynchronous processing, 113
Committed read, 199
Atomicity, 193
Committing buffers, 69
Audit trail, 7
Compatibility, SQL Server, 136
Authentication, SQL Server, 28
Component-based, 189
Autocommit transaction, 37
Components, 180
B-Tree, 47
Composite index, 47
Backup/restore, 186
Concurrency, 28
Balanced Tree, 47
Concurrency control, 37
Bandwidth, 172
Conflict resolution, 150
Base table, 40
Connect strings, 61
Big integers, 234
Connecting to the server, 95
binary, 73
Connection Designer, 61
Binding connections, 113
connection errors, Handling, 97
bit, 73
Connection object, 212
Blocking, 203
Connection properties revisited, 115
Bookmark, 48
connection properties, other, 116
Buffer cache, 36
Connections, 57
Buffering, 68
Connections, Binding, 113
Built-in client/server support, 23
ConnectionTimeOut property, 117
Built-in local data engine, 23
238 Client/Server Applications with Visual FoxPro and SQL Server

ConnectTimeout, 62 DefaultValue, 72
Consistency, 193 DELETE operation, 54
Constraints, 43, 84 Deployment models, 179
constraints, DEFAULT, 45 Design Issues, 156
Constraints, PRIMARY KEY, 44 Development environment, 177
Cost, 16 Disadvantages of remote VFP data, 127
Create Database Wizard, 33 Disconnecting, 98
Creating a database, 33 Displaying RecordSets with ActiveX
Creating indexes, 47 controls, 217
Data access, 3 Displaying RecordSets with code, 215
data access functionality, 130 Displaying RecordSets with the VFPCOM
Data integrity mechanisms, 160 utility, 220
data integrity, enforcing, 41 DispLogin, 63, 116
Data location, 173 DispWarnings, 63
Data Source Names, 57 Distributing databases (creating), 181
Data types, 42, 160 Distributing MSDE applications, 141
Database backup, 6 Domain integrity, 41
Database files, 33 Downsizing, 125
Database objects, 39 DRI/foreign keys, 165
Database Properties dialog, 33 DSN, file, 58
Database updates, 190 DSN, system, 58
DataType, 73 DSN, user, 58
Datatype, binary, 73 DSNs, 57
Datatype, bit, 73 DTS, 185
Datatype, char, 73 Durability, 194
Datatype, decimal, 73 Durable transactions, 201
Datatype, float, 73 Editions, SQL Server, 29
Datatype, image, 73 Enforcing data integrity, 41
Datatype, smalldatetime, 73 Entity integrity, 41
Datatype, smallint, 73 Errors, 145
Datatype, smallmoney, 73 Exclusive locks, 38
Datatype, sysname, 73 Execution plan, 49
Datatype, text, 73 Existence of SQL Server, 181
Datatype, tinyint, 73 explicit transactions, 37
Datatype, varbinary, 73 Expression mapping, 82
Datatype, varchar, 73 Extent, 36
Deadlocks, 38, 208 Feature list, 225
Debugging, 145 Features of client/server databases, 3
Debugging stored procedures, 228 FetchAsNeeded, 70
Debugging tools, 152 FetchMemo, 71
decimal, 73 FetchSize, 70
Declarative data integrity, 42 Field properties, 72
Declarative security, 4 File server, 1
DEFAULT constraints, 45 File-server database, 1
Defaults, 10, 82, 160, 162 Filter conditions, 123
Index 239

First installation, 181 Local lookup data, 192


float, 73 Local variable, 10
FOREIGN KEY constraints, 45 Lock compatibility, 203
Form-level data handler, 132 Locking, 37. 202
Free licensing, 136 Locking and transactions, 37
Free run-time distribution, 136 Log files, 33
Fully qualified, 39 Logical name, 33
GenDBC, 89 Logins, SQL Server, 174
Generating keys, 163 Managing updates, 177, 188, 192
Handling connection errors, 97 Mapping data types, 78
Handling errors, 145 MaxRecords, 71
Handling input and output parameters, 109 metadata, 98
Identity columns, 11, 43 Microsoft Data Engine (MSDE), 136
IDENTITY property, 43 Microsoft Office 2000, 136
IdleTimeout, 62, 118 Migrating MSDE databases to SQL
image, 73 Server, 142
Implicit transactions, 37, 196 Missing property, 195
In-house tools, 140 Mixed extent, 36
Inconsistent analysis, 199 Model, 33
index, candidate, 9 Msdb, 33
index, composite, 47 MSDE, 136
Indexes, 8, 46, 81 MSDE vs. SQL Server, 136
Indexes, Choosing, 169 Multi-threaded application, 15
Indexing computed columns, 233 Multiple processors, 15
Informix, 3 Multiprotocol network libraries, 31
INSERT operation, 53 Named connection, 58
Installation, 29 Named Pipes network libraries, 31
Installation issues, 227 Network libraries, 31
Installing and distributing ADO, 211 Network libraries, Multiprotocol, 31
Integrated Security, Window NT, 6 Network libraries, Named Pipes, 31
Interchangeable back ends, 125 New data types, 234
Intermediate, 47 New Features, SQL Server 2000, ,193
Interprocess Communication (IPC), 31 New York Stock Exchange, 14
Intrinsic, 13 No user interface, 138
IPC, 31 Non-clustered indexes, 10
Isolation, 194 Non-leaf-level nodes, 47
Isolation levels, SQL Server, 198 Non-relational data, 210
KeyField properties, 66 NT Authentication, 28
Licensing, 30 Nullability, 43
Limitations, capacity, 138 Nulls, 161
Liscensing, Per-Seat, 30 NULLs, 160
Liscensing, Per-Server, 30 Object names, 39
Live backup, 6 Object names, SQL Server, 39
Local changes, 86 Object transfer (DTS), 185
Local database, 88 Object-oriented programming (OOP), 19
240 Client/Server Applications with Visual FoxPro and SQL Server

ODBC logs, 156 Remote views, 57, 63, 119


OOP, 19 Remote views of VFP data, 126, 127
Operating system compatibility, 136 Remote views vs. SQL pass through, 118
Optimizer, 38 Repeatable read, 199
Oracle, 3 Replication, 14
Other connection properties, 116 Reporting errors, 146
Other view properties, 70 Request, 2
Other view properties, other, 70 Reserved word, 82
Output parameters, 110 Resources, 38
Parameterization, advantage, 107 Retrieving multiple result sets, 102
Parameterized queries, 105 Review of data integrity, 165
Parameterized views, 63 Robustness, 28
Passing data, 209 Root, 47
Per-Seat Liscensing, 30 Row buffering, 68
Per-Server Liscensing, 30 Row size, 36
Performance, 3, 16 RuleExpression, 73
Performance Monitor, SQL Server, 155 Rules, 10
Phantom reads, 199 Rules and check constraints, 162
Point-in-time recovery, 6 Scalability, 14, 17, 172
Primary Data Files, 33 Secondary data files, 33
Primary data files:, 33 Security, 4, 16, 28, 173
PRIMARY KEY constraints, 44 SendUpdates property, 66
Primary key contraints, 9 Serializable, 199
Primary key generation, 10 Server, 181
Primary keys, 163 Set-based, 12
Prior existence, 182 Setting isolation levels, 200
Procedural data integrity, 42 ShareConnection property, 61
Profiler, 3 Shared locks, 38
Profiler, SQL Server, 107 Single code base, 125
Programming for deployment, 178 smalldatetime, 73
Qualifiers, 39 smallint, 73
Queries that modify data, 105 smallmoney, 73
Queries that return a result set, 101 Sort order, 31
Query Analyzer, 227 Sp_Attach_DB, 186
QueryTimeOut property, 117 Sp_Detach_DB, 186
Rapid Application Development (RAD), Sp_executesql stored procedure, 108, 119
25 SQL, 3
Read, committed, 199 SQL databases, 3
Read, uncommitted, 199 SQL pass through, 95, 118, 167, 182, 190
reads, phantom, 199 SQL pass through result sets, updatable,
RecordSet object, 215 108
Reducing network overhead, 2 SQL scripts, 183, 191
Referential integrity, 8, 41, 164, 231 SQL Server 2000, New Features, 193
Relationships, 84 SQL Server Agent, 33
Reliability, 14 SQL Server and Visual FoxPro, 178
Index 241

SQL Server Authentication, 28 TCP/IP, 31


SQL Server compatibility, 136 Tempdb, 33
SQL Server editions, 29 Terabyte, 14
SQL Server isolation levels, 198 text, 73
SQL Server logins and permissions, 174 Timestamp, 67
SQL Server object names, 39 tinyint, 73
SQL Server Performance Monitor, 155 Transact-SQL, 12
SQL Server Profiler, 107 Transaction basics, 193
SQL Server storage allocation, 36 Transaction gotcha!, 208
SQL Server transactions, 196 Transaction log, 6, 36
SQL Server, existence of tables, 181 Transaction management, 111
SQL-DMO, 184, 191 transaction, Autocommit, 37
SQL-DMO, 191 Transactions, 14, 122, 193
SQLColumns(), 100 Transactions and locking, 37
SQLCommit(), 112 transactions, SQL Server, 196
SQLConnect(), 95 Trapping errors, 145
SQLDisconnect(), 98 Trigger enhancements, 232
SQLExec(), 101 Triggers, 7, 52, 165
SQLGetProp(), 103 Triggers, delete, 8
SQLMoreResults(), 104 Triggers, insert, 8
SQLRollback(), 112 Triggers, update, 8
SQLSetProp(), 103 Type, 13
SQLStringConnect(), 95 Types of databases,
SQLTables() function, 99 Uncommitted read, 199
storage allocation, 36 Uniform extent, 36
Stored procedures, 11, 49, 122, 166, 210 UNIQUE constraints, 44
Stored procedures, calling, 109 Unique index, 9
Structured Query Language, 3 Unix, 3
Submitting queries, 101 Updatable properties, 66
Substituting local views for remote views, Updatable views, 65
128 Update locks, 38
Support for COM, 21 UPDATE operation, 54
Support for other data-access Update using, 67
technologies, 24 UpdateName, 73
Sybase, 3 Updates, managing updates, 177, 188, 192
Synchronizing multiple copies, 14 UpdateType property, 67
SYSCOMMENTS system table, 49 Updating SQL pass through result sets,
sysname, 73 108
System Catalog, 32 Upsizing, 75
T-SQL, 12 Upsizing Wizard, modification of results,
Table buffering, 68 87
TABLEREVERT(), 69 User limitations, 136
Tables, 40, 72 User-defined data types, 13
Tables as variables, 235 User-defined functions, 229
TABLEUPDATE(), 69 Using ADO within Visual FoxPro, 212
242 Client/Server Applications with Visual FoxPro and SQL Server

Using remote views and SPT together, 122


Using the SQL Server Upsizing Wizard,
76
Validation rules, 85
varbinary, 73
varchar, 73
Variants, 234
Version control, 188, 190
Version control coordination, 191
VFP deficiencies-ADO to the rescue, 210
VFP developer vs. SQL Server DBA, 168
VFPCOM, 221
VFPCOM utility, Displaying RecordSets
with, 220
View errors, 151
Viewing lock activity, 205
Views, 12, 48
Views DBC, 134
Views, remote, 57, 63, 119
Visual FoxPro transactions, 194
Visual InterDev 6.0, 139
What is MSDE?, 136
WhereType property, 67
Why ADO?, 209
Why move to SQL Server?, 27
Wildcards, 64
Window NT Integrated Security, 6
Work space, 33
Write-ahead log, 36

You might also like