15

I'm using Delphi 7 and Firebird 1.5.

I have a query that I create at runtime where some of the values might be null. I can't work out how to get Firebird to accept explicit nulls for values that I need to leave as null. At this stage I'm building the SQL so that I don't include parameters that are null but this is tedious and error-prone.

var
  Qry: TSQLQuery;
begin
  SetConnection(Query); // sets the TSQLConnection property to a live database connection
  Query.SQL.Text := 'INSERT INTO SomeTable (ThisColumn) VALUES (:ThisValue)';
  Query.ParamByName('ThisValue').IsNull := true; // read only, true by default
  Query.ParamByName('ThisValue').Clear; // does not fix the problem
  Query.ParamByName('ThisValue').IsNull = true; // still true
  Query.ParamByName('ThisValue').Bound := true; // does not fix the problem
  Query.ExecSQL;

Currently an EDatabaseError "No value for parameter 'ThisValue'"' is raised in DB.pas so I suspect this is by design rather than a firebird problem.

Can I set parameters to NULL? If so, how?

(edit: sorry for not being explicit about trying .Clear before. I left it out in favour of mentioning IsNull. Have added declaration and more code)

Sorry, one more thing: there is no "NOT NULL" constraint on the table. I don't think it's getting that far, but thought I should say.

Complete console app that displays the problem at my end:

program InsertNull;

{$APPTYPE CONSOLE}

uses
  DB,
  SQLExpr,
  Variants,
  SysUtils;

var
  SQLConnection1: TSQLConnection;
  Query: TSQLQuery;
begin
  SQLConnection1 := TSQLConnection.Create(nil);

  with SQLConnection1 do
  begin
    Name := 'SQLConnection1';
    DriverName := 'Interbase';
    GetDriverFunc := 'getSQLDriverINTERBASE';
    LibraryName := 'dbexpint.dll';
    LoginPrompt := False;
    Params.clear;
    Params.Add('Database=D:\Database\ZMDDEV12\clinplus');
    Params.Add('RoleName=RoleName');

    //REDACTED Params.Add('User_Name=');
    //REDACTED Params.Add('Password=');

    Params.Add('ServerCharSet=');
    Params.Add('SQLDialect=1');
    Params.Add('BlobSize=-1');
    Params.Add('CommitRetain=False');
    Params.Add('WaitOnLocks=True');
    Params.Add('ErrorResourceFile=');
    Params.Add('LocaleCode=0000');
    Params.Add('Interbase TransIsolation=ReadCommited');
    Params.Add('Trim Char=False');
    VendorLib := 'gds32.dll';
    Connected := True;
  end;
  SQLConnection1.Connected;
  Query := TSQLQuery.Create(nil);
  Query.SQLConnection := SQLConnection1;
  Query.Sql.Text := 'INSERT INTO crs_edocument (EDOC_ID, LINKAGE_TYPE) VALUES (999327, :ThisValue)';
  //Query.ParamByName('ThisValue').IsNull := true; // read only, true by default
//  Query.ParamByName('ThisValue').Value := NULL;
  Query.ParamByName('ThisValue').clear; // does not fix the problem
  Query.ParamByName('ThisValue').Bound := True; // does not fix the problem
//  Query.ParamByName('ThisValue').IsNull; // still true
  Query.ExecSQL;
