SIMPOL Documentation

Working with report1

The report1 type is used as the basis for all three engines. In essence, the other two are variations and enhancements to the core report engine. The quickreport1 and graphicreport1 types each contain a report1 type, but their implementation is primarily about different ways of dealing with the output of the report. It is entirely possible to create other report engine wrappers that handle output in other ways, though it is probably easiest to extend the quickreport1 type to deal with them, since it is already prepared for output to CSV and HTML format as well as sending the output to the clipboard, in addition to print and print preview.

This section will go into some detail regarding the design and usage of the report1 core engine.

The Design of report1

The suite of data types that make up the core report engine consists of:

Basic report types
  • report1

  • report1aggregate

  • report1aggregatevalue

  • report1group

  • report1groupinst

  • report1inst

In practice, these represent three pairs of types: report1 and report1inst, report1aggregate and report1aggregatevalue, and report1group and report1groupinst.

In each case, the first of the pair is used to define the starting information, and the second is used during execution of the report to preserve current state information as it is updated and changes. More on this after we have examined each of the types.

The report1 Type

The type definition is probably the most compact way to look at these types. Here is the type definition of report1:

type report1(sqlq1, report1aggregatecontainer) export
  reference
  sqlq1 query resolve  readonly

  embed
  string orderclause   readonly
  boolean distinct     readonly
  
  event onreportstart
  event onreportend
  event onoutputrow
  
  dring groups
  dring aggregates
  
  reference
  type(*) _
  type(*) __ resolve
  type(reportoutputtarget) outputtarget
  SBLNumSettings numlocale
  SBLlocaledateinfo datelocale
  
  function addgroup         readonly
  function removegroup      readonly
  function addaggregate     readonly
  function removeaggragate  readonly
  function run              readonly
  function setorderclause   readonly
end type
  

Let's start at the beginning. The first thing we see is two type tags: sqlq1, and report1aggregatecontainer. This allows a report1 object to be assigned to any variable that has been declared as able to contain one of these two types. Following that, we see as the first thing, a sqlq1 type parameter called query, which is marked as resolve. Since it is marked that way, all of the properties and methods of the sqlq1 type will appear as part of the report1 type.

[Note]Note

Column names are case-sensitive, so when using them in various parts of the report, such as the where or order clause, make sure to use the exact name as specified in the select clause.

The next two items are the orderclause and the distinct properties. The sqlq1 carries out the query, but does not handle the ordering of the output. This is handled by the report1 engine. The name used in the orderclause must match the names used in the columns passed to the select clause. If a field name has its name changed using the AS operator, then the name following the AS operator must be used in the order clause. Sorting is done in ascending order by default. To reverse it, add the DESC key word preceded by a space following the column name. Sorting of text currently does not support any other collation order except native Unicode number, which means that lowercase letters will sort out of sequence with uppercase characters. The second property, distinct, if set ensures that if an entire output line is duplicated, that duplicates do not appear in the output. This can happen under certain circumstances with various filters and joins between tables.

Following on from there, three events are listed: onreportstart, onreportend, and onoutputrow. If defined, these events will be called at the appropriate times, as can be inferred from their names. When implementing some code that makes use of the report engine, at the very least you would want to create a handler for the onoutputrow event. This will get called each time a row is read. The function is passed the following parameters:

onoutputrow Event Handler Parameter Data Types
  1. report1

  2. report1inst

  3. array of column information

  4. array containing the current column values

  5. * optional reference if defined for the event

The first two are the report itself and the current running instance. The details about the instance will be found below. The third parameter is a 2-dimensional array that starts at 1, and in the n,0 position is the data type, and in the n,1 position is the display format for that column. The columns are in the same order as when they are passed in to the select clause of the query (these are not necessarily 1:1 with fields, since the select clause allows the use of SQL92 functions to create calculated columns). The number of columns can be retrieved using the report1.getcolumncount() method. The fourth parameter is a 1-dimensional array starting at 1 that contains the values for the current row for each of the columns. The values will be of the same data type (or a compatible one) as that of the column.

For completeness, the parameter lists of the other two events are:

onreportstart Event Handler Parameter Data Types
  1. report1

  2. report1inst

  3. * optional reference if defined for the event

