Use SQL Server table-valued parameters without creating a type on the SQL Server - sql-server

I'm trying to pass several values on a single parameter (table-valued parameter) and it works fine following the sample
C:\Users\Public\Documents\Embarcadero\Studio\21.0\Samples\Object Pascal\Database\FireDAC\Samples\DBMS Specific\MSSQL\TVP
SQL:
create type TVPType as table(Code integer, Name varchar(100), RegDate datetime, Notes varchar(max))
go
create table TVPTab(Code integer, Name varchar(100), RegDate datetime, Notes varchar(max))
go
Delphi:
procedure TForm1.btnQryManualSetupClick(Sender: TObject);
var
oDS: TFDMemTable;
i: Integer;
begin
Start;
FDQuery2.SQL.Text := 'insert into TVPTab (Code, Name, RegDate, Notes) ' +
'select Code, Name, RegDate, Notes from :t';
oDS := TFDMemTable.Create(nil);
oDS.FieldDefs.Add('Code', ftInteger);
oDS.FieldDefs.Add('Name', ftString, 100);
oDS.FieldDefs.Add('RegDate', ftTimeStamp);
oDS.FieldDefs.Add('Notes', ftMemo);
FDQuery2.Params[0].DataTypeName := 'TVPType';
FDQuery2.Params[0].AsDataSet := oDS;
(FDQuery2.Params[0].AsDataSet as TFDMemTable).EmptyDataSet;
for i := 1 to C_Recs do
with FDQuery2.Params[0].AsDataSet do begin
Append;
Fields[0].AsInteger := i;
Fields[1].AsString := 'str' + IntToStr(i * 10);
Fields[2].AsDateTime := Now() + i;
Fields[3].AsString := StringOfChar('x', 1000);
Post;
end;
FDConnection1.StartTransaction;
FDQuery2.Execute;
FDConnection1.Commit;
Done('TVP Qry manual setup');
end;
The problem with this code is that it forces me to create a type (in this case the TVPType type) on the server, and I would like to use these table-valued parameters without having to create any object on the server.
Can it be done ?. I have tried defining the parameter as ftDataset but I get an error: The data type READONLY cannot be found.
procedure TForm1.btnQryManualSetupClick(Sender: TObject);
var
oDS: TFDMemTable;
i: Integer;
begin
Start;
FDQuery2.SQL.Text := 'insert into TVPTab (Code, Name, RegDate, Notes) ' +
'select Code, Name, RegDate, Notes from :t';
oDS := TFDMemTable.Create(nil);
oDS.FieldDefs.Add('Code', ftInteger);
oDS.FieldDefs.Add('Name', ftString, 100);
oDS.FieldDefs.Add('RegDate', ftTimeStamp);
oDS.FieldDefs.Add('Notes', ftMemo);
//FDQuery2.Params[0].DataTypeName := 'TVPType';
FDQuery2.Params[0].DataType := ftDataset;
FDQuery2.Params[0].AsDataSet := oDS;
(FDQuery2.Params[0].AsDataSet as TFDMemTable).EmptyDataSet;
for i := 1 to C_Recs do
with FDQuery2.Params[0].AsDataSet do begin
Append;
Fields[0].AsInteger := i;
Fields[1].AsString := 'str' + IntToStr(i * 10);
Fields[2].AsDateTime := Now() + i;
Fields[3].AsString := StringOfChar('x', 1000);
Post;
end;
FDConnection1.StartTransaction;
FDQuery2.Execute;
FDConnection1.Commit;
Done('TVP Qry manual setup');
end;

No. If your multiple values are just a single row you can look into passing them as a comma-separated string and then use the STRING_SPLIT function, or using temp or permanent tables to store multi-row values.

Related

Storing TTreeView inside SQL Server database table

Am trying to store my TTreeView inside SQL Server database table by using the next procedure:
procedure Save;
var
BlobField :TBlobField;
Query:TADOQuery;
Stream:TStream;
begin
Stream := TMemoryStream.Create;
Query := TADOQuery.Create(Self);
Query.SQL.Add('Select * From MyTable') ;
Query.Active := True;
Query.First;
Query.Edit;
BlobField := Query.FieldByName('MyTableField') as TBlobField;
Stream := Query.CreateBlobStream(BlobField, bmWrite);
TreeView1.SaveToStream(Stream);
Query.Refresh;
Query.Free;
Stream.Free;
end;
But every time I am getting the error: DataSet is not in edit or insert mode.
I'm using Delphi 10.1, Win 10, SQL server 2019.
Change Query.Refresh; to Query.Post;
Also, you need to Free the blob stream to finalize writing to the blob field before you then Post to commit the new data into the DB.
Also, you are leaking an unused TMemoryStream object.
Try this:
procedure Save;
var
BlobField: TField;
Query: TADOQuery;
Stream: TStream;
begin
Query := TADOQuery.Create(nil);
try
Query.SQL.Text := 'Select TOP(1) * From MyTable';
Query.Open;
try
Query.First;
Query.Edit;
try
BlobField := Query.FieldByName('MyTableField');
Stream := Query.CreateBlobStream(BlobField, bmWrite);
try
TreeView1.SaveToStream(Stream);
finally
Stream.Free;
end;
Query.Post;
except
Query.Cancel;
raise;
end;
finally
Query.Close;
end;
finally
Query.Free;
end;
end;

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;

