There are times when I want to generate a list of numbers from 1 to n in Netsuite and it usually involves dates.

For example, if I wanted to calculate the number of actual work days a staff member was attending work from their initial hire date to their termination date I like using the DUAL view to help calculate these days.

Let’s say Mr. Smith was employed from 14th May 2022 to the 3rd of September 2024 and I need to calculate the number of actual days he was in attendance at work.

There are several ways you can attack this approach and I’ll outline each specific approach:

Using DUAL and CONNECT BY

One approach is to simply create a DUAL view using CONNECT BY with LEVEL that creates an incrementing loop from 1 to n.

This type of query would look something like this:

1
2
3
4
5
6
7
8
9
-- @param hire_date - the hire date in ISO date format (YYYY-MM-DD)
-- @param end_date - (OPTIONAL, empty string permitted) the termination date in ISO date format (YYYY-MM-DD)
-- @param hire_date
SELECT
    TO_CHAR(TO_DATE(?, 'YYYY-MM-DD') + LEVEL - 1, 'YYYY-MM-DD') AS dates
FROM
    DUAL
CONNECT BY
    LEVEL <= NVL(TO_DATE(?, 'YYYY-MM-DD'), CURRENT_DATE) - TO_DATE(?, 'YYYY-MM-DD') + 1

By injecting into the parameters an array of ISO date strings: ['2022-05-14', '2024-09-03', '2022-05-14'] you would achieve a list of all dates from the 14th May 2022 to the 3rd of September (inclusive).

Any further work you would need to do on the result from this query.

But that’s about all you can do with queries involving DUAL and CONNECT BY and LEVEL.

As the DUAL table is a view it doesn’t connect terribly well when using CONNECT BY where the DUAL view is joined with other tables.

Therefore, this type of approach is relatively simple provided the requirement is simple.

Using DUAL in a subquery

When you want to do more with the DUAL table you’ll soon find that it isn’t as easy when joining it alongside other tables.

Remember that it is a view and therefore you only want to use it for simple cases, such as generating numbers from 1 to n.

Should you prefer instead to inject an employee id and for the query to fetch the relevant hire and termination dates then the structure of your query will need to change slightly.

