    PROGRAM T24CLIENT

    $INCLUDE I_COMMON
    $INCLUDE I_EQUATE
    $INCLUDE I_F.USER
    $INCLUDE I_F.COMPANY
    $INCLUDE I_F.STANDARD.SELECTION
    $INCLUDE I_T24.COMMON

    EQU START.FLAG      TO CHAR(10):"&START&"
    EQU END.FLAG        TO "&END&"
    EQU ERROR.FLAG      TO "&ERROR&"
    EQU ID.SEPARATOR    TO CHAR(255)
    EQU ITEM_SEPARATOR  TO "_$$_"
    EQU PARAM_SEPARATOR   TO "_$_"
    EQU BATCH.SIZE      TO 1000
    DIM BATCH.DIM(BATCH.SIZE)
    DIM UDF.DIM(100)
    DIM LINK.DIM(100)
    DIM LINK.FILES.DIM(100)
    DIM AGG.TYPES(100)
    DIM AGG.FIELD.NO(100)
    DIM AGG.RECORD(100)
    DIM AGG.RECORD.COUNT(100)
    DIM EVAL.DIM(100)

    GOSUB INIT

    LOOP
        INPUT COMMAND
        GOSUB PARSE.PARAMS
        GOSUB DO.RUN.COMMAND
    REPEAT

    GOSUB DONE

    STOP

************************************************
DO.RUN.COMMAND:
************************************************
    COMMAND.FAILED = 0

    BEGIN CASE
    CASE PARAM.1 = "Q"
        STOP
    CASE PARAM.1 = "STARTUP"
        GOSUB DO.STARTUP
    CASE PARAM.1 = "GETPID"
        CRT @USERSTATS<4>
        GOSUB END.DATA.OUT
    CASE PARAM.1 = "GETSYSINFO"
        GOSUB GET.SYS.INFO
    CASE PARAM.1 = "GETTABLELIST"
        GOSUB GET.TABLE.LIST
    CASE PARAM.1 = "GETRECORD"
        GOSUB GET.RECORD
    CASE PARAM.1 = "SELECT"
        GOSUB DO.SELECT
    CASE PARAM.1 = "AGG"
        GOSUB DO.AGGREGATE
    CASE PARAM.1 = "PREPARE"
        GOSUB DO.PREPARE
    CASE PARAM.1 = "READ.BATCH"
        GOSUB DO.READ.BATCH
    CASE PARAM.1 = "FINISH"
        GOSUB DO.FINISH.JOB
    CASE PARAM.1 = "UDF.TYPE"
        GOSUB GET.UDF.TYPE
    END CASE

    RETURN

************************************************
DO.STARTUP:
************************************************
    EXECUTE "CLEAR-FILE ":FILE.NAME CAPTURING DUMMY
    CRT "File ":FILE.NAME:" cleared."

    CRT "STARTUP OK"

    GOSUB END.DATA.OUT

    RETURN

************************************************
GET.FILE.NAME:
************************************************
    FILE.NAME = "T24DAC.BP/F.JOB.LIST"

    RETURN

************************************************
INIT:
************************************************
    GOSUB GET.FILE.NAME

    OPEN FILE.NAME TO F.JOB.LIST THEN
        CRT "File ":FILE.NAME:" opened."
    END ELSE
        EXECUTE "CREATE-FILE DATA ":FILE.NAME:" TYPE=J4 1001" CAPTURING DUMMY

        OPEN FILE.NAME TO F.JOB.LIST ELSE
            ERROR.MSG = "Cannot create job list file ":FILE.NAME
            GOTO ERROR.AND.RETURN
        END

        CRT "File ":FILE.NAME:" created."
    END

    MAT AGG.TYPES = ""
    MAT AGG.FIELD.NO = ""

    RETURN

************************************************
DONE:
************************************************
    CLOSE F.JOB.LIST

    RETURN

************************************************
GET.SYS.INFO:
************************************************
    GOSUB START.DATA.OUT

    CRT '{"IDM": "' : ID.SEPARATOR : '", "FM": "' : @FM : '", "VM": "' : @VM : '", "SM": "' : @SM : '", "OS": "' : SYSTEM(1017) : '"}'

    GOSUB END.DATA.OUT

    RETURN

