0

I'm trying to create a scalar function to determine whether a user of a provided ID or any of their subordinates have orders under a collection of provided order IDs.

Note I am using my own User-Defined Table Type of IntegerIdTableType to take in the collection of OrderIds.

CREATE FUNCTION DoOrdersExistUnderUserOrUsersSubordinates
(
    @orderIds dbo.IntegerIdTableType READONLY,
    @userId INT
)
RETURNS BIT
AS
BEGIN
    RETURN 
    (
        WITH GetUserIds(ordinateUserId)
        AS
        (
            SELECT ordinateUserId UserId
            UNION ALL
            SELECT GetUserIds(Subordinate.Id)
            FROM UsersAccounts.Users Subordinates
            WHERE Subordinates.SupervisorId = @ordinateUserId
        )
        SELECT CASE WHEN EXISTS
        (
            SELECT 1
            FROM Orders
            WHERE Orders.Id IN
            (
                SELECT Id
                FROM @orderIds
            )
            AND Orders.UserId IN
            (
                SELECT UserId
                FROM GetUserIds(@userId)
            )
        )
        THEN CAST(1 AS BIT)
        ELSE CAST(0 AS BIT)
        END
    )
END

Here is some sample data for both my Orders and Users tables.

Users

Users Sample Data

Orders

Orders Sample Data

Expected Results

When calling DoOrdersExistUnderUserOrUsersSubordinates with the following values, I expect the following results.

Expected Orders

I'm having 2 issues with this function:

  1. Syntax errors:

    Incorrect syntax near the keyword 'WITH'.

    Incorrect syntax near ')'.

  2. 'GetUserIds' is not a recognized built-in function name

    The above seems to happen even without being wrapped in a function.

I don't know what the correct way to pass a parameter to a recursive CTE is but I have seen examples where the declaration of the CTE has a name in brackets which I assumed to be a parameter

I've tried putting a semi-colon immediately before the WITH even though it's the only statement in the function and I just get Incorrect syntax near ';'. instead of Incorrect syntax near the keyword 'WITH'.

I've also tried getting rid of the BEGIN and END and that gives me Incorrect syntax near 'RETURN'., plus Incorrect syntax near the keyword 'with'. If this statement is a common table expression, an xmlnamespaces clause or a change tracking context clause, the previous statement must be terminated with a semicolon. If I don't include the redundant semi-colon.

How do I get around all of this?

Surely Recursive CTE's must be able to take in a parameter or what would they recurse on?

UPDATE:

After battling with Example F of the documentation linked by Zohar_Peled, I eventually figured out that parameters aren't passed into the CTE as such, but rather joined to it then persisted within it through the brackets of its declaration. Whatever is then defined in the corresponding SELECTs is output through the parameters to whatever called the CTE (in this case, either the outer SELECT Id FROM UserNodes statement or the CTE itself (for the recursion)).

I changed the SQL statement within the function to the following and it worked as expected outside of the function.

WITH UserNodes([Root User ID], Id, SupervisorId)
AS
(
    SELECT Users.Id, Users.Id, Users.SupervisorId
    FROM UsersAccounts.Users
    WHERE Users.SupervisorId IS NULL
    UNION ALL
    SELECT [Root User ID],
        Users.Id,
        Users.SupervisorId
    FROM UsersAccounts.Users
    JOIN UserNodes [Subordinate Descendant Users] ON [Subordinate Descendant Users].Id = Users.SupervisorId
)
SELECT CASE WHEN EXISTS
(
    SELECT 1
    FROM Orders
    WHERE Orders.Id IN
    (
        SELECT Id
        FROM @orderIds
    )
    AND Orders.UserId IN
    (
        SELECT Id
        FROM UserNodes
        WHERE [Root User ID] = @userId
    )
)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END

This works fine alone (with the required variables provided to substitute the missing function parameters) but as soon as I put it back into the CREATE FUNCTION block, I'm faced with the same syntax errors as before (excluding 2.).