onreportend Event Handler Parameter Data Types
  1. report1

  2. report1inst

  3. * optional reference if defined for the event

The next parameter is a dring called groups. This contains the ring of report1group objects that will be processed by the report. Groups are processed in the order they are added, so the outermost group will be the first one added, and the innermost group will be the last one added. For example, if you are reporting on name and address data, and grouping on city and then by surname, the city group should be added first, and then the surname group. Also, the sort order should be "city,surname" in order to get the results that are expected.

The aggregates property is also a dring that contains the report-level aggregate values to be computed, each of which is of type report1aggregate. All aggregates only work with numeric columns, except for the count aggregate which is not associated with a column at all (the column number should be set to 0). The supported aggregates currently are: sum, mean, median, mode, and count.

The outputtarget property is not used by the report1 type, since it is not actually concerned with the output at all. It is there to be used by types that deal with output.

The numlocale and datelocale properties should be passed in so as to ensure that the output is formatted correctly. If the application is using the appframework.sml library, then the application object will make these available using the exact same types, so that consistency can be assured across the application.

The usage of the methods should be pretty clear from their names. They provide a method for adding and removing groups and aggregates, setting the order clause, and running the report. There will be a large number of additional methods exposed that are part of the sqlq1 type. These include:

sqlq1 Methods of Interest Before the Report is Run
  • adddb1table() – Use this to add database tables to the report

  • setselectclause() – Call this to set the string representing the select clause

  • setwhereclause() – This establishes the filter and joins for the report

  • setdefaultformats() – It is important to add the default formats for the various data types

  • prepare() – Prepares the report to be run and checks the select and where clauses

The other methods are used while the report is running but are used by the report engine itself, so you shouldn't need to use them unless you are trying to use the sqlq1 type on its own, which is an advanced topic.

The report1aggregate Type

Aggregate values can be calculated at the report or group level. In each case they make use of the same types: report1aggregate and report1aggregateinst. The creation of an aggregate for a qualified column is quite simple, and is done the same for both report-level and group-level aggregates. The key is the first parameter to the new() method of the type. Here are the parameters to the method:

Parameters to the report1aggregate.new() Method
  1. type(report1aggregatecontainer) container

  2. function getvalue

  3. integer colno

  4. type datatype

  5. integer typeid

  6. integer error

The first two parameters must be passed, or the creation of the object will fail. In the case of the count aggregate, the colno parameter is not required (but in the quick report and graphic report versions is set to 0). In all other cases the colno parameter will also be needed, as will the datatype parameter. The typeid parameter is used by both the quick report and graphic report libraries, but is not used by this one. The final parameter is as usual, the error parameter, and should be a pre-initialized integer in order to get the value back should the object fail to be created.

The only other thing that needs to be done to use the aggregate in the report is to assign the onupdate event handler. Each time a row is read, the aggregate values need to be updated. The function assigned to this event for the specific aggregate handles doing the appropriate type of update.

The report library contains ten functions that are used together with the aggregates, five of them for providing the getval functionality and five for providing the update functionality. These are:

Aggregate Management Functions Included in the Library
  • report1_agg_getval_count()

  • report1_agg_update_count()

  • report1_agg_getval_mean()

  • report1_agg_update_mean()

  • report1_agg_getval_median()

  • report1_agg_update_median()

  • report1_agg_getval_mode()

  • report1_agg_update_mode()

  • report1_agg_getval_sum()

  • report1_agg_update_sum()

The report1aggregatevalue Type

The only place that you might encounter this type, is if you decide to implement your own aggregate value type and handler. This type is one of the parameters passed to the getval and update functions of an aggregate implementation. Unless you need to do that, you don't really need to worry about this type. Doing this is an advanced topic.

The report1group Type

In order to provide a grouping functionality within the report, we implemented the report1group type. This type contains the static definition of a group that is used in a report. This includes the two events: ongroupstart and ongroupend, the column number (colno), the name of the group (typically the column name), its data type, and if defined, any aggregate values. Aggregates work exactly the same way as with the report, and use the same type. When adding a group to a report, the addgroup() method of the report is called. To add an aggregate to a group, call the addaggregate() method of the group.

The report1groupinst Type