************************************************
DO.AGGREGATE:
************************************************
    GOSUB LOAD.JOB.FILE

    IF AGG.FIELDS = "C0" THEN ;* SELECT COUNT(*) FROM ... WHERE ...
        GOSUB DO.AGG.SIMPLE.COUNT
        IF COMMAND.FAILED = 1 THEN RETURN
    END ELSE
        SELECT.STR = "SELECT ":TABLE.NAME:" ":WITH.CONDITIONS

        EXECUTE SELECT.STR RTNLIST ID.LIST CAPTURING DUMMY SETTING RET.CODE

        IF RET.CODE<1, 3> <> "QLNUMSEL" AND RET.CODE<1, 2> <> "QLNONSEL" THEN
            ERROR.MSG = RET.CODE
            GOTO ERROR.AND.RETURN
        END
        
        GOSUB OPEN.TABLE

        MAT AGG.RECORD = ""
        MAT AGG.RECORD.COUNT = 0

        LOOP
            READNEXT ID.SOURCE FROM ID.LIST ELSE BREAK

            READ R.SOURCE FROM F.SOURCE, ID.SOURCE ELSE CONTINUE

            FOR AGG.I = 1 TO AGG.FIELD.COUNT
                FIELD.NO = AGG.FIELD.NO(AGG.I)
                BEGIN CASE
                CASE AGG.TYPES(AGG.I) = "C"
                    IF FIELD.NO = 0 THEN AGG.RECORD(AGG.I)++          ;* COUNT(*)
                    ELSE IF R.SOURCE<FIELD.NO> <> "" THEN AGG.RECORD(AGG.I)++   ;* COUNT(FIELD)
                CASE AGG.TYPES(AGG.I) = "S"
                    AGG.RECORD(AGG.I) += R.SOURCE<FIELD.NO>
                CASE AGG.TYPES(AGG.I) = "A" AND R.SOURCE<FIELD.NO> <> ""
                    AGG.RECORD.COUNT(AGG.I)++
                    AGG.RECORD(AGG.I) += R.SOURCE<FIELD.NO>
                END CASE
            NEXT AGG.I
        REPEAT

        CLOSE F.SOURCE

        FOR AGG.I = 1 TO AGG.FIELD.COUNT
            IF AGG.TYPES(AGG.I) = "A" AND AGG.RECORD.COUNT(AGG.I) > 0 THEN
                AGG.RECORD(AGG.I) = FMT(AGG.RECORD(AGG.I) / AGG.RECORD.COUNT(AGG.I), "L4")
            END
        NEXT AGG.I

        MATBUILD AGG.RESULT FROM AGG.RECORD, 1, AGG.FIELD.COUNT
    END

    GOSUB START.DATA.OUT

    CRT AGG.RESULT

    GOSUB END.DATA.OUT

    RETURN

************************************************
DO.AGG.SIMPLE.COUNT:
*IN: TABLE.NAME, WITH.CONDITIONS
*OUT: AGG.RESULT
************************************************
    SELECT.STR = "COUNT ":TABLE.NAME:" ":WITH.CONDITIONS

    EXECUTE SELECT.STR CAPTURING DUMMY SETTING RET.CODE

    IF RET.CODE<1, 3> <> "QLNUMCNT" AND RET.CODE<1, 2> <> "QLNONCNT" THEN
        ERROR.MSG = RET.CODE
        GOTO ERROR.AND.RETURN
    END
    
    IF RET.CODE<1, 3> = "QLNUMCNT" THEN AGG.RESULT = RET.CODE<1, 2>
    ELSE AGG.RESULT = 0

    RETURN

************************************************
DO.SELECT:
************************************************
    GOSUB LOAD.JOB.FILE

    IF WITH.CONDITIONS = "" AND ORDER.BY = "" THEN
        GOSUB SELECT.ENTIRE.FILE
        RETURN
    END

    SELECT.STR = "SELECT ":TABLE.NAME:" ":WITH.CONDITIONS:" ":ORDER.BY

    IF MAX.ROWS > 0 THEN
        SELECT.STR := " SAMPLE ":MAX.ROWS
    END

    EXECUTE SELECT.STR RTNLIST ID.LIST CAPTURING DUMMY SETTING RET.CODE

    GOSUB PRINT.SELECT.RESULT

    RETURN