How can I use %TYPE with Oracle associative arrays?

I have declared an associative array type in some Oracle package header like:
TYPE ParamArray IS TABLE OF VARCHAR2(4096) INDEX BY VARCHAR2(512);
In my package body I would like to iterate through the array without repeating the string sizes to avoid mismatches when updating the package header.
My try was:
PROCEDURE IterateArray( Params ParamArray )
AS
v_ParamName ParamArray%TYPE;
BEGIN
v_ParamName := Params.First;
WHILE v_ParamName IS NOT NULL LOOP
-- do something with the array entry
v_ParamName := Params.Next(v_ParamName);
END LOOP;
END;
But this didn't work on my Oracle 10g test server.
You can define you own type to handle the varchar2 and the use your type, with no need to repeat the size.
For example:
CREATE OR REPLACE PACKAGE testpck AS
SUBTYPE myType IS VARCHAR2(4096); /* a type for the values */
SUBTYPE myIndexType is VARCHAR2(512); /* a type for the index */
TYPE ParamArray IS TABLE OF myType
INDEX BY myIndexType;
PROCEDURE IterateArray(Params ParamArray);
END;
CREATE OR REPLACE PACKAGE BODY testpck AS
PROCEDURE IterateArray(Params ParamArray) AS
v_ParamName myType;
v_index myIndexType;
BEGIN
v_index := Params.FIRST;
WHILE v_index IS NOT NULL
LOOP
-- do something with the array entry
v_ParamName := Params(v_index);
dbms_output.put_line('value of ' || v_index || ' is ' || v_ParamName);
v_index := Params.NEXT(v_index);
END LOOP;
END;
END;
The call:
declare
myArray testpck.ParamArray;
myValue testpck.myType;
myIndex testpck.myIndexType;
begin
myIndex := 'ONE';
myValue := 'VALUE OF ONE';
myArray(myIndex) := myValue;
--
myIndex := 'TWO';
myValue := 'VALUE OF TWO';
myArray(myIndex) := myValue;
--
testpck.IterateArray(myArray);
end;

Eof not triggering

