5

I'm converting a BDE query (Paradox) to a Firebird (2.5, not 3.x) and I have a very convenient conversion in it:

select TRIM('      1') as order1, CAST('      1' AS INTEGER) AS order2    --> 1
select TRIM('  1 bis') as order1, CAST('  1 bis' AS INTEGER) AS order2    --> 1

Then ordering by the cast value then the trimmed value (ORDER order2, order1) provide me the result I need:

1
1 bis
2 ter
100
101 bis

However, in Firebird casting an incorrect integer will raise an exception and I did not find any way around to provide same result. I think I can tell if a number is present with something like below, but I couldn't find a way to extract it.

TRIM('    1 bis') similar to '[ [:ALPHA:]]*[[:DIGIT:]]+[ [:ALPHA:]]*' 

[EDIT]

I had to handle cases where text were before the number, so using @Arioch'The's trigger, I got this running great:

SET TERM ^ ;
CREATE TRIGGER SET_MYTABLE_INTVALUE FOR MYTABLE ACTIVE
BEFORE UPDATE OR INSERT POSITION 0
AS 
DECLARE I INTEGER;
DECLARE S VARCHAR(13);
DECLARE C VARCHAR(1);
DECLARE R VARCHAR(13);
BEGIN 
  IF (NEW.INTVALUE is not null) THEN EXIT;
  S = TRIM( NEW.VALUE );
  R = NULL;
  I = 1;
  WHILE (I <= CHAR_LENGTH(S)) DO
  BEGIN
    C = SUBSTRING( S FROM I FOR 1 );
    IF ((C >= '0') AND (C <= '9')) THEN LEAVE;
    I = I + 1;
  END
  WHILE (I <= CHAR_LENGTH(S)) DO
  BEGIN
    C = SUBSTRING( S FROM I FOR 1 );
    IF (C < '0') THEN LEAVE;
    IF (C > '9') THEN LEAVE;
    IF (C IS NULL) THEN LEAVE;
    IF (R IS NULL) THEN R=C; ELSE R = R || C;
    I = I + 1; 
  END
  NEW.INTVALUE = CAST(R AS INTEGER);
