Calling SQL Server stored procs from Delphi with arguments? - sql-server

I'm trying to call SP_SPACEUSED from Delphi 2010 using ADO. I can call it without arguments by using TSQLStoredProc, and setting the StoredProcName. This gives me the database size. I now need a specific table size, and SP_SPACEUSED takes a single arugument, #objname. How do I pass this as an argument. I have tried passing this as a parameter, but this doesn't work. Is it a parameter? Can I do this from Delphi?

Quick and dirty example (in D6 but it should work without any change in 2010):
var
I: Integer;
adStoredProc : TADOStoredProc;
begin
adStoredProc := TADOStoredProc.Create(nil);
try
adStoredProc.Connection := ADOConnection1;
adStoredProc.ProcedureName := 'SP_SPACEUSED';
adStoredProc.Parameters.Refresh;
for I := 0 to adStoredProc.Parameters.Count - 1 do // Iterate
begin
if Sametext(adStoredProc.Parameters[i].Name,'#objname') then
adStoredProc.Parameters[i].Value := 't_config';
end; // for
adStoredProc.Open;
for I := 0 to adStoredProc.FieldCount - 1 do // Iterate
begin
memo1.Lines.Append(format('%s : %s', [adStoredProc.Fields[i].Fieldname, adStoredProc.Fields[i].AsString]));
end; // for
finally
FreeAndNil(adStoredProc);
end;
end;

Related

Delphi TFDStoredProc parameter default values missing (MS SQL)

I am migrating from ADO to FireDAC. After replacing TADOStoredProc to TFDStoredProc I have the following issue. My _OpenStp procedure opens a stored procedure having default values in its parameter list, and I don't want to pass all those parameters. E.g.
CREATE PROCEDURE [dbo].[usp_SearchDocument]
#User_Id INT
, #Window_Id INT = 10
, #Page INT = 1
...
The core of my procedure:
procedure _OpenStp(
const AConnection: TFDConnection;
var AStp: TFDStoredProc;
const AStpName: string;
const AParamNameA: array of string;
const AParamValueA: array of Variant);
var
i: Integer;
begin
if AStp <> nil then
begin
if AStp.Active then
AStp.Close;
end
else
AStp := TFDStoredProc.Create(nil);
AStp.Connection := AConnection;
AStp.StoredProcName := AStpName;
AStp.Prepare;
for i := Low(AParamNameA) to High(AParamNameA) do
AStp.Params.ParamByName(AParamNameA[i]).Value := AParamValueA[i];
AStp.Open;
end;
The Delphi code of the call:
_OpenStp(SomeConnection, SomeStp, 'usp_SearchDocument',
['User_Id'], [150]);
According to SQL Server Profiler the call was:
exec [dbo].[usp_SearchDocument]
#User_Id=150,
#Window_Id=NULL,
#Page=NULL
TFDStoredProc.Prepare doesn't seem to query the default values of the sp parameters. When I was using the ADO counterpart of my _OpenStp procedure, the TADOStoredProc.Parameters.Refresh method did that job:
procedure _OpenStp(
const AConnection: TADOConnection;
var AStp: TADOStoredProc;
const AStpName: string;
const AParamNameA: array of string;
const AParamValueA: array of Variant);
begin
if AStp <> nil then
begin
if AStp.Active then
AStp.Close;
end
else
AStp := TADOStoredProc.Create(nil);
AStp.Connection := AConnection;
AStp.ProcedureName := AStpName;
AStp.Parameters.Refresh;
for i := 0 to Length(AParamNameA) - 1 do
AStp.Parameters.ParamByName(AParamNameA[i]).Value := AParamValueA[i];
AStp.Open;
end;
SQL Server Profiler:
exec usp_SearchDocument 150,default,default
Unfortunately it isn't an option to rewrite the code to pass all of the parameters, I have to rely on sp parameter default values. Is there a way to modify the FireDAC version of my _OpenStp procedure to achieve this goal?
Edit: I don't even have information about the type of the parameters (see the _OpenStp procedure), I only know their names and the values to be set, so I can't create the TFDParams programmatically.
Edit#2: An EArgumentOutOfRangeException was thrown after deleting the unnecessary parameters:
for i := AStp.ParamCount - 1 downto 0 do
if AStp.Params[i].Name <> '#RETURN_VALUE' then
begin
ExistsInArray := False;
for j := Low(AParamNameA) to High(AParamNameA) do
if Char.ToLower(AStp.Params[i].Name) = Char.ToLower(Format('#%s', [AParamNameA[j]])) then
begin
ExistsInArray := True;
Break;
end;
if not ExistsInArray then
AStp.Params.Delete(i);
end;