I have a function where I get data from a DB, my test data set returns 6500 rows (I extracted the formatted SQL statement from the SQLText Variable and ran it as a test), but then when I run the following code Eof never triggers and I have seen over 100k rows imported.
ADOQuery := TADOQuery.Create(nil);
ADOQuery.ConnectionString := CONNECT_STRING;
// Build SQL Query
SQLText := Format( 'Select Temp.Serial, Temp.QCSample , Temp.Scrap , Temp.StationID , Temp.Defect , Temp.AddData , Temp2.Serial as Parent_Serial ' +
'from TAB_ELEMENT as Temp ' +
'left join TAB_ELEMENT as Temp2 on Temp.Parent_Id = Temp2.Element_Id ' +
'where Temp.Batch_ID = %d and Temp.StationID = 0 ',[iSearchID]);
ADOQuery.SQL.Clear; // Clear query of garbage values
ADOQuery.SQL.Text := SQLText; // Add query text to query module
ADOQuery.Open;
// Handle Results
iIndexPos := 0;
tDataImport.BeginUpdate;
while not ADOQuery.Eof do
begin
tDataImport.Items[iIndexPos].Serial := ADOQuery.FieldByName('Serial').AsString;
tDataImport.Items[iIndexPos].QCStatus := ADOQuery.FieldByName('QCSample').AsBoolean;
tDataImport.Items[iIndexPos].Scrap := ADOQuery.FieldByName('Scrap').AsInteger;
tDataImport.Items[iIndexPos].StationID := ADOQuery.FieldByName('StationID').AsInteger;
tDataImport.Items[iIndexPos].Defect := ADOQuery.FieldByName('Defect').AsBoolean;
tDataImport.Items[iIndexPos].AddData := ADOQuery.FieldByName('AddData').AsString;
tDataImport.Items[iIndexPos].ParentSerial := ADOQuery.FieldByName('Parent_Serial').AsString;
inc(iIndexPos);
end;
So in summery running this query with these parameters I expect 6500 rows, when I run this it never ends even after 100k+ rows are processed.
Open() places the cursor on the first record and sets Eof accordingly. You are not calling Next() to advance the cursor to the next record and update Eof, so you are processing the same record over and over:
ADOQuery.Open;
while not ADOQuery.Eof do
begin
//...
ADOQuery.Next; // <-- add this!
end;
On a side note, you should be using a parameterized query instead of a formatted SQL query string. It is safer, faster, and more efficient on the DB:
ADOQuery := TADOQuery.Create(nil);
ADOQuery.ConnectionString := CONNECT_STRING;
ADOQuery.SQL.Text := 'Select Temp.Serial, Temp.QCSample , Temp.Scrap , Temp.StationID , Temp.Defect , Temp.AddData , Temp2.Serial as Parent_Serial ' +
'from TAB_ELEMENT as Temp ' +
'left join TAB_ELEMENT as Temp2 on Temp.Parent_Id = Temp2.Element_Id ' +
'where Temp.Batch_ID = :iSearchID and Temp.StationID = 0 ';
with ADOQuery.Parameters.ParamByName('iSearchID') do
begin
DataType := ftInteger;
Value := iSearchID;
end;
ADOQuery.Open;
try
iIndexPos := 0;
tDataImport.BeginUpdate;
try
while not ADOQuery.Eof do
begin
tDataImport.Items[iIndexPos].Serial := ADOQuery.FieldByName('Serial').AsString;
tDataImport.Items[iIndexPos].QCStatus := ADOQuery.FieldByName('QCSample').AsBoolean;
tDataImport.Items[iIndexPos].Scrap := ADOQuery.FieldByName('Scrap').AsInteger;
tDataImport.Items[iIndexPos].StationID := ADOQuery.FieldByName('StationID').AsInteger;
tDataImport.Items[iIndexPos].Defect := ADOQuery.FieldByName('Defect').AsBoolean;
tDataImport.Items[iIndexPos].AddData := ADOQuery.FieldByName('AddData').AsString;
tDataImport.Items[iIndexPos].ParentSerial := ADOQuery.FieldByName('Parent_Serial').AsString;
inc(iIndexPos);
ADOQuery.Next;
finally
tDataImport.EndUpdate;
end;
end;
finally
ADOQuery.Close;
end;

How can I display only specified database results based on TListbox items?