************************************************
DO.FINISH.JOB:
*IN: PARAM.2 - Job id
************************************************
    GOSUB START.DATA.OUT

    DELETE F.JOB.LIST, PARAM.2:"-PARAMS"
    DELETE F.JOB.LIST, PARAM.2:"-UDF"
    DELETE F.JOB.LIST, PARAM.2:"-SP"
    DELETE F.JOB.LIST, PARAM.2:"-LINK"
    DELETE F.JOB.LIST, PARAM.2:"-EVAL"

    GOSUB END.DATA.OUT

    RETURN

************************************************
DO.READ.BATCH:
*IN: PARAM.2 - TABLE.NAME, PARAM.3 - BATCH id
************************************************
    TABLE.NAME = PARAM.2
    ID.BATCH = PARAM.3

    IF TABLE.NAME[1, 1] <> "@" THEN
        GOSUB OPEN.TABLE
    END ELSE
        F.SOURCE = ""
        SP.NAME = TABLE.NAME[2, 99]
    END

    READ R.JOB FROM F.JOB.LIST, ID.BATCH ELSE
    END

    ID.JOB = ID.BATCH["#", 1, 1]

    READ R.UDF FROM F.JOB.LIST, ID.JOB : "-UDF" THEN
        MATPARSE UDF.DIM FROM R.UDF SETTING UDF.COUNT
    END ELSE UDF.COUNT = 0

    READ R.EVAL FROM F.JOB.LIST, ID.JOB : "-EVAL" THEN
        MATPARSE EVAL.DIM FROM R.EVAL SETTING EVAL.COUNT
        FOR EVAL.I = 1 TO EVAL.COUNT
            CHANGE ITEM_SEPARATOR TO @FM IN EVAL.DIM(EVAL.I)
        NEXT EVAL.I
    END ELSE EVAL.COUNT = 0

    READ R.LINK FROM F.JOB.LIST, ID.JOB : "-LINK" THEN
        MATPARSE LINK.DIM FROM R.LINK SETTING LINK.COUNT
    END ELSE LINK.COUNT = 0

    READ SP.PARAMS FROM F.JOB.LIST, ID.JOB : "-SP" ELSE
        SP.PARAMS = ""
    END

    READ R.JOB.PARAMS FROM F.JOB.LIST, ID.JOB : "-PARAMS" THEN
        CALL LOAD.COMPANY(R.JOB.PARAMS<1>)
    END

    GOSUB OPEN.LINK.FILES
    IF COMMAND.FAILED = 1 THEN RETURN

    GOSUB START.DATA.OUT

    R.SRC = ""
    LOOP
        READNEXT ID.SRC FROM R.JOB ELSE BREAK

        IF F.SOURCE <> "" THEN          ;*if it's select from table
            READ R.SRC FROM F.SOURCE, ID.SRC ELSE
            END
        END ELSE    ;* otherwise it's a stored procedure
            PARAMS = "SELECT"
            PARAMS<2> = SP.PARAMS
            R.SRC = ""
            CALL @SP.NAME(PARAMS, R.SRC, ID.SRC)
        END

        IF UDF.COUNT > 0 OR LINK.COUNT > 0 OR EVAL.COUNT > 0 THEN
            GOSUB GET.UDF.VALUES
            GOSUB GET.LINK.VALUES
            GOSUB GET.EVAL.VALUES
            EXTRA.VALUES = ID.SEPARATOR : UDF.VALUES : LINK.VALUES : EVAL.VALUES
        END
        ELSE EXTRA.VALUES = ""

        CRT ID.SRC : ID.SEPARATOR : R.SRC : EXTRA.VALUES
    REPEAT

    DELETE F.JOB.LIST, ID.BATCH

    GOSUB END.DATA.OUT

    CLOSE F.SOURCE

    GOSUB CLOSE.LINK.FILES

    RETURN

