How to make ADO parameter to update SQL Server datetime column? - sql-server

edit I should have mentioned that I'm trying to do this with Delphi 2006.
OK, I think I have hit on a question with no previous answers.
I have a SQL Server database with columns of type datetime. When I try to insert a row with a parametrized command, I get
Exception class EOleException with message
'[Microsoft][ODBC SQL Server Driver]Optional feature not implemented'.
My insert procedure looks like this:
procedure TForm1.btnZoomClick(Sender: TObject);
const
InsCmd = 'insert into dbo.foo (added, edited, editor, narrative) ' +
'values (:dateAdded, :dateEdited, :theEditor, :theNarrative);';
begin
dmDbToy2.DataModule2.ADOCommand1.CommandText := InsCmd;
with DataModule2.ADOCommand1.Parameters do
begin
// the following line was an attempt to trick VarAsType into making a
// adDbTimeStamp: VarAsType is having none of it.
// FindParam('dateAdded').Value := VarAsType(VarFromDateTime(Now), 135);
FindParam('dateAdded').Value := VarFromDateTime(Now);
FindParam('dateEdited').Value := Null;
FindParam('theEditor').Value := 'wades';
FindParam('theNarrative').Value := Null;
end;
DataModule2.ADOCommand1.Execute;
end;
I found some postings via google which seem to indicate that SQL Server wants a adDbTimeStamp type to update these columns, but VarAsType does not want to make one for me.
Is there a way to create a value for the dateAdded and dateEdited parameters in the code sample?

In the comments thread on the original question, user RRUZ made a suggestion that turned out to resolve the issue: The problem was with the provider. Namely, I was using the OLEDB Provider for ODBC rather than the OLEDB Provider for SQL Server. Changing the provider as suggested made the 'Optional feature not implemented' error message go away and enabled the insert to work with a simple assignment of TDateTime to TParameter.Value, thusly:
FindParam('dateAdded').Value := Now;

Set the datatype for the parameter, it might do a difference in how the parameters is treated. I would also recommend that you use ParamByName instead of FindParam. With ParamByName you get a Param xx not found exception if the parameters does not exist in the Parameters collection. FindParam returns nil if it is not found. I have never needed to use any variant conversion stuff when assigning parameters for a TADOCommand so think you should remove that as well.
Try this.
with ParamByName('dateAdded') do
begin
DataType := ftDateTime;
Value := Now;
end;
with ParamByName('dateEdited') do
begin
DataType := ftDateTime;
Value := Null;
end;
with ParamByName('theEditor') do
begin
DataType := ftString; // or ftWideString if you use nchar/nvarchar
Value := 'wades';
end;
with ParamByName('theNarrative') do
begin
//DataType := ftString // Don't know datatype here
Value := Null;
end;

Just set the parameter as a datetime. I do it all the time in ADO and other conection layers
DataModule2.ADOCommand1.Parameters.ParamByName('dateAdded').Value := Now();
//other code
DataModule2.ADOCommand1.Parameters.ParamByName('dateEdited').Value := Null;
//other code
DataModule2.AdoCommand1.Execute;

Related

Storing Files in SQL Server Table (varbinary(max) column) using ADOQuery Component