Using the same example as above, here’s how your query could look should you just want to insert the internal id of the employee:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
--- @param employee_id - Netsuite internal id of the employee
WITH inputs AS (
    SELECT
        ? AS employee_id
),
-- Fetch the relevant date values from the employee record
employee_dates AS (
    SELECT
        hiredate AS hire_date,
        -- Use termination date, or current_date if employee is still employed
        NVL(releasedate, CURRENT_DATE) AS end_date,
        -- Calculate the number of days employee has been hired, TRUNC is used to remove decimal
        TRUNC(NVL(releasedate, CURRENT_DATE) - hiredate) + 1 AS days_employed
    FROM
        employee, inputs
    WHERE 
        id = inputs.employee_id
),
-- Fetch the numbers from 0 to 1000
numbers AS (
    SELECT
        a.n + (10 * b.n) + (100 * c.n) AS n
    FROM
        (SELECT 0 AS n FROM DUAL UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a,
        (SELECT 0 AS n FROM DUAL UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b,
        (SELECT 0 AS n FROM DUAL UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) c,
        employee_dates
    WHERE
        a.n + (10 * b.n) + (100 * c.n) <= employee_dates.days_employed
    ORDER BY
        a.n + (10 * b.n) + (100 * c.n)
)
-- Now to work on the desired output, in this case just the dates is needed
SELECT
    TO_CHAR(employee_dates.hire_date + numbers.n, 'YYYY-MM-DD') AS dt
FROM
    employee_dates, numbers

Obtaining a simple number list from 1 to n can seems quite rudimentary with the CTE numbers, but this works without SuiteQL throwing any errors. It does produce issues in the sense that it is limited to the first 1000 numbers from 0 to 1000, but you could easily add another SELECT clause if you needed to produce 10000 numbers by adding a column d, like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
-- Fetch the numbers from 0 to 10,000
numbers AS (
    SELECT
        a.n + (10 * b.n) + (100 * c.n) + (1000 * d.n) AS n
    FROM
        (SELECT 0 AS n FROM DUAL UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a,
        (SELECT 0 AS n FROM DUAL UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b,
        (SELECT 0 AS n FROM DUAL UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) c,
        (SELECT 0 AS n FROM DUAL UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) d,
        employee_dates
    WHERE
        a.n + (10 * b.n) + (100 * c.n) + (1000 * d.n) <= employee_dates.days_employed
    ORDER BY
        a.n + (10 * b.n) + (100 * c.n) + (1000 * d.n)
)

Ideally it would be great if SuiteQL could handle the DUAL and CONNECT BY clause within a CTE to handle the generation of numbers, but unfortunately the following code DOES NOT WORK

1
2
3
4
5
6
7
8
numbers AS (
    SELECT
        LEVEL AS n
    FROM
	    DUAL, employee_dates
    CONNECT BY
	    LEVEL <= employee_dates.days_employed
)

As it produces the following error where the CONNECT BY line is stated in your query…

1
syntax error, state:961(10102) near: BY(25,13, token code:0)

Anyway, to finish off the initial query for this exercise, to calculate the number of working days the employee has worked you can tackle this a number of different ways.

My preference is to have the system count the number of days in the week between the hire date and release date and for the rest of the code to perform any further calculations on the returned data set.

If that’s where you’d like to go to as well then this code will provide that detail:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
--- @param employee_id - Netsuite internal id of the employee
WITH inputs AS (
    SELECT
        ? AS employee_id
),
-- Fetch the relevant date values from the employee record
employee_dates AS (
    SELECT
        hiredate AS hire_date,
        -- Use termination date, or current_date if employee is still employed
        NVL(releasedate, CURRENT_DATE) AS end_date,
        -- Calculate the number of days employee has been hired, TRUNC is used to remove decimal
        TRUNC(NVL(releasedate, CURRENT_DATE) - hiredate) + 1 AS days_employed
    FROM
        employee, inputs
    WHERE 
        id = inputs.employee_id
),
-- Fetch the numbers from 0 to 1000
numbers AS (
    SELECT
        a.n + (10 * b.n) + (100 * c.n) AS n
    FROM
        (SELECT 0 AS n FROM DUAL UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a,
        (SELECT 0 AS n FROM DUAL UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b,
        (SELECT 0 AS n FROM DUAL UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) c,
        employee_dates
    WHERE
        a.n + (10 * b.n) + (100 * c.n) <= employee_dates.days_employed
    ORDER BY
        a.n + (10 * b.n) + (100 * c.n)
)
-- Now to work on the desired output, in this case just the dates is needed
SELECT
    SUM(CASE WHEN TO_CHAR(employee_dates.hire_date + numbers.n, 'DY', 'nls_date_language=english') = 'MON' THEN 1 ELSE 0 END) AS mondays,
    SUM(CASE WHEN TO_CHAR(employee_dates.hire_date + numbers.n, 'DY', 'nls_date_language=english') = 'TUE' THEN 1 ELSE 0 END) AS tuesdays,
    SUM(CASE WHEN TO_CHAR(employee_dates.hire_date + numbers.n, 'DY', 'nls_date_language=english') = 'WED' THEN 1 ELSE 0 END) AS wednesdays,
    SUM(CASE WHEN TO_CHAR(employee_dates.hire_date + numbers.n, 'DY', 'nls_date_language=english') = 'THU' THEN 1 ELSE 0 END) AS thursdays,
    SUM(CASE WHEN TO_CHAR(employee_dates.hire_date + numbers.n, 'DY', 'nls_date_language=english') = 'FRI' THEN 1 ELSE 0 END) AS fridays,
    SUM(CASE WHEN TO_CHAR(employee_dates.hire_date + numbers.n, 'DY', 'nls_date_language=english') = 'SAT' THEN 1 ELSE 0 END) AS saturdays,
    SUM(CASE WHEN TO_CHAR(employee_dates.hire_date + numbers.n, 'DY', 'nls_date_language=english') = 'SUN' THEN 1 ELSE 0 END) AS sundays
FROM
    employee_dates, numbers

The output from this result is a table producing the number of days of the week from the hire date to the termination date:

mondaystuesdayswednesdaysthursdaysfridayssaturdayssundays
121121121120120121121

From this you can then use these counts to do further analysis.