This type is only used by event handlers that are dealing with the ongroupstart and ongroupend events. The report1groupinst type contains the current information about this instance of the group, including its value and in the ongroupend event also the various aggregates that may have been defined for the group.

Creating a Report in Source Code

Creating a report is not particularly complicated. Using the address.sbm from the Address Book example (see the SIMPOL Quick Start Guide), a sample report can be seen in the code below. This report outputs a tab-delimited carriage-return and linefeed delimited file of the data from the selected columns.

function main()
  report1 report
  sbme1 sbmfile
  sbme1table address
  integer e, erridx
  string s, errmsg
  fsfileoutputstream fpo

  e = 0
  sbmfile =@ sbme1.new("address.sbm", error=e)
  if sbmfile =@= .nul
    s = "Error number " + .tostr(e, 10) + \
        " opening ""address.sbm""{d}{a}"
  else
    address =@ sbmfile.opentable("Address", \
               recordidfieldname="recid_ro_internal", error=e)
    if address =@= .nul
      s = "Error number " + .tostr(e, 10) + " opening the \
           ""Address"" table{d}{a}"
    else
      errmsg = ""
      erridx = 0
      report =@ report1.new()
      report.setselectclause("AddressID, FirstNames, Surname, \
        City, CountryCode", errmsg, erridx)
      report.setwhereclause("", errmsg, erridx)
      report.adddb1table(address)
      report.setorderclause("Surname")
      fpo =@ fsfileoutputstream.new("addresslist.txt", error=e)
      if fpo =@= .nul
        s = "Error number " + .tostr(e, 10) + " opening output \
             file 'addresslist.txt'{d}{a}"
      else
        report.onreportstart.function =@ \
          report1_tabbed_output_reportheader
        report.onreportstart.reference =@ fpo
        report.onoutputrow.function =@ report1_tabbed_output_row
        report.onoutputrow.reference =@ fpo

        report.run(errmsg, erridx, error=e)
        if not (errmsg > "" or e != 0)
          s = "Success!{d}{a}"
        else
          if errmsg > ""
            s = errmsg + "{d}{a}"
          else
            s = "Error number " + .tostr(e, 10) + " running \
                 report{d}{a}"
          end if
        end if
      end if
    end if
  end if
end function s


function report1_tabbed_output_reportheader(report1 report, \
  report1inst reportinst, fsfileoutputstream fpo)
              
  integer cnt, i
  string title, outline, emsg

  emsg = ""
  outline = ""
  cnt = report.getcolumncount()
  i = 1
  while i <= cnt
    title = report.getcolumntitle(i, emsg)
    if title > ""
      outline = outline + .if(i > 1, '{9}', '') + title
    else
      outline = outline + .if(i > 1, '{9}', '') + ""
    end if
    i = i + 1
  end while
  
  outline = outline + "{d}{a}"
  fpo.putstring(outline, 1) 
end function


function report1_tabbed_output_row(report1 report, report1inst \
  reportinst, array columns, array currcolvals, \
  fsfileoutputstream fpo)

  integer cnt, i
  anyvalue value
  string svalue
  string outline
  string displayformat
  type datatype

  outline = ""
  value =@ anyvalue.new()
  cnt = report.getcolumncount()
  i = 1
  while i <= cnt
    value = currcolvals[i]
    datatype =@ columns[i,0]
    displayformat = columns[i,1] 
    svalue = val2string(datatype, value, report.datelocale, \
               report.numlocale, displayformat, .false)
    if svalue > ""
      outline = outline + .if(i > 1, '{9}', '') + svalue
    else
      outline = outline + .if(i > 1, '{9}', '') + ""
    end if
    i = i + 1
  end while
  
  outline = outline + "{d}{a}"
  fpo.putstring(outline, 1) 
end function
            

The previous sample program demonstrates the use of two events to handle the initial output of the header, and then to output the data for each row. It also sorts the results according to the Surname column. As can be seen from the source code, there isn't much required to create a report using code, especially once the event handlers have been written. The two event handlers here are not specific to the data, so they can be used to output any result in tab-delimited format.

report1 Summary

In this section we have learned about the design of and how to work with the report1 type. We have also discovered that although it doesn't take much code to create a report this way, that it doesn't actual produce output unless we write it ourselves. In the next two sections, we will have no more effort, but we can get output to a window or the printer.