Project

General

Profile

Feature #9455

port native user defined functions to SQL Server

Added by Eric Faulhaber over 1 year ago. Updated 11 months ago.

Status:
Internal Test
Priority:
Normal
Assignee:
Target version:
-
Start date:
Due date:
% Done:

100%

billable:
No
vendor_id:
GCD
case_num:
version_reported:
version_resolved:
reviewer:
production:
No
env_name:
topics:

fwd.df (50.7 KB) Eduard Soltan, 07/09/2025 12:17 PM

udftests.d (144 KB) Eduard Soltan, 07/09/2025 12:17 PM

fwd.df (14.9 KB) Eduard Soltan, 07/14/2025 02:48 PM


Related issues

Related to Conversion Tools - Feature #10265: schema-only conversion Closed

History

#2 Updated by Ovidiu Maxiniuc about 1 year ago

The goal of this task is to track the porting of native UDF functions for Microsoft SQL Server.

I already did partial work on this and saved the result in branch 9456a, together with other fixes of that task. My assumption was that the pieces of code are very separated and even if different developers work on that branch the result is easily merged or split. The UDF s are strictly contained within the udf/mssql/ directory of FWD.

#3 Updated by Constantin Asofiei about 1 year ago

  • Assignee set to Eduard Soltan

#4 Updated by Eduard Soltan about 1 year ago

I see that in T-SQL, statements like RAISERROR('Dates before October 15, 1582 are not yet supported', 16, 1);. could be used only in PROCEDURE, and not in FUNCTION.

#5 Updated by Constantin Asofiei about 1 year ago

What if you call a PROCEDURE from a FUNCTION?

#6 Updated by Eduard Soltan about 1 year ago

Constantin Asofiei wrote:

What if you call a PROCEDURE from a FUNCTION?

This could be done. And error is raised.

#7 Updated by Ovidiu Maxiniuc about 1 year ago

At the time I wrote CREATE FUNCTION dbo.modulo_ii (@p1 INT, @p2 INT) RETURNS BIGINT AS I was not able to test it. As you have seen, I tried using THROW and RAISERROR, but they do NOT work. The UDF currently looks like this:

CREATE FUNCTION dbo.modulo_ll (@p1 BIGINT, @p2 BIGINT) RETURNS BIGINT AS
BEGIN
   IF @p1 IS NULL OR @p2 IS NULL
      RETURN NULL;
   IF @p2 <= 0
      -- This does not work:  THROW 99009900, '** The second argument to MOD must be positive. % is invalid. (89)', 0;
      -- This either:         RAISERROR('** The second argument to MOD must be positive. % is invalid. (89)', -1, -1, 'FWD00');
      RETURN CAST(CONCAT('FWD00: ** The second argument to MOD must be positive. ' , @p2,  ' is invalid. (89)') AS INT);
   RETURN CAST(@p1 AS BIGINT) % CAST(@p2 AS BIGINT);
END;

Now, with improvements in 9456a, I returned to that UDF and wrote the following testcase:

find first book where book-id modulo author-id eq 8.
Of course, the database contains a single record like this:
insert into book (recid, book_id, book_title, author_id) values (101010, 99, 'my Book', 0)
so that the modulo should fail.

When this converted 4GL code is executed on SQL Server, FWD is notified by the following exception:

com.microsoft.sqlserver.jdbc.SQLServerException: An error occurred during the current command (Done status 0). Conversion failed when converting the varchar value 'FWD00: ** The second argument to MOD must be positive. 0 is invalid. (89)' to data type int.
at com.microsoft.sqlserver.jdbc.SQLServerException.makeFromDatabaseError(SQLServerException.java:276)
at com.microsoft.sqlserver.jdbc.SQLServerResultSet$FetchBuffer$FetchBufferTokenHandler.onDone(SQLServerResultSet.java:5455)
at com.microsoft.sqlserver.jdbc.TDSParser.parse(tdsparser.java:103)
at com.microsoft.sqlserver.jdbc.TDSParser.parse(tdsparser.java:42)
at com.microsoft.sqlserver.jdbc.SQLServerResultSet$FetchBuffer.nextRow(SQLServerResultSet.java:5558)
at com.microsoft.sqlserver.jdbc.SQLServerResultSet.fetchBufferNext(SQLServerResultSet.java:1821)
at com.microsoft.sqlserver.jdbc.SQLServerResultSet.next(SQLServerResultSet.java:1079)
at com.mchange.v2.c3p0.impl.NewProxyResultSet.next(NewProxyResultSet.java:685)
at com.goldencode.p2j.persist.orm.SQLQuery.uniqueResult(SQLQuery.java:549)
[...]

