Search code examples
delphifirebird2.5firedac

Delphi/FireDAC StrsTrim2Len has no impact on ParamByName


Using Delphi XE7 and Tokyo with Firebird 2.5 I've come to the conclusion that StrsTrim2Len has no impact whatsoever when doing updates/inserts with TFDQuery and ParamByName, which makes oversized strings raise an exception.

Are there any other approaches than to truncate all strings in code like:

ParamByName('Field1').AsString := SomeVar.SubString(0, 50);

With the added need to keep track of the field length also?


The source and form is:

unit Unit3;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  System.StrUtils,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Error, FireDAC.UI.Intf,
  FireDAC.Phys.Intf, FireDAC.Stan.Def, FireDAC.Stan.Pool, FireDAC.Stan.Async, FireDAC.Phys, FireDAC.Phys.FB,
  FireDAC.Phys.FBDef, FireDAC.VCLUI.Wait, FireDAC.Stan.Param, FireDAC.DatS, FireDAC.DApt.Intf, FireDAC.DApt,
  Vcl.StdCtrls, Data.DB, FireDAC.Comp.DataSet, FireDAC.Comp.Client, FireDAC.Comp.UI, FireDAC.Phys.IBBase;

type
  TForm3 = class(TForm)
    FDConnection1: TFDConnection;
    FDPhysFBDriverLink1: TFDPhysFBDriverLink;
    FDGUIxWaitCursor1: TFDGUIxWaitCursor;
    FDQuery1: TFDQuery;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form3: TForm3;

implementation

{$R *.dfm}

procedure TForm3.Button1Click(Sender: TObject);
begin
  FDConnection1.Open;

  FDQuery1.FormatOptions.StrsTrim2Len := True;

  FDQuery1.SQL.Text := 'INSERT INTO MyTable (ID, MyField) VALUES (:ID, :MyField)';
  FDQuery1.ParamByName('ID').AsInteger := 1;
  FDQuery1.ParamByName('MyField').AsString := DupeString('0', 21); { ← field is 20 chars }
  FDQuery1.ExecSQL;

  FDQuery1.SQL.Text := 'SELECT MyField FROM MyTable WHERE ID = 1';
  FDQuery1.Open;

  Assert(Length(FDQuery1.FieldByName('MyField').AsString) = 20); { ← trimmed to 20 chars? }

  FDConnection1.Close;
end;

end.

The corresponding .dfm file:

object Form3: TForm3
  Left = 0
  Top = 0
  Caption = 'Form3'
  ClientHeight = 294
  ClientWidth = 161
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
  object Button1: TButton
    Left = 40
    Top = 16
    Width = 75
    Height = 25
    Caption = 'Button1'
    TabOrder = 0
    OnClick = Button1Click
  end
  object FDConnection1: TFDConnection
    Params.Strings = (
      'Database=MyUTF8Db'
      'User_Name=Sysdba'
      'Password='
      'Server=127.0.0.1'
      'CharacterSet=UTF8'
      'DriverID=FB')
    FormatOptions.AssignedValues = [fvStrsTrim2Len]
    FormatOptions.StrsTrim2Len = True
    Left = 48
    Top = 48
  end
  object FDPhysFBDriverLink1: TFDPhysFBDriverLink
    Left = 48
    Top = 104
  end
  object FDGUIxWaitCursor1: TFDGUIxWaitCursor
    Provider = 'Forms'
    ScreenCursor = gcrHourGlass
    Left = 48
    Top = 160
  end
  object FDQuery1: TFDQuery
    Connection = FDConnection1
    Left = 48
    Top = 216
  end
end

Solution

  • Do I need to manually trim parameter values with StrsTrim2Len enabled?

    No, when you enable StrsTrim2Len option, even assigned parameter values should be trimmed to the limit of the binded field. So, direct string assignment should trim the value in length for a string field parameter, IMHO:

    ParamByName('MyFieldUTF8').AsWideString := 'Value to be encoded by FireDAC';
    

    Why IMHO? Because in this particular case (Delphi Tokyo, UTF-8 field in Firebird), implementation of this option works like DataTrim2Size rather than how it reads, and I am not sure if there is some reason for it left behind. String fields in Firebird are constrained by string length limit not by storage data size limit when defined (as far as I know).

    What's wrong with automatic string parameter value trimming then?

    FireDAC, as implemented now for Firebird driver, trims parameter data buffer for a UTF-8 field only when data size of the encoded parameter value exceeds storage data size of the parameter field (when the StrsTrim2Len option is enabled). So it's based on data size rather than string length.

    Parameter data size (if not specified) seems to be obtained from RDB$FIELD_LENGTH meta field of the RDB$FIELDS system table (cannot confirm that, but it seems the sqllen member is filled by that field value, from a quick Firebird code browse). But it's not that important for now.

    The problem is this code and analogy of what the StrsTrim2Len option should actually do here (it's the TIBVariable.SetData method implementation, UTF-8 branch; comments added by me):

    { this encodes the value to UTF-8 and returns number of bytes written to the buffer }
    iByteLen := FVars.Statement.Database.Encoder.Encode(ApData, ALen, pUTF8, ecUTF16);
    { DataSize here equals to the RDB$FIELD_LENGTH, so let's try a calculation for field
      let's say VARCHAR(20), and to the parameter binded to it assign e.g. 21 chars long
      string consisting from ANSI chars:
    
      iByteLen → 21 ANSI chars occupies 21 bytes
      DataSize → XSQLVAR.sqllen → RDB$FIELD_LENGTH → 80 (20 chars * max UTF-8 char size?)
    
      Now, is 21 > 80? No? No trimming then. }
    if iByteLen > DataSize then
      if FVars.Statement.StrsTrim2Len then
        iByteLen := DataSize
      else
        ErrorDataTooLarge(DataSize, iByteLen);
    

    As you may guess, trimming of the parameter value buffer would actually happen, but the encoded string was too small in size, so the trimming was skipped. And you can also predict that if you used such parameter e.g. for inserting a value, you would end up with the engine exception because the engine works with inserted string value length rather than data size when validating constraints.

    I cannot find practical meaning of working with data size here. Well, you will never overflow storage data size, but on the other hand you can easily exceed length of the field. And the discussed option is about length, not about size.

    I'll open a bug report and try to wait for some comment around this topic as I don't believe this code was an accident. Will report back..