Matt Arnold
  • 668
  • 2
  • 8
  • 21
  • Seems like you have some confusion on how to use common table expressions. `SELECT GetUserIds(Subordinate.Id)` looks like an attempt to execute a scalar function and `FROM GetUserIds(@userId)` seems like an attempt to execute a table valued function. This confusion makes the entire question very unclear. – Zohar Peled Aug 14 '19 at 12:10
  • From the looks of your `Function`, I would say you are not returning the value of the function correctly. You need to specify the return value correctly. Please have a look at this [link](https://learn.microsoft.com/en-us/sql/relational-databases/user-defined-functions/create-user-defined-functions-database-engine?view=sql-server-2017), this will give you an idea how to customize your Function to... – Attie Wagner Aug 14 '19 at 12:22
  • @Birel I'm just trying to return a boolean (`BIT`) value from the function, is that not what I'm doing with the `THEN CAST(1 AS BIT)` and `ELSE CAST(0 AS BIT)` part? – Matt Arnold Aug 14 '19 at 12:27
  • If I can suggest, declare a variable called `@bln bit`, then set the `bln` value to the CTE results, then in the function return the `bln` value... I'll suggest something like this in an answer, but I won't be able to test, as I don't have your data... – Attie Wagner Aug 14 '19 at 12:29
  • 1
    @ZoharPeled Sorry, this is my first attempt at using the `WITH` syntax and CTEs; I was under the impression they were like localized functions which you could call upon multiple times from the SELECT query which follows them. I'd like to avoid creating a separate table-valued function for `GetUserIds` as I've heard about the performance implications of having functions called multiple times from within a query (in this case, for the user then, from within itself, each of their descendants). – Matt Arnold Aug 14 '19 at 12:32
  • 1
    Well, TSQL is very well documented, you can start by reading [WITH common_table_expression](https://learn.microsoft.com/en-us/sql/t-sql/queries/with-common-table-expression-transact-sql?view=sql-server-2017) and work your way through examples to figure out what's wrong with your code. – Zohar Peled Aug 14 '19 at 12:46
  • How is user 6 their own supervisor? – Thom A Aug 20 '19 at 08:52
  • Also, why is UserID 1 related to all the orders? Order 6 is related to User 5, who has no supervisor. – Thom A Aug 20 '19 at 08:54
  • I made several mistakes is why. I've corrected the Users table so that User 5 is the Supervisor of User 6 (no circular reference anymore), the Orders table so that Order 6 was made by supervised-user 6 and the expected results so that the final expected result is for when User ID 5 is passed in (as to demonstrate a user needn't have their own order for 1 to be returned. – Matt Arnold Aug 20 '19 at 10:47
  • I checked your edit @MattArnold, but based on your CTE now, you did not declare `@orderIds` and `@userid`. Where do you get those from? – Attie Wagner Aug 20 '19 at 10:59
  • Those are the parameters of the scalar function. – Matt Arnold Aug 20 '19 at 11:03

1 Answers1

1

As stated, I'm not able to test this, but this is what I'm suggesting you change:

    CREATE FUNCTION DoOrdersExistUnderUserOrUsersSubordinates
(
    @orderIds dbo.IntegerIdTableType READONLY,
    @userId INT
)
RETURNS BIT
AS
BEGIN

    declare @bln bit

    ;WITH UserNodes([Root User ID], Id, SupervisorId)
    AS
    (
        SELECT Users.Id, Users.Id, Users.SupervisorId
        FROM UsersAccounts.Users
        WHERE Users.SupervisorId IS NULL
        UNION ALL
        SELECT [Root User ID],
            Users.Id,
            Users.SupervisorId
        FROM UsersAccounts.Users
        JOIN UserNodes [Subordinate Descendant Users] ON [Subordinate Descendant Users].Id = Users.SupervisorId
    )
    SELECT @bln = CASE WHEN EXISTS
    (
        SELECT 1
        FROM Orders
        WHERE Orders.Id IN
        (
            SELECT Id
            FROM @orderIds
        )
        AND Orders.UserId IN
        (
            SELECT Id
            FROM UserNodes
            WHERE [Root User ID] = @userId
        )
    )
    THEN CAST(1 AS BIT)
    ELSE CAST(0 AS BIT)
    END

    RETURN @bln
END

Let me know if it works...

Attie Wagner
  • 1,312
  • 14
  • 28
  • That's not made any difference; it's still giving a syntax error of `Incorrect syntax near the keyword 'WITH'.` – Matt Arnold Aug 14 '19 at 12:37
  • And if you add the semicolon in now? – Attie Wagner Aug 14 '19 at 12:38
  • If this persists, provide me with sample data and expected results, then I'll be able to assist more efficiently... – Attie Wagner Aug 14 '19 at 12:38
  • It's the same as before: `Incorrect syntax near ';'.`. I'll have to create some sample data as I changed the example from the actual database as to not expose any company-bespoke data structure on the web. – Matt Arnold Aug 14 '19 at 12:41
  • 1
    Great, I'll wait for the sample data, I also noticed now that the `@ordinateUserId` field is no where declared? – Attie Wagner Aug 14 '19 at 12:42
  • Ah good spot! I believe I renamed the CTE parameter (or what I thought was a parameter) and missed that reference. I've corrected that now though and it's not made a difference - I guess the parser didn't get that far into evaluating the script before it gave up and errored! – Matt Arnold Aug 14 '19 at 12:44
  • Also does the CTE work without creating the function? Check if it returns the correct answer, before creating the function...? – Attie Wagner Aug 14 '19 at 12:45
  • Without the function, the `Incorrect syntax near...` errors are no longer present; however, I'm still faced with `'GetUserIds' is not a recognized built-in function name.`. This is probably due to me calling `GetUserIds` incorrectly - how should I be calling a CTE with a parameter? – Matt Arnold Aug 14 '19 at 12:49
  • I'm talking under correction, but as far as I'm aware, you cannot call the function you're requesting a return in the same function, because that will create a loop. So to conclude, I think you need to rethink your CTE, then only create the function after a successful CTE. If you provide sample data **and** expected results, I will be able to assist better... – Attie Wagner Aug 14 '19 at 12:51
  • Ok, @MattArnold, I had a look at your sample data and expected results. Can you please ammend your CTE script to work based on the sample data? I've been trying to wrap my head around your expected results, but I'm unable to figure out your logic based on your current CTE... – Attie Wagner Aug 15 '19 at 07:48
  • The CTE should be returning `1` if any of the provided `@orderIds` exist with the provided `@userId` or the Id of any User who is supervised directly or indirectly by the User of the provided `@userId`; otherwise, `0` should be returned. I don't see any, but were there any fields in the CTE which didn't match the fields in the sample data? I just removed the irrelevant fields from the sample data for clarity. – Matt Arnold Aug 16 '19 at 08:58