************************************************
GET.LINK.VALUES:
************************************************
    LINK.VALUES = ""

    FOR LINK.I = 1 TO LINK.COUNT
        ID.LINK.FIELD.NO = LINK.DIM(LINK.I)<1, 1>
        IF ID.LINK.FIELD.NO = 0 THEN ID.LINK = ID.SRC
        ELSE ID.LINK = R.SRC<ID.LINK.FIELD.NO>

        LINK.VALUE = ""
        IF ID.LINK <> "" THEN
            READ R.LINK FROM LINK.FILES.DIM(LINK.I), ID.LINK THEN
                LINK.VALUE = R.LINK<LINK.DIM(LINK.I)<1, 3>>
            END
        END

        LINK.VALUES := LINK.VALUE : @FM
    NEXT LINK.I

    RETURN

************************************************
OPEN.LINK.FILES:
* Input: LINK.COUNT, LINK.DIM
************************************************
    FOR LINK.I = 1 TO LINK.COUNT
        LINK.FILE.NAME = LINK.DIM(LINK.I)<1, 2>
        OPEN LINK.FILE.NAME TO LINK.FILES.DIM(LINK.I) ELSE
            ERROR.MSG = "Cannot open " : LINK.FILE.NAME : " file"
            GOTO ERROR.AND.RETURN
        END
    NEXT LINK.I

    RETURN

************************************************
CLOSE.LINK.FILES:
* Input: LINK.COUNT, LINK.FILES.DIM
************************************************
    FOR LINK.I = 1 TO LINK.COUNT
        CLOSE LINK.FILES.DIM(LINK.I)
    NEXT LINK.I

    RETURN

************************************************
GET.UDF.VALUES:
*Input:
*  ID.SRC - id or current record
*  R.SRC  - current record
*  UDF.DIM - list of UDF functions to call and parameters
*Output:
*  UDF.VALUES - a dynamic array of UDF values
************************************************
    UDF.VALUES = ""

    FOR UDF.I = 1 TO UDF.COUNT
        UDF.RESULT = ""
        UDF.NAME = UDF.DIM(UDF.I)<1, 1>
        UDF.PARAMS = "SELECT"
        UDF.PARAMS<2> = UDF.DIM(UDF.I)[@VM, 2, 999]
        GOSUB EXTRACT.RECORD.VALUES
        CALL @UDF.NAME(UDF.PARAMS, UDF.RESULT, ID.SRC, R.SRC)
        UDF.VALUES := UDF.RESULT : @FM
    NEXT UDF.I

    RETURN

************************************************
GET.EVAL.VALUES:
*Input:
*  ID.SRC - id of current record
*  R.SRC  - current record
*  EVAL.DIM - list of EVAL functions to call
*Output:
*  EVAL.VALUES - a dynamic array of UDF values
************************************************
    EVAL.VALUES = ""
    @ID = ID.SRC
    @RECORD = R.SRC
    @FILENAME = TABLE.NAME

    FOR EVAL.I = 1 TO EVAL.COUNT
        EVAL.RESULT = ITYPE(EVAL.DIM(EVAL.I))
        EVAL.VALUES := EVAL.RESULT : @FM
    NEXT UDF.I

    RETURN

************************************************
EXTRACT.RECORD.VALUES:
*Input: UDF.PARAMS, R.SRC
*Output: UDF.PARAMS
************************************************
    FOR PARAM.I = 1 TO DCOUNT(UDF.PARAMS<2>, @VM)
        PARAM.VALUE = UDF.PARAMS<2, PARAM.I>
        IF PARAM.VALUE[1, 1] = "#" THEN
            FIELD.NO = PARAM.VALUE[2, -1]
            IF FIELD.NO = 0 THEN UDF.PARAMS<2, PARAM.I> = ID.SRC
            ELSE UDF.PARAMS<2, PARAM.I> = R.SRC<FIELD.NO>
        END
    NEXT PARAM.I

    RETURN

************************************************
GET.UDF.TYPE:
*Input:
*  PARAM.2 - name of UDF
************************************************
    GOSUB START.DATA.OUT

    CALL @PARAM.2("TYPE", UDF.TYPE, "", "")
    CRT UDF.TYPE

    GOSUB END.DATA.OUT

    RETURN

