Skip to content

Kittox allows you to put macros in yaml files. A macro is a symbol that is later expanded to (meaning replaced with) its actual meaning. Macros can be used in place of variables, to make certain parts of your definitions dynamic or context-sensitive. For example, if you wanted to display the current date you would use the %DATE% macro. If you needed the name of the currently authenticated user, you would use %Auth:UserName%.

Predefined macros

A number of macros are predefined, meaning available by default in every Kittox application. Have a look in the reference guide for a list.

Config macros

You can access the value of any key in Config.yaml (or whatever config file was used to start the application) through the syntax %Config:Path/To/Value%. Slashes are used to descend the yaml hierarchy.

This is useful to set two config keys to the same value without wirting it twice, or to set a view property in terms of a config setting.

Auth macros

All authentication data is available as macros in the Auth: namespace. Most common among them are %Auth:UserName% and %Auth:Password%, but you can add your own values by customizing the auth definition in the config file.

A few macros are populated automatically by the framework:

MacroSet whenValue
%Auth:UserName%After successful loginThe user name as authenticated
%Auth:DatabaseName%After successful loginRaw config name of the database connection currently active for this session — either the user's choice from the Environment combo or the configured DefaultDatabaseName (e.g. FireDAC_PostgreSQL)
%Auth:Environment%After successful loginUser-friendly label of the active database, taken from Databases/<Name>/DisplayLabel if defined, falling back to %Auth:DatabaseName% otherwise. Matches the text shown in the login combo, so it is the right macro for the StatusBar or dialog titles

Custom columns returned by a ReadUserCommandText query are also exposed under the Auth: namespace — see Custom User Columns.

Database macros

Macros in the DB. namespace are expanded by the current DB adapter (more precisely, by the DB engine type associated to the current connection: TEFSQLServerDBEngineType, TEFPostgreSQLDBEngineType, TEFFirebirdDBEngineType, TEFOracleDBEngineType). This lets you write a single SQL expression that automatically translates to the right dialect at runtime.

The expansion happens transparently before the SQL command is sent to the database, both for FireDAC, ADO and DBExpress access layers.

%DB.CURRENT_DATE%

Expands to the dialect-specific function returning the current date/time:

DialectExpansion
ANSI / PostgreSQL / Firebirdcurrent_date
SQL Servergetdate()

Example (computes how old a row is, in days, against the server clock):

yaml
  Expression: |
    case
      when %DB.CURRENT_DATE% - PARTY.PARTY_DATE < 0 then cast('Future' as varchar(10))
      when %DB.CURRENT_DATE% - PARTY.PARTY_DATE = 0 then cast('Today' as varchar(10))
      when %DB.CURRENT_DATE% - PARTY.PARTY_DATE = 1 then cast('Yesterday' as varchar(10))
      when %DB.CURRENT_DATE% - PARTY.PARTY_DATE between 2 and 7 then cast('Last Week' as varchar(10))
      when %DB.CURRENT_DATE% - PARTY.PARTY_DATE between 8 and 30 then cast('Last Month' as varchar(10))
      else cast('Older' as varchar(10))
    end

%DB.FROM_DUAL%

Expands to the dialect-specific dummy FROM clause needed by SELECT statements that do not query a real table:

DialectExpansion
ANSI / PostgreSQL / OracleFROM DUAL
FirebirdFROM RDB$DATABASE
SQL Serverempty (SQL Server allows SELECT without FROM)

%DB.TRUE% and %DB.FALSE%

Expand to the dialect-specific literal for a boolean true/false value. Useful when a YAML SQL fragment compares a column declared as Boolean in the model: PostgreSQL and Firebird 3+ store it as a native boolean, while SQL Server and Oracle store it as a smallint / NUMBER(1) (PostgreSQL does not allow comparing a boolean column to 1 or 0 — it has no implicit boolean ↔ integer cast).