Drag & Drop Component Suite: Drop files and if path isn't exists get data from file

I use the Drag and Drop Component Suite for Delphi.
I try to create a drag & drop area which accepts files (ie, from Windows Explorer) and data (ie, from Outlook attachments). So, I use the demo (CombatTargetDemo) to learn how it works, and after this I create a wrapper class which creates a TDropComboTarget object:
constructor TDragAndDrop.Create( vpntOwner: TWinControl);
begin
fpntDragAndDrop := TDropComboTarget.Create(vpntOwner);
fpntDragAndDrop.Name := 'DropComboTarget_'+vpntOwner.Name;
fpntDragAndDrop.DragTypes := [dtCopy, dtLink];
fpntDragAndDrop.OnDrop := DropFiles;
fpntDragAndDrop.Target := vpntOwner;
fpntDragAndDrop.Formats := [mfFile, mfData];
end;
procedure TDragAndDrop.DropFiles(Sender: TObject; ShiftState: TShiftState; Point: TPoint; var Effect: Integer);
var
intCnt: Integer;
pntStream: TStream;
strFileName: String;
strDragAndDropFile: String;
begin
try
fstlDroppedFilePaths.Clear;
fstlDroppedFilePaths.Assign(fpntDragAndDrop.Files);
for intCnt := 0 to fpntDragAndDrop.Data.Count-1 do begin
strFileName := fpntDragAndDrop.Data.Names[intCnt];
if (strFileName = '') then begin
strFileName := IntToStr(intCnt)+'_'+FormatDateTime('yyyymmddhhnnss', Now())+'.dat';
end;
strDragAndDropFile := GetDragAndDropSavePath+strFileName;
pntStream := TFileStream.Create(strDragAndDropFile, fmCreate);
try
pntStream.CopyFrom(fpntDragAndDrop.Data[intCnt], fpntDragAndDrop.Data[intCnt].Size);
finally
pntStream.Free;
end;
if FileExists(strDragAndDropFile, false) then begin
fstlDroppedFilePaths.Add(strDragAndDropFile);
end;
end;
except
end;
end;
First of all, the code works.
If I drop a Windows Explorer file on the area:
fpntDragAndDrop.Files.Count is 1 (contains the path+name from file)
fpntDragAndDrop.Data.Count is 1 (contains the file as a stream)
If I drop a file from Outlook on the area:
fpntDragAndDrop.Files.Count is 0 (contains nothing)
fpntDragAndDrop.Data.Count is 1 (contains the file as a stream)
Now my problem:
If I drop very large files from Windows Explorer, the component does the following:
Read the file header and add an item to fpntDragAndDrop.Files
Create a TMemoryStream and try to load the data from the file into the stream
Step 1 is perfect, but on step 2 I get an exception because of insufficient memory.
My solution:
I want that the component does Step 1. If Step 1 gives a result, then the component should skip Step 2. After this, the variables in the DropFiles procedure should have the following values:
If I drop a Windows Explorer file on the area:
fpntDragAndDrop.Files.Count is 1 (contaims the path+name from the file)
fpntDragAndDrop.Data.Count is 0 (No memory stream is loaded)
If I drop a file from Outlook on the area:
fpntDragAndDrop.Files.Count is 0 (comtains nothing)
fpntDragAndDrop.Data.Count is 1 (contains the file as a stream)
Does somebody have an idea? Or maybe the component has a setting for that?
I'm not overly familiar with this suite, but just browsing through its source, I think you can use the OnAcceptFormat event to reject formats you don't want on a per-drop basis.
So, even though you have enabled drops of mfData doesn't mean you have to actually accept a dropped stream (TDataStreamDataFormat) if a file path (TFileDataFormat or TFileMapDataFormat) is available. So, query the fpntDragAndDrop.DataObject to see what formats it actually holds, such as by passing it to the HasValidFormats() method of the various formats in the fpntDragAndDrop.DataFormats property.
For example:
fpntDragAndDrop.OnAcceptFormat := AcceptStreams;
...
procedure TDragAndDrop.AcceptStreams(Sender: TObject;
const DataFormat: TCustomDataFormat; var Accept: boolean);
var
Fmt: TCustomDataFormat;
i: Integer;
begin
if DataFormat is TDataStreamDataFormat then
begin
// FYI, TFileDataFormat should be in DataFormats[0],
// and TFileMapDataFormat should be in DataFormats[5],
// if you want to avoid this loop...
for i := 0 to fpntDragAndDrop.DataFormats.Count-1 do
begin
Fmt := fpntDragAndDrop.DataFormats[i];
if (Fmt <> DataFormat) and ((Fmt is TFileDataFormat) or (Fmt is TFileMapDataFormat)) then
begin
if Fmt.HasValidFormats(fpntDragAndDrop.DataObject) then
begin
Accept := False;
Exit;
end;
end;
end;
end;
Accept := True; // should already be True by default...
end;