************************************************
DO.PREPARE:
************************************************
    MAT BATCH.DIM = ""

    GOSUB LOAD.JOB.FILE

    IF MAX.ROWS = -1 THEN     ;* no records selection required
        GOSUB START.DATA.OUT
        GOSUB END.DATA.OUT
        RETURN
    END

    GOSUB PREPARE.PARAMS.RECORD

    GOSUB PREPARE.UDF.RECORD

    GOSUB PREPARE.LINK.RECORD

    GOSUB PREPARE.EVAL.RECORD
    IF COMMAND.FAILED = 1 THEN RETURN

    IF TABLE.NAME[1, 1] = "@" THEN
        GOSUB PREPARE.STORED.PROCEDURE
        IF COMMAND.FAILED = 1 THEN RETURN
    END ELSE
        IF WITH.CONDITIONS = "" AND ORDER.BY = "" THEN
            ID.LIST = ""
            GOSUB PREPARE.ID.LIST
            RETURN
        END

        SELECT.STR = "SELECT ":TABLE.NAME:" ":WITH.CONDITIONS:" ":ORDER.BY

        IF MAX.ROWS > 0 THEN
            SELECT.STR := " SAMPLE ":MAX.ROWS
        END

        EXECUTE SELECT.STR RTNLIST ID.LIST CAPTURING DUMMY SETTING RET.CODE

        IF RET.CODE<1, 3> <> "QLNUMSEL" AND RET.CODE<1, 2> <> "QLNONSEL" THEN
            ERROR.MSG = RET.CODE
            GOTO ERROR.AND.RETURN
        END
    END

    IF ID.LIST = "" THEN      ;* no records selected
        GOSUB START.DATA.OUT
        GOSUB END.DATA.OUT
        RETURN
    END

    GOSUB PREPARE.ID.LIST

    RETURN

************************************************
PREPARE.LINK.RECORD:
************************************************
    R.LINK = LINK.LIST
    CHANGE PARAM_SEPARATOR TO @VM IN R.LINK
    CHANGE ITEM_SEPARATOR TO @FM IN R.LINK

    WRITE R.LINK TO F.JOB.LIST, ID.JOB : "-LINK"

    RETURN

************************************************
PREPARE.PARAMS.RECORD:
* Input: T24.COMPANY, ID.JOB
************************************************
    R.PARAMS = T24.COMPANY

    WRITE R.PARAMS TO F.JOB.LIST, ID.JOB : "-PARAMS"

    RETURN

************************************************
PREPARE.STORED.PROCEDURE:
* Input:
*   TABLE.NAME - stored procedure name
*   T24.COMPANY - company code
* Output:
*   ID.LIST - list of ids to prepare
************************************************
    CALL LOAD.COMPANY(T24.COMPANY)

    SP.NAME = TABLE.NAME[2, 99]

    R.SP = SP.LIST
    CHANGE PARAM_SEPARATOR TO @VM IN R.SP
    WRITE R.SP TO F.JOB.LIST, ID.JOB : "-SP"

    PARAMS = ""
    PARAMS<1> = "PREPARE"
    PARAMS<2> = R.SP
    ID.LIST = ""
    CALL @SP.NAME(PARAMS, ID.LIST, "")

    RETURN

************************************************
PREPARE.UDF.RECORD:
*Input:
*  ID.JOB, UDF.LIST
************************************************
    R.UDF = UDF.LIST

    CHANGE PARAM_SEPARATOR TO @VM IN R.UDF
    CHANGE ITEM_SEPARATOR TO @FM IN R.UDF

    WRITE R.UDF TO F.JOB.LIST, ID.JOB : "-UDF"

    RETURN

************************************************
PREPARE.EVAL.RECORD:
*IN: ID.JOB, EVAL.LIST
************************************************
    CHANGE ITEM_SEPARATOR TO @FM IN EVAL.LIST

    OPEN "DICT", TABLE.NAME TO F.DICT ELSE
        RETURN
    END