Dialect%DB.TRUE%%DB.FALSE%
SQL Server10
Oracle10
PostgreSQLTRUEFALSE
Firebird 3+ (default)TRUEFALSE

Example (filter active rows in a SQL fragment that must work on every dialect):

yaml
Auth: TasKitto
  ReadUserCommandText: |
    select USER_NAME, PASSWORD_HASH, EMAIL_ADDRESS, MUST_CHANGE_PASSWORD, FIRST_NAME, LAST_NAME
    from KITTO_USERS
    where IS_ACTIVE = %DB.TRUE% and UPPER(USER_NAME) = UPPER(:USER_NAME)

%DB.DATEDIFF(unit, expr1, expr2)%

Computes (expr2 - expr1) in the given unit. Supported units are MILLISECOND, SECOND (default), MINUTE, HOUR, DAY (case-insensitive). Translation per dialect:

DialectExpansion (for SECOND)
SQL ServerDATEDIFF(second, expr1, expr2)
PostgreSQLEXTRACT(EPOCH FROM (expr2 - expr1))
FirebirdDATEDIFF(SECOND FROM expr1 TO expr2)
Oracle((expr2 - expr1) * 86400)

PostgreSQL: operand-type assumptions

PostgreSQL produces different result types from - depending on operand types: time - time and timestamp - timestamp yield an interval, but date - date yields an integer (number of days). The macro's PostgreSQL expansion follows these conventions:

  • DAY assumes the operands are date columns and emits a plain (expr2 - expr1) (integer days).
  • SECOND, MILLISECOND, MINUTE, HOUR assume the operands are time or timestamp columns and use EXTRACT(EPOCH FROM (expr2 - expr1)).

For unusual combinations (e.g. SECOND between two date columns, or DAY between two timestamp columns) wrap the operands in an explicit CAST inside the YAML expression.

Example (computes activity duration in hours):

yaml
  DURATION: Float
    DisplayWidth: 8
    Expression: |
      case
        when ({Q}START_TIME is null) or ({Q}END_TIME is null) then 0
        else CAST(%DB.DATEDIFF(SECOND, {Q}START_TIME, {Q}END_TIME)% / 3600.0 AS DECIMAL(8,4))
      end

Argument parsing

The macro is parsed with a regular expression. The unit argument must be a single word (e.g. SECOND). The two expression arguments must not contain commas at the top level (column references, simple arithmetic and function calls without comma-separated arguments are fine).

%DB.DATETIME_FROM(date_expr, time_expr)%

Combines a DATE column and a TIME column into a single DATETIME/TIMESTAMP value. Translation per dialect:

DialectExpansion
SQL Server(CAST(date_expr AS datetime) + CAST(time_expr AS datetime))
PostgreSQL(date_expr + time_expr) (the + operator on date + time already returns timestamp)
FirebirdDATEADD(MILLISECOND, DATEDIFF(MILLISECOND FROM '00:00:00' TO time_expr), CAST(date_expr AS TIMESTAMP))
Oracle(TRUNC(date_expr) + (time_expr - TRUNC(time_expr))) (assumes time_expr is a DATE with arbitrary day)

Example (calendar event start/end built from a date column plus two time columns):

yaml
Fields:
  StartDate: DateTime
    Expression: %DB.DATETIME_FROM({Q}ACTIVITY_DATE, {Q}START_TIME)%
  EndDate: DateTime
    Expression: %DB.DATETIME_FROM({Q}ACTIVITY_DATE, {Q}END_TIME)%

%DB.EXTRACT(unit, expr)%

Extracts a date/time component (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, etc.) from a date/time expression. Needed because SQL Server has no EXTRACT built-in; the rest of the SQL world (PostgreSQL, Firebird, Oracle, MySQL) supports the ANSI form. Translation per dialect:

DialectExpansion (for YEAR)
SQL ServerDATEPART(year, expr)
PostgreSQLEXTRACT(YEAR FROM expr)
FirebirdEXTRACT(YEAR FROM expr)
OracleEXTRACT(YEAR FROM expr)
MySQLEXTRACT(YEAR FROM expr)

The unit is normalized to uppercase for EXTRACT (YEAR/MONTH/DAY/HOUR/MINUTE/SECOND) and lowercase for DATEPART (year/month/...) — both engines accept the unit as an unquoted identifier in their respective syntax.

Example — "Current Month" / "Current Year" filter buttons (TasKitto Activities Period filter):

yaml
List: Period
  Items:
    CurrMonth: Current Month
      Expression: |
        %DB.EXTRACT(month, ACTIVITY_DATE)% = %DB.EXTRACT(month, %DB.CURRENT_DATE%)%
        and %DB.EXTRACT(year, ACTIVITY_DATE)% = %DB.EXTRACT(year, %DB.CURRENT_DATE%)%
    CurrYear: Current Year
      Expression: %DB.EXTRACT(year, ACTIVITY_DATE)% = %DB.EXTRACT(year, %DB.CURRENT_DATE%)%

Nesting %DB.CURRENT_DATE% inside %DB.EXTRACT(...)%

The expansion of %DB.CURRENT_DATE% runs before the EXTRACT regex pass in TEFDBEngineType.ExpandCommandText, so the % characters of the inner macro do not interfere with the EXTRACT pattern. The example above expands to DATEPART(year, getdate()) on SQL Server and EXTRACT(YEAR FROM current_date) on PostgreSQL/Firebird/Oracle.


These are special macros that are only valid inside SQL command texts.


Record field macros — {FieldName}

Tool controllers and certain expression-bearing config nodes (ClientFileName, PersistentFileName, layout Expression nodes, PDF merge layout, ecc.) accept the {FieldName} syntax to expand to the value of a field on the current record. The expansion is performed by TKRecord.ExpandExpression, which iterates the record fields and substitutes each {FieldName} with the field's value as string.

The lookup is case-insensitive: {Description}, {DESCRIPTION} and {description} all match a field declared as DESCRIPTION in the model YAML. This is consistent with the rest of the framework — TEFTree.FindChild / FindField / FindNode all use SameText, so two field names differing only in case can't usefully coexist.

Example — PDF report filename built from the activity description:

yaml
ToolViews:
  PDFReport:
    DisplayLabel: Activity report PDF
    Controller: MergePDFTool
      ClientFileName: Activity_{Description}.pdf      # matches DESCRIPTION
      BaseFileName: %APP_PATH%ReportTemplates\ActivityReport.pdf
      RequireSelection: True

Example — text expression in a PDF merge layout:

yaml
Items:
  Text:
    Expression: Activity: {Description} on %DATETIME%
    XPos: 10
    YPos: 600

{FieldName} is distinct from {Q}FieldName used in SQL Expression: nodes — that's the SQL-side qualifier prefix that the framework substitutes with the table alias before sending the query to the database.

Adding custom macros

You can add custom macros in your application by creating and registering a macro expander class. A macro expander is a class inherited from EF.Macros.TEFMacroExpander. Use the following (which is a real pre-defined macro expander, probably the simplest one) as an example and code template:

pascal
uses
  EF.Macros;

type
  TEFEntityMacroExpander = class(TEFMacroExpander)
  strict protected
    function InternalExpand(const AString: string): string; override;
  end;

implementation

{ TEFEntityMacroExpander }

function TEFEntityMacroExpander.InternalExpand(const AString: string): string;
begin
  Result := inherited InternalExpand(AString);

  Result := ExpandMacros(Result, '%TAB%', #9);
  Result := ExpandMacros(Result, '%SPACE', ' ');
end;

initialization
  TEFMacroExpansionEngine.Instance.AddExpander(TEFEntityMacroExpander.Create);

Released under Apache License, Version 2.0.