I have two forms: frmMakeQuote and frmQuoteTemp
In the frmMakeQuote there are two TListboxes: lboMtrlList and lboSelectedMtrl
lboMtrlList displays the Product Description column from the database.
procedure TfrmMakeQuote.FormCreate(Sender: TObject);
begin
con := TFDConnection.Create(nil);
query := TFDQuery.Create(con);
con.LoginPrompt := false;
con.Open('DriverID=SQLite;Database=C:\Users\kasio\Documents\Embarcadero\' +
'Studio\Projects\ProgramDatabase;');
query.Connection := con;
query.SQL.Text :=
'SELECT [Material Description] FROM MtrlDatabase ORDER BY MtrlID';
try
query.Open;
lboMtrlList.Items.Clear;
while not query.EOF do
begin
lboMtrlList.Items.Add(query.Fields[0].AsString);
query.Next;
end;
finally
query.Close;
end;
end;
When the person double clicks on any 'product' in the lboMtrlList, it's moved to the lboSelectedMtrl. (Basically, it shows the selected 'products'.)
procedure TfrmMakeQuote.lboMtrlListDblClick(Sender: TObject);
begin
lboSelectedMtrl.Items.Add(lboMtrlList.Items.Strings[lboMtrlList.ItemIndex]);
end;
I want to be able to display the Product Description and Price columns from the database, of ONLY the selected 'products' from the lboSelectedMtrl. They should be displayed in the TStringGrid called sgdMaterials on the frmQuoteTemp.
I wrote something like this:
procedure TfrmMakeQuote.performMtrlQuery;
var
i: integer;
begin
for i := 1 to frmMakeQuote.lboSelectedMtrl.ItemIndex do
begin
query.SQL.Text := 'SELECT [Material Description], Price FROM MtrlDatabase ' +
'WHERE [Material Description] = "'
+ frmMakeQuote.lboSelectedMtrl.Items.Strings[1]
+ '" ORDER BY MtrlID';
query.Open;
query.First;
end;
end;
It doesn't show any error, but it doesn't work and displays nothing and I'm aware that it's probably completely wrong.
Your loop inside of performMtrlQuery() is wrong. If nothing is actually selected in lboSelectedMtrl, its ItemIndex will be -1 and the loop will not iterate through any items. Not only that, but even if an item were selected, your loop would not iterate through ALL of the available items. Also, when you are indexing into the Strings[] property, you are using a hard-coded 1 instead of the loop variable i.
As for your TStringGrid, why not use a TDBGrid instead, and tie it to a DataSource that is filtering the database by the desired items? In any case, performMtrlQuery() is not doing anything to populate the grid at all, whether it is to store the search results in the grid directly, or to store the results in a list somewhere that frmQuoteTemp can then read from.
Try this instead:
procedure TfrmMakeQuote.performMtrlQuery;
var
i: integer;
begin
for i := 0 to frmMakeQuote.lboSelectedMtrl.Items.Count-1 do
begin
query.SQL.Text := 'SELECT [Material Description], Price FROM MtrlDatabase' +
' WHERE [Material Description] = "'
+ frmMakeQuote.lboSelectedMtrl.Items.Strings[i]
+ '" ORDER BY MtrlID';
query.Open;
query.First;
// do something with query.Fields[0] and query.Fields[1] ...
query.Close;
end;
end;
That being said, searching your materials by their descriptions is not the most efficient search option. You should search by their IDs instead. I would suggest an alternative approach to accomplish that - use your TListBox controls in virtual mode (Style=lbVirtual) instead, and store your search results in separate TStringList objects. That way you can store both IDs and Descriptions together in memory while displaying the descriptions in the UI and using the IDs in queries.
Try something more like this:
procedure TfrmMakeQuote.FormCreate(Sender: TObject);
begin
allmaterials := TStringList.Create;
selectedmaterials := TStringList.Create;
con := TFDConnection.Create(Self);
con.LoginPrompt := false;
con.Open('DriverID=SQLite;Database=C:\Users\kasio\Documents\Embarcadero\Studio\Projects\ProgramDatabase;');
query := TFDQuery.Create(con);
query.Connection := con;
query.SQL.Text := 'SELECT MtrlID, [Material Description] FROM MtrlDatabase ORDER BY MtrlID';
try
query.Open;
while not query.EOF do
begin
allmaterials.Add(query.Fields[0].AsString + '=' + query.Fields[1].AsString);
query.Next;
end;
finally
query.Close;
end;
lboMtrlList.Count = allmaterials.Count;
end;
procedure TfrmMakeQuote.FormDestroy(Sender: TObject);
begin
allmaterials.Free;
selectedmaterials.Free;
end;
// lboMtrlList OnData event handler
procedure TfrmMakeQuote.lboMtrlListData(Control: TWinControl; Index: Integer; var Data: string);
begin
Data := allmaterials.ValueFromIndex[Index];
end;
// lboSelectedMtrl OnData event handler
procedure TfrmMakeQuote.lboSelectedMtrlData(Control: TWinControl; Index: Integer; var Data: string);
begin
Data := selectedmaterials.ValueFromIndex[Index];
end;
procedure TfrmMakeQuote.lboMtrlListDblClick(Sender: TObject);
var
Idx: Integer;
begin
Idx := lboMtrlList.ItemIndex;
if Idx = -1 then Exit;
if selectedmaterials.IndexOfName(allmaterials.Names[Idx]) <> -1 then Exit;
selectedmaterials.Add(allmaterials.Strings[Idx]);
lboSelectedMtrl.Count := selectedmaterials.Count;
end;
procedure TfrmMakeQuote.performMtrlQuery;
var
i: integer;
begin
for i := 0 to selectedmaterials.Count-1 do
begin
query.SQL.Text := 'SELECT [Material Description], Price FROM MtrlDatabase' +
' WHERE MtrlID = '
+ selectedmaterials.Names[i];
query.Open;
query.First;
// do something with query.Fields[0] and query.Fields[1] ...
query.Close;
end;
end;
Lastly, if you switch to a single TCheckListBox or TListView control instead of 2 TListBox controls, you can take advantage of their ability to have checkboxes on each item, then you don't need to deal with the OnDblClick event anymore, and don't need to show two copies of your materials in your UI. The user can just check the desired items before invoking performMtrlQuery().
I would also suggest using a virtual TListView for the search results instead of a TStringGrid. The UI will look better (TStringGrid is not the best looking UI control), and you can utilize memory more efficiently (TStringGrid can be a memory hog if you have a lot of data to display).

Resources