* compile EVALs
    R.EVAL = ""
    LOOP
        READNEXT EVAL.EXPR FROM EVAL.LIST ELSE BREAK

        DICT.ID = "EVAL-" : ID.JOB
        R.DICT<1> = "I"
        R.DICT<2> = EVAL.EXPR
        R.DICT<5> = "255L"
        WRITE R.DICT TO F.DICT, DICT.ID

        EXECUTE "ICOMP ":TABLE.NAME:" ":DICT.ID CAPTURING ICOMP.CAP SETTING ICOMP.RES

        IF ICOMP.RES<1, 1> = "ICOMP_ERROR" THEN
            ERROR.MSG = ICOMP.CAP<3>
            DELETE F.DICT, DICT.ID
            CLOSE F.DICT
            GOTO ERROR.AND.RETURN
        END

        READ R.DICT FROM F.DICT, DICT.ID THEN
        END

        CHANGE @FM TO ITEM_SEPARATOR IN R.DICT
        R.EVAL<-1> = R.DICT
    REPEAT

    DELETE F.DICT, DICT.ID
    CLOSE F.DICT

    WRITE R.EVAL TO F.JOB.LIST, ID.JOB : "-EVAL"

    RETURN

************************************************
PREPARE.ID.LIST:
* IN: TABLE.NAME, ID.LIST
************************************************
    IF TABLE.NAME[1, 1] <> "@" THEN
        GOSUB OPEN.TABLE
    END ELSE
        F.SOURCE = ""
    END

    GOSUB START.DATA.OUT

    SELECTED.RECORDS = 0

    IF ID.LIST = "" THEN ENTIRE.FILE = 1
    ELSE ENTIRE.FILE = 0

    IF ENTIRE.FILE = 1 THEN SELECT F.SOURCE

    BATCH.COUNT = 0
    ID.PREFIX = ID.JOB

    LOOP
        IF ENTIRE.FILE = 1 THEN
            READNEXT ID.SOURCE ELSE BREAK
        END ELSE
            READNEXT ID.SOURCE FROM ID.LIST ELSE BREAK
        END

        SELECTED.RECORDS++

        IF BATCH.COUNT = 0 THEN
            JOB.LIST.ID = ID.PREFIX:"#":SELECTED.RECORDS
        END

        BATCH.DIM(++BATCH.COUNT) = ID.SOURCE

        IF BATCH.COUNT = BATCH.SIZE THEN
            MATBUILD BATCH.REC FROM BATCH.DIM
            WRITE BATCH.REC TO F.JOB.LIST, JOB.LIST.ID
            BATCH.COUNT = 0

            CRT JOB.LIST.ID
        END

        IF MAX.ROWS > 0 AND SELECTED.RECORDS >= MAX.ROWS THEN
            BREAK
        END
    REPEAT

    IF BATCH.COUNT > 0 THEN
        MATBUILD BATCH.REC FROM BATCH.DIM, 1, BATCH.COUNT
        WRITE BATCH.REC TO F.JOB.LIST, JOB.LIST.ID

        CRT JOB.LIST.ID
    END

    GOSUB END.DATA.OUT

    IF F.SOURCE <> "" THEN CLOSE F.SOURCE         ;* it's empty in case of stored procedure

    RETURN

************************************************
SELECT.ENTIRE.FILE:
************************************************
    GOSUB OPEN.TABLE

    GOSUB START.DATA.OUT

    SELECTED.RECORDS = 0

    SELECT F.SOURCE

    LOOP
        READNEXT ID.SOURCE ELSE BREAK

        READ R.SOURCE FROM F.SOURCE, ID.SOURCE ELSE
            ERROR.MSG = "Cannot read " : TABLE.NAME : ">" : ID.SOURCE
            GOTO ERROR.AND.RETURN
        END

        CRT ID.SOURCE : ID.SEPARATOR : R.SOURCE

        IF MAX.ROWS > 0 AND ++SELECTED.RECORDS >= MAX.ROWS THEN
            BREAK
        END
    REPEAT

    GOSUB END.DATA.OUT

    CLOSE F.SOURCE

    RETURN