end.
  • @moz, which components are you using to execute the sql statement? – RRUZ Jun 15 '11 at 02:36
  • The line in SQlExpr.pas that throws this is a response to checking for "if iFldType = fldUNKNOWN then", but setting the .DataType := ftInteger gives a Firebird ISC ERROR CODE:335544347 - validation error. –  Jun 15 '11 at 03:38
  • @moz, apparently you ran into a bug in dbexpress. Googling actually finds a similar QC for SQL server, and several other lookalike bugs related to various data types. Some are supposed to be fixed by an update to Delphi 2010 (I'm on D2010 here and getting the same error, will be able to test on XE when I get at the office). `NULL` always worked for me but I use the `Interbase Express` components (even those they're not supported for Firebird). – Cosmin Prund Jun 15 '11 at 04:01
  • @Cosmin Prund: that's heaps for finding that out, it's very reassuring. Now, if only we could change the connection components we use :( –  Jun 15 '11 at 04:02
  • @moz, if you do switch, make sure you switch to something that's both maintained and supported for your database (ie: not Interbase Express). – Cosmin Prund Jun 15 '11 at 04:17
  • Does it matter if you first set Bound to True, and then Clear the param? – The_Fox Jun 15 '11 at 09:44
  • 1
    @moz You shouldn't accept the answer just to accept it. Did it solve your problem? If not, do not accept: post updates with the results, get more answers. That way, community gains knowledge you will eventually get your problem solved. Even if YOU are to post the final answer. – Adriano Carneiro Jun 15 '11 at 14:32
  • @Adrian: except that I don't have time to keep chasing the underlying problem, so I'm going with rewriting the query. So accepting the "this should work and it's probably a known bug that stops it" seems reasonable. –  Jun 15 '11 at 21:08
  • @moz - Have you seen the answer I posted? You cannot make a parameterized query work having undefined data typed parameters. – Sertac Akyuz Jun 15 '11 at 21:14
  • It's not the connection components or a bug in dbExpress, it's the wrong driver info being used. – Sam Jun 30 '11 at 01:02

5 Answers5

11

The reason of the error is 'dbx' does not know the data type of the parameter. Since it is never assigned a value, it's data type is ftUnknown in execute time, hence the error. Same for 'ParamType', but 'ptInput' is assumed by default, so no problem with that.

  Query.ParamByName('ThisValue').DataType := ftString;


You definitely don't need to Clear the parameter because it is already NULL. How do we know it? IsNull is returning true...

From TParam.Clear Method:

Use Clear to assign a NULL value to a parameter.

From TParam.IsNull Property:

Indicates whether the value assigned to the parameter is NULL (blank).


You definitely don't need to Bound the parameter as it is completely irrelevant. When 'Bound' is false, the dataset will attempt to provide a default value from its datasource for the parameter. But your dataset is not even linked to a data source. From the documentation:

[...] Datasets that represent queries and stored procedures use the value of Bound to determine whether to assign a default value for the parameter. If Bound is false, datasets that represent queries attempt to assign a value from the dataset indicated by their DataSource property. [...]

If the documentation is not enough, refer to the code in TCustomSQLDataSet.SetParamsFromCursor in 'sqlexpr.pas'. It is the only place where the 'Bound' of a parameter is referred in dbx framework.

Sertac Akyuz
  • 52,752
  • 4
  • 91
  • 157
  • This actually led me to a workaround - I got a different error when I set the DataType which led me to a page that suggested a different database driver. But that's not something I can use in production so I've gone with modifying the SQL for null parameters. But thank you for the suggestion, it helped. –  Jun 15 '11 at 21:22
  • @moz - I, kind of, understand that you don't want to accept the correct answer, as probably because your dbx driver is not suitable for the task it does not help you. But, *please*, do not accept an incorrect answer, no matter how many votes it has got.. – Sertac Akyuz Jun 16 '11 at 15:33
  • This works for string and DateTime types, I'm using it now. But for FireBird enumerated types, no good. So I'm accepting it. –  Jun 23 '11 at 07:02
  • 1
    For Firebird enumerated types (ie. Domains) you need to modify the constraint check to allow null CREATE DOMAIN db_enum AS varchar(20) CHECK (value IS NULL or VALUE IN ('Firebird','MySQL','MSSQL')); ALTER DOMAIN mydomain DROP CONSTRAINT; ALTER DOMAIN mydomain ADD CONSTRAINT CHECK () – Sam Jun 30 '11 at 00:59
10

Use TParam.Clear

Query.ParamByName('ThisValue').Clear;

"Use Clear to assign a NULL value to a parameter." (from the Docs)

RRUZ
  • 130,998
  • 15
  • 341
  • 467
awmross
  • 3,639
  • 3
  • 34
  • 47
  • Additionally you must set the `Bound` property to true `Query.ParamByName('ThisValue').Bound:=True;` – RRUZ Jun 15 '11 at 02:29
  • This does not fix the problem. The parameter is null before calling clear and the exception is thrown. After calling it the parameter is null and the exception is thrown. –  Jun 15 '11 at 02:31
  • @moz the comment which I post is for the user @awmross, try doing this `Query.ParamByName('ThisValue').Clear; ` `Query.ParamByName('ThisValue').Bound:=True` – RRUZ Jun 15 '11 at 02:33
  • 2
    @RRUZ: I have already tried that. IsNull is true before I call Clear and even more true afterwards. The error still occurs. –  Jun 15 '11 at 02:36
  • @moz, which components are you using to execute the sql statement? – RRUZ Jun 15 '11 at 02:37
  • @RRUZ: Edited the question. It's a simple TSQLConnection/TSQLQuery against Firebird. –  Jun 15 '11 at 02:40
  • +1, because this is what the documentation says. But you also need to make `Bound=True`. – Cosmin Prund Jun 15 '11 at 02:56
  • OK, I'm accepting this because it is the right thing to do, even if in my case it doesn't work (and I still don't know why)... except Cosmin has pointed out a possible answer in a comment above –  Jun 15 '11 at 04:01
  • 1
    I use `TParam.DataType := ...`, `TParam.Clear`, and `TParam.Bound := True` (in that order), and it has always worked fine for me. – Remy Lebeau Jun 16 '11 at 07:30
  • 1
    -1 because there's no point in assigning NULL to a parameter which is already assigned NULL. The parameter's 'FNull' is set to true at creation time while the SQL is parsed. Since it is not assigned any value after that, it is still NULL. – Sertac Akyuz Jun 16 '11 at 22:22
  • Sertac is correct, you don't need to Clear (b/c already NULL) or set bound to True (b/c it's not bound to anything in this case), but you need to set the DataType. – Sam Jun 30 '11 at 00:30
  • @RemyLebeau I tried that with dbx + informix, if the datetime value is 0, I set DataType to ftDateTime, Clear, set Bound := True, and it worked. – nurettin Feb 07 '17 at 10:48
1

Have some property on TConnection Options named HandlingStringType/Convert empty strings to null. Keep it true and assume Query.ParamByName('ThisValue').AsString:=''; You can access it in

TConnection.FetchOptions.FormatOptions.StrsEmpty2Null:=True
1

Sertac's answer is most correct, but I also found the choice of driver makes a difference.

For the benefit of others, here's an improved test program that demonstrates how you could insert nulls with a parameterised query with Firebird 1.5.

program InsertNull;

{$APPTYPE CONSOLE}

uses
  DB,
  SQLExpr,
  Variants,
  SysUtils;

var
  SQLConnection1: TSQLConnection;
  Query: TSQLQuery;
  A, B, C: variant;
begin
  SQLConnection1 := TSQLConnection.Create(nil);
  Query := TSQLQuery.Create(nil);

  try
    try
      with SQLConnection1 do
      begin
        Name := 'SQLConnection1';
        DriverName := 'InterXpress for Firebird';
        LibraryName := 'dbxup_fb.dll';
        VendorLib := 'fbclient.dll';
        GetDriverFunc := 'getSQLDriverFB';
        //DriverName := 'Interbase';
        //GetDriverFunc := 'getSQLDriverINTERBASE';
        //LibraryName := 'dbexpint.dll';
        LoginPrompt := False;
        Params.clear;
        Params.Add('Database=127.0.0.1:D:\Database\testdb');
        Params.Add('RoleName=RoleName');
        Params.Add('User_Name=SYSDBA');
        Params.Add('Password=XXXXXXXXXXXX');
        Params.Add('ServerCharSet=');
        Params.Add('SQLDialect=1');
        Params.Add('BlobSize=-1');
        Params.Add('CommitRetain=False');
        Params.Add('WaitOnLocks=True');
        Params.Add('ErrorResourceFile=');
        Params.Add('LocaleCode=0000');
        Params.Add('Interbase TransIsolation=ReadCommited');
        Params.Add('Trim Char=False');
        //VendorLib := 'gds32.dll';
        Connected := True;
      end;

      Query.SQLConnection := SQLConnection1;
      Query.SQL.Clear;
      Query.Params.Clear;
      // FYI
      // A is Firebird Varchar
      // B is Firebird Integer
      // C is Firebird Date
      Query.Sql.Add('INSERT INTO tableX (A, B, C) VALUES (:A, :B, :C)');
      Query.ParamByName('A').DataType := ftString;
      Query.ParamByName('B').DataType := ftInteger;
      Query.ParamByName('C').DataType := ftDateTime;

      A := Null;
      B := Null;
      C := Null;

      Query.ParamByName('A').AsString := A;
      Query.ParamByName('B').AsInteger := B;
      Query.ParamByName('C').AsDateTime := C;

      Query.ExecSQL;
      writeln('done');
      readln;
    except
      on E: Exception do
      begin
        writeln(E.Message);
        readln;
      end;
    end;
  finally
    Query.Free;
    SQLConnection1.Free;
  end;
end.
Sam
  • 2,573
  • 10
  • 39
  • 58
0

Are you sure the params have been created by just setting the text of the SQL?

try

if Query.Params.count <> 0 then
// set params
.
.

Anyway why not make the SQL text:

'INSERT INTO crs_edocument (EDOC_ID, LINKAGE_TYPE) VALUES (999327, NULL)';

if you know the value is going to be null...

Despatcher
  • 1,725
  • 12
  • 18
  • because if I'm going to vary the SQL for every parameter that's a pretty ugly bit of code. This problem actually occurs on an insert with ~20 parameters (admittedly only ~10 of them can bu null). Likewise, if paramcount=0 the query would never have worked, rather than only failing if a parameter was null. –  Jun 15 '11 at 21:18