I'm using SQL Server 2019 and Delphi 10.3.
I need to store any kind of files ( like pdf, txt, docx, etc) in a 'Personal_Files' table.
This table is composed by a column with the file extension ( as varchar) and a varbinary(max) column to store the file itself.
I did some research on how to store these files on a table, but without success. Below some example:
var
Input,Output: TStream;
FName: TFileName;
begin
...
//Create Streams and encode Base64:
Input := TFileStream.Create(FName,fmOpenRead);
Output := TFileStream.Create(FName+'Temp',fmCreate);
TNetEncoding.Base64.Encode(Input,Output);
... // Some validations
// In the ADOQuery component, I did this:
with ADOQuery, sql do
begin
close;
clear;
add('INSERT INTO MyDatabase.dbo.MyFilesTable (EXTENSION,FILEBIN)');
add('VALUES (:wextension, :wfilebin)');
Parameters.ParamByName('wextension').Value := TPath.GetExtension(FName);
Parameters.ParamByName('wfilebin').Value := Output.toString;
ExecSQL;
end;
In this example, I tried to parse the stream as String, after the encode, but when I look in the SQL Table, it's the same stream for all the archives I tried. The parameter doesn't accept TStream type. Thank you in advance.
After some research, and some advices, I found a way to send the file to my SQL Server table with ADOQuery. Altough, I learned that this isn't recommended, so it's just to answer my question directly:
Just a change on the final part of the code answers my question ( but again, it's not the recommended way to store files, as commented on the question.):
with ADOQuery, sql do
begin
close;
clear;
add('INSERT INTO MyDatabase.dbo.MyFilesTable (EXTENSION,FILEBIN)');
add('VALUES (:wextension, :wfilebin)');
Parameters.ParamByName('wextension').Value := TPath.GetExtension(FName);
Parameters.Items[1].LoadFromStream(Output,ftVarBytes);
ExecSQL;
end;
Just changing the way I was setting the parameter solved the problem. In this example, using the 'LoadFromStream' on the Paremeters.Items[n], where n is the parameter index, it worked very well. The ftVarBytes is the field type parameter.

Reading DateTime from Database using Delphi and FireDAC

I'm failing dismally to read a datetime from an SQLite database using Delphi 10.3 and FireDAC.
As the simplest example, I create a sample database using sqlite as follows:
.open Test.db
CREATE TABLE "TABLE1" ("Name"VarChar(16), "Time" datetime);
INSERT INTO Table1 (Name,Time) VALUES("Fred",time('now'));
Then
select * from Table1
gives Fred|16:52:57 as expected.
If I generate a Delphi program with an FDConnection1 and FDQuery1 linked to a datasource and DBgrid it will read "Fred" but not the time. The value returned by FDquery1 asstring is '' and asfloat is 0.
If I try the FireDAC explorer tool to look at the database it also fails to read the time value but I notice it does read datetimes from some of the example databases so it clearly can work.
Can anyone tell me what I'm missing.
Thanks
Trying to construct a SQL statement as a string in Delphi code can be a bit error prone. However, you should find that the following code executes correctly
procedure TForm2.btnInsertRowClick(Sender: TObject);
const
sInsertRow = ' INSERT INTO Table1 (Name,time) VALUES(''Fred'',datetime(''now''))';
begin
FDConnection1.ExecSql(sInsertRow);
end;
Btw, in general it would be better to use a parameterised INSERT statement than a literal one like the above, but it's not really practical here as you are inserting the now value returned by Sqlite rather than the Delphi now
function.
**Update: ** The code below is from a minimal project which creates your example table,
inserts a row into it and then presents it in db-aware controls (DBGrid, DBEdit) for editing.
It all works exactly as it ought to. In particular, any changes to the row data made through
those controls are retained when tha app is next run. Note that the SQL which creates the table specifies a primary key on the Name column/field: this is necessary for the FDQuery1 to generate the UPDATE statements needed to save changes back to the on-disk table.
The code in the btnSelectClick handler shows how to set the time field's DisplayFormat and EditMask properties so that it only displays the time part of the stored DateTime data.
type
TForm2 = class(TForm)
FDConnection1: TFDConnection;
FDQuery1: TFDQuery;
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
DataSource1: TDataSource;
DBEdit1: TDBEdit;
btnCreateTable: TButton;
btnInsertRow: TButton;
btnSelect: TButton;
[...]
public
[...]
const
sCreateTable = 'CREATE TABLE ''TABLE1'' (''Name'' VarChar(16) primary key, ''Time'' datetime)';
sInsertRow = ' INSERT INTO Table1 (Name,time) VALUES(''Fred'',datetime(''now''))';
sSelect = 'select * from table1';
procedure TForm2.btnCreateTableClick(Sender: TObject);
begin
FDConnection1.Connected := True;
FDConnection1.ExecSql(sCreateTable);
end;
procedure TForm2.btnInsertRowClick(Sender: TObject);
const
begin
FDConnection1.ExecSql(sInsertRow);
end;
procedure TForm2.btnSelectClick(Sender: TObject);
var
AField : TDateTimeField;
begin
FDQuery1.SQL.Text := sSelect;
FDQuery1.Open;
// The following shows how to control how the time field is formatted for display
// in gui controls using the field's DisplayFormat
// and how to set up its EditMask for editing
AField := FDQuery1.FieldByName('time') as TDateTimeField;
AField.DisplayFormat := 'hh:nn:ss';
AField.EditMask := '!90:00:00;1;_';
end;
end.
.DFM file
object Form2: TForm2
[...]
object FDConnection1: TFDConnection
Params.Strings = (
'Database=D:\Delphi\Code\FireDAC\db1.sqlite'
'DriverID=SQLite')
LoginPrompt = False
end
object FDQuery1: TFDQuery
Connection = FDConnection1
end
object DataSource1: TDataSource
DataSet = FDQuery1
end
object DBGrid1: TDBGrid
DataSource = DataSource1
end
object DBNavigator1: TDBNavigator
DataSource = DataSource1
end
object DBEdit1: TDBEdit
DataField = 'Time'
DataSource = DataSource1
end
end

How to change Clientdataset field datatype at runtime

For Delphi ClientDataSets where fields have been define at design time, is there a way at runtime to change a specific field's datatype ( change the cds.Fields[n].DataType) ?
I have a legacy Delphi 7 program with both SQLDataSet and ClientDataSet fields set at design time (in order to override various properties).
These are connected to a 3rd-party Sybase SQL Anywhere 11 database.
Recently the vendor changed all 'Description' fields from VarChar(128) to long varchar, but only for certain of his customers. So, my code has to support both types of fields when I query on these 'Description' fields.
I was hoping to set conditional compilation on the class field types (then add the fields before opening the SQL/CLient Dataset), but the compiler ignores {$IFDEF } conditionals in the component definition section of the class (which, the more I think about it, makes good sense)!
There are dozens of modules, with hundreds of fields affected, so any insight is appreciated.
I have also faced this problem before, not with CDS, but with TADODataSet using persistent fields at design time. I think that the code below will help you get the idea of how to fix/patch your CDS datasets.
The idea is to query the relevant table schema; get the actual fileds data type; and "change" the persistent field type by un-attaching it from the DataSet and adding a new matching persistent filed instead:
// TData class
procedure TData.DataModuleCreate(Sender: TObject);
var
I: Integer;
begin
for I := 0 to ComponentCount - 1 do
if (Components[I] is TCustomADODataSet) then
DataSetPrepareMemoFields(TDataSet(Components[I]));
end;
procedure TData.DataSetPrepareMemoFields(DataSet: TDataSet);
var
Fld: TField;
I: Integer;
FldName, CompName: string;
AOwner: TComponent;
begin
// Here you need to query the actual table schema from the database
// e.g. ADOConnection.GetFieldNames and act accordingly
// check which DataSet you need to change
// if (DataSet = dsOrders) or ... then...
if DataSet.FieldList.Count > 0 then
for I := DataSet.FieldList.Count - 1 downto 0 do
begin
if DataSet.FieldList.Fields[I].ClassNameIs('TMemoField') and (DataSet.FieldList.Fields[I].FieldKind = fkData) then
begin
// save TMemoField properties
AOwner := DataSet.FieldList[I].Owner;
CompName := DataSet.FieldList[I].Name;
FldName := DataSet.FieldList.Fields[I].FieldName;
// dispose of TMemoField
DataSet.FieldList[I].DataSet := nil; // Un-Attach it from the DataSet
// create TWideADOMemoField instead
Fld := TWideADOMemoField.Create(AOwner); // Create new persistent Filed instead
Fld.Name := CompName + '_W';
Fld.FieldName := FldName;
Fld.DataSet := DataSet;
end;
end;
end;
That said, After I have fixed that issue, I have never ever used persistent fields again.
All my fields are generated in run time. including calculated/lookup/internal fields.

Format a date according to ADO provider

I have a Delphi 2010 application using ADO to support a database that can be either SQL Server or MS Access. When sending SQL to the database using parameterized queries differences in date representation are handled correctly. But I occasionally have the need to form dynamic SQL and send that to the database as well.
Is there any way to either have the TADOConnection format my date into a text string appropriate for the current database, or to interrogate the connection to learn how I should format the date? Otherwise I'm stuck building a table of provider names and date formatting functions.
You should be able to use parameters with dynamic sql as well. I have done this in several versions of my own OPF framework.
Just write a SQL statement using parameters, assign that as a string to the SQL text of a TAdoQuery (or TAdoCommand). The component should then parse your text and set up the parameters collections for you. After that you should be able to assign the values to your parameters and call Open or Execute...
To give you an idea:
DS := DatasetClass.Create( self.Connection );
try
DS.QueryText := SQL.Text;
FillSelectParams( DS );
DS.Open;
try
...
finally
DS.Close;
end;
finally
DS.Free;
end;
In which the FillSelectParams calls the following FillParams procedure:
procedure TSQLDataManager.FillParams(ADS: TCustomDataset);
var
i: integer;
ParamName: string;
Attr: TCustomDomainAttribute;
Ref: TCustomDomainObject;
Value: Variant;
begin
for i := 0 to ADS.ParamCount - 1 do begin
Value := Null;
ParamName := ADS.ParamName[i];
if ParamName = 'Id' then begin
ParamName := 'Identity';
end;
Attr := CDO.AttrByName[ParamName];
if Attr <> nil then begin
Value := ADS.AdjustParamValue( Attr );
end else begin
Ref := CDO.ReferenceByName[ParamName];
if ( Ref <> nil ) and ( Ref.Identity <> C_UnassignedIdentity ) then begin
Value := Ref.Identity;
end;
end;
if Value <> Null then begin
ADS.ParamValue[i] := Value;
end;
end;
end;
In this case the param values are taken from a custom domain object (CDO), but you can substitute your own flavour here.
The AdjustParamValue function takes care of a couple of conversions. It has been implemented in the ADO version of the TCustomDataSet class descendant used, to take care of component variations with different TDataSet descendants, but nowhere does the SQL database type come into play:
function TADODCDataset.AdjustParamValue(Attr: TCustomDomainAttribute): Variant;
begin
if Attr is TIdentityAttribute then begin
if Attr.AsInteger = 0 then begin
Result := Null;
end else begin
Result := Attr.Value;
end;
end else if Attr is TBooleanAttribute then begin
if Attr.AsBoolean then begin
Result := Integer( -1 );
end else begin
Result := Integer( 0 );
end;
end else if Attr is TDateTimeAttribute then begin
if Attr.AsDateTime = 0 then begin
Result := Null;
end else begin
Result := Attr.Value;
end;
end else if Attr is TEnumAttribute then begin
Result := Attr.AsString
end else begin
Result := Attr.Value;
end;
end;
Larry, i will give you an answer for the SQL Server Part.
you can use the sys.syslanguages system view to get information about the languages installed in the sql server, one of the columns returned by this view is called dateformat which indicate the Date order, for example, DMY.
also using the ##langid (which returns the local language identifier (ID) of the language that is currently being used.) function you can write something like this to obtain the current date format used by the Sql Server.
select dateformat from sys.syslanguages where langid=##langid
so now you will have a string which you can parse in delphi to format your date.
Another option is pick one of the predefined formats of SQL Server and use the CONVERT function in your SQL sentence for convert the string to the date.
check this sample which uses the ISO format to convert
//The ISO format is yyyymmdd so i can use the FormatDateTime function to convert any TdateTime to a Iso format sdatetiem string
DateStr:=FormatDateTime('YYYYMMDD',Now);
//Now construct the sql server sentence
SqlSentence:=Format('UPDATE MyTable SET DateField=CONVERT(DATETIME,%s,112)',QuotedStr(DateStr));
Try to use the ODBC date escape sequence, which may be supported by SQL Server and Access OLEDB providers. For example:
{d '2011-01-14'}
As Marjan suggests, you shoud always use parameters. (try searching for SQL-injection)
Another reason to use parameters is that query plans can be reused on the sql server.
If the first statement generated is SELECT * from customer where created>'2010-12-21' and the next statetement generated is SELECT * from customer where created>'2010-12-22', the query optimizer/plan gets generated/compiled both times (different statement). But if the statement both times are SELECT * from customer where created>?, the plan is reused -> (slightly) lower presure on SQL server.
I you just wan't a quick and dirty solution all the SQL (implementations) i have tried (I havn't tries Access) can accept and understand the ISO date format (eg. '2010-12-21')