************************************************
LOAD.JOB.FILE:
*IN: PARAM.2 - the job file name
*OUT: TABLE.NAME, MAX.ROWS, WITH.CONDITIONS, ORDER.BY, ID.JOB, UDF.LIST, SP.LIST, LINK.LIST, T24.COMPANY, AGG.FIELD.COUNT, EVAL.LIST
************************************************
    OPENSEQ PARAM.2 TO F.JOB ELSE
        ERROR.MSG = "Failed to open job file ":PARAM.2
        GOTO ERROR.AND.RETURN
    END

    READSEQ TABLE.NAME FROM F.JOB ELSE
        ERROR.MSG = "Failed to read job file ":PARAM.2
        GOTO ERROR.AND.RETURN
    END

    READSEQ MAX.ROWS FROM F.JOB ELSE
        MAX.ROWS = 0
    END

    READSEQ WITH.CONDITIONS FROM F.JOB THEN
        IF WITH.CONDITIONS <> "" THEN WITH.CONDITIONS = "WITH " : WITH.CONDITIONS
    END
    ELSE
        WITH.CONDITIONS = ""
    END

    READSEQ ORDER.BY FROM F.JOB ELSE
        ORDER.BY = ""
    END

    READSEQ ID.JOB FROM F.JOB ELSE
        ID.JOB = ""
    END

    READSEQ UDF.LIST FROM F.JOB ELSE
        UDF.LIST = ""
    END

    READSEQ SP.LIST FROM F.JOB ELSE
        SP.LIST = ""
    END

    READSEQ LINK.LIST FROM F.JOB ELSE
        LINK.LIST = ""
    END

    READSEQ T24.COMPANY FROM F.JOB ELSE
        T24.COMPANY = ""
    END

    READSEQ AGG.FIELDS FROM F.JOB THEN
        GOSUB PARSE.AGG.FIELDS
    END ELSE
        AGG.FIELD.COUNT = 0
    END
    
    READSEQ EVAL.LIST FROM F.JOB ELSE
        EVAL.LIST = ""
    END

    CLOSESEQ F.JOB

    OSDELETE PARAM.2

    RETURN

************************************************
PARSE.AGG.FIELDS:
*IN: AGG.FIELDS
*OUT: AGG.FIELD.COUNT, AGG.TYPES, AGG.FIELD.NO
************************************************
    AGG.FIELD.COUNT = DCOUNT(AGG.FIELDS, ",")

    FOR AGG.I = 1 TO AGG.FIELD.COUNT
        AGG.INFO = AGG.FIELDS[",", AGG.I, 1]
        AGG.TYPES(AGG.I) = AGG.INFO[1, 1]
        AGG.FIELD.NO(AGG.I) = AGG.INFO[2, 99]
    NEXT AGG.I

    RETURN

************************************************
PRINT.SELECT.RESULT:
*IN: ID.LIST, TABLE.NAME, RET.CODE
************************************************
    GOSUB OPEN.TABLE

    GOSUB START.DATA.OUT

    IF RET.CODE<1, 3> = "QLNUMSEL" THEN
        REC.COUNT = RET.CODE<1, 2>
    END ELSE
        REC.COUNT = DCOUNT(ID.LIST, @FM)
    END

    LOOP
        READNEXT ID.SOURCE FROM ID.LIST ELSE BREAK

        READ R.SOURCE FROM F.SOURCE, ID.SOURCE ELSE
            ERROR.MSG = "Cannot read ":TABLE.NAME:">":ID.SOURCE
            GOTO ERROR.AND.RETURN
        END

        CRT ID.SOURCE : ID.SEPARATOR : R.SOURCE
    REPEAT

    GOSUB END.DATA.OUT

    CLOSE F.SOURCE

    RETURN

************************************************
GET.RECORD:
* PARAM.2 - table name;
* PARAM.3 - record id
************************************************
    IF PARAM.2 = "F.STANDARD.SELECTION" AND PARAM.3[1, 1] = "@" THEN
        GOSUB GET.SP.FIELDS
        RETURN
    END

    OPEN PARAM.2 TO F.FILE ELSE
        ERROR.MSG = 'Invalid table name "' : PARAM.2 : '"'
        GOTO ERROR.AND.RETURN
    END

    GOSUB START.DATA.OUT

    READ R.RECORD FROM F.FILE, PARAM.3 THEN
        CRT PARAM.3 : ID.SEPARATOR : R.RECORD
    END

    GOSUB END.DATA.OUT

    CLOSE F.FILE

    RETURN

