5

We have a legacy application that is written in Delphi 2007 and is still using the BDE (yes it needs to be switched to ADO but with over 500K lines, that's a BIG job). It connects to an SQL Server 2008 DB using an SQL SERVER ODBC connection. I am playing around with switching to the SQL Server Native Client 10.0 instead and have come across an interesting issue. When trying to insert a record in to a table that contains datetime fields, we are getting the following error:

Project DateTimeParamTest.exe raised exception class EDBEngineError with message 'General SQL error.
[Microsoft][SQL Server Native Client 10.0]Datetime field overflow. Fractional second precision exceeds the scale specified in
the parameter binding.'.

In doing some research, I have seen comments to play with the NumericScale, Precision, and Size parameters of the TParameter object. A TADOQuery will automatically set the parameters to 3, 23, and 16 respectively and has no problem with the insert. If I set the parameters to the same on the TQuery object, I get the same error as above.

Does anyone have any experience with this and know of an easy work-around? I created the following sample code for anyone wanting to try. You will just need to change the connection and SQL code.

DateTimeParamTest_Main.dfm:

object Form10: TForm10
  Left = 0
  Top = 0
  Caption = 'Form10'
  ClientHeight = 111
  ClientWidth = 181
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
  object Button2: TButton
    Left = 20
    Top = 16
    Width = 75
    Height = 25
    Caption = 'BDE'
    TabOrder = 0
    OnClick = Button2Click
  end
  object dbPMbde: TDatabase
    AliasName = 'PMTest'
    DatabaseName = 'DB'
    LoginPrompt = False
    SessionName = 'Default'
    Left = 20
    Top = 52
  end
  object qryBDE: TQuery
    DatabaseName = 'DB'
    SQL.Strings = (
      'INSERT INTO TRAN_DETAIL (ID, STARTDATE, ENDDATE)'
      'VALUES (:ID, :STARTDATE, :ENDDATE);')
    Left = 88
    Top = 52
    ParamData = <
      item
        DataType = ftInteger
        Name = 'ID'
        ParamType = ptInput
      end
      item
        DataType = ftDateTime
        Precision = 23
        NumericScale = 3
        Name = 'STARTDATE'
        ParamType = ptInput
        Size = 16
      end
      item
        DataType = ftDateTime
        Precision = 23
        NumericScale = 3
        Name = 'ENDDATE'
        ParamType = ptInput
        Size = 16
      end>
  end
end

DateTimeParamTest_Main.pas:

unit DateTimeParamTest_Main;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, DB, DBTables;

type
  TForm10 = class(TForm)
    Button2: TButton;
    dbPMbde: TDatabase;
    qryBDE: TQuery;
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form10: TForm10;

implementation

{$R *.dfm}

procedure TForm10.Button2Click(Sender: TObject);
begin
  dbPMbde.Open;
  with qryBDE do
  begin
    parambyname('ID').Value := 99999999;
    parambyname('StartDate').Value := now;
    parambyname('EndDate').Value := now;
    execsql;
  end;
  dbPMbde.Close;
end;

end.    
TLama
  • 75,147
  • 17
  • 214
  • 392
Caynadian
  • 747
  • 2
  • 12
  • 37
  • With ADO, you should better use the OleDB native provider, not the ODBC one: you are adding an unnecessary layer in-between your app and MS SQL. And ODBC need an explicit configuration in its control panel, whereas you can set an OleDB connection by code. See also third party components (paid or [or Open Source layer](http://blog.synopse.info/post/2011/06/27/SynOleDB%3A-OpenSource-Unit-for-direct-access-to-any-database-via-OleDB)). – Arnaud Bouchez Aug 29 '12 at 17:40
  • 3
    Start with using 'AsString' for the `ID`, and `AsDateTime` for the dates instead of `Value`. Always use native Delphi types instead of variants when possible. – Ken White Aug 29 '12 at 18:06
  • I'm not using ADO unfortunately - the whole project is BDE. I tried changing the .value parameters to .AsInteger/.AsDateTime but I still get the same error. – Caynadian Aug 29 '12 at 18:59
  • Are the field definitions in the DFM from the old (pre v10) connection? If so, delete the parameters and re-add them. The definition of `Precision`, `NumericScale`, and `Size` seem strange (they may not be, because I'm not using BDE or ODBC, but it's worth checking), and the error refers to fractional part overflow. You might also try using `.AsInteger := Trunc(Now);`, since you appear to only be using the date portion and not the time part of `TDateTime`. – Ken White Aug 29 '12 at 19:34
  • I set the Precision, NumericScale and Size parameters manually to the same values that an TADOQuery object set automatically. Normally, they would all be 0. But it doesn't work either way. I do need the time portion so I'm not sure if maybe I can try dropping the milliseconds part if that would work. I will try it and see what happens. – Caynadian Aug 29 '12 at 19:43
  • Dropping the milliseconds part of the time will work such as `parambyname('StartDate').asDateTime := EncodeDate(2012,8,29) + EncodeTime(14,50,8,0);` works. So I guess I will need to drop the milliseconds from all time fields prior to posting. That's gonna be a pain! – Caynadian Aug 29 '12 at 19:51
  • You could probably use `Math.RoundTo` to remove the milliseconds; it would be easier. (Just FYI, if you're replying to a specific person in comments, use `@personsname` in the comment (usually at the start) and they get notified you've responded. You get notified automatically, because it's your question that's being commented on. – Ken White Aug 29 '12 at 20:20
  • Prior to v10, only scale 0 and 3 exists (i.e. small datetime and datetime) in sql server. The sql server 2008 native driver v10 has to deal with datetime2 (scale 7). So the native driver has to use scale 7 and do conversion at the server side, which will fail if the field is not datetime2. If you change the database field from datetime to datetime2 will fix this. – Hendra Aug 30 '12 at 03:42
  • A good explaination found here (http://rightondevelopment.blogspot.com/2009/10/sql-server-native-client-100-datetime.html) – Hendra Aug 30 '12 at 03:53
  • Please don't use the BDE with SQL server. Use the dbGo components instead. – whosrdaddy Aug 30 '12 at 06:03
  • @Hendra I had considered changing the field type but I wasn't sure of the impact as we still have other applications using the same DB that won't get upgraded to use Native Client 10.0. Do you know of any issues in switching? – Caynadian Aug 31 '12 at 11:16
  • @whosrdaddy I wouldn't use the BDE for a new application of course. But this app is more than 15 years old (it was started in Delphi 3) and over 250,000 lines long. One of my goals is to get it switched to ADO. – Caynadian Aug 31 '12 at 11:18
  • @Caynadian: I can feel your pain ;) – whosrdaddy Aug 31 '12 at 11:20
  • @Caynadian, issue I can think off is using datetime2 might give different values than datetime, because of possible rounding-off operation done by sql server to fit into datetime. – Hendra Sep 01 '12 at 12:02

1 Answers1

4

This appears to be an issue with the milliseconds causing a data type overflow (based on the comments above). If so, there is a quick fix that comes to mind.

Replace the assignment to the date columns like this:

qryBDE.ParamByName('STARTDATE').AsDateTime := FixBDEDateTime(Now);

where FixBDEDateTime is simply

function FixBDEDateTime(const Value: TDateTime): TDateTime;
var
  Year, Mon, Day, Hr, Min, Sec, MS: Word;
begin
  DecodeDate(Value, Year, Mon, Day);
  DecodeTime(Value, Hr, Min, Sec, MS);
  Result := EncodeDate(Year, Mon, Day) + 
            EncodeTime(Hr, Min, Sec, 0);
end;

Edit: As @TLama points out in the comments (and I didn't mention because I missed the Delphi 2007 in the opening sentence and didn't see a version in the tags), you can also just use:

uses
  DateUtils;
...

qryBDE.ParamByName('STARTDATE').AsDateTime := RecodeMillisecond(Now, 0);
Ken White
  • 123,280
  • 14
  • 225
  • 444
  • 4
    +1, anyway, the [`RecodeMilliSecond`](http://docwiki.embarcadero.com/Libraries/en/System.DateUtils.RecodeMilliSecond) will simplify the millisecond stripping. – TLama Aug 29 '12 at 22:00
  • I should add that as an alternative; I didn't in the first place, because there's no I missed the Delphi version info provided and, since it's still BDE, I didn't want to make the assumption they had that function available. Do you mind if I add that to my answer? – Ken White Aug 29 '12 at 22:09
  • As you wish :-) Anyway, there's Delphi 2007 mentioned in the Q, but now I checked that it exists already in Delphi 7. – TLama Aug 29 '12 at 22:16
  • 1
    @TLama, thanks. :-) As I mentioned in my edited comment, I missed the version in the first sentence. I updated to make it more easily seen, but gave you the credit for the addition. If you want to add it as a different answer, you can roll my edit back to remove it - I won't be offended. :-) – Ken White Aug 29 '12 at 22:18
  • @KenWhite, thanks! I knew there had to be a simpler way to drop the milliseconds then decoding and re-encoding! – Caynadian Aug 31 '12 at 11:13
  • 1
    @TLama thanks as well for finding the RecodeMillisecond function! – Caynadian Aug 31 '12 at 11:20