END^
SET TERM ; ^
Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Darkendorf
  • 463
  • 11
  • 28
  • There is nothing in Firebird 2.5 itself that would do this. You will need to find (or write) a UDF that does this, or do some hacky string manipulation in a stored procedure. – Mark Rotteveel Sep 05 '17 at 15:01
  • 2
    Note, that this question is for Firebird 2.5. In 3.0, one could use [SubString with Regular expressions](https://firebirdsql.org/file/documentation/release_notes/html/en/3_0/bk01ch09s05.html#rnfb30-dml-substring). – NineBerry Sep 05 '17 at 15:08
  • Thanks @NineBerry for editing the title, of course BDE had nothing to do in the objective of my request... and of course yes, i'm aware of Firebird 3 improvements... I think a new field with the integer value would be the best solution to avoid calculation each time it's needed... – Darkendorf Sep 05 '17 at 16:26
  • He can kind of write SQL Function in FB 2.5 too, though it is kind of ugly: https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-psql-storedfuncs.html – Arioch 'The Sep 05 '17 at 17:02

3 Answers3

3

Converting such a table, you have to add a special indexed integer column for keeping the extracted integer data.

Note, this query while using "very convenient conversion" is actually rather bad: you should use indexed columns to sort (order) large amounts of data, otherwise you are going into slow execution and waste a lot of memory/disk for temporary sorting tables.

So you have to add an extra integer indexed column and to use it in the query.

Next question is how to populate that column.

Better would be to do it once, when you move your entire database and application from BDE to Firebird. And from that point make your application when entering new data rows fill BOTH varchar and integer columns properly.

One time conversion can be done by your convertor application, then. Or you can use selectable Stored Procedure that would repeat the table with such and added column. Or you can make Execute Block that would iterate through the table and update its rows calculating the said integer value.

How to SELECT a PROCEDURE in Firebird 2.5

If you would need to keep legacy applications, that only insert text column but not integer column, then I think you would have to use BEFORE UPDATE OR INSERT triggers in Firebird, that would parse the text column value letter by letter and extract integer from it. And then make sure your application never changes that integer column directly.

See a trigger example at Trigger on Update Firebird

PSQL language documentation: https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-psql.html

Whether you would write procedure or trigger to populate the said added integer indexed column, you would have to make simple loop over characters, copying string from first digit until first non-digit.

https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-functions-scalarfuncs.html#fblangref25-functions-string

https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-psql-coding.html#fblangref25-psql-declare-variable

Something like that

CREATE TRIGGER my_trigger FOR my_table 
BEFORE UPDATE OR INSERT
AS 
DECLARE I integer;
DECLARE S VARCHAR(100);
DECLARE C VARCHAR(100);
DECLARE R VARCHAR(100);
BEGIN 
  S = TRIM( NEW.MY_TXT_COLUMN );
  R = NULL;
  I = 1;
  WHILE (i <= CHAR_LENGTH(S)) DO
  BEGIN
    C = SUBSTRING( s FROM i FOR 1 );
    IF (C < '0') THEN LEAVE;
    IF (C > '9') THEN LEAVE;
    IF (C IS NULL) THEN LEAVE;

    IF (R IS NULL) THEN R=C; ELSE R = R || C;
    I = I + 1; 
  END

  NEW.MY_INT_COLUMN = CAST(R AS INTEGER);
END;

In this example your ORDER order2, order1 would become

SELECT ..... FROM my_table ORDER BY MY_INT_COLUMN, MY_TXT_COLUMN 

Additionally, it seems your column actually contains a compound data: an integer index and an optional textual postfix. If so, then the data you have is not normalized and the table better be restructured.

CREATE TABLE my_table (
  ORDER_Int INTEGER NOT NULL,
  ORDER_PostFix VARCHAR(24) CHECK( ORDER_PostFix = TRIM(ORDER_PostFix) ),

  ......

  ORDER_TXT COMPUTED BY (ORDER_INT || COALESCE( ' ' || ORDER_PostFix, '' )),
  PRIMARY KEY (ORDER_Int, ORDER_PostFix )
);

When you would move your data from Paradox to Firebird - make your convertor application check and split those values like "1 bis" into two new columns.

And your query then would be like

SELECT ORDER_TXT, ...  FROM my_table ORDER BY ORDER_Int, ORDER_PostFix 
Arioch 'The
  • 15,799
  • 35
  • 62
  • 1
    full of examples and advices, I think i'll go with your first example since the legacy application still exists. with this and a condition on 'my_int_column' beeing already filled or not, and an index on it, it could become even faster ! well done :) – Darkendorf Sep 06 '17 at 13:22
  • @Darkendorf ur legacy application works with outdated and very problematic today BDE. I think to move it to SQL you would have to revamp the app anyway, at very least into using some modern data access library. So it MAY be good time to revise this specific field access method all over the app also, if you anyway would have to redesign it a bit. – Arioch 'The Sep 06 '17 at 13:25
  • I'm not the one that have even the right to look at this app source code... I hope for things moving on later this year, but for now I have to make my app ready to work side-by-side with the old one... with at least the same result. – Darkendorf Sep 06 '17 at 15:10
  • @Darkendorf then perhaps per-row trigger is your only way to go. Unless you would go for full tables virtualization via UPDATABLE VIEWs – Arioch 'The Sep 06 '17 at 15:12
  • no, your previous solution is perfect because those are bases for the other application to grab the information once registered. – Darkendorf Sep 07 '17 at 12:23
  • @Darkendorf if they are "for new applications" then I suggest using the latest variant. – Arioch 'The Sep 07 '17 at 13:44
2

if you're using fb2.5 you can use the following:

execute block (txt varchar(100) = :txt )
returns (res integer)
as
declare i integer;
begin
   i=1;
   while (i<=char_length(:txt)) do begin
    if (substring(:txt from i for 1) not similar to '[[:DIGIT:]]')
    then txt =replace(:txt,substring(:txt from i for 1),'');
    else i=i+1;
 end
res = :txt;
suspend;

end

in fb3.0 you have more convenient way to do the same

select
cast(substring(:txt||'#' similar '%#"[[:DIGIT:]]+#"%' escape '#') as integer)
from rdb$database
-1
--assuming that the field is varchar(15))
select cast(field as integer) from table;

Worked in firebird version 2.5.

Ivan Silkin
  • 381
  • 1
  • 7
  • 15
  • This doesn't work for the problem asked about in the question: the values in the question aren't only numeric values in a string: they contain additional non-numerical characters. In fact, the OP is asking the question because using a plain `cast` doesn't work, as they describe in their question. – Mark Rotteveel Apr 12 '22 at 11:53
  • @MarkRotteveel, then the input is garbage if it doesn't work. According to Steve Mcconnell. Garbage in garbage out. Especially when it comes from a database. – Ivan Silkin Apr 12 '22 at 12:22
  • That doesn't change the fact that your answer doesn't address the question. – Mark Rotteveel Apr 12 '22 at 15:35