************************************************
GET.SP.FIELDS:
* PARAM.3 - stored procedure name
************************************************
    SP.NAME = PARAM.3[2, 99]

    CALL @SP.NAME("FIELDS", SP.FIELDS, "")

    IF INDEX(SP.FIELDS<1>, ";", 1) > 0 THEN       ;* if it's in format <field name>;<field format>
        SP.SS.RECORD = ""
        SP.I = 0
* build stored procedure's SS record
        LOOP
            READNEXT SP.INFO FROM SP.FIELDS ELSE BREAK

            SP.SS.RECORD<SSL.SYS.FIELD.NAME, -1> = SP.INFO[";", 1, 1]
            SP.SS.RECORD<SSL.SYS.TYPE, -1> = "D"
            SP.SS.RECORD<SSL.SYS.FIELD.NO, -1> = ++SP.I
            SP.SS.RECORD<SSL.SYS.DISPLAY.FMT, -1> = SP.INFO[";", 2, 1]
            SINGLE.MULTI = SP.INFO[";", 3, 1]
            IF SINGLE.MULTI <> "M" THEN SINGLE.MULTI = "S"
            SP.SS.RECORD<SSL.SYS.SINGLE.MULT, -1> = SINGLE.MULTI
        REPEAT
    END ELSE        ;* otherwise it's SS record itself
        SP.SS.RECORD = SP.FIELDS
    END

    SP.SS.RECORD = SP.NAME : ID.SEPARATOR : SP.SS.RECORD

    GOSUB START.DATA.OUT
    CRT SP.SS.RECORD
    GOSUB END.DATA.OUT

    RETURN

************************************************
GET.TABLE.LIST:
************************************************
    IF NOT(GETENV("JEDIFILENAME_MD", VOC.FILE.NAME)) THEN
        VOC.FILE.NAME = "VOC"
    END

    OPEN VOC.FILE.NAME TO F.VOC ELSE
        ABORT 201, VOC.FILE.NAME
    END

    SELECT.STR = "SELECT " : VOC.FILE.NAME : " WITH *A0 LIKE 'F...' AND (*A1 LIKE 'F...' 'Q' 'TABLE') AND (*A2 <> '') BY *A0"

    EXECUTE SELECT.STR RTNLIST FILE.LIST

    GOSUB START.DATA.OUT

    LOOP
        READNEXT FILE.NAME FROM FILE.LIST ELSE BREAK

        READ R.VOC FROM F.VOC, FILE.NAME ELSE CONTINUE

        IF R.VOC<2> = "" THEN CONTINUE

        IF R.VOC<2> = "XMLMSSQLInit" AND R.VOC<4> = "" THEN CONTINUE  ;* skip invalid tables

        CRT FILE.NAME
    REPEAT

    GOSUB END.DATA.OUT

    CLOSE F.VOC

    RETURN

************************************************
START.DATA.OUT:
************************************************
    CRT START.FLAG

    RETURN

************************************************
END.DATA.OUT:
************************************************
    CRT END.FLAG

    RETURN

************************************************
PARSE.PARAMS:
* OUT: PARAM.1, PARAM.2, ..., PARAM.N
************************************************
    PARAM.1 = COMMAND[" ", 1, 1]
    CHANGE "%20" TO " " IN PARAM.1
    PARAM.2 = COMMAND[" ", 2, 1]
    CHANGE "%20" TO " " IN PARAM.2
    PARAM.3 = COMMAND[" ", 3, 1]
    CHANGE "%20" TO " " IN PARAM.3

    RETURN

************************************************
OPEN.TABLE:
*IN: TABLE.NAME
*OUT: F.SOURCE
************************************************
*TODO: cache opened file to avoid opening on every batch read

    OPEN TABLE.NAME TO F.SOURCE ELSE
        ERROR.MSG = "Invalid table name ":TABLE.NAME
        GOTO ERROR.AND.RETURN
    END

    RETURN

************************************************
ERROR.AND.RETURN:
* IN: ERROR.MSG
************************************************
    CHANGE @FM TO " " IN ERROR.MSG
    CHANGE @VM TO " " IN ERROR.MSG
    CHANGE @SM TO " " IN ERROR.MSG
    
    CRT CHAR(10):ERROR.FLAG:" ":ERROR.MSG

    GOSUB END.DATA.OUT

    COMMAND.FAILED = 1

    RETURN

END