I think this is enough to detect the UDF failed. We can parse it and identify the exact error and even the legacy 4GL message. Se we should stick to this approach is no more straightforward solution is identified.

#8 Updated by Ovidiu Maxiniuc about 1 year ago

As noted in the Monday technical meet, revision 16004 of 9456 contains full support for recognising and parsing custom errors raised in UDF code. For example, the following code:

find first book where int64(book-id) modulo int64(author-id) eq 8.
will raise the following error condition:
** The second argument to MOD must be positive. 0 is invalid. (89)

#9 Updated by Eduard Soltan about 1 year ago

Ovidiu, should I commit my udfs implementation on 9456a?

#10 Updated by Ovidiu Maxiniuc about 1 year ago

  • Status changed from New to WIP

Yes, of course!

#11 Updated by Eduard Soltan about 1 year ago

Committed on 9456a, rev. 16006.

Add most of the remaining udfs. There still some edge cases to handle, especially the functions like tostring (@var datatype, @format NVARCHAR(50)).

Ovidiu, do you know whether we could run queries with debugger in SQL Server Management Studio?

#12 Updated by Ovidiu Maxiniuc about 1 year ago

Eduard Soltan wrote:

Committed on 9456a, rev. 16006.

Thank you, I am grabbing the changes right now.

Add most of the remaining udfs. There still some edge cases to handle, especially the functions like tostring (@var datatype, @format NVARCHAR(50)).

Yes, these must also be implemented. Please have a look at the UDFs I wrote, some of them are not complete (lack the exception cases).

Ovidiu, do you know whether we could run queries with debugger in SQL Server Management Studio?

