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:
| Macro | Set when | Value |
|---|---|---|
%Auth:UserName% | After successful login | The user name as authenticated |
%Auth:DatabaseName% | After successful login | Raw 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 login | User-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:
| Dialect | Expansion |
|---|---|
| ANSI / PostgreSQL / Firebird | current_date |
| SQL Server | getdate() |
Example (computes how old a row is, in days, against the server clock):
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:
| Dialect | Expansion |
|---|---|
| ANSI / PostgreSQL / Oracle | FROM DUAL |
| Firebird | FROM RDB$DATABASE |
| SQL Server | empty (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 Server | 1 | 0 |
| Oracle | 1 | 0 |
| PostgreSQL | TRUE | FALSE |
| Firebird 3+ (default) | TRUE | FALSE |
Example (filter active rows in a SQL fragment that must work on every dialect):
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:
| Dialect | Expansion (for SECOND) |
|---|---|
| SQL Server | DATEDIFF(second, expr1, expr2) |
| PostgreSQL | EXTRACT(EPOCH FROM (expr2 - expr1)) |
| Firebird | DATEDIFF(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:
DAYassumes the operands aredatecolumns and emits a plain(expr2 - expr1)(integer days).SECOND,MILLISECOND,MINUTE,HOURassume the operands aretimeortimestampcolumns and useEXTRACT(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):
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))
endArgument 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:
| Dialect | Expansion |
|---|---|
| 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) |
| Firebird | DATEADD(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):
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:
| Dialect | Expansion (for YEAR) |
|---|---|
| SQL Server | DATEPART(year, expr) |
| PostgreSQL | EXTRACT(YEAR FROM expr) |
| Firebird | EXTRACT(YEAR FROM expr) |
| Oracle | EXTRACT(YEAR FROM expr) |
| MySQL | EXTRACT(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):
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:
ToolViews:
PDFReport:
DisplayLabel: Activity report PDF
Controller: MergePDFTool
ClientFileName: Activity_{Description}.pdf # matches DESCRIPTION
BaseFileName: %APP_PATH%ReportTemplates\ActivityReport.pdf
RequireSelection: TrueExample — text expression in a PDF merge layout:
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:
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);