Delphi: "Parameter object is improperly defined. Inconsistent or incomplete information was provided."

I'm trying to insert a record into a table in a 3-tier database setup, and the middle-tier server generates the error message above as an OLE exception when it tries to add the first parameter to the query.
I've Googled this error, and I find the same result consistently: it comes from having a colon in a string somewhere in your query, which b0rks ADO's SQL parser. This is not the case here. There are no spurious colons anywhere. I've checked and rechecked the object definition against the schema for the table I'm trying to insert into. Everything checks out, and this has my coworkers stumped. Does anyone know what else could be causing this? I'm at my wits' end here.
I'm using Delphi 2007 and SQL Server 2005.
I can get this error, using Delphi 2007 and MSSQL Server 2008, and I found a workaround. (which is pretty crappy IMHO, but maybe its useful to you if yours is caused by the same thing.)
code to produce the error:
with TADOQuery.Create(nil)
do try
Connection := ADOConnection;
SQL.Text := ' (SELECT * FROM Stock WHERE InvCode = :InvCode ) '
+' (SELECT * FROM Stock WHERE InvCode = :InvCode ) ';
Prepared := true;
Parameters.ParamByName('InvCode').Value := 1;
Open; // <<<<< I get the "parameter object is...etc. error here.
finally
Free;
end;
I found two ways to fix it:
1) remove the brackets from the SQL, ie:
SQL.Text := ' SELECT * FROM Stock WHERE InvCode = :InvCode '
+' SELECT * FROM Stock WHERE InvCode = :InvCode ';
2) use two parameters instead of one:
with TADOQuery.Create(nil)
do try
Connection := ADOConnection;
SQL.Text := ' (SELECT * FROM Stock WHERE InvCode = :InvCode1 ) '
+' (SELECT * FROM Stock WHERE InvCode = :InvCode2 ) ';
Prepared := true;
Parameters.ParamByName('InvCode1').Value := 1;
Parameters.ParamByName('InvCode2').Value := 1;
Open; // <<<<< no error now.
finally
Free;
end;
I found this thread while searching the previously mentioned Exception message. In my case, the cause was an attempt to embed a SQL comment /* foo */ into my query.sql.text.
(I thought it would have been handy to see a comment go floating past in my profiler window.)
Anyhow - Delphi7 hated that one.
Here a late reply. In my case it was something completely different.
I tried to add a stored procedure to the database.
Query.SQL.Text :=
'create procedure [dbo].[test]' + #13#10 +
'#param int ' + #13#10 +
'as' + #13#10 +
'-- For the parameter you can pick two values:' + #13#10 +
'-- 1: Value one' + #13#10 +
'-- 2: Value two';
When I removed the colon (:) it worked. As it saw the colon as a parameter.
I just encountered this error myself. I'm using Delphi 7 to write to a 2003 MS Access database using a TAdoQuery component. (old code) My query worked fine directly in MS Access, but fails in Delphi through the TAdoQuery object. My error came from a colon (apologies to the original poster) from a date/time value.
As I understand it, Jet SQL date/time format is #mm/dd/yyyy hh:nn:ss# (0 left-padding is not required).
If the TAdoQuery.ParamCheck property is True then this format fails. (Thank you posters!) Two work-arounds are: a) set ParamCheck to False, or b) use a different date/time format, namely "mm/dd/yyyy hh:nn:ss" (WITH the double quotes).
I tested both of these options and they both worked.
Even though that double-quoted date/time format isn't the Jet date/time format, Access is pretty good at being flexible on these date/time formats. I also suspect it has something to do with the BDE/LocalSQL/Paradox (Delphi 7's native SQL and database engine) date/time format (uses double quotes, as above). The parser is probably designed to ignore quoted strings (double quotes are the string value delimiter in BDE LocalSQL), but may stumble somewhat on other non-native date/time formats.
SQL Server uses single quotes to delimit strings, so that might work instead of double quotes when writing to SQL Server tables (not tested). Or maybe the Delphi TAdoQuery object will still stumble. Turning off ParamCheck in that case may be the only option. If you plan to toggle the ParamCheck property value in code, you'll save some processing time by ensuring the SQL property is empty before enabling it, if you're not planning on parsing the current SQL.
I'm facing the same error described in your question. I've traced the error into ADODB.pas -> procedure TParameters.AppendParameters; ParameterCollection.Append(Items[I].ParameterObject).
By using breakpoints, the error was raised, in my case, by a parameter which should fill a DateTime field in the database and I've never filled up the parameter. Setting up the parameter().value:='' resolved the issue (I've tried also with varNull, but there is a problem - instead of sending Null in the database, query is sending 1 - the integer value of varNull).
PS: I know is a 'late late late' answer, but maybe somebody will reach at the same error.
If I remember well, you have to explicit put NULL value to the parameter. If you are using a TAdoStoredProc component, you should do this in design time.
Are you using any threading? I seem to remember getting this error when a timer event started a query while the ADO connection was being used for another synchronous query. (The timer was checking a "system available" flag every minute).
Have you set the DataType of the parameter or did you leave it as ftUnknown?
I have also had the same problem, but with a dynamic command (e.g. an Update statement).
Some of the parameters could be NULL.
The only way i could get it working, was setting the parameter.DataType := ftString and parameter.Size := 1 and not setting the value.
cmdUpdate := TADOCommand.Create(Self);
try
cmdUpdate.Connection := '**Conections String**';
cmdUpdate.CommandText := 'UPDATE xx SET yy = :Param1 WHERE zz = :Param2';
cmdUpdate.Parameters.ParamByName('Param2').Value := WhereClause;
if VarIsNull(SetValue) then
begin
cmdUpdate.Parameters.ParamByName('Param1').DataType := ftString;
cmdUpdate.Parameters.ParamByName('Param1').Size := 1;
end else cmdUpdate.Parameters.ParamByName('Param1').Value := SetValue;
cmdUpdate.Execute;
finally
cmdUpdate.Free;
end;
I just ran into this error today on a TADOQuery which has ParamCheck := False and has no colons in the SQL.
Somehow passing the OLECMDEXECOPT_DODEFAULT parameter to TWebBrowser.ExecWB() was causing this for me:
This shows the problem:
pvaIn := EmptyParam;
pvaOut := EmptyParam;
TWebBrowser1.ExecWB(OLECMDID_COPY, OLECMDEXECOPT_DODEFAULT, pvaIn, pvaOut);
This does not show the problem:
pvaIn := EmptyParam;
pvaOut := EmptyParam;
TWebBrowser1.ExecWB(OLECMDID_COPY, OLECMDEXECOPT_DONTPROMPTUSER, pvaIn, pvaOut);
A single double quote in the query can also raise this error from what I just experienced and I am not using parameters at all ...
You can get this error when attempting to use a time value in the SQL and forget to wrap it with QuotedStr().
I got the same error. Turned out, that it is because a parameter of the stored procedure was declared as varchar(max). Made it varchar(4000) and error disappeared.

Resources