I do not know. I used it only to write the UDF, but debugging was difficult since you SQL Server prohibit printing intermediary values/messages. My solution was to make the UFD temporarily return strings and force returning the intermediary values. Not the best debugging experience.... :(
I heard/read that the VS would be useful, but I am not a fan of it.

Have you tested the implementations? including edge cases like null and other? For example, to test my implementations of lte UDFs I used something like:

select dbo.lte_ss('aB ', 'Ab'), dbo.lte_ss('aB ', ' Ab'), dbo.lte_ss(null, null), dbo.lte_ss(null, ' Ab'), dbo.lte_ss('x', null);
select dbo.lte_ll(123, 123), dbo.lte_ll(123, 456), dbo.lte_ll(null, null), dbo.lte_ll(null, 123), dbo.lte_ll(456, null);
select dbo.lte_ii(123, 123), dbo.lte_ii(123, 456), dbo.lte_ii(null, null), dbo.lte_ii(null, 123), dbo.lte_ii(456, null);
select dbo.lte_rr(123.5, 123.5), dbo.lte_rr(123.5, 456.5), dbo.lte_rr(null, null), dbo.lte_rr(null, 123.5), dbo.lte_rr(456.5, null);
select dbo.lte_bb(1, 1), dbo.lte_bb(1, 0), dbo.lte_bb(null, null), dbo.lte_bb(null, 1), dbo.lte_bb(0, null);
select dbo.lte_dd(CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + 0), dbo.lte_dd(CURRENT_TIMESTAMP, CURRENT_TIMESTAMP+10), 
       dbo.lte_dd(null, null), dbo.lte_dd(null, CURRENT_TIMESTAMP), dbo.lte_dd(CURRENT_TIMESTAMP, null);
select dbo.lte_tt(CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + 0), dbo.lte_tt(CURRENT_TIMESTAMP, CURRENT_TIMESTAMP+10), 
       dbo.lte_tt(null, null), dbo.lte_tt(null, CURRENT_TIMESTAMP), dbo.lte_tt(CURRENT_TIMESTAMP, null);

#13 Updated by Ovidiu Maxiniuc about 1 year ago

  • Assignee changed from Eduard Soltan to Eric Faulhaber
  • vendor_id deleted (GCD)

Eduard,

I looked a bit over the UDFs you wrote. There is a bit of work still to be done. Please see my 5 notes blow:

1. Please use the following type decorations:

Legacy 4GL Type SQL Data Type Suffix Decoration
logical bit b (boolean)
integer int i (int)
int64 bigint l (long)
decimal decimal r (real)
character nvarchar(max) s (string)
date date d (date)
datetime datetime2 t (timestamp)
datetime-tz datetimeoffset z (timeZone)

If the decorations are not honoured, the SQL will fail to locate the UDF and the call/query/execute statement will fail. Note: they are different from MariaDb.

2. When a UDF is defined, the pattern looks like this:

IF OBJECT_ID(N'dbo.getday_t', N'FN') IS NOT NULL DROP FUNCTION dbo.getday_t;
GO
CREATE FUNCTION dbo.getday_t (@ts DATETIME2) RETURNS BIGINT AS
BEGIN
RETURN DAY(
@ts);
END;
GO
GRANT EXEC ON
dbo.getday_t TO PUBLIC;
GO

Notice the name of the UDF occurs 4 times (in bold): for checking its existence, for deleting if it exists, to declare it and for granting the required permissions.

3. All string / character literals must be prefixed with N to mark it as a 'national' (Unicode) data. Otherwise, SQL Server will do a non-trivial character encoding conversion which means a bit of performance penalty. Let's avoid that a price of one keystroke ๐Ÿ˜‰. Therefore, N'year', instead of 'year'.

4. If possible, please keep the order from my last revision (I grep-ed it from the PostgreSQL variant, which is the reference / more complete) to be sure we do not miss any UDF.

5. Currently there is a mix of comments formats in both JavaDoc (/** */) and SQL (--). While the normal processing will be done through a FWD script runner, keeping it this way will prevent easy execution of the script from command line or even from SQL Server Management Studio. My choice is to keep ONLY the file header in javadoc format. If the script is needed to be (re-)applied to a database, the header can easily be dropped and the rest of the file executed as a normal SQL script.

LE:
6. The errors reported back to FWD should use the following pattern: "FWD00: " + <legacy error message, including the error id>

#14 Updated by Ovidiu Maxiniuc about 1 year ago

  • vendor_id set to GCD
  • Assignee changed from Eric Faulhaber to Eduard Soltan

Sorry, I did these changes unintentionally. I do not have an explanation for them ๐Ÿ˜•.

#15 Updated by Eduard Soltan about 1 year ago

Changed udfs names and comments formats in rev. 16007. I confused the formats with mariadb udfs, and it is a bit different with that from SQL Server.

#16 Updated by Ovidiu Maxiniuc about 1 year ago

Eduard,

Do you have any pending changes to 9456a as part of this task?

The reason to ask is that I have some of my changes to sqlserver/udf.sql meant to fix the issues I encountered while installing the UDFs into SQL Server. This is not a small changeset so I would prefer to merge them after yours.

#17 Updated by Eduard Soltan about 1 year ago

Ovidiu Maxiniuc wrote:

Eduard,

Do you have any pending changes to 9456a as part of this task?

For now no, I will check for missed functions and will add them after your changes.

#18 Updated by Eduard Soltan about 1 year ago

Question, can the tests from testcases/udfs be adopted to SQL Server and properly tested?

#19 Updated by Ovidiu Maxiniuc about 1 year ago

  • % Done changed from 0 to 50

Yes, I think so. That's a good idea. Although, at a quick look they are not very extensive, but it's better than nothing.
I guess the conversion is possible, although will require a few hours to do that.

BTW, I committed my update related to this task in 9456a earlier. Please update.

#20 Updated by Greg Shah about 1 year ago

Eduard Soltan wrote:

Question, can the tests from testcases/udfs be adopted to SQL Server and properly tested?

These tests are what Igor used to check the original implementation when he first wrote the native SQL UDFs. At some point (not necessarily now but it would help) we need to migrate these to ABLUnit.

Independently, Marian's team built tests for Built-in Function (Database) Tests and WHERE Clause Tests which may overlap to some degree. These tests may not have been run on FWD yet, but will have been run on OE at one point.

#21 Updated by Eduard Soltan about 1 year ago

Adopted the sql tests for T-SQL and posted on testcases/udfs/t-sql.

#22 Updated by Eduard Soltan about 1 year ago

Committed functions fixups on 9456a, rev. 16040.

#23 Updated by Eduard Soltan about 1 year ago

  • % Done changed from 50 to 80

Added 3 more SQL test files to testcases/udfs/t-sql.

Committed on 9456a, rev. 16042 - 16043, fix ups of tostring_rs(@v DECIMAL, @format NVARCHAR) and addinterval udf functions.

#24 Updated by Eduard Soltan about 1 year ago

Manage to set up Hotel Gui with connection to a SQL Server instance running on a virtual machine. Did a smoke test with the application, did not enconter any major issue either in the application or the logs.

Next step is to set up a customer application with the SQL Server.

#25 Updated by Ovidiu Maxiniuc about 1 year ago

  • % Done changed from 80 to 0

See my note #9991-52. The import of the smaller database was (pretty much) successful.
There are some issues with the word table support.

But you need to do a quick conversion (temporarily replace abl/ content with a single .p file) with 9456a. For the smaller database I think there are no issues, but if you want to try the big one, you need to do a couple of the changes in the DDLs you obtain to avoid data to be truncated. Let me know if you need further details here.

#26 Updated by Ovidiu Maxiniuc about 1 year ago

  • % Done changed from 0 to 80

#27 Updated by Greg Shah about 1 year ago

#28 Updated by Eduard Soltan about 1 year ago

I think there is a issue with some _meta dmos.

The following field in _meta.MetaDb dmo:

@Property(id = 6, propertyId = 7, name = "dbMisc12", column = "db_misc1_2", legacy = "_Db-misc1", format = ">>>>9", initialNull = true, width = 96, order = 8, position = 7, index = 2, extent = 8, original = "dbMisc1")
   public integer getDbMisc12();

In latest trunk propertyId field in annotation.Property is not present anymore.

#29 Updated by Stefanel Pezamosca about 1 year ago

Eduard Soltan wrote:

I think there is a issue with some _meta dmos.

The following field in _meta.MetaDb dmo:
[...]

In latest trunk propertyId field in annotation.Property is not present anymore.

You need a new conversion.

#30 Updated by Ovidiu Maxiniuc about 1 year ago

Branch 9456a was rebased after 7020a was merged in. The denormalisation of extent fields was replaced with expanded columns.

I do not know (most likely not yet) whether 9457c was also rebased. Therefore they are most likely not compatible.

#31 Updated by Eduard Soltan about 1 year ago

Ovidiu Maxiniuc wrote:

I do not know (most likely not yet) whether 9457c was also rebased. Therefore they are most likely not compatible.

I rebased it locally to latest trunk, otherwise I could not apply the 9456a changes on it. And it seems that indeed a new conversion will be necessary.

#32 Updated by Constantin Asofiei about 1 year ago

Ovidiu Maxiniuc wrote:

I do not know (most likely not yet) whether 9457c was also rebased. Therefore they are most likely not compatible.

The conversion will be available tomorrow with, for 9457c rebase with trunk 16029.

#33 Updated by Eduard Soltan about 1 year ago

Tried to import the import the testcases database in sql server and got the following error.

[java] java.lang.RuntimeException: ERROR!  Active rule could not be determined: Error loading configuration: schema/import; XML path: /cfg/variable
     [java]     at com.goldencode.p2j.pattern.PatternEngine.run(PatternEngine.java:1109)
     [java]     at com.goldencode.p2j.pattern.PatternEngine.main(PatternEngine.java:2220)
     [java] Caused by: com.goldencode.p2j.cfg.ConfigurationException: Error loading configuration: schema/import; XML path: /cfg/variable
     [java]     at com.goldencode.p2j.pattern.ConfigLoader.loadImpl(ConfigLoader.java:651)
     [java]     at com.goldencode.p2j.pattern.ConfigLoader.load(ConfigLoader.java:552)
     [java]     at com.goldencode.p2j.pattern.PatternEngine.initialize(PatternEngine.java:1200)
     [java]     at com.goldencode.p2j.pattern.PatternEngine.run(PatternEngine.java:1036)
     [java]     ... 1 more
     [java] Caused by: com.goldencode.p2j.cfg.ConfigurationException: Error initializing user variable maxThreads
     [java]     at com.goldencode.p2j.pattern.ConfigLoader.variable(ConfigLoader.java:884)
     [java]     at com.goldencode.p2j.pattern.ConfigLoader.processChildElements(ConfigLoader.java:718)
     [java]     at com.goldencode.p2j.pattern.ConfigLoader.loadImpl(ConfigLoader.java:621)
     [java]     ... 4 more
     [java] Caused by: com.goldencode.expr.ExpressionException: Expression error [${db.import_threads}]
     [java]     at com.goldencode.expr.Expression.getCompiledInstance(Expression.java:695)
     [java]     at com.goldencode.expr.Expression.getReturnType(Expression.java:287)
     [java]     at com.goldencode.expr.ExpressionInitializer.getReturnType(ExpressionInitializer.java:104)
     [java]     at com.goldencode.expr.Variable.<init>(Variable.java:179)
     [java]     at com.goldencode.expr.SymbolResolver.registerVariable(SymbolResolver.java:575)
     [java]     at com.goldencode.p2j.pattern.RuleContainer.registerVariable(RuleContainer.java:321)
     [java]     at com.goldencode.p2j.pattern.PatternEngine.registerVariable(PatternEngine.java:926)
     [java]     at com.goldencode.p2j.pattern.ConfigLoader.variable(ConfigLoader.java:879)
     [java]     ... 6 more
     [java] Caused by: com.goldencode.expr.CompilerException: Error parsing expression
     [java]     at com.goldencode.expr.Compiler.process(Compiler.java:378)
     [java]     at com.goldencode.expr.Compiler.compile(Compiler.java:298)
     [java]     at com.goldencode.expr.Expression.getCompiledInstance(Expression.java:687)
     [java]     ... 13 more
     [java] Caused by: line 1:1: unexpected char: '$'
     [java]     at com.goldencode.expr.ExpressionLexer.nextToken(ExpressionLexer.java:418)
     [java]     at antlr.TokenBuffer.fill(TokenBuffer.java:69)
     [java]     at antlr.TokenBuffer.LA(TokenBuffer.java:80)
     [java]     at antlr.LLkParser.LA(LLkParser.java:52)
     [java]     at com.goldencode.expr.ExpressionParser.expression(ExpressionParser.java:667)
     [java]     at com.goldencode.expr.Compiler.parse(Compiler.java:482)
     [java]     at com.goldencode.expr.Compiler.process(Compiler.java:348)
     [java]     ... 15 more

#34 Updated by Ovidiu Maxiniuc about 1 year ago

I think the cause for this exception is the absence from build.properties of the definition for db.import_threads. I see it in the bzr project so it might have been commented out or deleted from your file. The default value is 4:

db.import_threads=4

#35 Updated by Eduard Soltan about 1 year ago

Ovidiu Maxiniuc wrote:

I think the cause for this exception is the absence from build.properties of the definition for db.import_threads. I see it in the bzr project so it might have been commented out or deleted from your file.

I managed to import the database to SQL Server in the end.There was also a problem with the schema definition in testcases/data/tstcasedb.df, attaching the good version of the schema. Thank you!

#36 Updated by Eduard Soltan 11 months ago

  • Status changed from WIP to Internal Test
  • % Done changed from 80 to 100

Tests performed with udfs are:

- testcases/udfs/t-sql these are refactored tests in T-SQL language from testcases/udfs/sql.

- testcases/udfs/4gl.

We issue that remains so far is then the udf function returns NULL.

def temp-table tt1 field f1 as decimal.

create tt1.
tt1.f1 = ?.
release tt1.

def var a as int.

a = ?.

find first tt1 where integer(tt1.f1) = a no-error.

message 'abc' available(tt1).

integer(tt1.f1) = a comparison (is SQL) will return null and the record will not be found. But this problem is not SQLServer specific, it happens in other dialects too.

Also available in: Atom PDF