TADODataset Field AsString performance

I have been experimenting with retrieving data from an MSSQL server from a Delphi program (using Delphi 2007).
The program has so far been using TADODataset to retrieve data. During tests we found that TQuery with an ODBC provider fetched a large select of data much quicker.
But on the other hand TADODataset preformed much better with parametrized+prepared selects.
Both TADODataset and TQuery connect to the same MSSQL database table. For this experiment I used MSSQL Native Client as provider for both.
TADODataset is set up with a connection string to the database, TQuery uses TDatabase with ODBC connection.
Here is some code from the experiment:
procedure TForm2.Button1Click(Sender: TObject);
begin
ADODataSet1.CommandText := 'select top 100000 '+FDataFields+' from transactions';
GetData(ADODataSet1);
ReadData(ADODataset1);
ADODataSet1.Close;
end;
procedure TForm2.Button2Click(Sender: TObject);
var
I: Integer;
begin
ADODataSet1.CommandText := 'select '+FDataFields+' from transactions where originaltransno = :FieldValue order by Key2';
ADODataSet1.Parameters[0].Value := '000';
ADODataSet1.Prepared := True;
GetData(ADODataSet1);
for I := 0 to 2000 do
begin
ADODataSet1.Parameters[0].Value := '123';
//no match in DB, just a check if it is there
ADODataSet1.Requery;
ReadData(ADODataset1);
end;
ADODataSet1.Close;
ADODataSet1.Prepared := False;
end;
procedure TForm2.Button3Click(Sender: TObject);
begin
Query1.SQL.Clear;
Query1.SQL.Add('select top 100000 '+FDataFields+' from transactions');
GetData(Query1);
ReadData(Query1);
Query1.Close;
end;
procedure TForm2.Button4Click(Sender: TObject);
var
I: Integer;
begin
Query1.SQL.Clear;
Query1.SQL.Add('select '+FDataFields+' from transactions where originaltransno = :FieldValue order by Key2');
Query1.Params[0].Value := '000';
Query1.Prepare;
GetData(Query1);
for I := 0 to 2000 do
begin
Query1.Params[0].Value := '123';
Query1.Close;
Query1.Open;
ReadData(Query1);
end;
Query1.Close;
Query1.UnPrepare;
end;
procedure TForm2.GetData(ADataSet: TDataSet);
begin
ADataSet.DisableControls;
ADataSet.Open;
end;
procedure TForm2.ReadData(ADataSet: TDataSet);
begin
while not ADataSet.EOF do
begin
ReadLine(ADataSet);
ADataSet.Next;
end;
end;
function TForm2.ReadLine(ADataSet: TDataSet): string;
var
I: Integer;
begin
for I := 0 to ADataSet.Fields.Count - 1 do
begin
if ADataSet.FieldDefs[I].DataType = ftString then
Result := Result + ADataSet.Fields[I].AsString;
end;
end;
A rough timing (using CPU Time in task manager)
Button1 - TADODataset large select: 13 seconds
Button2 - TADODataset small selects: 7 seconds
Button3 - TQuery large select: 4 seconds
Button4 - TQuery small selects: 24 seconds
Futher testing indicated the slow time with large TADODataset select is from reading the fields AsString
I tried replacing ReadLine:
function TForm2.ReadLine(ADataSet: TDataSet): int64;
var
I: Integer;
begin
for I := 0 to ADataSet.Fields.Count - 1 do
begin
if ADataSet.FieldDefs[I].DataType = ftInteger then
Result := Result + ADataSet.Fields[I].AsInteger;
end;
end;
With integer reading I got these timings:
Button1 - TADODataset large select: 3 seconds
Button3 - TQuery large
select: 3 seconds
I tried using Value instead, but "adding" a Variant to string was much slower
Can I do anything in reading the strings that speed up TADODataset reading?

