Passing NULL value into parameterized delphi SQL server query - sql-server

I am trying to pass in a null value to a TSQLDataset parameter. The query has the form:
Query_text:='MERGE INTO [Table]
USING (VALUES (:A,:B)) AS Source (Source_A, Source_B)
....
WHEN MATCHED THEN
UPDATE SET A = :A
WHEN NOT MATCHED THEN
INSERT(A, B) VALUES (:A,:B);
SQL_dataset.CommandType:=ctQuery;
SQL_dataset.CommandText:=Query_text;
SQL_dataset.ParamByName('A').AsString:='A';
SQL_dataset.ParamByName('B').AsString:={ COULD BE NULL, OR A STRING };
SQL_dataset.ExecSQL;
Parameter B is nullable, but is also a foreign key. If the user enters something in this field, then B must be validated against values in another table. If it is blank then I want it to be ignored. I was passing in '', but this obviously produces a FK violation error.
I tried:
SQL_dataset.ParamByName('B').Value:=Null;
..but then I get a "dbexpress driver does not support the tdbxtypes.unknown data type" error.
I also tried:
SQL_dataset.ParamByName('B').DataType:=ftVariant;
SQL_dataset.ParamByName('B').Value:=Null;
..but then got "dbexpress driver does not support the tdbxtypes.variant data type" error.
Not sure what I am doing wrong, any help would be appreciated. I am currently drawing up a parameter list based on whether the string is populated or not, and this works well; it's just a bit clunky (in my actual query) as there are quite a few parameters to validate.
I am using Delphi XE4 and SQL server 2012.
Update:
Thanks for all the help, your suggestions were right all along, it was something else that produced that 'dbexpress driver' error. I was creating a 'flexible' parameter list in an effort to get around my problem, and this caused the exception:
Parameter_string:='';
If B<>'' then Parameter_string:='B = :B,'
Query_text:='MERGE ...'
'...'
'UPDATE SET A = :A, '+Parameter_string+' C = :C' ....
... the idea being that if B is blank then the parameter won't be 'listed' in the query.
This doesn't work, or my implementation of it doesn't work (not sure why, I'm obviously missing a step somewhere).
Anyway, the working code:
Query_text:='MERGE ...'
'...'
'UPDATE SET A = :A, B = :B, C = :C' ....
SQL_dataset.CommandType:=ctQuery;
SQL_dataset.CommandText:=Query_text;
If B<>'' then
begin
SQL_dataset.ParamByName('B').AsString:='B';
end
else
begin
SQL_dataset.ParamByName('B').DataType:=ftString;
SQL_dataset.ParamByName('B').Value:=Null;
end;

what about:
SQL_dataset.ParamByName('B').Clear;

If I recall correctly, the db-null equivalent in Delphi is Variants.Null

Usual approach would be using parameters once per query and assign the appropriate datatype.
Value may be assigned to NULL.
var
Query_text:String;
begin
Query_text:='Declare #A varchar(100) ' // or e.g. integer
+#13#10'Declare #B varchar(100)'
+#13#10'Select #A=:A'
+#13#10'Select #B=:B'
+#13#10'Update Adressen Set Vorname=#A,Strasse=#B where Name=#B';
SQL_dataset.CommandType := ctQuery;
SQL_dataset.CommandText := Query_text;
SQL_dataset.Params.ParseSQL(SQL_dataset.CommandText,true);
Showmessage(IntToStr(SQL_dataset.Params.Count));
SQL_dataset.ParamByName('B').DataType := ftString;
SQL_dataset.ParamByName('B').Value := 'MyText';
SQL_dataset.ParamByName('A').DataType := ftString; // or e.g. ftInteger
SQL_dataset.ParamByName('A').Value := NULL;
SQL_dataset.ExecSQL;
end;

Related

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

Query to fetch data between two characters in informix

I have a value in informix which is like this :
value AMOUNT: <15000000.00> USD
I need to fetch 15000000.00 afrom the above.
I am using this query to fetch the data between <> as workaround
select substring (value[15,40]
from 1 for length (value[15,40]) -5 )
from tablename p where value like 'AMOUNT%';
But, this is not generic as the lenght may vary.
Please help me with a generic query for this, fetch the data between <>.
The database I am using is Informix version 9.4.
It's a diabolical problem, created by whoever chose to break one of the fundamental rules of database design: that the content of a column should be a single, indivisible value.
The best solution would be to modify the table to contain a value_descr = "AMOUNT", a value = 15000000.00, and a value_type = "USD", and ensure that the incoming data is stored in that fashion. Easier said than done, I know.
Failing that, you'll have to write a UDR that parses the string and returns the numeric portion of it. This would be feasible in SPL, but probably very slow. Something along the lines of:
CREATE PROCEDURE extract_value (inp VARCHAR(255)) RETURNING DECIMAL;
DEFINE s SMALLINT;
DEFINE l SMALLINT;
DEFINE i SMALLINT;
FOR i = 1 TO LENGTH(inp)
IF SUBSTR(inp, i, 1) = "<" THEN
LET s = i + 1;
ELIF SUBSTR(inp, i, 1) = ">" THEN
LET l = i - s - 1;
RETURN SUBSTR(inp, s, l)::DECIMAL;
END IF;
END FOR;
RETURN NULL::DECIMAL; -- could not parse out number
END PROCEDURE;
... which you would execute thus:
SELECT extract_value(p.value)
FROM tablename AS p
WHERE p.value LIKE 'AMOUNT%'
NB: that procedure compiles and produces output in my limited testing on version 11.5. There is no validation done to ensure the string between the <> parses as a number. I don't have an instance of 9.4 handy, but I haven't used any features not available in 9.4 TTBOMK.

Passing Array to Oracle Function

I am passing an array to a PL/SQL package function. I am doing this to use this array in a query inside the function which has IN clause.
My declaration of package looks like :
create or replace
PACKAGE selected_pkg IS
TYPE NUM_ARRAY IS TABLE OF NUMBER;
FUNCTION get_selected_kml(
in_layer IN NUMBER,
in_id IN NUMBER,
in_feature_ids IN selected_pkg.NUM_ARRAY,
in_lx IN NUMBER,
in_ly IN NUMBER,
in_ux IN NUMBER,
in_uy IN NUMBER
)
RETURN CLOB;
END selected_pkg;
In my PL/SQL function I am firing a query like following
select a.id, a.geom from Table_FIELD a where a.id in (select * from table (in_feature_ids)) and sdo_filter(A.GEOM,mdsys.sdo_geometry(2003,4326,NULL,mdsys.sdo_elem_info_array(1,1003,3), mdsys.sdo_ordinate_array(0,57,2.8,59)),'querytype= window') ='TRUE'
The same query runs fine if I run it from anonymous block like
CREATE TYPE num_arr1 IS TABLE OF NUMBER;
declare
myarray num_arr1 := num_arr1(23466,13396,14596);
BEGIN
FOR i IN (select a.id, a.geom from Table_FIELD a where a.id in (select * from table (myarray)) and sdo_filter(A.GEOM,mdsys.sdo_geometry(2003,4326,NULL,mdsys.sdo_elem_info_array(1,1003,3), mdsys.sdo_ordinate_array(0,57,2.8,59)),'querytype= window') ='TRUE'
loop
dbms_output.put_line(i.id);
end loop;
end;
If I try to run it by calling function as below
--Running function from passing array for IDs
declare
result CLOB;
myarray selected_pkg.num_array := selected_pkg.num_array(23466,13396,14596);
begin
result:=SELECTED_PKG.get_selected_kml(3, 19, myarray, 0.0,57.0,2.8,59);
end;
I am getting error
ORA-00904: "IN_FEATURE_IDS": invalid identifier
Could someone please help me understand the cause of it?
Thanks,
Alan
You cannot query a type declared in plsql in a sql query, as the sql engine doesn't recognise it.
Your first example works because you have declared the type numarr1 in the database, whereas the type selected_pkg.num_array is declared in a package.
Good summary here
I can't quite recreate the error you're getting; the anonymous block doesn't refer to in_feature_ids, and the package ought to only report that if it doesn't recognise it on compilation rather than at runtime - unless you're using dynamic SQL. Without being able to see the function body I'm not sure how that's happening.
But you can't use a PL/SQL-defined type in an SQL statement. At some point the table(in_feature_ids) will error; I'm getting an ORA-21700 when I tried it, which is a new one for me, I'd expect ORA-22905. Whatever the error, you have to use a type defined at schema level, not within the package, so this will work (skipping the spatial stuff for brevity):
CREATE TYPE num_array IS TABLE OF NUMBER;
/
CREATE OR REPLACE PACKAGE selected_pkg IS
FUNCTION get_selected_kml(
in_layer IN NUMBER,
in_id IN NUMBER,
in_feature_ids IN NUM_ARRAY,
in_lx IN NUMBER,
in_ly IN NUMBER,
in_ux IN NUMBER,
in_uy IN NUMBER
) RETURN CLOB;
END selected_pkg;
/
CREATE OR REPLACE PACKAGE BODY selected_pkg IS
FUNCTION get_selected_kml(
in_layer IN NUMBER,
in_id IN NUMBER,
in_feature_ids IN NUM_ARRAY,
in_lx IN NUMBER,
in_ly IN NUMBER,
in_ux IN NUMBER,
in_uy IN NUMBER
) RETURN CLOB IS
BEGIN
FOR i IN (select * from table(in_feature_ids)) LOOP
DBMS_OUTPUT.PUT_LINE(i.column_value);
END LOOP;
RETURN null;
END get_selected_kml;
END selected_pkg;
/
... and calling that also using the schema-level type:
set serveroutput on
declare
result CLOB;
myarray num_array := num_array(23466,13396,14596);
begin
result:=SELECTED_PKG.get_selected_kml(3, 19, myarray, 0.0,57.0,2.8,59);
end;
/
23466
13396
14596
PL/SQL procedure successfully completed.
Also note that you have to use exactly the same type, not just one that looks the same, as discussed in a recent question. You wouldn't be able to call your function with a variable of num_arr1 type, for example; they look the same on the surface but to Oracle they are different and incompatible.

Problems with writing to a MS Access Database (Delphi)

I'm trying to write bits of code to a Microsoft access database from Delphi. I'm getting data from a TStringGrid. The first column has the ItemID, and the 2nd column has the Quantity. I'd like it to loop through the TStringGrid and save each row as a reperate row in my database and also save the Order ID with it on every column (The order ID stays the same for each order so that doesn't need to change) .
I'm getting an error when running which says
"Project Heatmat.exe raised an exception class EVarientInvalidArgError with message 'Invalid Argument'. Process Stopped."
I can't figure out why it's giving me this error, and as you can probably see i'm not very good at coding yet. Any help would be appreciated!
Thank you.
procedure TCreateNewOrder.btnSaveClick(Sender: TObject);
var
intNumber, count : integer;
begin
Count:= 0;
if messagedlg ('Are you sure?', mtWarning, [mbyes, mbno], 0) = mryes then
begin
with HeatmatConnection.HeatmatDatabase do
begin
intNumber:= TBLOrder.RecordCount;
TBLOrder.Append;
TBLOrder['CustomerID']:= CompanyName.ItemIndex+1;
TBLOrder['OrderID']:= intNumber +1;
for count:= 1 to StringGrid1.RowCount-1 do
begin
TBLOrderedItem.Append;
TBLOrderedItem['OrderID']:= intNumber+1;
TBLOrderedItem['ItemID']:= StringGrid1.Cells[1, count];
TBLOrderedItem['Quantity']:= StringGrid1.Cells[2, count];
TBLOrderedItem.Post;
end;
end;
end;
end;
TStringGrid cells are strings. trying to assign a string directly to a numeric field will raise an Exception.
So a good practice is to assign values to database fields via AsString, AsInteger, AsBoolean etc... this will make the correct conversion.
In your code use:
TBLOrderedItem.FieldByName('ItemID').AsString := StringGrid1.Cells[1, count];
The same is true for Quantity.
To assign an Integer value use:
TBLOrderedItem.FieldByName('OrderID').AsInteger := intNumber + 1;
BTW, you are forgetting TBLOrder.Post i.e:
....
TBLOrder.Append;
TBLOrder.FieldByName('CustomerID').AsInteger := CompanyName.ItemIndex + 1;
TBLOrder.FieldByName('OrderID').AsInteger := intNumber + 1;
TBLOrder.Post;
...
Finally, I would also suggest to rename TBLOrder to tblOrder so that it's name wont imply that it is a Type.

Update after a table field change

I have a problem with a old Delphi system, this system insert data into a SQL Server table.
After 10 years change a field of the table from 100 to 255 chars long.
The system select all the registris of the table, and put them on an other table after a transformation. That works fine.
The problems are when the system update a field.
That show me the error
EDBEngineError with message 'Couldn't perform the edit because another user changed the record.
sConsulta:='SELECT * FROM cuentas WHERE (WALL= 2) AND (SEND_DATE = '01/01/1970')';
m_oQryLeg.Close;
m_oQryLeg.SQL.Clear;
m_oQryLeg.SQL.Add(sConsulta);
m_oQryLeg.Open;
m_oTblNov.Close;
m_oTblNov.TableName:='des_table';
m_oTblNov.Open;
with m_oTblNov do
begin
while (not m_oQryLeg.EOF) do
begin
Insert;
FieldbyName('COD_HOME').AsString:= m_oQryLeg.FieldByName('USR_HOME').AsString;
(...)
Post;
m_oQryLeg.Edit;
m_oQryLeg.FieldByName('SEND_DATE').AsDateTime:= Date; //<-- HERE THE ERROR
m_oQryLeg.Post;
m_oQryLeg.First;
m_oQryLeg.MoveBy(i);
inc(i);
end;
end;
m_oTblNov.Close;
m_oQryLeg.Close;
UpdateMode: upWhereAll
cuentas table:
NUM_SOL nvarchar 6 *PK
WALL tinyint 1
SEND_DATE smalldatetime 4
OBS_CRED nvarchar 255
FLCC real 4
STREET nvarchar 30
You're trying to update a query you're using on a table you're editing at the same time, and it's not going to work.
Since you know you're going to insert every row from the query into m_oTblNov, why not just do it like this instead?
sConsultaSELECT :='SELECT * FROM cuentas';
sConsultaUPDATE := 'UPDATE cuentas SET Send_Date = :New_Date';
// Separate WHERE so you can use it twice. Note the leading space
// between the first ' and WHERE.
sWhere := ' WHERE (WALL = 2) and (SEND_DATE = ''01/01/1970'')';
m_oQryLeg.Close;
m_oQryLeg.SQL.Text := sConsulta + sWhere;
m_oQryLeg.Open;
m_oTblNov.Close;
m_oTblNov.TableName:='des_table';
m_oTblNov.Open;
with m_oTblNov do
begin
while (not m_oQryLeg.EOF) do
begin
Insert;
FieldbyName('COD_HOME').AsString:= m_oQryLeg.FieldByName('USR_HOME').AsString;
(...)
Post;
// Don't run these any more. See below.
// m_oQryLeg.Edit;
// m_oQryLeg.FieldByName('SEND_DATE').AsDateTime:= Date; //<-- HERE THE ERROR
// m_oQryLeg.Post;
m_oQryLeg.First;
m_oQryLeg.MoveBy(i);
inc(i);
end;
end;
m_oTblNov.Close;
m_oQryLeg.Close;
m_oQryLeg.SQL.Text := sConsultaUPDATE + sWHERE;
m_oQryLeg.ParamByName('New_Date').AsDateTime := Date;
try
m_oQryLeg.ExecSQL;
finally
m_oQryLeg.Close;
end;
The problem here is that your object is trying to refresh the row from the database to make sure nothing has changed and more than likely the object truncated or rounded some value that is causing the refresh to return no rows.
This could be caused by either a float value being truncated or some other value being off.
if you don't/can't change that column to fix this issue I would advise changing to either upWhereChanged or upWhereKeyOnly.
Given that dates are treated as doubles in most windows databases, I would think that upWhereKeyOnly would be best.
EDIT:
After looking at your table, it may have to do with the fact you are using a single based smalldatetime. Delphi treats all DateTime data as a double and the conversion back and forth may be causing small rounding issues.

Resources