Cannot use MSSQL Timestamp as a parameter in Delphi XE8

We are in the process of upgrading one of our projects from Delphi XE to XE8. Our audit code makes use of a TIMESTAMP field in a MSSQL (2012 in this instance) database and selects from a table using this as a parameter in the WHERE clause.
We now are no longer getting any results running the following code:
procedure TForm2.Button1Click(Sender: TObject);
begin
ADODataset1.CommandText := 'SELECT * FROM CURRENCYAUDIT';
ADODataset2.CommandText := 'SELECT * FROM CURRENCYAUDIT WHERE Audit_Timestamp = :Timestamp';
ADODataset2.Parameters.Refresh;
ADODataset1.Open;
if ADODataset1.FieldByName('audit_timestamp').IsNull or ADODataset1.IsEmpty then
begin
showmessage('nothing to compare');
end;
ADODataset2.Parameters[0].Value := ADODataset1.FieldByName('audit_timestamp').Value;
ADODataset2.Open;
caption := inttostr(ADODataset2.RecordCount);
end;
Where CurrencyAudit is any old MSSQL table containing an notnull timestamp audit_timestamp field.
The caption of the form is 0 with no message shown.
Any idea how I can get this to work? Tried AsString (nonsense string, 0 results), AsSQLTimestamp (parameter doesn't accept) and AsBytes (0 return). Unfortunately the return of the .Value only evalates as 'variant array of byte' which isn't helpful to visualise/see what it is.
Edit: Running it as .AsBytes and viewing that in the debugger I can see that the XE verison is returning 0,0,0,0,0,8,177,22 whereas the XE8 is returning 17,32,0,0,0,0,0,0. Checking other fields of the (real) database shows the record is the same. Looks like a bug in reading TIMESTAMPs from the DB
I'm using two AdoQueries. The following works fine for me in D7, correctly returning 1 row in AdoQuery2, but 0 records in XE8, so obviously has the same XE8 problem as you've run into.
var
S : String;
V : Variant;
begin
AdoQuery1.Open;
S := AdoQuery1.FieldByName('ATimeStamp').AsString;
V := AdoQuery1.FieldByName('ATimeStamp').AsVariant;
Caption := S;
AdoQuery2.Parameters.ParamByName('ATimeStamp').Value := V;
AdoQuery2.Open;
Just for testing, I'm running my AdoQuery1 and AdoQuery2 against the same server table.
Update : I've got a similar method to the one in your answer working that avoids the need for your Int64ToByteArray, at the expense of some slightly messier (and less efficient) Sql, which may not be to your taste.
In my source AdoQuery, I have this Sql
select *, convert(int, atimestamp) as inttimestamp from timestamps
and in the destination one
select * from timestamps where convert(int, atimestamp) = :inttimestamp
which of course avoids the need for a varBytes parameter on the second AdoQuery, since one can pick up the integer version of the timestamp column value and assign it to the inttimestamp param.
Btw, in your original q
if ADODataset1.FieldByName('audit_timestamp').IsNull or ADODataset1.IsEmpty then
the two expressions would better be written the other way around. Unless ADODataset1 has persistent fields, if it contains no records when opened, referring to the audit_timestamp should raise a "Field not found" exception.
It appears that EMBT have broken the conversion of a TIMESTAMP into a byte array. The XE version of the bytearray is correct and manually pulling down the data as an int64 and then building the bytearray by hand (is there an out-of-the-box function for this?) and using that as the parameter works in XE8.
I've no idea whether this is a similar issue with other binary data types. Hope not!
Working code:
procedure TForm2.Button1Click(Sender: TObject);
var
TestArray: TArray<Byte>;
j: integer;
function Int64ToByteArray(const inInt: uint64): TArray<Byte>;
var
i: integer;
lInt: int64;
begin
SetLength(result, 8);
lInt := inint;
for i := low(result) to high(result) do
begin
result[high(result)-i] := lInt and $FF;
lInt := lInt shr 8;
end;
end;
begin
ADODataset1.CommandText := 'SELECT *, cast(audit_timestamp as bigint) tmp FROM CURRENCYAUDIT';
ADODataset2.CommandText := 'SELECT * FROM CURRENCYAUDIT WHERE Audit_Timestamp = :Timestamp';
ADODataset2.Parameters.Refresh;
ADODataset1.Open;
if ADODataset1.FieldByName('audit_timestamp').IsNull or ADODataset1.IsEmpty then
begin
showmessage('nothing to compare');
end;
ADODataset2.Parameters[0].Value := Int64ToByteArray(ADODataset1.FieldByName('tmp').asInteger);
ADODataset2.Open;
caption := inttostr(ADODataset2.RecordCount);
end;
I've also checked this going down my entire (real) table and ensuring all other fields match to make sure it's not a one-off!
I'll raise a ticket with EMBT for them to sit on and ignore for 5 years ;-)
https://quality.embarcadero.com/browse/RSP-11644

What is the proper way to dynamically create and call a stored procedure in Delphi using FireDac?

I am relatively new to FireDAC. I want to be able to call a stored procedure "on the fly", dynamically. So far I have the following:
function TForm21.ExecuteStoredProc(aSPName: string; aParams: TADParams): Boolean;
var
LSP: TADStoredProc;
i: Integer;
begin
LSP := TADStoredProc.Create(nil);
try
LSP.Connection := ADConnection1;
LSP.StoredProcName := aSPName;
LSP.Prepare;
for i := 0 to aParams.Count - 1 do
begin
LSP.Params[i].Value := aParams[i].Value;
end;
LSP.ExecProc;
finally
LSP.Free;
end;
Result := True;
end;
I call it with
procedure TForm21.Button1Click(Sender: TObject);
var
LParams: TADParams;
begin
LParams := TADParams.Create;
LParams.Add.Value := 612;
LParams.Add.Value := '2008';
ExecuteStoredProc('HDMTEST.dbo.spARCHIVE_HISTORY_DATA', LParams);
end;
However, the stored procedure fails to execute. That is, the code runs fine, no error message is shown, but the stored procedure doesn't run.
Further info -- it runs fine if I drop a component and set up the params in code.
Anyone have any idea what I am missing?
Seeing as this q has been left unanswered for a while, I thought I'd try to get the code working without using the clues from the comments and found it not quite as easy as I'd imagined.
I immediately got stuck with the SP params. I found this
http://docwiki.embarcadero.com/RADStudio/XE5/en/TFDQuery,_TFDStoredProc_and_TFDUpdateSQL_Questions
which says
"If you have difficulties with manual definition of parameters,
populate the Params collection automatically and check how the
parameters are defined. Then compare that to your code. "
but I couldn't find a way of "automatically" populating the Params. I asked on the EMBA
FireDac newsgroup and the FD author, Dimitry Arefiev, kindly explained that
you can do that by checking that the FetchOptions include fiMeta, and then clearing and setting the FDStoredProc's StoredProcName.
Using a StoredProc in the pubs demo database on my SqlServer defined as follows:
create procedure test(#ANumber int, #AName varchar(20))
as
begin
select
#ANumber * 2 as "Number",
#AName + #AName as "Name"
end
I changed a couple of sections of the OP's code like this
[...]
LSP.Params.Clear;
LSP.StoredProcName := '';
LSP.FetchOptions.Items := LSP.FetchOptions.Items + [fiMeta];
LSP.StoredProcName := aSPName;
LSP.Prepare;
Assert(LSP.ParamCount > 0);
for i := 0 to aParams.Count - 1 do
begin
LSP.Params.ParamByName(aParams[i].Name).Value := aParams[i].Value;
end;
[...]
procedure TForm21.Button1Click(Sender: TObject);
var
LParams: TFDParams;
Param : TFDParam;
begin
LParams := TFDParams.Create;
Param := LParams.Add;
Param.Name := '#ANumber';
Param.Value := 612;
Param := LParams.Add;
Param.Name := '#AName';
Param.Value := '2008';
ExecuteStoredProc('test', LParams);
end;
and it worked fine.
The OP mentions in the q he'd first had the problem that the SP failed to execute
but that he'd found that it worked if he "[dropped] a component and set up the params in code" so I thought I'd include here a console application where of course necessarily everything is done in code. This wasn't difficult, but the time it took me to get the Uses clause right is my main reason for posting this as an answer, for future reference. W/o the correct uses, you get errors complaining about various class factories being missing.
Console app (created and tested in XE6):
program ConsoleStoredProcProject3;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, FireDac.DApt, FireDAC.Stan.Def, FireDAC.Stan.ASync,
FireDAC.Stan.Param, FireDAC.Stan.Option, FireDAC.Comp.Client,
FireDAC.Phys.MSSQL, VCL.ClipBrd;
procedure TestSP;
var
Connection : TFDConnection;
StoredProc : TFDStoredProc;
Param : TFDParam;
begin
Connection := TFDConnection.Create(Nil);
Connection.DriverName := 'MSSQL';
Connection.Params.Values['Server'] := // your server name'';
Connection.Params.Values['Database'] := 'pubs';
Connection.Params.Values['user_name'] := 'user'; // adjust to suit
Connection.Params.Values['password'] := 'password'; // ditto
Connection.LoginPrompt := False;
Connection.Connected := True;
StoredProc := TFDStoredProc.Create(Nil);
StoredProc.Connection := Connection;
StoredProc.FetchOptions.Items := StoredProc.FetchOptions.Items + [fiMeta];
StoredProc.StoredProcName := 'test';
StoredProc.Prepare;
Param := StoredProc.Params.ParamByName('#ANumber');
Param.Value := 333;
Param := StoredProc.Params.ParamByName('#AName');
Param.Value := 'A';
StoredProc.Active := True;
WriteLn(StoredProc.FieldByName('Number').AsInteger);
WriteLn(StoredProc.FieldByName('Name').AsString);
ReadLn;
end;
begin
try
TestSP;
except
on E: Exception do
Clipboard.AsText := E.Message;
end;
end.

Resources