diff --git a/instance/emf/complex/accounting/pom.xml b/instance/emf/complex/accounting/pom.xml
new file mode 100644
index 000000000..e01cc769d
--- /dev/null
+++ b/instance/emf/complex/accounting/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.daanse
+ org.eclipse.daanse.rolap.mapping.instance.emf.complex
+ ${revision}
+
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.complex.accounting
+ ${project.artifactId}
+ ${project.artifactId}
+
+
+ org.eclipse.daanse
+
+ org.eclipse.daanse.rolap.mapping.instance.api
+
+ 0.0.1-SNAPSHOT
+
+
+
diff --git a/instance/emf/complex/accounting/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accounting/CatalogSupplier.java b/instance/emf/complex/accounting/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accounting/CatalogSupplier.java
new file mode 100644
index 000000000..d568ee20d
--- /dev/null
+++ b/instance/emf/complex/accounting/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accounting/CatalogSupplier.java
@@ -0,0 +1,1050 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.complex.accounting;
+
+import java.util.List;
+
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Column;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Schema;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Table;
+import org.eclipse.daanse.cwm.util.resource.relational.SqlSimpleTypes;
+
+import org.eclipse.daanse.rolap.mapping.instance.api.CatalogRef;
+import org.eclipse.daanse.rolap.mapping.instance.api.DocSection;
+import org.eclipse.daanse.rolap.mapping.instance.api.Kind;
+import org.eclipse.daanse.rolap.mapping.instance.api.MappingInstance;
+import org.eclipse.daanse.rolap.mapping.instance.api.Source;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescription;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescriptionSupplier;
+
+import org.eclipse.daanse.rolap.mapping.model.access.common.AccessCatalogGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.common.AccessRole;
+import org.eclipse.daanse.rolap.mapping.model.access.common.CatalogAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.common.CommonFactory;
+import org.eclipse.daanse.rolap.mapping.model.access.database.AccessDatabaseSchemaGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.database.AccessTableGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.database.DatabaseFactory;
+import org.eclipse.daanse.rolap.mapping.model.access.database.DatabaseSchemaAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.database.TableAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.AccessCubeGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.AccessHierarchyGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.AccessMemberGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.CubeAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.HierarchyAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.MemberAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.OlapFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.catalog.Catalog;
+import org.eclipse.daanse.rolap.mapping.model.catalog.CatalogFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.database.relational.OrderedColumn;
+import org.eclipse.daanse.rolap.mapping.model.database.relational.RelationalFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.source.SourceFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.source.TableSource;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackAttribute;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackTable;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.CubeFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.Kpi;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.MeasureGroup;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.PhysicalCube;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.VirtualCube;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.MeasureFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.SumMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.TextAggMeasure;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionConnector;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.NamedSet;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.StandardDimension;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.TimeDimension;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.ExplicitHierarchy;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.HierarchyFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.RollupPolicy;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.CalculatedMember;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.CalculatedMemberProperty;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.Level;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.LevelDefinition;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.LevelFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.provider.CatalogMappingSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+@MappingInstance(kind = Kind.COMPLEX, source = Source.EMF, number = "99.1.8", group = "Full Examples")
+@Component(service = { CatalogMappingSupplier.class, TutorialDescriptionSupplier.class })
+public class CatalogSupplier implements CatalogMappingSupplier, TutorialDescriptionSupplier {
+
+ private static final String CATALOG_NAME = "Accounting";
+ private static final String CUBE_IST_NAME = "AccountingIst";
+ private static final String CUBE_WB_NAME = "AccountingWb";
+ private static final String CUBE_V_NAME = "Accounting";
+
+ private static final String TABLE_BOOKING = "BOOKING";
+ private static final String TABLE_BOOKINGWB = "BOOKINGWB";
+ private static final String TABLE_ACCOUNT = "ACCOUNT";
+ private static final String TABLE_YEAR = "YEAR";
+ private static final String TABLE_ORGUNIT = "ORGUNIT";
+
+ private static final String DIM_YEAR = "Year";
+ private static final String DIM_ACCOUNT = "Account";
+ private static final String DIM_ORGUNIT = "OrgUnit";
+
+ private static final String HIGHEST_YEAR = "2027";
+
+ /**
+ * Fully-qualified unique member name for {@code setDefaultMember}. daanse
+ * resolves these against the level's {@code nameColumn}, not its key column,
+ * so the trailing segment must be the displayed name, not the database KEY.
+ */
+ private static final String DEFAULT_YEAR_MEMBER = "[Year].[Year].[" + HIGHEST_YEAR + "]";
+
+ private static final String CURRENCY_FORMAT = "#,##0 €";
+ private static final String PERCENT_FORMAT = "#,##0.00%";
+
+ /**
+ * MDX-standard "section" syntax: positive;negative. Negative values render with
+ * the literal `[Red]` token, which daanse and most other engines map to red
+ * foreground.
+ */
+ private static final String VARIANCE_FORMAT = "#,##0 €;[Red]-#,##0 €";
+ private static final String VARIANCE_PCT_FORMAT = "#,##0.00%;[Red]-#,##0.00%";
+
+ private Catalog catalog;
+ private Schema databaseSchema;
+
+ private PhysicalCube cubeIst;
+ private PhysicalCube cubeWb;
+ private VirtualCube vCube;
+ private WritebackTable writebackTable;
+ private Table writebackPhysicalTable;
+
+ private TableSource bookingSource;
+ private TableSource accountSource;
+ private TableSource yearSource;
+ private TableSource orgUnitSource;
+
+ private TimeDimension yearDimension;
+ private StandardDimension accountDimension;
+ private StandardDimension orgUnitDimension;
+
+ private ExplicitHierarchy yearHierarchy;
+ private ExplicitHierarchy accountHierarchy;
+ private ExplicitHierarchy orgUnitHierarchy;
+ private Level orgUnitLevelL1;
+ private Level orgUnitLevelL3;
+
+ private SumMeasure amountIstMeasure;
+ private SumMeasure amountPlanMeasure;
+ private TextAggMeasure commentsMeasure;
+ // KPI and named sets temporarily disabled — the KPI status/value formulas
+ // reference measures that are not yet resolvable in the current daanse
+ // engine, and the named-set Descendants() formulas need adjusting now that
+ // the Account hierarchy is a three-level explicit hierarchy
+ // (Category → Group → Account) instead of the original parent-child shape.
+ // Re-enable once the formulas parse cleanly.
+ // private Kpi budgetUtilizationKpi;
+ // private NamedSet topExpenseAccountsSet;
+ // private NamedSet planOverrunSet;
+ // private NamedSet accountsWithoutCommentSet;
+
+ private AccessRole roleDeptA1;
+ private AccessRole roleDeptA2;
+ private AccessRole roleDeptB1;
+ private AccessRole roleDivisionA;
+ private AccessRole roleAccounting;
+ private AccessRole roleReadonly;
+
+ private static final String catalogBody = """
+ The `Accounting` catalog is a realistic financial-controlling example. It is
+ intentionally small enough to read end-to-end yet covers most modeling
+ features a real bookkeeping cube needs.
+
+ Both actual postings (`IST`) and planned postings (`PLAN`) are recorded into
+ the *same* fact table `BOOKING`, exposed as two separate `SumMeasure`s with
+ a currency format string. A third measure aggregates the per-booking
+ free-text `COMMENT` via a `TextAggMeasure`.
+
+ The cube further demonstrates:
+
+ - **Three-level `Account` (Sachkonto) dimension** — a snowflake-free
+ `ExplicitHierarchy` on a single denormalised table (`Category` →
+ `Group` → `Account`). For a worked example of writeback against a
+ true parent-child hierarchy see `tutorial.writeback.parentchild`.
+ - **`Year` time dimension** with `TIME_YEARS` semantics and a
+ `defaultMember` pinned to the highest year (`2027`).
+ - **Three-level `OrgUnit` dimension** on a single denormalised table; access
+ rights are anchored at the lowest level.
+ - **Currency-formatted measures** (`#,##0 €`) — `AmountIst` and
+ `AmountPlan` render as `1.234.567 €` in tools that respect format strings.
+ - **`Variance` and `VariancePct` calculated members** — IST vs. PLAN gap
+ in currency and percent, with `iif` guarding division by zero. Both
+ members carry a MDX-section format string that paints negative values
+ red (`[Red]` token), demonstrating conditional cell coloring.
+
+ *Note:* a `BudgetUtilization` KPI and three named sets
+ (`Top5ExpenseAccounts`, `PlanOverrun`, `AccountsWithoutComment`) are
+ kept in the source code as commented-out blocks. They are temporarily
+ disabled because their MDX formulas need adjusting to the new
+ three-level `Account` hierarchy (Category → Group → Account).
+ Re-enable them once the formulas resolve cleanly.
+ - **`BOOKINGWB` writeback table** — plan amounts and comments can be entered
+ per org-unit, account and year; IST stays read-only because the writeback
+ table simply has no `AMOUNT_IST` column.
+ - **Six access roles** — one per leaf org unit, one whole-division role, a
+ full-access accounting role, and a read-only role that explicitly hides
+ the writeback table at the schema level.
+ """;
+
+ private static final String databaseSchemaBody = """
+ The database schema contains the following tables:
+
+ - **`BOOKING`** (fact) — `BOOKING_ID`, `YEAR_KEY`, `ACCOUNT_KEY`,
+ `ORG_UNIT_KEY`, `AMOUNT_IST`, `AMOUNT_PLAN`, `COMMENT`. Both IST and
+ PLAN amounts live in the same row and are exposed as two separate
+ measures. The `COMMENT` column feeds the text-aggregator measure.
+ - **`BOOKINGWB`** (writeback target) — same dimensional key columns as
+ `BOOKING`, plus `ID` and `USER` for audit, plus `AMOUNT_PLAN` and
+ `COMMENT`. Only plan data and comments can be written; `AMOUNT_IST` has
+ no writeback column on purpose.
+ - **`ACCOUNT`** — single denormalised table with three level keys
+ and names: `L1_KEY`/`L1_NAME` (Category, e.g. `EXPENSES`),
+ `L2_KEY`/`L2_NAME` (Group, e.g. `PERSONNEL`), `L3_KEY`/`L3_NAME`
+ (leaf account, e.g. `SALARIES`). The fact table joins on
+ `BOOKING.ACCOUNT_KEY = ACCOUNT.L3_KEY`.
+ - **`YEAR`** — `YEAR_KEY`, `YEAR_NAME` (one row per business year).
+ - **`ORGUNIT`** — single denormalised table with three level keys and
+ names: `L1_KEY`/`L1_NAME`, `L2_KEY`/`L2_NAME`, `L3_KEY`/`L3_NAME`. The
+ fact table joins on the lowest level (`L3_KEY`) via `ORG_UNIT_KEY`.
+ """;
+
+ private static final String factQueryBody = """
+ The fact `TableSource` reads all columns of the `BOOKING` table. Both the
+ sum measures and the text-aggregation measure source their columns from
+ this query.
+ """;
+
+ private static final String accountDimensionBody = """
+ The `Account` (Sachkonto) dimension is a 3-level `ExplicitHierarchy`
+ on a single denormalised `ACCOUNT` table (same snowflake-free
+ pattern as `OrgUnit`). The levels are:
+
+ - **`Category`** (L1) — e.g. `Expenses`, `Revenue`.
+ - **`Group`** (L2) — e.g. `Personnel`, `Rent`, `Travel`, `Sales`.
+ - **`Account`** (L3, leaf) — e.g. `Salaries`, `Office Rent`,
+ `Flights`, `Product Sales`.
+
+ The fact table joins on the leaf level via
+ `BOOKING.ACCOUNT_KEY = ACCOUNT.L3_KEY`. Aggregations across an L2
+ or L1 member roll up the underlying leaves through the usual SQL
+ `GROUP BY` path.
+
+ *Note:* if the accounting domain calls for a variable-depth tree,
+ switch this dimension to a `ParentChildHierarchy` — the
+ `tutorial.writeback.parentchild` example demonstrates that
+ variant together with writeback.
+ """;
+
+ private static final String yearDimensionBody = """
+ `Year` is modelled as a `TimeDimension` so that MDX time-functions like
+ `Lag`, `ParallelPeriod` and `YTD` can be used against it. The single
+ `Year` level is declared with `LevelDefinition.TIME_YEARS`.
+
+ The hierarchy's `defaultMember` is set to the fully-qualified unique
+ member name `[Year].[Year].[2027]` — the highest year in the data — so
+ that, unless the user picks another year, all queries implicitly run
+ against the most recent year. The unique-name form is required;
+ passing the bare key (`"2027"`) would fail at cube initialisation with
+ `Can not find Default Member`.
+ """;
+
+ private static final String orgUnitDimensionBody = """
+ `OrgUnit` is a three-level explicit hierarchy that lives on a single
+ denormalised table. The three levels are `L1` (e.g. the company), `L2`
+ (e.g. division) and `L3` (e.g. department). The fact table joins on
+ `ORG_UNIT_KEY = ORGUNIT.L3_KEY`, i.e. on the lowest level — that is the
+ level at which postings happen.
+
+ Access rights are also pinned to the lowest level: there is one role per
+ `L3` member that grants `MemberAccess.ALL` on that member only (its
+ ancestors stay visible via `RollupPolicy.FULL`).
+ """;
+
+ private static final String measuresBody = """
+ Three stored measures are exposed by the cube:
+
+ - **`AmountIst`** — `SumMeasure` over the `AMOUNT_IST` column. Read-only.
+ `formatString = "#,##0 €"` so values render as currency in client tools
+ that honour MDX format strings.
+ - **`AmountPlan`** — `SumMeasure` over the `AMOUNT_PLAN` column. Same
+ currency format. Writeable via the writeback table.
+ - **`Comments`** — `TextAggMeasure` over the `COMMENT` column, with
+ separator `" | "` and ordering by the comment text. Aggregating
+ free-text comments across slicer selections is how the cube exposes the
+ per-cell notes to the user — every slice produces a single, readable
+ string concatenating all matching comments.
+
+ Two **calculated members** complement the stored measures (see the
+ "Calculated Members" section below). One **KPI** wraps the planning ratio
+ (see the "KPI" section), and one **named set** picks the top expense
+ accounts (see the "Named Set" section).
+ """;
+
+ private static final String currencyFormatBody = """
+ `AmountIst` and `AmountPlan` are declared with `formatString = "#,##0 €"`.
+ The format string follows the standard MDX/OLE-DB syntax:
+
+ - `#` — optional digit
+ - `,` — thousands separator
+ - `0` — required digit
+ - trailing literal `€`
+
+ Client tools (Saiku, Excel, Power BI, the daanse front-end, …) that respect
+ format strings will display `1234567` as `1.234.567 €` (locale-dependent
+ grouping). The calculated member `Variance` reuses the same format string
+ via a `CalculatedMemberProperty` named `FORMAT_STRING`; `VariancePct` uses
+ `"#,##0.00%"` to render percentages.
+ """;
+
+ private static final String calculatedMembersBody = """
+ Two calculated members compare IST against PLAN:
+
+ - **`Variance`** — `[Measures].[AmountIst] - [Measures].[AmountPlan]`.
+ Negative values indicate plan overrun, positive values indicate plan
+ underrun.
+ - **`VariancePct`** — the relative version,
+ `(IST - PLAN) / PLAN`, wrapped in
+ `iif([Measures].[AmountPlan] = 0, NULL, …)` so that cells without a plan
+ return `NULL` instead of `#DIV/0!`.
+
+ **Color formatting (negative-in-red).** Both members carry a
+ `FORMAT_STRING` property whose value uses the MDX "section" syntax
+ `positive;negative`. Each section can prefix the format with the literal
+ `[Red]` (or any colour name the engine recognises) to colour matching
+ values:
+
+ ```
+ Variance → "#,##0 €;[Red]-#,##0 €"
+ VariancePct → "#,##0.00%;[Red]-#,##0.00%"
+ ```
+
+ **Alternative: daanse-style `BACK_COLOR` / `FORE_COLOR` properties.**
+ Instead of (or in addition to) the `[Red]` token, daanse recognises
+ `BACK_COLOR=` and `FORE_COLOR=` properties appended to
+ the format string. Examples (see
+ `tutorial/cube/calculatedmember.color`):
+
+ ```
+ "$#,##0.00;BACK_COLOR=32768;FORE_COLOR=0" // green background, black text
+ "$#,##0.00;BACK_COLOR=16711680;FORE_COLOR=0" // red background, black text
+ ```
+
+ The `[Red]` syntax is the portable MDX standard and is honoured by most
+ front-ends; the `BACK_COLOR=` properties are a daanse-specific
+ extension that pivot tools render as cell shading.
+
+ Calculated members are attached to the cube via
+ `cube.getCalculatedMembers().add(...)`. They behave exactly like stored
+ measures in MDX but are evaluated on the fly — no fact-table column is
+ required.
+ """;
+
+ private static final String kpiBody = """
+ The `BudgetUtilization` KPI wraps "what fraction of the plan has actually
+ been spent" into one model element that KPI-aware clients can render with
+ a status indicator and a trend arrow.
+
+ - **`value`** — `IST / PLAN`, guarded by `iif` against a zero plan.
+ - **`goal`** — the literal `1.0` (100 % consumed = exactly on plan).
+ - **`status`** — returns `1` (good, traffic light green) when utilisation
+ is ≤ 90 %, `0` (warning, yellow) when between 90 % and 100 %, and `-1`
+ (bad, red) when the plan is over-consumed. Pure MDX, no separate measure
+ required.
+ - **`trend`** — points at `[Measures].[Variance]` so KPI-aware clients can
+ show the absolute over/under-spending alongside the ratio.
+ - **`displayFolder`** — `"KPIs"` keeps it grouped in the client's folder
+ view.
+ - **`statusGraphic`** — `"Traffic Light"`.
+ - **`trendGraphic`** — `"Standard Arrow"`.
+
+ KPIs attach to the cube via `cube.getKpis().add(...)`.
+ """;
+
+ private static final String namedSetBody = """
+ Three named sets ship with the cube, grouped under the `"Analysis"`
+ display folder:
+
+ **`Top5ExpenseAccounts`** — five most expensive leaf accounts under the
+ `EXPENSES` root, ranked by `AmountIst`:
+
+ ```mdx
+ TopCount(
+ Descendants([Account].[Account].[EXPENSES], [Account].[Account]),
+ 5,
+ [Measures].[AmountIst])
+ ```
+
+ **`PlanOverrun`** — every account whose actual spending exceeds its plan
+ in the current slicer:
+
+ ```mdx
+ Filter(
+ Descendants([Account].[Account].[All Accounts], [Account].[Account]),
+ [Measures].[AmountIst] > [Measures].[AmountPlan])
+ ```
+
+ Because named-set formulas are re-evaluated against the current slicer,
+ picking a different `Year` or `OrgUnit` automatically refreshes
+ the list of overrun accounts.
+
+ **`AccountsWithoutComment`** — accounts where no booking carried a
+ comment. Useful for "did the controller forget to annotate?" reports:
+
+ ```mdx
+ Filter(
+ Descendants([Account].[Account].[All Accounts], [Account].[Account]),
+ IsEmpty([Measures].[Comments]))
+ ```
+
+ `IsEmpty` returns true when the `TextAggMeasure` produced no aggregated
+ text for the cell.
+
+ All three sets attach to the cube via `cube.getNamedSets().addAll(...)`.
+ Clients can reference them in MDX as `[Top5ExpenseAccounts]`,
+ `[PlanOverrun]` and `[AccountsWithoutComment]`.
+ """;
+
+ private static final String writebackBody = """
+ The `BOOKINGWB` writeback table lets users enter planned amounts and free
+ comments while preserving IST data as read-only.
+
+ For every dimensional foreign key on the fact, the writeback table has a
+ matching column wired through a `WritebackAttribute`:
+
+ | Cube dimension | Writeback column |
+ |---|---|
+ | `Year` | `YEAR_KEY` |
+ | `Account` | `ACCOUNT_KEY` |
+ | `OrgUnit` | `ORG_UNIT_KEY` |
+
+ Two `WritebackMeasure` entries describe the writeable measures:
+
+ - `AmountPlan` → `AMOUNT_PLAN`
+ - `Comments` → `COMMENT`
+
+ `AMOUNT_IST` deliberately has no writeback mapping. The extra `ID` and
+ `USER` columns are technical bookkeeping columns that the writeback engine
+ populates with the row id and the current user.
+
+ **Note on the model package.** `WritebackMeasure` lives in the
+ `olap/cube/measure/` ecore package alongside `SumMeasure`,
+ `TextAggMeasure` and the other base measures — it is conceptually a
+ measure (named by its logical cube-measure name, paired with a
+ database column for persistence). Only the writeback *infrastructure*
+ (`WritebackTable`, `WritebackAttribute`) remains in
+ `database/writeback/`. In Java that means
+ `MeasureFactory.eINSTANCE.createWritebackMeasure()` (not
+ `WritebackFactory`), and the XMI tag carries the `rolapmeas:`
+ namespace prefix.
+
+ **`Comments` writeback specifically.** Because `Comments` is a
+ `TextAggMeasure`, the daanse runtime detects its character bind type
+ automatically (no extra declaration on the model). The
+ `[Measures].[Comments]` cell is written as a single row at the
+ cell's exact coordinates — allocation is skipped — and the
+ read-side `ListAggAggregator` aggregates the new comment alongside
+ any fact-table comments via the standard SQL `LISTAGG` path.
+ """;
+
+ private static final String rolesBody = """
+ Six roles cover the typical org-rights matrix:
+
+ - **`role_dept_A1`**, **`role_dept_A2`**, **`role_dept_B1`** — one role
+ per L3 org unit ("I am a department head, I only see my own
+ department"). Each role grants:
+ - `CatalogAccess.ALL_DIMENSIONS` on the catalog,
+ - `DatabaseSchemaAccess.ALL` on the database schema,
+ - `CubeAccess.ALL` on the `Accounting` cube,
+ - `HierarchyAccess.CUSTOM` on the `OrgUnit` hierarchy with
+ `RollupPolicy.FULL`, `topLevel = bottomLevel = L3`, and one
+ `AccessMemberGrant` granting `MemberAccess.ALL` on the matching
+ L3 member only.
+ `RollupPolicy.FULL` keeps the parent (`L1`/`L2`) totals visible while
+ hiding sibling departments.
+ - **`role_division_A`** — whole-subtree access ("I am the head of
+ Division A, I see Division A and all its departments"). The
+ `AccessMemberGrant` targets the L2 member
+ `[OrgUnit].[OrgUnit].[Company].[Division A]`, which automatically includes
+ its descendants. `topLevel = L1` and `bottomLevel = L3` keep all three
+ levels navigable.
+ - **`role_accounting`** — central accounting role with full access to the
+ catalog, schema and cube. No hierarchy restriction.
+ - **`role_readonly`** — full read access on the cube, but the writeback
+ table `BOOKINGWB` is hidden via an `AccessTableGrant(TableAccess.NONE)`
+ wrapped inside an `AccessDatabaseSchemaGrant(DatabaseSchemaAccess.CUSTOM)`.
+ The access model does not have a dedicated "writeback" flag; restricting
+ the underlying physical table is the recommended workaround and is the
+ pattern the application layer can use to disable the writeback UI.
+ """;
+
+ private static final String divisionRoleBody = """
+ `role_division_A` demonstrates **whole-subtree access**: a division head
+ should see their division plus every department in it, but not other
+ divisions.
+
+ The trick is that `AccessMemberGrant` with `MemberAccess.ALL` on a
+ non-leaf member implicitly grants access to *all descendants* of that
+ member. So a single grant on
+ `[OrgUnit].[OrgUnit].[Company].[Division A]` covers both
+ `[Department A1]` and `[Department A2]`. Setting `topLevel = L1` and
+ `bottomLevel = L3` on the `AccessHierarchyGrant` keeps all three levels
+ navigable so the user can still drill from `[Company]` down to
+ `[Division A]` down to its departments.
+
+ **Important:** member names are taken from the level's `nameColumn` (the
+ display value, e.g. `"Department A1"`), not from the database `KEY`
+ column (e.g. `"DEPT_A1"`). Passing the bare key in the unique-name path
+ would fail with `MemberNotFoundException`.
+ """;
+
+ private static final String readonlyRoleBody = """
+ The access model has no dedicated "writeback allowed/denied" attribute.
+ To express *"can read everything, cannot write back"* the cleanest
+ pattern is:
+
+ 1. Grant the cube with `CubeAccess.ALL` (full read).
+ 2. Grant the database schema with `DatabaseSchemaAccess.CUSTOM` and add a
+ single `AccessTableGrant(TableAccess.NONE)` for the `BOOKINGWB`
+ physical table.
+
+ Hiding the writeback target at the schema level is what the application
+ layer can detect to disable any "save" / "submit plan" UI. Because the
+ grant only touches the writeback table, all other tables (the fact, the
+ dimensions) remain accessible.
+ """;
+
+ private static final String cubeIstBody = """
+ `AccountingIst` is the read-only `PhysicalCube` over `BOOKING`. It carries
+ **only** the `AmountIst` measure and has no `writebackTable` — actuals are
+ read but never written. All three dimensions are wired through their own
+ `DimensionConnector` instances (`yearConn1`, `accountConn1`,
+ `orgUnitConn1`), distinct from the `AccountingWb` cube's connectors but
+ pointing at the same shared `StandardDimension` instances.
+ """;
+
+ private static final String cubeWbBody = """
+ `AccountingWb` is the writeback-enabled `PhysicalCube` over the same
+ `BOOKING` fact table. It holds two measures — `AmountPlan` (numeric
+ writeback) and `Comments` (text writeback via `TextAggMeasure` /
+ `ListAggAggregator`) — and binds the `BOOKINGWB` writeback table.
+ Each cell-update on either measure produces a row in `BOOKINGWB`: a
+ numeric value for `AmountPlan` via allocation, a single per-coordinate
+ row for `Comments` via the text short-path.
+ """;
+
+ private static final String cubeVBody = """
+ `Accounting` is a `VirtualCube` that combines `AccountingIst` and
+ `AccountingWb` and exposes all three measures (`AmountIst`,
+ `AmountPlan`, `Comments`) together. It is the public-facing cube — the
+ name `Accounting` is preserved so existing client tools, MDX
+ statements and saved reports continue to work after the split.
+ `defaultMeasure` points at `AmountIst`.
+ """;
+
+ @Override
+ public Catalog get() {
+ if (catalog != null) {
+ return catalog;
+ }
+
+ databaseSchema = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE
+ .createSchema();
+
+ Column bookingIdColumn = createColumn("BOOKING_ID", SqlSimpleTypes.Sql99.integerType());
+ Column bookingYearKeyColumn = createColumn("YEAR_KEY", SqlSimpleTypes.Sql99.integerType());
+ Column bookingAccountKeyColumn = createColumn("ACCOUNT_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column bookingOrgUnitKeyColumn = createColumn("ORG_UNIT_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column bookingAmountIstColumn = createColumn("AMOUNT_IST", SqlSimpleTypes.Sql99.integerType());
+ Column bookingAmountPlanColumn = createColumn("AMOUNT_PLAN", SqlSimpleTypes.Sql99.integerType());
+ Column bookingCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+
+ Table bookingTable = createTable(TABLE_BOOKING,
+ List.of(bookingIdColumn, bookingYearKeyColumn, bookingAccountKeyColumn,
+ bookingOrgUnitKeyColumn, bookingAmountIstColumn,
+ bookingAmountPlanColumn, bookingCommentColumn));
+ databaseSchema.getOwnedElement().add(bookingTable);
+
+ Column wbIdColumn = createColumn("ID", SqlSimpleTypes.Sql99.varcharType());
+ Column wbUserColumn = createColumn("USER", SqlSimpleTypes.Sql99.varcharType());
+ Column wbYearKeyColumn = createColumn("YEAR_KEY", SqlSimpleTypes.Sql99.integerType());
+ Column wbAccountKeyColumn = createColumn("ACCOUNT_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column wbOrgUnitKeyColumn = createColumn("ORG_UNIT_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column wbAmountPlanColumn = createColumn("AMOUNT_PLAN", SqlSimpleTypes.Sql99.integerType());
+ Column wbCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+
+ writebackPhysicalTable = createTable(TABLE_BOOKINGWB,
+ List.of(wbIdColumn, wbUserColumn, wbYearKeyColumn, wbAccountKeyColumn,
+ wbOrgUnitKeyColumn, wbAmountPlanColumn, wbCommentColumn));
+ databaseSchema.getOwnedElement().add(writebackPhysicalTable);
+
+ // Account dimension is a 3-level explicit hierarchy on a single
+ // denormalised table (same shape as ORGUNIT). L1 is the top
+ // category (EXPENSES / REVENUE), L2 the account group
+ // (PERSONNEL / RENT / ...) and L3 the leaf ledger account.
+ Column accountL1KeyColumn = createColumn("L1_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column accountL1NameColumn = createColumn("L1_NAME", SqlSimpleTypes.Sql99.varcharType());
+ Column accountL2KeyColumn = createColumn("L2_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column accountL2NameColumn = createColumn("L2_NAME", SqlSimpleTypes.Sql99.varcharType());
+ Column accountL3KeyColumn = createColumn("L3_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column accountL3NameColumn = createColumn("L3_NAME", SqlSimpleTypes.Sql99.varcharType());
+
+ Table accountTable = createTable(TABLE_ACCOUNT,
+ List.of(accountL1KeyColumn, accountL1NameColumn,
+ accountL2KeyColumn, accountL2NameColumn,
+ accountL3KeyColumn, accountL3NameColumn));
+ databaseSchema.getOwnedElement().add(accountTable);
+
+ Column yearKeyColumn = createColumn("YEAR_KEY", SqlSimpleTypes.Sql99.integerType());
+ Column yearNameColumn = createColumn("YEAR_NAME", SqlSimpleTypes.Sql99.varcharType());
+
+ Table yearTable = createTable(TABLE_YEAR, List.of(yearKeyColumn, yearNameColumn));
+ databaseSchema.getOwnedElement().add(yearTable);
+
+ Column ouL1KeyColumn = createColumn("L1_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column ouL1NameColumn = createColumn("L1_NAME", SqlSimpleTypes.Sql99.varcharType());
+ Column ouL2KeyColumn = createColumn("L2_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column ouL2NameColumn = createColumn("L2_NAME", SqlSimpleTypes.Sql99.varcharType());
+ Column ouL3KeyColumn = createColumn("L3_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column ouL3NameColumn = createColumn("L3_NAME", SqlSimpleTypes.Sql99.varcharType());
+
+ Table orgUnitTable = createTable(TABLE_ORGUNIT,
+ List.of(ouL1KeyColumn, ouL1NameColumn, ouL2KeyColumn, ouL2NameColumn, ouL3KeyColumn, ouL3NameColumn));
+ databaseSchema.getOwnedElement().add(orgUnitTable);
+
+ bookingSource = SourceFactory.eINSTANCE.createTableSource();
+ bookingSource.setTable(bookingTable);
+
+ accountSource = SourceFactory.eINSTANCE.createTableSource();
+ accountSource.setTable(accountTable);
+
+ yearSource = SourceFactory.eINSTANCE.createTableSource();
+ yearSource.setTable(yearTable);
+
+ orgUnitSource = SourceFactory.eINSTANCE.createTableSource();
+ orgUnitSource.setTable(orgUnitTable);
+
+ Level yearLevel = LevelFactory.eINSTANCE.createLevel();
+ yearLevel.setName("Year");
+ yearLevel.setType(LevelDefinition.TIME_YEARS);
+ yearLevel.setColumn(yearKeyColumn);
+ yearLevel.setNameColumn(yearNameColumn);
+ yearLevel.setUniqueMembers(true);
+
+ yearHierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ yearHierarchy.setName(DIM_YEAR);
+ yearHierarchy.setHasAll(true);
+ yearHierarchy.setAllMemberName("All Years");
+ yearHierarchy.setPrimaryKey(yearKeyColumn);
+ yearHierarchy.setSource(yearSource);
+ yearHierarchy.setDefaultMember(DEFAULT_YEAR_MEMBER);
+ yearHierarchy.getLevels().add(yearLevel);
+
+ yearDimension = DimensionFactory.eINSTANCE.createTimeDimension();
+ yearDimension.setName(DIM_YEAR);
+ yearDimension.getHierarchies().add(yearHierarchy);
+
+ Level accountLevelL1 = LevelFactory.eINSTANCE.createLevel();
+ accountLevelL1.setName("Category");
+ accountLevelL1.setColumn(accountL1KeyColumn);
+ accountLevelL1.setNameColumn(accountL1NameColumn);
+ accountLevelL1.setUniqueMembers(true);
+
+ Level accountLevelL2 = LevelFactory.eINSTANCE.createLevel();
+ accountLevelL2.setName("Group");
+ accountLevelL2.setColumn(accountL2KeyColumn);
+ accountLevelL2.setNameColumn(accountL2NameColumn);
+ accountLevelL2.setUniqueMembers(false);
+
+ Level accountLevelL3 = LevelFactory.eINSTANCE.createLevel();
+ accountLevelL3.setName("Account");
+ accountLevelL3.setColumn(accountL3KeyColumn);
+ accountLevelL3.setNameColumn(accountL3NameColumn);
+ accountLevelL3.setUniqueMembers(false);
+
+ accountHierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ accountHierarchy.setName(DIM_ACCOUNT);
+ accountHierarchy.setHasAll(true);
+ accountHierarchy.setAllMemberName("All Accounts");
+ accountHierarchy.setPrimaryKey(accountL3KeyColumn);
+ accountHierarchy.setSource(accountSource);
+ accountHierarchy.getLevels().addAll(List.of(accountLevelL1, accountLevelL2, accountLevelL3));
+
+ accountDimension = DimensionFactory.eINSTANCE.createStandardDimension();
+ accountDimension.setName(DIM_ACCOUNT);
+ accountDimension.getHierarchies().add(accountHierarchy);
+
+ orgUnitLevelL1 = LevelFactory.eINSTANCE.createLevel();
+ orgUnitLevelL1.setName("L1");
+ orgUnitLevelL1.setColumn(ouL1KeyColumn);
+ orgUnitLevelL1.setNameColumn(ouL1NameColumn);
+ orgUnitLevelL1.setUniqueMembers(true);
+
+ Level orgUnitLevelL2 = LevelFactory.eINSTANCE.createLevel();
+ orgUnitLevelL2.setName("L2");
+ orgUnitLevelL2.setColumn(ouL2KeyColumn);
+ orgUnitLevelL2.setNameColumn(ouL2NameColumn);
+ orgUnitLevelL2.setUniqueMembers(false);
+
+ orgUnitLevelL3 = LevelFactory.eINSTANCE.createLevel();
+ orgUnitLevelL3.setName("L3");
+ orgUnitLevelL3.setColumn(ouL3KeyColumn);
+ orgUnitLevelL3.setNameColumn(ouL3NameColumn);
+ orgUnitLevelL3.setUniqueMembers(false);
+
+ orgUnitHierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ orgUnitHierarchy.setName(DIM_ORGUNIT);
+ orgUnitHierarchy.setHasAll(true);
+ orgUnitHierarchy.setAllMemberName("All Org Units");
+ orgUnitHierarchy.setPrimaryKey(ouL3KeyColumn);
+ orgUnitHierarchy.setSource(orgUnitSource);
+ orgUnitHierarchy.getLevels().addAll(List.of(orgUnitLevelL1, orgUnitLevelL2, orgUnitLevelL3));
+
+ orgUnitDimension = DimensionFactory.eINSTANCE.createStandardDimension();
+ orgUnitDimension.setName(DIM_ORGUNIT);
+ orgUnitDimension.getHierarchies().add(orgUnitHierarchy);
+
+ amountIstMeasure = MeasureFactory.eINSTANCE.createSumMeasure();
+ amountIstMeasure.setName("AmountIst");
+ amountIstMeasure.setColumn(bookingAmountIstColumn);
+ amountIstMeasure.setFormatString(CURRENCY_FORMAT);
+
+ amountPlanMeasure = MeasureFactory.eINSTANCE.createSumMeasure();
+ amountPlanMeasure.setName("AmountPlan");
+ amountPlanMeasure.setColumn(bookingAmountPlanColumn);
+ amountPlanMeasure.setFormatString(CURRENCY_FORMAT);
+
+ OrderedColumn commentOrderedColumn = RelationalFactory.eINSTANCE.createOrderedColumn();
+ commentOrderedColumn.setColumn(bookingCommentColumn);
+
+ commentsMeasure = MeasureFactory.eINSTANCE.createTextAggMeasure();
+ commentsMeasure.setName("Comments");
+ commentsMeasure.setColumn(bookingCommentColumn);
+ commentsMeasure.setSeparator(" | ");
+ commentsMeasure.getOrderByColumns().add(commentOrderedColumn);
+
+ MeasureGroup measureGroupIst = CubeFactory.eINSTANCE.createMeasureGroup();
+ measureGroupIst.getMeasures().add(amountIstMeasure);
+
+ MeasureGroup measureGroupWb = CubeFactory.eINSTANCE.createMeasureGroup();
+ measureGroupWb.getMeasures().addAll(List.of(amountPlanMeasure, commentsMeasure));
+
+ // KPI and named sets temporarily disabled — formulas need rework
+ // (see field declarations above).
+ //
+ // budgetUtilizationKpi = CubeFactory.eINSTANCE.createKpi();
+ // budgetUtilizationKpi.setName("BudgetUtilization");
+ // budgetUtilizationKpi.setDescription("Share of the plan that has already been consumed by actual postings, "
+ // + "evaluated per OrgUnit and Account cell.");
+ // budgetUtilizationKpi.setValue(
+ // "iif([Measures].[AmountPlan] = 0, NULL," + " [Measures].[AmountIst] / [Measures].[AmountPlan])");
+ // budgetUtilizationKpi.setGoal("1.0");
+ // budgetUtilizationKpi.setStatus("iif([Measures].[AmountPlan] = 0, 0,"
+ // + " iif([Measures].[AmountIst] / [Measures].[AmountPlan] <= 0.9, 1,"
+ // + " iif([Measures].[AmountIst] / [Measures].[AmountPlan] <= 1.0, 0, -1)))");
+ // budgetUtilizationKpi.setTrend("[Measures].[Variance]");
+ // budgetUtilizationKpi.setDisplayFolder("KPIs");
+ // budgetUtilizationKpi.setStatusGraphic("Traffic Light");
+ // budgetUtilizationKpi.setTrendGraphic("Standard Arrow");
+ //
+ // topExpenseAccountsSet = DimensionFactory.eINSTANCE.createNamedSet();
+ // topExpenseAccountsSet.setName("Top5ExpenseAccounts");
+ // topExpenseAccountsSet.setFormula("TopCount("
+ // + "Descendants([Account].[Account].[EXPENSES], [Account].[Account])," + " 5, [Measures].[AmountIst])");
+ // topExpenseAccountsSet.setDisplayFolder("Analysis");
+ //
+ // planOverrunSet = DimensionFactory.eINSTANCE.createNamedSet();
+ // planOverrunSet.setName("PlanOverrun");
+ // planOverrunSet.setFormula("Filter(" + "Descendants([Account].[Account].[All Accounts], [Account].[Account]),"
+ // + " [Measures].[AmountIst] > [Measures].[AmountPlan])");
+ // planOverrunSet.setDisplayFolder("Analysis");
+ //
+ // accountsWithoutCommentSet = DimensionFactory.eINSTANCE.createNamedSet();
+ // accountsWithoutCommentSet.setName("AccountsWithoutComment");
+ // accountsWithoutCommentSet
+ // .setFormula("Filter(" + "Descendants([Account].[Account].[All Accounts], [Account].[Account]),"
+ // + " IsEmpty([Measures].[Comments]))");
+ // accountsWithoutCommentSet.setDisplayFolder("Analysis");
+
+ // Per-cube DimensionConnectors. Both PhysicalCubes need their own
+ // connector instances pointing at the same shared StandardDimension —
+ // mirrors the pattern used in tutorial/virtualcube/dimensions.
+ DimensionConnector yearConn1 = createConnector(DIM_YEAR, yearDimension, bookingYearKeyColumn);
+ DimensionConnector accountConn1 = createConnector(DIM_ACCOUNT, accountDimension, bookingAccountKeyColumn);
+ DimensionConnector orgUnitConn1 = createConnector(DIM_ORGUNIT, orgUnitDimension, bookingOrgUnitKeyColumn);
+
+ DimensionConnector yearConn2 = createConnector(DIM_YEAR, yearDimension, bookingYearKeyColumn);
+ DimensionConnector accountConn2 = createConnector(DIM_ACCOUNT, accountDimension, bookingAccountKeyColumn);
+ DimensionConnector orgUnitConn2 = createConnector(DIM_ORGUNIT, orgUnitDimension, bookingOrgUnitKeyColumn);
+
+ // Writeback attributes target the writeback cube's connectors (Conn2).
+ WritebackAttribute wbYearAttribute = createWritebackAttribute(yearConn2, wbYearKeyColumn);
+ WritebackAttribute wbAccountAttribute = createWritebackAttribute(accountConn2, wbAccountKeyColumn);
+ WritebackAttribute wbOrgUnitAttribute = createWritebackAttribute(orgUnitConn2, wbOrgUnitKeyColumn);
+
+ WritebackMeasure wbAmountPlanMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbAmountPlanMeasure.setName("AmountPlan");
+ wbAmountPlanMeasure.setColumn(wbAmountPlanColumn);
+
+ WritebackMeasure wbCommentsMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbCommentsMeasure.setName("Comments");
+ wbCommentsMeasure.setColumn(wbCommentColumn);
+
+ writebackTable = WritebackFactory.eINSTANCE.createWritebackTable();
+ writebackTable.setName(TABLE_BOOKINGWB);
+ writebackTable.getWritebackAttribute().addAll(List.of(wbYearAttribute, wbAccountAttribute, wbOrgUnitAttribute));
+ writebackTable.getWritebackMeasure().addAll(List.of(wbAmountPlanMeasure, wbCommentsMeasure));
+
+ // Cube 1 — read-only, holds only AmountIst. No writeback table.
+ cubeIst = CubeFactory.eINSTANCE.createPhysicalCube();
+ cubeIst.setName(CUBE_IST_NAME);
+ cubeIst.setSource(bookingSource);
+ cubeIst.getDimensionConnectors().addAll(List.of(yearConn1, accountConn1, orgUnitConn1));
+ cubeIst.getMeasureGroups().add(measureGroupIst);
+
+ // Cube 2 — writeback-enabled, holds AmountPlan + Comments and binds
+ // the BOOKINGWB writeback table.
+ cubeWb = CubeFactory.eINSTANCE.createPhysicalCube();
+ cubeWb.setName(CUBE_WB_NAME);
+ cubeWb.setSource(bookingSource);
+ cubeWb.getDimensionConnectors().addAll(List.of(yearConn2, accountConn2, orgUnitConn2));
+ cubeWb.getMeasureGroups().add(measureGroupWb);
+ cubeWb.setWritebackTable(writebackTable);
+
+ // VirtualCube — combines both physical cubes and exposes all three
+ // measures together. Keeps the original public name "Accounting" so
+ // existing clients don't break.
+ vCube = CubeFactory.eINSTANCE.createVirtualCube();
+ vCube.setName(CUBE_V_NAME);
+ vCube.setDefaultMeasure(amountIstMeasure);
+ vCube.getDimensionConnectors().addAll(List.of(
+ yearConn1, accountConn1, orgUnitConn1,
+ yearConn2, accountConn2, orgUnitConn2));
+ vCube.getReferencedMeasures().addAll(List.of(amountIstMeasure, amountPlanMeasure, commentsMeasure));
+
+ // Member unique names use the level's nameColumn value (the display name),
+ // not the database KEY — same rule as setDefaultMember above. The
+ // ORGUNIT table maps DEPT_A1 → "Department A1", DIV_A → "Division A", etc.
+ roleDeptA1 = createOrgUnitRole("role_dept_A1",
+ "[OrgUnit].[OrgUnit].[Company].[Division A].[Department A1]");
+ roleDeptA2 = createOrgUnitRole("role_dept_A2",
+ "[OrgUnit].[OrgUnit].[Company].[Division A].[Department A2]");
+ roleDeptB1 = createOrgUnitRole("role_dept_B1",
+ "[OrgUnit].[OrgUnit].[Company].[Division B].[Department B1]");
+ roleDivisionA = createDivisionRole("role_division_A",
+ "[OrgUnit].[OrgUnit].[Company].[Division A]");
+ roleAccounting = createAccountingRole();
+ roleReadonly = createReadonlyRole();
+
+ catalog = CatalogFactory.eINSTANCE.createCatalog();
+ catalog.setName(CATALOG_NAME);
+ catalog.setDescription("Accounting catalog split into three cubes: "
+ + "AccountingIst (read-only, holds AmountIst), "
+ + "AccountingWb (writeback-enabled, holds AmountPlan + Comments with the BOOKINGWB writeback table), "
+ + "and Accounting (VirtualCube combining both — the public-facing cube). "
+ + "All three share the same three dimensions (Year, Account, OrgUnit) "
+ + "and read from the same BOOKING fact table. Access roles ranging from single-department "
+ + "to division-wide grant on all three cubes.");
+ catalog.getDbschemas().add(databaseSchema);
+ catalog.getCubes().addAll(List.of(cubeIst, cubeWb, vCube));
+ catalog.getAccessRoles()
+ .addAll(List.of(roleDeptA1, roleDeptA2, roleDeptB1, roleDivisionA, roleAccounting, roleReadonly));
+
+ return catalog;
+ }
+
+ @Override
+ public TutorialDescription describe() {
+ Catalog c = get();
+ return new TutorialDescription(List.of(new DocSection("Accounting Catalog", catalogBody, 1, 0, 0, null, 0),
+ new DocSection("Database Schema", databaseSchemaBody, 1, 1, 0, databaseSchema, 3),
+ new DocSection("Fact Query", factQueryBody, 1, 2, 0, bookingSource, 2),
+ new DocSection("Account (parent-child Sachkonto)", accountDimensionBody, 1, 3, 0, accountDimension, 0),
+ new DocSection("Year (TimeDimension, default = " + HIGHEST_YEAR + ")", yearDimensionBody, 1, 4, 0,
+ yearDimension, 0),
+ new DocSection("OrgUnit (three-level)", orgUnitDimensionBody, 1, 6, 0, orgUnitDimension, 0),
+ new DocSection("Measures (IST / PLAN / Comments)", measuresBody, 1, 8, 0, null, 0),
+ new DocSection("Currency Format", currencyFormatBody, 1, 9, 0, amountIstMeasure, 0),
+ // KPI and named-set DocSections disabled while their underlying
+ // model elements are commented out (bad formulas):
+ // new DocSection("KPI (BudgetUtilization)", kpiBody, 1, 11, 0, budgetUtilizationKpi, 0),
+ // new DocSection("Named Sets (Top5ExpenseAccounts, PlanOverrun, AccountsWithoutComment)", namedSetBody,
+ // 1, 12, 0, topExpenseAccountsSet, 0),
+ new DocSection("Writeback (BOOKINGWB)", writebackBody, 1, 13, 0, writebackTable, 2),
+ new DocSection("Access Roles", rolesBody, 1, 14, 0, roleAccounting, 0),
+ new DocSection("Whole-Division Role (role_division_A)", divisionRoleBody, 1, 15, 0, roleDivisionA, 2),
+ new DocSection("Read-Only Role (role_readonly)", readonlyRoleBody, 1, 16, 0, roleReadonly, 2),
+ new DocSection("Cube AccountingIst (read-only)", cubeIstBody, 1, 17, 0, cubeIst, 2),
+ new DocSection("Cube AccountingWb (writeback)", cubeWbBody, 1, 18, 0, cubeWb, 2),
+ new DocSection("Virtual Cube Accounting", cubeVBody, 1, 19, 0, vCube, 2)),
+ List.of(new CatalogRef("catalog", () -> c)));
+ }
+
+ private static Column createColumn(String name,
+ org.eclipse.daanse.cwm.model.cwm.resource.relational.SQLSimpleType type) {
+ Column c = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createColumn();
+ c.setName(name);
+ c.setType(type);
+ return c;
+ }
+
+ private static Table createTable(String name, List columns) {
+ Table t = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createTable();
+ t.setName(name);
+ t.getFeature().addAll(columns);
+ return t;
+ }
+
+ private static WritebackAttribute createWritebackAttribute(DimensionConnector connector, Column column) {
+ WritebackAttribute a = WritebackFactory.eINSTANCE.createWritebackAttribute();
+ a.setDimensionConnector(connector);
+ a.setColumn(column);
+ return a;
+ }
+
+ private static DimensionConnector createConnector(String name,
+ org.eclipse.daanse.rolap.mapping.model.olap.dimension.Dimension dim, Column foreignKey) {
+ DimensionConnector c = DimensionFactory.eINSTANCE.createDimensionConnector();
+ c.setOverrideDimensionName(name);
+ c.setDimension(dim);
+ c.setForeignKey(foreignKey);
+ return c;
+ }
+
+ /**
+ * Builds an {@link AccessCubeGrant} for the given cube with full access
+ * and (optionally) a single hierarchy restriction. Used to fan a single
+ * role out across all three cubes (Ist, Wb, virtual) after the split.
+ */
+ private AccessCubeGrant cubeGrantAll(org.eclipse.daanse.rolap.mapping.model.olap.cube.Cube targetCube,
+ AccessHierarchyGrant hierarchyGrant) {
+ AccessCubeGrant cubeGrant = OlapFactory.eINSTANCE.createAccessCubeGrant();
+ cubeGrant.setCube(targetCube);
+ cubeGrant.setCubeAccess(CubeAccess.ALL);
+ if (hierarchyGrant != null) {
+ cubeGrant.getHierarchyGrants().add(hierarchyGrant);
+ }
+ return cubeGrant;
+ }
+
+ private AccessHierarchyGrant orgUnitHierarchyGrant(String memberName, Level topLevel, Level bottomLevel) {
+ AccessMemberGrant memberGrant = OlapFactory.eINSTANCE.createAccessMemberGrant();
+ memberGrant.setMemberAccess(MemberAccess.ALL);
+ memberGrant.setMember(memberName);
+
+ AccessHierarchyGrant hierarchyGrant = OlapFactory.eINSTANCE.createAccessHierarchyGrant();
+ hierarchyGrant.setHierarchy(orgUnitHierarchy);
+ hierarchyGrant.setHierarchyAccess(HierarchyAccess.CUSTOM);
+ hierarchyGrant.setTopLevel(topLevel);
+ hierarchyGrant.setBottomLevel(bottomLevel);
+ hierarchyGrant.setRollupPolicy(RollupPolicy.FULL);
+ hierarchyGrant.getMemberGrants().add(memberGrant);
+ return hierarchyGrant;
+ }
+
+ private AccessRole createOrgUnitRole(String roleName, String memberName) {
+ AccessDatabaseSchemaGrant dbGrant = DatabaseFactory.eINSTANCE.createAccessDatabaseSchemaGrant();
+ dbGrant.setDatabaseSchemaAccess(DatabaseSchemaAccess.ALL);
+ dbGrant.setDatabaseSchema(databaseSchema);
+
+ // Each cube needs its own AccessHierarchyGrant — they aren't reusable
+ // across CubeGrants because each grant owns its hierarchy-grant child.
+ AccessCatalogGrant catalogGrant = CommonFactory.eINSTANCE.createAccessCatalogGrant();
+ catalogGrant.setCatalogAccess(CatalogAccess.ALL_DIMENSIONS);
+ catalogGrant.getCubeGrants().add(
+ cubeGrantAll(cubeIst, orgUnitHierarchyGrant(memberName, orgUnitLevelL3, orgUnitLevelL3)));
+ catalogGrant.getCubeGrants().add(
+ cubeGrantAll(cubeWb, orgUnitHierarchyGrant(memberName, orgUnitLevelL3, orgUnitLevelL3)));
+ catalogGrant.getCubeGrants().add(
+ cubeGrantAll(vCube, orgUnitHierarchyGrant(memberName, orgUnitLevelL3, orgUnitLevelL3)));
+ catalogGrant.getDatabaseSchemaGrants().add(dbGrant);
+
+ AccessRole role = CommonFactory.eINSTANCE.createAccessRole();
+ role.setName(roleName);
+ role.getAccessCatalogGrants().add(catalogGrant);
+ return role;
+ }
+
+ private AccessRole createAccountingRole() {
+ AccessDatabaseSchemaGrant dbGrant = DatabaseFactory.eINSTANCE.createAccessDatabaseSchemaGrant();
+ dbGrant.setDatabaseSchemaAccess(DatabaseSchemaAccess.ALL);
+ dbGrant.setDatabaseSchema(databaseSchema);
+
+ AccessCatalogGrant catalogGrant = CommonFactory.eINSTANCE.createAccessCatalogGrant();
+ catalogGrant.setCatalogAccess(CatalogAccess.ALL_DIMENSIONS);
+ catalogGrant.getCubeGrants().add(cubeGrantAll(cubeIst, null));
+ catalogGrant.getCubeGrants().add(cubeGrantAll(cubeWb, null));
+ catalogGrant.getCubeGrants().add(cubeGrantAll(vCube, null));
+ catalogGrant.getDatabaseSchemaGrants().add(dbGrant);
+
+ AccessRole role = CommonFactory.eINSTANCE.createAccessRole();
+ role.setName("role_accounting");
+ role.getAccessCatalogGrants().add(catalogGrant);
+ return role;
+ }
+
+ private AccessRole createDivisionRole(String roleName, String memberName) {
+ AccessDatabaseSchemaGrant dbGrant = DatabaseFactory.eINSTANCE.createAccessDatabaseSchemaGrant();
+ dbGrant.setDatabaseSchemaAccess(DatabaseSchemaAccess.ALL);
+ dbGrant.setDatabaseSchema(databaseSchema);
+
+ AccessCatalogGrant catalogGrant = CommonFactory.eINSTANCE.createAccessCatalogGrant();
+ catalogGrant.setCatalogAccess(CatalogAccess.ALL_DIMENSIONS);
+ catalogGrant.getCubeGrants().add(
+ cubeGrantAll(cubeIst, orgUnitHierarchyGrant(memberName, orgUnitLevelL1, orgUnitLevelL3)));
+ catalogGrant.getCubeGrants().add(
+ cubeGrantAll(cubeWb, orgUnitHierarchyGrant(memberName, orgUnitLevelL1, orgUnitLevelL3)));
+ catalogGrant.getCubeGrants().add(
+ cubeGrantAll(vCube, orgUnitHierarchyGrant(memberName, orgUnitLevelL1, orgUnitLevelL3)));
+ catalogGrant.getDatabaseSchemaGrants().add(dbGrant);
+
+ AccessRole role = CommonFactory.eINSTANCE.createAccessRole();
+ role.setName(roleName);
+ role.getAccessCatalogGrants().add(catalogGrant);
+ return role;
+ }
+
+ private AccessRole createReadonlyRole() {
+ AccessTableGrant denyWriteback = DatabaseFactory.eINSTANCE.createAccessTableGrant();
+ denyWriteback.setTableAccess(TableAccess.NONE);
+ denyWriteback.setTable(writebackPhysicalTable);
+
+ AccessDatabaseSchemaGrant dbGrant = DatabaseFactory.eINSTANCE.createAccessDatabaseSchemaGrant();
+ dbGrant.setDatabaseSchemaAccess(DatabaseSchemaAccess.CUSTOM);
+ dbGrant.setDatabaseSchema(databaseSchema);
+ dbGrant.getTableGrants().add(denyWriteback);
+
+ AccessCatalogGrant catalogGrant = CommonFactory.eINSTANCE.createAccessCatalogGrant();
+ catalogGrant.setCatalogAccess(CatalogAccess.ALL_DIMENSIONS);
+ catalogGrant.getCubeGrants().add(cubeGrantAll(cubeIst, null));
+ catalogGrant.getCubeGrants().add(cubeGrantAll(cubeWb, null));
+ catalogGrant.getCubeGrants().add(cubeGrantAll(vCube, null));
+ catalogGrant.getDatabaseSchemaGrants().add(dbGrant);
+
+ AccessRole role = CommonFactory.eINSTANCE.createAccessRole();
+ role.setName("role_readonly");
+ role.getAccessCatalogGrants().add(catalogGrant);
+ return role;
+ }
+}
diff --git a/instance/emf/complex/accounting/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accounting/CheckSuiteSupplier.java b/instance/emf/complex/accounting/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accounting/CheckSuiteSupplier.java
new file mode 100644
index 000000000..bfe190d55
--- /dev/null
+++ b/instance/emf/complex/accounting/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accounting/CheckSuiteSupplier.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.complex.accounting;
+
+import org.eclipse.daanse.olap.check.model.check.CatalogCheck;
+import org.eclipse.daanse.olap.check.model.check.CubeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttribute;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttributeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseSchemaCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseTableCheck;
+import org.eclipse.daanse.olap.check.model.check.DimensionCheck;
+import org.eclipse.daanse.olap.check.model.check.HierarchyCheck;
+import org.eclipse.daanse.olap.check.model.check.LevelCheck;
+import org.eclipse.daanse.olap.check.model.check.MeasureCheck;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckFactory;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckSuite;
+import org.eclipse.daanse.olap.check.model.check.OlapConnectionCheck;
+import org.eclipse.daanse.olap.check.runtime.api.OlapCheckSuiteSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Check suite for the Accounting complex mapping. Asserts that the catalog,
+ * the three cubes (AccountingIst read-only, AccountingWb writeback, and
+ * Accounting VirtualCube), all measures, dimensions/hierarchies/levels, and
+ * the full database schema (tables + column types) materialise as expected.
+ */
+@Component(service = OlapCheckSuiteSupplier.class)
+public class CheckSuiteSupplier implements OlapCheckSuiteSupplier {
+
+ private static final OlapCheckFactory factory = OlapCheckFactory.eINSTANCE;
+
+ private static final String CATALOG_NAME = "Accounting";
+ private static final String CUBE_IST_NAME = "AccountingIst";
+ private static final String CUBE_WB_NAME = "AccountingWb";
+ private static final String CUBE_V_NAME = "Accounting";
+
+ @Override
+ public OlapCheckSuite get() {
+ // The full dimension shape — surfaced on every cube.
+ DimensionCheck yearDim = createDimensionCheck("Year", createHierarchyCheck("Year", createLevelCheck("Year")));
+ DimensionCheck accountDim = createDimensionCheck("Account",
+ createHierarchyCheck("Account",
+ createLevelCheck("Category"),
+ createLevelCheck("Group"),
+ createLevelCheck("Account")));
+ DimensionCheck orgUnitDim = createDimensionCheck("OrgUnit", createHierarchyCheck("OrgUnit",
+ createLevelCheck("L1"), createLevelCheck("L2"), createLevelCheck("L3")));
+
+ // AccountingIst — read-only, holds only AmountIst.
+ CubeCheck cubeIstCheck = factory.createCubeCheck();
+ cubeIstCheck.setName("CubeCheck-" + CUBE_IST_NAME);
+ cubeIstCheck.setDescription("Read-only cube AccountingIst — AmountIst measure only, no writeback");
+ cubeIstCheck.setCubeName(CUBE_IST_NAME);
+ cubeIstCheck.getMeasureChecks().add(createMeasureCheck("AmountIst"));
+ addAllDimensions(cubeIstCheck);
+
+ // AccountingWb — writeback-enabled, holds AmountPlan + Comments.
+ CubeCheck cubeWbCheck = factory.createCubeCheck();
+ cubeWbCheck.setName("CubeCheck-" + CUBE_WB_NAME);
+ cubeWbCheck.setDescription("Writeback cube AccountingWb — AmountPlan + Comments, bound to BOOKINGWB");
+ cubeWbCheck.setCubeName(CUBE_WB_NAME);
+ cubeWbCheck.getMeasureChecks().add(createMeasureCheck("AmountPlan"));
+ cubeWbCheck.getMeasureChecks().add(createMeasureCheck("Comments"));
+ addAllDimensions(cubeWbCheck);
+
+ // Accounting — VirtualCube exposing all three measures.
+ CubeCheck cubeVCheck = factory.createCubeCheck();
+ cubeVCheck.setName("CubeCheck-" + CUBE_V_NAME);
+ cubeVCheck.setDescription("VirtualCube Accounting — combines AccountingIst + AccountingWb, exposes all three measures");
+ cubeVCheck.setCubeName(CUBE_V_NAME);
+ cubeVCheck.getMeasureChecks().add(createMeasureCheck("AmountIst"));
+ cubeVCheck.getMeasureChecks().add(createMeasureCheck("AmountPlan"));
+ cubeVCheck.getMeasureChecks().add(createMeasureCheck("Comments"));
+ cubeVCheck.getDimensionChecks().add(yearDim);
+ cubeVCheck.getDimensionChecks().add(accountDim);
+ cubeVCheck.getDimensionChecks().add(orgUnitDim);
+
+ DatabaseTableCheck bookingTable = createTableCheck("BOOKING", createColumnCheck("BOOKING_ID", "INTEGER"),
+ createColumnCheck("YEAR_KEY", "INTEGER"),
+ createColumnCheck("ACCOUNT_KEY", "VARCHAR"), createColumnCheck("ORG_UNIT_KEY", "VARCHAR"),
+ createColumnCheck("AMOUNT_IST", "INTEGER"),
+ createColumnCheck("AMOUNT_PLAN", "INTEGER"), createColumnCheck("COMMENT", "VARCHAR"));
+
+ DatabaseTableCheck bookingWbTable = createTableCheck("BOOKINGWB", createColumnCheck("ID", "VARCHAR"),
+ createColumnCheck("USER", "VARCHAR"), createColumnCheck("YEAR_KEY", "INTEGER"),
+ createColumnCheck("ACCOUNT_KEY", "VARCHAR"),
+ createColumnCheck("ORG_UNIT_KEY", "VARCHAR"),
+ createColumnCheck("AMOUNT_PLAN", "INTEGER"), createColumnCheck("COMMENT", "VARCHAR"));
+
+ DatabaseTableCheck accountTable = createTableCheck("ACCOUNT",
+ createColumnCheck("L1_KEY", "VARCHAR"),
+ createColumnCheck("L1_NAME", "VARCHAR"),
+ createColumnCheck("L2_KEY", "VARCHAR"),
+ createColumnCheck("L2_NAME", "VARCHAR"),
+ createColumnCheck("L3_KEY", "VARCHAR"),
+ createColumnCheck("L3_NAME", "VARCHAR"));
+
+ DatabaseTableCheck yearTable = createTableCheck("YEAR", createColumnCheck("YEAR_KEY", "INTEGER"),
+ createColumnCheck("YEAR_NAME", "VARCHAR"));
+
+ DatabaseTableCheck orgUnitTable = createTableCheck("ORGUNIT", createColumnCheck("L1_KEY", "VARCHAR"),
+ createColumnCheck("L1_NAME", "VARCHAR"), createColumnCheck("L2_KEY", "VARCHAR"),
+ createColumnCheck("L2_NAME", "VARCHAR"), createColumnCheck("L3_KEY", "VARCHAR"),
+ createColumnCheck("L3_NAME", "VARCHAR"));
+
+ DatabaseSchemaCheck databaseSchemaCheck = factory.createDatabaseSchemaCheck();
+ databaseSchemaCheck.setName("Database Schema Check for " + CATALOG_NAME);
+ databaseSchemaCheck.setDescription("Database Schema Check for Accounting mapping");
+ databaseSchemaCheck.getTableChecks().add(bookingTable);
+ databaseSchemaCheck.getTableChecks().add(bookingWbTable);
+ databaseSchemaCheck.getTableChecks().add(accountTable);
+ databaseSchemaCheck.getTableChecks().add(yearTable);
+ databaseSchemaCheck.getTableChecks().add(orgUnitTable);
+
+ CatalogCheck catalogCheck = factory.createCatalogCheck();
+ catalogCheck.setName(CATALOG_NAME);
+ catalogCheck.setDescription("Catalog '" + CATALOG_NAME + "' with three cubes (Ist / Wb / Virtual) and full dimensions");
+ catalogCheck.setCatalogName(CATALOG_NAME);
+ catalogCheck.getCubeChecks().add(cubeIstCheck);
+ catalogCheck.getCubeChecks().add(cubeWbCheck);
+ catalogCheck.getCubeChecks().add(cubeVCheck);
+ catalogCheck.getDatabaseSchemaChecks().add(databaseSchemaCheck);
+
+ OlapConnectionCheck connectionCheck = factory.createOlapConnectionCheck();
+ connectionCheck.setName("Connection Check " + CATALOG_NAME);
+ connectionCheck.setDescription("Connection check for the Accounting mapping example");
+ connectionCheck.getCatalogChecks().add(catalogCheck);
+
+ OlapCheckSuite suite = factory.createOlapCheckSuite();
+ suite.setName("Accounting Example Suite");
+ suite.setDescription("Check suite for the Accounting complex mapping example");
+ suite.getConnectionChecks().add(connectionCheck);
+
+ return suite;
+ }
+
+ /** Adds the full 3-dimension shape onto the given CubeCheck. */
+ private void addAllDimensions(CubeCheck cube) {
+ cube.getDimensionChecks().add(createDimensionCheck("Year",
+ createHierarchyCheck("Year", createLevelCheck("Year"))));
+ cube.getDimensionChecks().add(createDimensionCheck("Account",
+ createHierarchyCheck("Account",
+ createLevelCheck("Category"),
+ createLevelCheck("Group"),
+ createLevelCheck("Account"))));
+ cube.getDimensionChecks().add(createDimensionCheck("OrgUnit", createHierarchyCheck("OrgUnit",
+ createLevelCheck("L1"), createLevelCheck("L2"), createLevelCheck("L3"))));
+ }
+
+ private MeasureCheck createMeasureCheck(String measureName) {
+ MeasureCheck measureCheck = factory.createMeasureCheck();
+ measureCheck.setName("MeasureCheck-" + measureName);
+ measureCheck.setDescription("Check that measure '" + measureName + "' exists");
+ measureCheck.setMeasureName(measureName);
+ return measureCheck;
+ }
+
+ private DimensionCheck createDimensionCheck(String dimensionName, HierarchyCheck... hierarchyChecks) {
+ DimensionCheck dimensionCheck = factory.createDimensionCheck();
+ dimensionCheck.setName("DimensionCheck for " + dimensionName);
+ dimensionCheck.setDimensionName(dimensionName);
+ for (HierarchyCheck hc : hierarchyChecks) {
+ dimensionCheck.getHierarchyChecks().add(hc);
+ }
+ return dimensionCheck;
+ }
+
+ private HierarchyCheck createHierarchyCheck(String hierarchyName, LevelCheck... levelChecks) {
+ HierarchyCheck hierarchyCheck = factory.createHierarchyCheck();
+ hierarchyCheck.setName("HierarchyCheck-" + hierarchyName);
+ hierarchyCheck.setHierarchyName(hierarchyName);
+ for (LevelCheck lc : levelChecks) {
+ hierarchyCheck.getLevelChecks().add(lc);
+ }
+ return hierarchyCheck;
+ }
+
+ private LevelCheck createLevelCheck(String levelName) {
+ LevelCheck levelCheck = factory.createLevelCheck();
+ levelCheck.setName("LevelCheck-" + levelName);
+ levelCheck.setLevelName(levelName);
+ return levelCheck;
+ }
+
+ private DatabaseColumnCheck createColumnCheck(String columnName, String columnType) {
+ DatabaseColumnAttributeCheck columnTypeCheck = factory.createDatabaseColumnAttributeCheck();
+ columnTypeCheck.setAttributeType(DatabaseColumnAttribute.TYPE);
+ columnTypeCheck.setExpectedValue(columnType);
+
+ DatabaseColumnCheck columnCheck = factory.createDatabaseColumnCheck();
+ columnCheck.setName("Database Column Check " + columnName);
+ columnCheck.setColumnName(columnName);
+ columnCheck.getColumnAttributeChecks().add(columnTypeCheck);
+ return columnCheck;
+ }
+
+ private DatabaseTableCheck createTableCheck(String tableName, DatabaseColumnCheck... columnChecks) {
+ DatabaseTableCheck tableCheck = factory.createDatabaseTableCheck();
+ tableCheck.setName("Database Table Check " + tableName);
+ tableCheck.setTableName(tableName);
+ for (DatabaseColumnCheck cc : columnChecks) {
+ tableCheck.getColumnChecks().add(cc);
+ }
+ return tableCheck;
+ }
+}
diff --git a/instance/emf/complex/accounting/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accounting/package-info.java b/instance/emf/complex/accounting/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accounting/package-info.java
new file mode 100644
index 000000000..e60a8e327
--- /dev/null
+++ b/instance/emf/complex/accounting/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accounting/package-info.java
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+@org.osgi.annotation.bundle.Export
+@org.osgi.annotation.versioning.Version("0.0.1")
+package org.eclipse.daanse.rolap.mapping.instance.emf.complex.accounting;
diff --git a/instance/emf/complex/accounting/src/main/resources/data/ACCOUNT.csv b/instance/emf/complex/accounting/src/main/resources/data/ACCOUNT.csv
new file mode 100644
index 000000000..18fea91af
--- /dev/null
+++ b/instance/emf/complex/accounting/src/main/resources/data/ACCOUNT.csv
@@ -0,0 +1,10 @@
+L1_KEY,L1_NAME,L2_KEY,L2_NAME,L3_KEY,L3_NAME
+VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR
+EXPENSES,Expenses,PERSONNEL,Personnel,SALARIES,Salaries
+EXPENSES,Expenses,PERSONNEL,Personnel,SOCIAL_SEC,Social Security
+EXPENSES,Expenses,RENT,Rent,OFFICE_RENT,Office Rent
+EXPENSES,Expenses,RENT,Rent,WAREHOUSE_RENT,Warehouse Rent
+EXPENSES,Expenses,TRAVEL,Travel,FLIGHTS,Flights
+EXPENSES,Expenses,TRAVEL,Travel,HOTELS,Hotels
+REVENUE,Revenue,SALES,Sales,PRODUCT_SALES,Product Sales
+REVENUE,Revenue,SERVICES,Services,CONSULTING,Consulting
diff --git a/instance/emf/complex/accounting/src/main/resources/data/BOOKING.csv b/instance/emf/complex/accounting/src/main/resources/data/BOOKING.csv
new file mode 100644
index 000000000..60332dedb
--- /dev/null
+++ b/instance/emf/complex/accounting/src/main/resources/data/BOOKING.csv
@@ -0,0 +1,38 @@
+BOOKING_ID,YEAR_KEY,ACCOUNT_KEY,ORG_UNIT_KEY,AMOUNT_IST,AMOUNT_PLAN,COMMENT
+INTEGER,INTEGER,VARCHAR,VARCHAR,INTEGER,INTEGER,VARCHAR
+1,2025,SALARIES,DEPT_A1,52000,50000,Hired one engineer in Q1
+2,2025,OFFICE_RENT,DEPT_A1,12000,12000,Annual lease
+3,2025,FLIGHTS,DEPT_A1,3200,4000,Less travel than planned
+4,2025,PRODUCT_SALES,DEPT_A1,98000,90000,Strong Q4
+5,2025,SALARIES,DEPT_A2,48000,48000,
+6,2025,OFFICE_RENT,DEPT_A2,9000,9000,
+7,2025,HOTELS,DEPT_A2,2200,2500,
+8,2025,SOCIAL_SEC,DEPT_A2,9600,9600,
+9,2025,SALARIES,DEPT_B1,75000,72000,Includes bonus
+10,2025,WAREHOUSE_RENT,DEPT_B1,28000,28000,
+11,2025,CONSULTING,DEPT_B1,42000,40000,Brand campaign
+12,2025,PRODUCT_SALES,DEPT_B1,60000,55000,
+13,2026,SALARIES,DEPT_A1,55000,55000,
+14,2026,OFFICE_RENT,DEPT_A1,12500,12500,Rent increase
+15,2026,FLIGHTS,DEPT_A1,4200,4000,Conference travel
+16,2026,PRODUCT_SALES,DEPT_A1,105000,100000,
+17,2026,SALARIES,DEPT_A2,50000,49000,
+18,2026,OFFICE_RENT,DEPT_A2,9200,9200,
+19,2026,HOTELS,DEPT_A2,2800,2600,
+20,2026,SOCIAL_SEC,DEPT_A2,10000,9800,
+21,2026,SALARIES,DEPT_B1,78000,75000,
+22,2026,WAREHOUSE_RENT,DEPT_B1,30000,30000,New warehouse
+23,2026,CONSULTING,DEPT_B1,45000,45000,
+24,2026,PRODUCT_SALES,DEPT_B1,68000,65000,
+25,2027,SALARIES,DEPT_A1,0,58000,Plan only
+26,2027,OFFICE_RENT,DEPT_A1,0,13000,Plan only
+27,2027,FLIGHTS,DEPT_A1,0,4500,Plan only
+28,2027,PRODUCT_SALES,DEPT_A1,0,115000,Plan only
+29,2027,SALARIES,DEPT_A2,0,52000,Plan only
+30,2027,OFFICE_RENT,DEPT_A2,0,9500,Plan only
+31,2027,HOTELS,DEPT_A2,0,3000,Plan only
+32,2027,SOCIAL_SEC,DEPT_A2,0,10400,Plan only
+33,2027,SALARIES,DEPT_B1,0,80000,Plan only
+34,2027,WAREHOUSE_RENT,DEPT_B1,0,31000,Plan only
+35,2027,CONSULTING,DEPT_B1,0,48000,Plan only
+36,2027,PRODUCT_SALES,DEPT_B1,0,72000,Plan only
diff --git a/instance/emf/complex/accounting/src/main/resources/data/BOOKINGWB.csv b/instance/emf/complex/accounting/src/main/resources/data/BOOKINGWB.csv
new file mode 100644
index 000000000..745fabbbc
--- /dev/null
+++ b/instance/emf/complex/accounting/src/main/resources/data/BOOKINGWB.csv
@@ -0,0 +1,2 @@
+ID,USER,YEAR_KEY,ACCOUNT_KEY,ORG_UNIT_KEY,AMOUNT_PLAN,COMMENT
+VARCHAR,VARCHAR,INTEGER,VARCHAR,VARCHAR,INTEGER,VARCHAR
diff --git a/instance/emf/complex/accounting/src/main/resources/data/ORGUNIT.csv b/instance/emf/complex/accounting/src/main/resources/data/ORGUNIT.csv
new file mode 100644
index 000000000..8476da575
--- /dev/null
+++ b/instance/emf/complex/accounting/src/main/resources/data/ORGUNIT.csv
@@ -0,0 +1,5 @@
+L1_KEY,L1_NAME,L2_KEY,L2_NAME,L3_KEY,L3_NAME
+VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR
+COMPANY,Company,DIV_A,Division A,DEPT_A1,Department A1
+COMPANY,Company,DIV_A,Division A,DEPT_A2,Department A2
+COMPANY,Company,DIV_B,Division B,DEPT_B1,Department B1
diff --git a/instance/emf/complex/accounting/src/main/resources/data/YEAR.csv b/instance/emf/complex/accounting/src/main/resources/data/YEAR.csv
new file mode 100644
index 000000000..a40e99015
--- /dev/null
+++ b/instance/emf/complex/accounting/src/main/resources/data/YEAR.csv
@@ -0,0 +1,5 @@
+YEAR_KEY,YEAR_NAME
+INTEGER,VARCHAR
+2025,2025
+2026,2026
+2027,2027
diff --git a/instance/emf/complex/accountingonecube/pom.xml b/instance/emf/complex/accountingonecube/pom.xml
new file mode 100644
index 000000000..408d1f214
--- /dev/null
+++ b/instance/emf/complex/accountingonecube/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.daanse
+ org.eclipse.daanse.rolap.mapping.instance.emf.complex
+ ${revision}
+
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.complex.accountingonecube
+ ${project.artifactId}
+ ${project.artifactId}
+
+
+ org.eclipse.daanse
+
+ org.eclipse.daanse.rolap.mapping.instance.api
+
+ 0.0.1-SNAPSHOT
+
+
+
diff --git a/instance/emf/complex/accountingonecube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accountingonecube/CatalogSupplier.java b/instance/emf/complex/accountingonecube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accountingonecube/CatalogSupplier.java
new file mode 100644
index 000000000..4f11cbc49
--- /dev/null
+++ b/instance/emf/complex/accountingonecube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accountingonecube/CatalogSupplier.java
@@ -0,0 +1,1016 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.complex.accountingonecube;
+
+import java.util.List;
+
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Column;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Schema;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Table;
+import org.eclipse.daanse.cwm.util.resource.relational.SqlSimpleTypes;
+
+import org.eclipse.daanse.rolap.mapping.instance.api.CatalogRef;
+import org.eclipse.daanse.rolap.mapping.instance.api.DocSection;
+import org.eclipse.daanse.rolap.mapping.instance.api.Kind;
+import org.eclipse.daanse.rolap.mapping.instance.api.MappingInstance;
+import org.eclipse.daanse.rolap.mapping.instance.api.Source;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescription;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescriptionSupplier;
+
+import org.eclipse.daanse.rolap.mapping.model.access.common.AccessCatalogGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.common.AccessRole;
+import org.eclipse.daanse.rolap.mapping.model.access.common.CatalogAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.common.CommonFactory;
+import org.eclipse.daanse.rolap.mapping.model.access.database.AccessDatabaseSchemaGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.database.AccessTableGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.database.DatabaseFactory;
+import org.eclipse.daanse.rolap.mapping.model.access.database.DatabaseSchemaAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.database.TableAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.AccessCubeGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.AccessHierarchyGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.AccessMemberGrant;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.CubeAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.HierarchyAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.MemberAccess;
+import org.eclipse.daanse.rolap.mapping.model.access.olap.OlapFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.catalog.Catalog;
+import org.eclipse.daanse.rolap.mapping.model.catalog.CatalogFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.database.relational.OrderedColumn;
+import org.eclipse.daanse.rolap.mapping.model.database.relational.RelationalFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.source.SourceFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.source.TableSource;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackAttribute;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackTable;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.CubeFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.Kpi;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.MeasureGroup;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.PhysicalCube;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.MeasureFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.SumMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.TextAggMeasure;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionConnector;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.NamedSet;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.StandardDimension;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.TimeDimension;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.ExplicitHierarchy;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.HierarchyFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.RollupPolicy;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.CalculatedMember;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.CalculatedMemberProperty;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.Level;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.LevelDefinition;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.LevelFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.provider.CatalogMappingSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+@MappingInstance(kind = Kind.COMPLEX, source = Source.EMF, number = "99.1.8", group = "Full Examples")
+@Component(service = { CatalogMappingSupplier.class, TutorialDescriptionSupplier.class })
+public class CatalogSupplier implements CatalogMappingSupplier, TutorialDescriptionSupplier {
+
+ private static final String CATALOG_NAME = "Accounting";
+ private static final String CUBE_NAME = "Accounting";
+
+ private static final String TABLE_BOOKING = "BOOKING";
+ private static final String TABLE_BOOKINGWB = "BOOKINGWB";
+ private static final String TABLE_ACCOUNT = "ACCOUNT";
+ private static final String TABLE_YEAR = "YEAR";
+ private static final String TABLE_ORGUNIT = "ORGUNIT";
+
+ private static final String DIM_YEAR = "Year";
+ private static final String DIM_ACCOUNT = "Account";
+ private static final String DIM_ORGUNIT = "OrgUnit";
+
+ private static final String HIGHEST_YEAR = "2027";
+
+ /**
+ * Fully-qualified unique member name for {@code setDefaultMember}. daanse
+ * resolves these against the level's {@code nameColumn}, not its key column,
+ * so the trailing segment must be the displayed name, not the database KEY.
+ */
+ private static final String DEFAULT_YEAR_MEMBER = "[Year].[Year].[" + HIGHEST_YEAR + "]";
+
+ private static final String CURRENCY_FORMAT = "#,##0 €";
+ private static final String PERCENT_FORMAT = "#,##0.00%";
+
+ /**
+ * MDX-standard "section" syntax: positive;negative. Negative values render with
+ * the literal `[Red]` token, which daanse and most other engines map to red
+ * foreground.
+ */
+ private static final String VARIANCE_FORMAT = "#,##0 €;[Red]-#,##0 €";
+ private static final String VARIANCE_PCT_FORMAT = "#,##0.00%;[Red]-#,##0.00%";
+
+ private Catalog catalog;
+ private Schema databaseSchema;
+
+ private PhysicalCube cube;
+ private WritebackTable writebackTable;
+ private Table writebackPhysicalTable;
+
+ private TableSource bookingSource;
+ private TableSource accountSource;
+ private TableSource yearSource;
+ private TableSource orgUnitSource;
+
+ private TimeDimension yearDimension;
+ private StandardDimension accountDimension;
+ private StandardDimension orgUnitDimension;
+
+ private ExplicitHierarchy yearHierarchy;
+ private ExplicitHierarchy accountHierarchy;
+ private ExplicitHierarchy orgUnitHierarchy;
+ private Level orgUnitLevelL1;
+ private Level orgUnitLevelL3;
+
+ private SumMeasure amountIstMeasure;
+ private SumMeasure amountPlanMeasure;
+ private TextAggMeasure commentsMeasure;
+ private CalculatedMember varianceMember;
+ private CalculatedMember variancePctMember;
+ // KPI and named sets temporarily disabled — the KPI status/value formulas
+ // reference measures that are not yet resolvable in the current daanse
+ // engine, and the named-set Descendants() formulas need adjusting now that
+ // the Account hierarchy is a three-level explicit hierarchy
+ // (Category → Group → Account) instead of the original parent-child shape.
+ // Re-enable once the formulas parse cleanly.
+ // private Kpi budgetUtilizationKpi;
+ // private NamedSet topExpenseAccountsSet;
+ // private NamedSet planOverrunSet;
+ // private NamedSet accountsWithoutCommentSet;
+
+ private AccessRole roleDeptA1;
+ private AccessRole roleDeptA2;
+ private AccessRole roleDeptB1;
+ private AccessRole roleDivisionA;
+ private AccessRole roleAccounting;
+ private AccessRole roleReadonly;
+
+ private static final String catalogBody = """
+ The `Accounting` catalog is a realistic financial-controlling example. It is
+ intentionally small enough to read end-to-end yet covers most modeling
+ features a real bookkeeping cube needs.
+
+ Both actual postings (`IST`) and planned postings (`PLAN`) are recorded into
+ the *same* fact table `BOOKING`, exposed as two separate `SumMeasure`s with
+ a currency format string. A third measure aggregates the per-booking
+ free-text `COMMENT` via a `TextAggMeasure`.
+
+ The cube further demonstrates:
+
+ - **Three-level `Account` (Sachkonto) dimension** — a snowflake-free
+ `ExplicitHierarchy` on a single denormalised table (`Category` →
+ `Group` → `Account`). For a worked example of writeback against a
+ true parent-child hierarchy see `tutorial.writeback.parentchild`.
+ - **`Year` time dimension** with `TIME_YEARS` semantics and a
+ `defaultMember` pinned to the highest year (`2027`).
+ - **Three-level `OrgUnit` dimension** on a single denormalised table; access
+ rights are anchored at the lowest level.
+ - **Currency-formatted measures** (`#,##0 €`) — `AmountIst` and
+ `AmountPlan` render as `1.234.567 €` in tools that respect format strings.
+ - **`Variance` and `VariancePct` calculated members** — IST vs. PLAN gap
+ in currency and percent, with `iif` guarding division by zero. Both
+ members carry a MDX-section format string that paints negative values
+ red (`[Red]` token), demonstrating conditional cell coloring.
+
+ *Note:* a `BudgetUtilization` KPI and three named sets
+ (`Top5ExpenseAccounts`, `PlanOverrun`, `AccountsWithoutComment`) are
+ kept in the source code as commented-out blocks. They are temporarily
+ disabled because their MDX formulas need adjusting to the new
+ three-level `Account` hierarchy (Category → Group → Account).
+ Re-enable them once the formulas resolve cleanly.
+ - **`BOOKINGWB` writeback table** — actual, plan amounts and comments can be
+ entered per org-unit, account and year.
+ - **Six access roles** — one per leaf org unit, one whole-division role, a
+ full-access accounting role, and a read-only role that explicitly hides
+ the writeback table at the schema level.
+ """;
+
+ private static final String databaseSchemaBody = """
+ The database schema contains the following tables:
+
+ - **`BOOKING`** (fact) — `BOOKING_ID`, `YEAR_KEY`, `ACCOUNT_KEY`,
+ `ORG_UNIT_KEY`, `AMOUNT_IST`, `AMOUNT_PLAN`, `COMMENT`. Both IST and
+ PLAN amounts live in the same row and are exposed as two separate
+ measures. The `COMMENT` column feeds the text-aggregator measure.
+ - **`BOOKINGWB`** (writeback target) — same dimensional key columns as
+ `BOOKING`, plus `ID` and `USER` for audit, plus `AMOUNT_IST`,
+ `AMOUNT_PLAN` and `COMMENT`.
+ - **`ACCOUNT`** — single denormalised table with three level keys
+ and names: `L1_KEY`/`L1_NAME` (Category, e.g. `EXPENSES`),
+ `L2_KEY`/`L2_NAME` (Group, e.g. `PERSONNEL`), `L3_KEY`/`L3_NAME`
+ (leaf account, e.g. `SALARIES`). The fact table joins on
+ `BOOKING.ACCOUNT_KEY = ACCOUNT.L3_KEY`.
+ - **`YEAR`** — `YEAR_KEY`, `YEAR_NAME` (one row per business year).
+ - **`ORGUNIT`** — single denormalised table with three level keys and
+ names: `L1_KEY`/`L1_NAME`, `L2_KEY`/`L2_NAME`, `L3_KEY`/`L3_NAME`. The
+ fact table joins on the lowest level (`L3_KEY`) via `ORG_UNIT_KEY`.
+ """;
+
+ private static final String factQueryBody = """
+ The fact `TableSource` reads all columns of the `BOOKING` table. Both the
+ sum measures and the text-aggregation measure source their columns from
+ this query.
+ """;
+
+ private static final String accountDimensionBody = """
+ The `Account` (Sachkonto) dimension is a 3-level `ExplicitHierarchy`
+ on a single denormalised `ACCOUNT` table (same snowflake-free
+ pattern as `OrgUnit`). The levels are:
+
+ - **`Category`** (L1) — e.g. `Expenses`, `Revenue`.
+ - **`Group`** (L2) — e.g. `Personnel`, `Rent`, `Travel`, `Sales`.
+ - **`Account`** (L3, leaf) — e.g. `Salaries`, `Office Rent`,
+ `Flights`, `Product Sales`.
+
+ The fact table joins on the leaf level via
+ `BOOKING.ACCOUNT_KEY = ACCOUNT.L3_KEY`. Aggregations across an L2
+ or L1 member roll up the underlying leaves through the usual SQL
+ `GROUP BY` path.
+
+ *Note:* if the accounting domain calls for a variable-depth tree,
+ switch this dimension to a `ParentChildHierarchy` — the
+ `tutorial.writeback.parentchild` example demonstrates that
+ variant together with writeback.
+ """;
+
+ private static final String yearDimensionBody = """
+ `Year` is modelled as a `TimeDimension` so that MDX time-functions like
+ `Lag`, `ParallelPeriod` and `YTD` can be used against it. The single
+ `Year` level is declared with `LevelDefinition.TIME_YEARS`.
+
+ The hierarchy's `defaultMember` is set to the fully-qualified unique
+ member name `[Year].[Year].[2027]` — the highest year in the data — so
+ that, unless the user picks another year, all queries implicitly run
+ against the most recent year. The unique-name form is required;
+ passing the bare key (`"2027"`) would fail at cube initialisation with
+ `Can not find Default Member`.
+ """;
+
+ private static final String orgUnitDimensionBody = """
+ `OrgUnit` is a three-level explicit hierarchy that lives on a single
+ denormalised table. The three levels are `L1` (e.g. the company), `L2`
+ (e.g. division) and `L3` (e.g. department). The fact table joins on
+ `ORG_UNIT_KEY = ORGUNIT.L3_KEY`, i.e. on the lowest level — that is the
+ level at which postings happen.
+
+ Access rights are also pinned to the lowest level: there is one role per
+ `L3` member that grants `MemberAccess.ALL` on that member only (its
+ ancestors stay visible via `RollupPolicy.FULL`).
+ """;
+
+ private static final String measuresBody = """
+ Three stored measures are exposed by the cube:
+
+ - **`AmountIst`** — `SumMeasure` over the `AMOUNT_IST` column. Read-only.
+ `formatString = "#,##0 €"` so values render as currency in client tools
+ that honour MDX format strings.
+ - **`AmountPlan`** — `SumMeasure` over the `AMOUNT_PLAN` column. Same
+ currency format. Writeable via the writeback table.
+ - **`Comments`** — `TextAggMeasure` over the `COMMENT` column, with
+ separator `" | "` and ordering by the comment text. Aggregating
+ free-text comments across slicer selections is how the cube exposes the
+ per-cell notes to the user — every slice produces a single, readable
+ string concatenating all matching comments.
+
+ Two **calculated members** complement the stored measures (see the
+ "Calculated Members" section below). One **KPI** wraps the planning ratio
+ (see the "KPI" section), and one **named set** picks the top expense
+ accounts (see the "Named Set" section).
+ """;
+
+ private static final String currencyFormatBody = """
+ `AmountIst` and `AmountPlan` are declared with `formatString = "#,##0 €"`.
+ The format string follows the standard MDX/OLE-DB syntax:
+
+ - `#` — optional digit
+ - `,` — thousands separator
+ - `0` — required digit
+ - trailing literal `€`
+
+ Client tools (Saiku, Excel, Power BI, the daanse front-end, …) that respect
+ format strings will display `1234567` as `1.234.567 €` (locale-dependent
+ grouping). The calculated member `Variance` reuses the same format string
+ via a `CalculatedMemberProperty` named `FORMAT_STRING`; `VariancePct` uses
+ `"#,##0.00%"` to render percentages.
+ """;
+
+ private static final String calculatedMembersBody = """
+ Two calculated members compare IST against PLAN:
+
+ - **`Variance`** — `[Measures].[AmountIst] - [Measures].[AmountPlan]`.
+ Negative values indicate plan overrun, positive values indicate plan
+ underrun.
+ - **`VariancePct`** — the relative version,
+ `(IST - PLAN) / PLAN`, wrapped in
+ `iif([Measures].[AmountPlan] = 0, NULL, …)` so that cells without a plan
+ return `NULL` instead of `#DIV/0!`.
+
+ **Color formatting (negative-in-red).** Both members carry a
+ `FORMAT_STRING` property whose value uses the MDX "section" syntax
+ `positive;negative`. Each section can prefix the format with the literal
+ `[Red]` (or any colour name the engine recognises) to colour matching
+ values:
+
+ ```
+ Variance → "#,##0 €;[Red]-#,##0 €"
+ VariancePct → "#,##0.00%;[Red]-#,##0.00%"
+ ```
+
+ **Alternative: daanse-style `BACK_COLOR` / `FORE_COLOR` properties.**
+ Instead of (or in addition to) the `[Red]` token, daanse recognises
+ `BACK_COLOR=` and `FORE_COLOR=` properties appended to
+ the format string. Examples (see
+ `tutorial/cube/calculatedmember.color`):
+
+ ```
+ "$#,##0.00;BACK_COLOR=32768;FORE_COLOR=0" // green background, black text
+ "$#,##0.00;BACK_COLOR=16711680;FORE_COLOR=0" // red background, black text
+ ```
+
+ The `[Red]` syntax is the portable MDX standard and is honoured by most
+ front-ends; the `BACK_COLOR=` properties are a daanse-specific
+ extension that pivot tools render as cell shading.
+
+ Calculated members are attached to the cube via
+ `cube.getCalculatedMembers().add(...)`. They behave exactly like stored
+ measures in MDX but are evaluated on the fly — no fact-table column is
+ required.
+ """;
+
+ private static final String kpiBody = """
+ The `BudgetUtilization` KPI wraps "what fraction of the plan has actually
+ been spent" into one model element that KPI-aware clients can render with
+ a status indicator and a trend arrow.
+
+ - **`value`** — `IST / PLAN`, guarded by `iif` against a zero plan.
+ - **`goal`** — the literal `1.0` (100 % consumed = exactly on plan).
+ - **`status`** — returns `1` (good, traffic light green) when utilisation
+ is ≤ 90 %, `0` (warning, yellow) when between 90 % and 100 %, and `-1`
+ (bad, red) when the plan is over-consumed. Pure MDX, no separate measure
+ required.
+ - **`trend`** — points at `[Measures].[Variance]` so KPI-aware clients can
+ show the absolute over/under-spending alongside the ratio.
+ - **`displayFolder`** — `"KPIs"` keeps it grouped in the client's folder
+ view.
+ - **`statusGraphic`** — `"Traffic Light"`.
+ - **`trendGraphic`** — `"Standard Arrow"`.
+
+ KPIs attach to the cube via `cube.getKpis().add(...)`.
+ """;
+
+ private static final String namedSetBody = """
+ Three named sets ship with the cube, grouped under the `"Analysis"`
+ display folder:
+
+ **`Top5ExpenseAccounts`** — five most expensive leaf accounts under the
+ `EXPENSES` root, ranked by `AmountIst`:
+
+ ```mdx
+ TopCount(
+ Descendants([Account].[Account].[EXPENSES], [Account].[Account]),
+ 5,
+ [Measures].[AmountIst])
+ ```
+
+ **`PlanOverrun`** — every account whose actual spending exceeds its plan
+ in the current slicer:
+
+ ```mdx
+ Filter(
+ Descendants([Account].[Account].[All Accounts], [Account].[Account]),
+ [Measures].[AmountIst] > [Measures].[AmountPlan])
+ ```
+
+ Because named-set formulas are re-evaluated against the current slicer,
+ picking a different `Year` or `OrgUnit` automatically refreshes
+ the list of overrun accounts.
+
+ **`AccountsWithoutComment`** — accounts where no booking carried a
+ comment. Useful for "did the controller forget to annotate?" reports:
+
+ ```mdx
+ Filter(
+ Descendants([Account].[Account].[All Accounts], [Account].[Account]),
+ IsEmpty([Measures].[Comments]))
+ ```
+
+ `IsEmpty` returns true when the `TextAggMeasure` produced no aggregated
+ text for the cell.
+
+ All three sets attach to the cube via `cube.getNamedSets().addAll(...)`.
+ Clients can reference them in MDX as `[Top5ExpenseAccounts]`,
+ `[PlanOverrun]` and `[AccountsWithoutComment]`.
+ """;
+
+ private static final String writebackBody = """
+ The `BOOKINGWB` writeback table lets users enter planned amounts and free
+ comments while preserving IST data as read-only.
+
+ For every dimensional foreign key on the fact, the writeback table has a
+ matching column wired through a `WritebackAttribute`:
+
+ | Cube dimension | Writeback column |
+ |---|---|
+ | `Year` | `YEAR_KEY` |
+ | `Account` | `ACCOUNT_KEY` |
+ | `OrgUnit` | `ORG_UNIT_KEY` |
+
+ Three `WritebackMeasure` entries describe the writeable measures:
+
+ - `AmountIst` → `AMOUNT_IST`
+ - `AmountPlan` → `AMOUNT_PLAN`
+ - `Comments` → `COMMENT`
+
+ The extra `ID` and `USER` columns are technical bookkeeping columns that
+ the writeback engine populates with the row id and the current user.
+
+ **Note on the model package.** `WritebackMeasure` lives in the
+ `olap/cube/measure/` ecore package alongside `SumMeasure`,
+ `TextAggMeasure` and the other base measures — it is conceptually a
+ measure (named by its logical cube-measure name, paired with a
+ database column for persistence). Only the writeback *infrastructure*
+ (`WritebackTable`, `WritebackAttribute`) remains in
+ `database/writeback/`. In Java that means
+ `MeasureFactory.eINSTANCE.createWritebackMeasure()` (not
+ `WritebackFactory`), and the XMI tag carries the `rolapmeas:`
+ namespace prefix.
+
+ **`Comments` writeback specifically.** Because `Comments` is a
+ `TextAggMeasure`, the daanse runtime detects its character bind type
+ automatically (no extra declaration on the model). The
+ `[Measures].[Comments]` cell is written as a single row at the
+ cell's exact coordinates — allocation is skipped — and the
+ read-side `ListAggAggregator` aggregates the new comment alongside
+ any fact-table comments via the standard SQL `LISTAGG` path.
+ """;
+
+ private static final String rolesBody = """
+ Six roles cover the typical org-rights matrix:
+
+ - **`role_dept_A1`**, **`role_dept_A2`**, **`role_dept_B1`** — one role
+ per L3 org unit ("I am a department head, I only see my own
+ department"). Each role grants:
+ - `CatalogAccess.ALL_DIMENSIONS` on the catalog,
+ - `DatabaseSchemaAccess.ALL` on the database schema,
+ - `CubeAccess.ALL` on the `Accounting` cube,
+ - `HierarchyAccess.CUSTOM` on the `OrgUnit` hierarchy with
+ `RollupPolicy.FULL`, `topLevel = bottomLevel = L3`, and one
+ `AccessMemberGrant` granting `MemberAccess.ALL` on the matching
+ L3 member only.
+ `RollupPolicy.FULL` keeps the parent (`L1`/`L2`) totals visible while
+ hiding sibling departments.
+ - **`role_division_A`** — whole-subtree access ("I am the head of
+ Division A, I see Division A and all its departments"). The
+ `AccessMemberGrant` targets the L2 member
+ `[OrgUnit].[OrgUnit].[Company].[Division A]`, which automatically includes
+ its descendants. `topLevel = L1` and `bottomLevel = L3` keep all three
+ levels navigable.
+ - **`role_accounting`** — central accounting role with full access to the
+ catalog, schema and cube. No hierarchy restriction.
+ - **`role_readonly`** — full read access on the cube, but the writeback
+ table `BOOKINGWB` is hidden via an `AccessTableGrant(TableAccess.NONE)`
+ wrapped inside an `AccessDatabaseSchemaGrant(DatabaseSchemaAccess.CUSTOM)`.
+ The access model does not have a dedicated "writeback" flag; restricting
+ the underlying physical table is the recommended workaround and is the
+ pattern the application layer can use to disable the writeback UI.
+ """;
+
+ private static final String divisionRoleBody = """
+ `role_division_A` demonstrates **whole-subtree access**: a division head
+ should see their division plus every department in it, but not other
+ divisions.
+
+ The trick is that `AccessMemberGrant` with `MemberAccess.ALL` on a
+ non-leaf member implicitly grants access to *all descendants* of that
+ member. So a single grant on
+ `[OrgUnit].[OrgUnit].[Company].[Division A]` covers both
+ `[Department A1]` and `[Department A2]`. Setting `topLevel = L1` and
+ `bottomLevel = L3` on the `AccessHierarchyGrant` keeps all three levels
+ navigable so the user can still drill from `[Company]` down to
+ `[Division A]` down to its departments.
+
+ **Important:** member names are taken from the level's `nameColumn` (the
+ display value, e.g. `"Department A1"`), not from the database `KEY`
+ column (e.g. `"DEPT_A1"`). Passing the bare key in the unique-name path
+ would fail with `MemberNotFoundException`.
+ """;
+
+ private static final String readonlyRoleBody = """
+ The access model has no dedicated "writeback allowed/denied" attribute.
+ To express *"can read everything, cannot write back"* the cleanest
+ pattern is:
+
+ 1. Grant the cube with `CubeAccess.ALL` (full read).
+ 2. Grant the database schema with `DatabaseSchemaAccess.CUSTOM` and add a
+ single `AccessTableGrant(TableAccess.NONE)` for the `BOOKINGWB`
+ physical table.
+
+ Hiding the writeback target at the schema level is what the application
+ layer can detect to disable any "save" / "submit plan" UI. Because the
+ grant only touches the writeback table, all other tables (the fact, the
+ dimensions) remain accessible.
+ """;
+
+ private static final String cubeBody = """
+ The `Accounting` physical cube binds everything together. It uses the
+ `BOOKING` fact `TableSource`, declares one `DimensionConnector` per
+ dimension (each with its own `ForeignKey` column on the fact), groups the
+ three measures `AmountIst`, `AmountPlan` and `Comments` in a single
+ `MeasureGroup`, and references the `BOOKINGWB` `WritebackTable` so plan
+ data and comments can be entered.
+ """;
+
+ @Override
+ public Catalog get() {
+ if (catalog != null) {
+ return catalog;
+ }
+
+ databaseSchema = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE
+ .createSchema();
+
+ Column bookingIdColumn = createColumn("BOOKING_ID", SqlSimpleTypes.Sql99.integerType());
+ Column bookingYearKeyColumn = createColumn("YEAR_KEY", SqlSimpleTypes.Sql99.integerType());
+ Column bookingAccountKeyColumn = createColumn("ACCOUNT_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column bookingOrgUnitKeyColumn = createColumn("ORG_UNIT_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column bookingAmountIstColumn = createColumn("AMOUNT_IST", SqlSimpleTypes.Sql99.integerType());
+ Column bookingAmountPlanColumn = createColumn("AMOUNT_PLAN", SqlSimpleTypes.Sql99.integerType());
+ Column bookingCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+
+ Table bookingTable = createTable(TABLE_BOOKING,
+ List.of(bookingIdColumn, bookingYearKeyColumn, bookingAccountKeyColumn,
+ bookingOrgUnitKeyColumn, bookingAmountIstColumn,
+ bookingAmountPlanColumn, bookingCommentColumn));
+ databaseSchema.getOwnedElement().add(bookingTable);
+
+ Column wbIdColumn = createColumn("ID", SqlSimpleTypes.Sql99.varcharType());
+ Column wbUserColumn = createColumn("USER", SqlSimpleTypes.Sql99.varcharType());
+ Column wbYearKeyColumn = createColumn("YEAR_KEY", SqlSimpleTypes.Sql99.integerType());
+ Column wbAccountKeyColumn = createColumn("ACCOUNT_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column wbOrgUnitKeyColumn = createColumn("ORG_UNIT_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column wbBookingAmountIstColumn = createColumn("AMOUNT_IST", SqlSimpleTypes.Sql99.integerType());
+ Column wbAmountIstColumn = createColumn("AMOUNT_IST", SqlSimpleTypes.Sql99.integerType());
+ Column wbAmountPlanColumn = createColumn("AMOUNT_PLAN", SqlSimpleTypes.Sql99.integerType());
+ Column wbCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+
+ writebackPhysicalTable = createTable(TABLE_BOOKINGWB,
+ List.of(wbIdColumn, wbUserColumn, wbYearKeyColumn, wbAccountKeyColumn,
+ wbOrgUnitKeyColumn, wbBookingAmountIstColumn, wbAmountPlanColumn, wbCommentColumn));
+ databaseSchema.getOwnedElement().add(writebackPhysicalTable);
+
+ // Account dimension is a 3-level explicit hierarchy on a single
+ // denormalised table (same shape as ORGUNIT). L1 is the top
+ // category (EXPENSES / REVENUE), L2 the account group
+ // (PERSONNEL / RENT / ...) and L3 the leaf ledger account.
+ Column accountL1KeyColumn = createColumn("L1_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column accountL1NameColumn = createColumn("L1_NAME", SqlSimpleTypes.Sql99.varcharType());
+ Column accountL2KeyColumn = createColumn("L2_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column accountL2NameColumn = createColumn("L2_NAME", SqlSimpleTypes.Sql99.varcharType());
+ Column accountL3KeyColumn = createColumn("L3_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column accountL3NameColumn = createColumn("L3_NAME", SqlSimpleTypes.Sql99.varcharType());
+
+ Table accountTable = createTable(TABLE_ACCOUNT,
+ List.of(accountL1KeyColumn, accountL1NameColumn,
+ accountL2KeyColumn, accountL2NameColumn,
+ accountL3KeyColumn, accountL3NameColumn));
+ databaseSchema.getOwnedElement().add(accountTable);
+
+ Column yearKeyColumn = createColumn("YEAR_KEY", SqlSimpleTypes.Sql99.integerType());
+ Column yearNameColumn = createColumn("YEAR_NAME", SqlSimpleTypes.Sql99.varcharType());
+
+ Table yearTable = createTable(TABLE_YEAR, List.of(yearKeyColumn, yearNameColumn));
+ databaseSchema.getOwnedElement().add(yearTable);
+
+ Column ouL1KeyColumn = createColumn("L1_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column ouL1NameColumn = createColumn("L1_NAME", SqlSimpleTypes.Sql99.varcharType());
+ Column ouL2KeyColumn = createColumn("L2_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column ouL2NameColumn = createColumn("L2_NAME", SqlSimpleTypes.Sql99.varcharType());
+ Column ouL3KeyColumn = createColumn("L3_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column ouL3NameColumn = createColumn("L3_NAME", SqlSimpleTypes.Sql99.varcharType());
+
+ Table orgUnitTable = createTable(TABLE_ORGUNIT,
+ List.of(ouL1KeyColumn, ouL1NameColumn, ouL2KeyColumn, ouL2NameColumn, ouL3KeyColumn, ouL3NameColumn));
+ databaseSchema.getOwnedElement().add(orgUnitTable);
+
+ bookingSource = SourceFactory.eINSTANCE.createTableSource();
+ bookingSource.setTable(bookingTable);
+
+ accountSource = SourceFactory.eINSTANCE.createTableSource();
+ accountSource.setTable(accountTable);
+
+ yearSource = SourceFactory.eINSTANCE.createTableSource();
+ yearSource.setTable(yearTable);
+
+ orgUnitSource = SourceFactory.eINSTANCE.createTableSource();
+ orgUnitSource.setTable(orgUnitTable);
+
+ Level yearLevel = LevelFactory.eINSTANCE.createLevel();
+ yearLevel.setName("Year");
+ yearLevel.setType(LevelDefinition.TIME_YEARS);
+ yearLevel.setColumn(yearKeyColumn);
+ yearLevel.setNameColumn(yearNameColumn);
+ yearLevel.setUniqueMembers(true);
+
+ yearHierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ yearHierarchy.setName(DIM_YEAR);
+ yearHierarchy.setHasAll(true);
+ yearHierarchy.setAllMemberName("All Years");
+ yearHierarchy.setPrimaryKey(yearKeyColumn);
+ yearHierarchy.setSource(yearSource);
+ yearHierarchy.setDefaultMember(DEFAULT_YEAR_MEMBER);
+ yearHierarchy.getLevels().add(yearLevel);
+
+ yearDimension = DimensionFactory.eINSTANCE.createTimeDimension();
+ yearDimension.setName(DIM_YEAR);
+ yearDimension.getHierarchies().add(yearHierarchy);
+
+ Level accountLevelL1 = LevelFactory.eINSTANCE.createLevel();
+ accountLevelL1.setName("Category");
+ accountLevelL1.setColumn(accountL1KeyColumn);
+ accountLevelL1.setNameColumn(accountL1NameColumn);
+ accountLevelL1.setUniqueMembers(true);
+
+ Level accountLevelL2 = LevelFactory.eINSTANCE.createLevel();
+ accountLevelL2.setName("Group");
+ accountLevelL2.setColumn(accountL2KeyColumn);
+ accountLevelL2.setNameColumn(accountL2NameColumn);
+ accountLevelL2.setUniqueMembers(false);
+
+ Level accountLevelL3 = LevelFactory.eINSTANCE.createLevel();
+ accountLevelL3.setName("Account");
+ accountLevelL3.setColumn(accountL3KeyColumn);
+ accountLevelL3.setNameColumn(accountL3NameColumn);
+ accountLevelL3.setUniqueMembers(false);
+
+ accountHierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ accountHierarchy.setName(DIM_ACCOUNT);
+ accountHierarchy.setHasAll(true);
+ accountHierarchy.setAllMemberName("All Accounts");
+ accountHierarchy.setPrimaryKey(accountL3KeyColumn);
+ accountHierarchy.setSource(accountSource);
+ accountHierarchy.getLevels().addAll(List.of(accountLevelL1, accountLevelL2, accountLevelL3));
+
+ accountDimension = DimensionFactory.eINSTANCE.createStandardDimension();
+ accountDimension.setName(DIM_ACCOUNT);
+ accountDimension.getHierarchies().add(accountHierarchy);
+
+ orgUnitLevelL1 = LevelFactory.eINSTANCE.createLevel();
+ orgUnitLevelL1.setName("L1");
+ orgUnitLevelL1.setColumn(ouL1KeyColumn);
+ orgUnitLevelL1.setNameColumn(ouL1NameColumn);
+ orgUnitLevelL1.setUniqueMembers(true);
+
+ Level orgUnitLevelL2 = LevelFactory.eINSTANCE.createLevel();
+ orgUnitLevelL2.setName("L2");
+ orgUnitLevelL2.setColumn(ouL2KeyColumn);
+ orgUnitLevelL2.setNameColumn(ouL2NameColumn);
+ orgUnitLevelL2.setUniqueMembers(false);
+
+ orgUnitLevelL3 = LevelFactory.eINSTANCE.createLevel();
+ orgUnitLevelL3.setName("L3");
+ orgUnitLevelL3.setColumn(ouL3KeyColumn);
+ orgUnitLevelL3.setNameColumn(ouL3NameColumn);
+ orgUnitLevelL3.setUniqueMembers(false);
+
+ orgUnitHierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ orgUnitHierarchy.setName(DIM_ORGUNIT);
+ orgUnitHierarchy.setHasAll(true);
+ orgUnitHierarchy.setAllMemberName("All Org Units");
+ orgUnitHierarchy.setPrimaryKey(ouL3KeyColumn);
+ orgUnitHierarchy.setSource(orgUnitSource);
+ orgUnitHierarchy.getLevels().addAll(List.of(orgUnitLevelL1, orgUnitLevelL2, orgUnitLevelL3));
+
+ orgUnitDimension = DimensionFactory.eINSTANCE.createStandardDimension();
+ orgUnitDimension.setName(DIM_ORGUNIT);
+ orgUnitDimension.getHierarchies().add(orgUnitHierarchy);
+
+ amountIstMeasure = MeasureFactory.eINSTANCE.createSumMeasure();
+ amountIstMeasure.setName("AmountIst");
+ amountIstMeasure.setColumn(bookingAmountIstColumn);
+ amountIstMeasure.setFormatString(CURRENCY_FORMAT);
+
+ amountPlanMeasure = MeasureFactory.eINSTANCE.createSumMeasure();
+ amountPlanMeasure.setName("AmountPlan");
+ amountPlanMeasure.setColumn(bookingAmountPlanColumn);
+ amountPlanMeasure.setFormatString(CURRENCY_FORMAT);
+
+ OrderedColumn commentOrderedColumn = RelationalFactory.eINSTANCE.createOrderedColumn();
+ commentOrderedColumn.setColumn(bookingCommentColumn);
+
+ commentsMeasure = MeasureFactory.eINSTANCE.createTextAggMeasure();
+ commentsMeasure.setName("Comments");
+ commentsMeasure.setColumn(bookingCommentColumn);
+ commentsMeasure.setSeparator(" | ");
+ commentsMeasure.getOrderByColumns().add(commentOrderedColumn);
+
+ MeasureGroup measureGroup = CubeFactory.eINSTANCE.createMeasureGroup();
+ measureGroup.getMeasures().addAll(List.of(amountIstMeasure, amountPlanMeasure, commentsMeasure));
+
+ varianceMember = LevelFactory.eINSTANCE.createCalculatedMember();
+ varianceMember.setName("Variance");
+ varianceMember.setFormula("[Measures].[AmountIst] - [Measures].[AmountPlan]");
+ CalculatedMemberProperty varianceFormatProp = LevelFactory.eINSTANCE.createCalculatedMemberProperty();
+ varianceFormatProp.setName("FORMAT_STRING");
+ varianceFormatProp.setValue(VARIANCE_FORMAT);
+ varianceMember.getCalculatedMemberProperties().add(varianceFormatProp);
+
+ variancePctMember = LevelFactory.eINSTANCE.createCalculatedMember();
+ variancePctMember.setName("VariancePct");
+ variancePctMember.setFormula("iif([Measures].[AmountPlan] = 0, NULL,"
+ + " ([Measures].[AmountIst] - [Measures].[AmountPlan]) / [Measures].[AmountPlan])");
+ CalculatedMemberProperty variancePctFormatProp = LevelFactory.eINSTANCE.createCalculatedMemberProperty();
+ variancePctFormatProp.setName("FORMAT_STRING");
+ variancePctFormatProp.setValue(VARIANCE_PCT_FORMAT);
+ variancePctMember.getCalculatedMemberProperties().add(variancePctFormatProp);
+
+ // KPI and named sets temporarily disabled — formulas need rework
+ // (see field declarations above).
+ //
+ // budgetUtilizationKpi = CubeFactory.eINSTANCE.createKpi();
+ // budgetUtilizationKpi.setName("BudgetUtilization");
+ // budgetUtilizationKpi.setDescription("Share of the plan that has already been consumed by actual postings, "
+ // + "evaluated per OrgUnit and Account cell.");
+ // budgetUtilizationKpi.setValue(
+ // "iif([Measures].[AmountPlan] = 0, NULL," + " [Measures].[AmountIst] / [Measures].[AmountPlan])");
+ // budgetUtilizationKpi.setGoal("1.0");
+ // budgetUtilizationKpi.setStatus("iif([Measures].[AmountPlan] = 0, 0,"
+ // + " iif([Measures].[AmountIst] / [Measures].[AmountPlan] <= 0.9, 1,"
+ // + " iif([Measures].[AmountIst] / [Measures].[AmountPlan] <= 1.0, 0, -1)))");
+ // budgetUtilizationKpi.setTrend("[Measures].[Variance]");
+ // budgetUtilizationKpi.setDisplayFolder("KPIs");
+ // budgetUtilizationKpi.setStatusGraphic("Traffic Light");
+ // budgetUtilizationKpi.setTrendGraphic("Standard Arrow");
+ //
+ // topExpenseAccountsSet = DimensionFactory.eINSTANCE.createNamedSet();
+ // topExpenseAccountsSet.setName("Top5ExpenseAccounts");
+ // topExpenseAccountsSet.setFormula("TopCount("
+ // + "Descendants([Account].[Account].[EXPENSES], [Account].[Account])," + " 5, [Measures].[AmountIst])");
+ // topExpenseAccountsSet.setDisplayFolder("Analysis");
+ //
+ // planOverrunSet = DimensionFactory.eINSTANCE.createNamedSet();
+ // planOverrunSet.setName("PlanOverrun");
+ // planOverrunSet.setFormula("Filter(" + "Descendants([Account].[Account].[All Accounts], [Account].[Account]),"
+ // + " [Measures].[AmountIst] > [Measures].[AmountPlan])");
+ // planOverrunSet.setDisplayFolder("Analysis");
+ //
+ // accountsWithoutCommentSet = DimensionFactory.eINSTANCE.createNamedSet();
+ // accountsWithoutCommentSet.setName("AccountsWithoutComment");
+ // accountsWithoutCommentSet
+ // .setFormula("Filter(" + "Descendants([Account].[Account].[All Accounts], [Account].[Account]),"
+ // + " IsEmpty([Measures].[Comments]))");
+ // accountsWithoutCommentSet.setDisplayFolder("Analysis");
+
+ DimensionConnector yearConnector = DimensionFactory.eINSTANCE.createDimensionConnector();
+ yearConnector.setOverrideDimensionName(DIM_YEAR);
+ yearConnector.setDimension(yearDimension);
+ yearConnector.setForeignKey(bookingYearKeyColumn);
+
+ DimensionConnector accountConnector = DimensionFactory.eINSTANCE.createDimensionConnector();
+ accountConnector.setOverrideDimensionName(DIM_ACCOUNT);
+ accountConnector.setDimension(accountDimension);
+ accountConnector.setForeignKey(bookingAccountKeyColumn);
+
+ DimensionConnector orgUnitConnector = DimensionFactory.eINSTANCE.createDimensionConnector();
+ orgUnitConnector.setOverrideDimensionName(DIM_ORGUNIT);
+ orgUnitConnector.setDimension(orgUnitDimension);
+ orgUnitConnector.setForeignKey(bookingOrgUnitKeyColumn);
+
+ WritebackAttribute wbYearAttribute = createWritebackAttribute(yearConnector, wbYearKeyColumn);
+ WritebackAttribute wbAccountAttribute = createWritebackAttribute(accountConnector, wbAccountKeyColumn);
+ WritebackAttribute wbOrgUnitAttribute = createWritebackAttribute(orgUnitConnector, wbOrgUnitKeyColumn);
+
+ WritebackMeasure wbAmountPlanMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbAmountPlanMeasure.setName("AmountPlan");
+ wbAmountPlanMeasure.setColumn(wbAmountPlanColumn);
+
+ WritebackMeasure wbAmountIstMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbAmountIstMeasure.setName("AmountIst");
+ wbAmountIstMeasure.setColumn(wbAmountIstColumn);
+
+ WritebackMeasure wbCommentsMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbCommentsMeasure.setName("Comments");
+ wbCommentsMeasure.setColumn(wbCommentColumn);
+
+ writebackTable = WritebackFactory.eINSTANCE.createWritebackTable();
+ writebackTable.setName(TABLE_BOOKINGWB);
+ writebackTable.getWritebackAttribute().addAll(List.of(wbYearAttribute, wbAccountAttribute, wbOrgUnitAttribute));
+ writebackTable.getWritebackMeasure().addAll(List.of(wbAmountIstMeasure, wbAmountPlanMeasure, wbCommentsMeasure));
+
+ cube = CubeFactory.eINSTANCE.createPhysicalCube();
+ cube.setName(CUBE_NAME);
+ cube.setSource(bookingSource);
+ cube.getDimensionConnectors().addAll(List.of(yearConnector, accountConnector, orgUnitConnector));
+ cube.getMeasureGroups().add(measureGroup);
+ cube.getCalculatedMembers().addAll(List.of(varianceMember, variancePctMember));
+ // KPI and named sets disabled — re-enable after fixing formulas.
+ // cube.getKpis().add(budgetUtilizationKpi);
+ // cube.getNamedSets().addAll(List.of(topExpenseAccountsSet, planOverrunSet, accountsWithoutCommentSet));
+ cube.setWritebackTable(writebackTable);
+
+ // Member unique names use the level's nameColumn value (the display name),
+ // not the database KEY — same rule as setDefaultMember above. The
+ // ORGUNIT table maps DEPT_A1 → "Department A1", DIV_A → "Division A", etc.
+ roleDeptA1 = createOrgUnitRole("role_dept_A1",
+ "[OrgUnit].[OrgUnit].[Company].[Division A].[Department A1]");
+ roleDeptA2 = createOrgUnitRole("role_dept_A2",
+ "[OrgUnit].[OrgUnit].[Company].[Division A].[Department A2]");
+ roleDeptB1 = createOrgUnitRole("role_dept_B1",
+ "[OrgUnit].[OrgUnit].[Company].[Division B].[Department B1]");
+ roleDivisionA = createDivisionRole("role_division_A",
+ "[OrgUnit].[OrgUnit].[Company].[Division A]");
+ roleAccounting = createAccountingRole();
+ roleReadonly = createReadonlyRole();
+
+ catalog = CatalogFactory.eINSTANCE.createCatalog();
+ catalog.setName(CATALOG_NAME);
+ catalog.setDescription("Accounting catalog with IST/PLAN postings, three-level accounts and "
+ + "org units, writeback for actual, plan data and text-aggregated comments. "
+ + "Includes Variance/VariancePct calculated members and a set of access roles "
+ + "ranging from single-department to division-wide, an accounting all-access "
+ + "role and a read-only role.");
+ catalog.getDbschemas().add(databaseSchema);
+ catalog.getCubes().add(cube);
+ catalog.getAccessRoles()
+ .addAll(List.of(roleDeptA1, roleDeptA2, roleDeptB1, roleDivisionA, roleAccounting, roleReadonly));
+
+ return catalog;
+ }
+
+ @Override
+ public TutorialDescription describe() {
+ Catalog c = get();
+ return new TutorialDescription(List.of(new DocSection("Accounting Catalog", catalogBody, 1, 0, 0, null, 0),
+ new DocSection("Database Schema", databaseSchemaBody, 1, 1, 0, databaseSchema, 3),
+ new DocSection("Fact Query", factQueryBody, 1, 2, 0, bookingSource, 2),
+ new DocSection("Account (parent-child Sachkonto)", accountDimensionBody, 1, 3, 0, accountDimension, 0),
+ new DocSection("Year (TimeDimension, default = " + HIGHEST_YEAR + ")", yearDimensionBody, 1, 4, 0,
+ yearDimension, 0),
+ new DocSection("OrgUnit (three-level)", orgUnitDimensionBody, 1, 6, 0, orgUnitDimension, 0),
+ new DocSection("Measures (IST / PLAN / Comments)", measuresBody, 1, 8, 0, null, 0),
+ new DocSection("Currency Format", currencyFormatBody, 1, 9, 0, amountIstMeasure, 0),
+ new DocSection("Calculated Members (Variance, VariancePct)", calculatedMembersBody, 1, 10, 0,
+ varianceMember, 0),
+ // KPI and named-set DocSections disabled while their underlying
+ // model elements are commented out (bad formulas):
+ // new DocSection("KPI (BudgetUtilization)", kpiBody, 1, 11, 0, budgetUtilizationKpi, 0),
+ // new DocSection("Named Sets (Top5ExpenseAccounts, PlanOverrun, AccountsWithoutComment)", namedSetBody,
+ // 1, 12, 0, topExpenseAccountsSet, 0),
+ new DocSection("Writeback (BOOKINGWB)", writebackBody, 1, 13, 0, writebackTable, 2),
+ new DocSection("Access Roles", rolesBody, 1, 14, 0, roleAccounting, 0),
+ new DocSection("Whole-Division Role (role_division_A)", divisionRoleBody, 1, 15, 0, roleDivisionA, 2),
+ new DocSection("Read-Only Role (role_readonly)", readonlyRoleBody, 1, 16, 0, roleReadonly, 2),
+ new DocSection("Cube", cubeBody, 1, 17, 0, cube, 2)), List.of(new CatalogRef("catalog", () -> c)));
+ }
+
+ private static Column createColumn(String name,
+ org.eclipse.daanse.cwm.model.cwm.resource.relational.SQLSimpleType type) {
+ Column c = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createColumn();
+ c.setName(name);
+ c.setType(type);
+ return c;
+ }
+
+ private static Table createTable(String name, List columns) {
+ Table t = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createTable();
+ t.setName(name);
+ t.getFeature().addAll(columns);
+ return t;
+ }
+
+ private static WritebackAttribute createWritebackAttribute(DimensionConnector connector, Column column) {
+ WritebackAttribute a = WritebackFactory.eINSTANCE.createWritebackAttribute();
+ a.setDimensionConnector(connector);
+ a.setColumn(column);
+ return a;
+ }
+
+ private AccessRole createOrgUnitRole(String roleName, String memberName) {
+ AccessDatabaseSchemaGrant dbGrant = DatabaseFactory.eINSTANCE.createAccessDatabaseSchemaGrant();
+ dbGrant.setDatabaseSchemaAccess(DatabaseSchemaAccess.ALL);
+ dbGrant.setDatabaseSchema(databaseSchema);
+
+ AccessMemberGrant memberGrant = OlapFactory.eINSTANCE.createAccessMemberGrant();
+ memberGrant.setMemberAccess(MemberAccess.ALL);
+ memberGrant.setMember(memberName);
+
+ AccessHierarchyGrant hierarchyGrant = OlapFactory.eINSTANCE.createAccessHierarchyGrant();
+ hierarchyGrant.setHierarchy(orgUnitHierarchy);
+ hierarchyGrant.setHierarchyAccess(HierarchyAccess.CUSTOM);
+ hierarchyGrant.setTopLevel(orgUnitLevelL3);
+ hierarchyGrant.setBottomLevel(orgUnitLevelL3);
+ hierarchyGrant.setRollupPolicy(RollupPolicy.FULL);
+ hierarchyGrant.getMemberGrants().add(memberGrant);
+
+ AccessCubeGrant cubeGrant = OlapFactory.eINSTANCE.createAccessCubeGrant();
+ cubeGrant.setCube(cube);
+ cubeGrant.setCubeAccess(CubeAccess.ALL);
+ cubeGrant.getHierarchyGrants().add(hierarchyGrant);
+
+ AccessCatalogGrant catalogGrant = CommonFactory.eINSTANCE.createAccessCatalogGrant();
+ catalogGrant.setCatalogAccess(CatalogAccess.ALL_DIMENSIONS);
+ catalogGrant.getCubeGrants().add(cubeGrant);
+ catalogGrant.getDatabaseSchemaGrants().add(dbGrant);
+
+ AccessRole role = CommonFactory.eINSTANCE.createAccessRole();
+ role.setName(roleName);
+ role.getAccessCatalogGrants().add(catalogGrant);
+ return role;
+ }
+
+ private AccessRole createAccountingRole() {
+ AccessDatabaseSchemaGrant dbGrant = DatabaseFactory.eINSTANCE.createAccessDatabaseSchemaGrant();
+ dbGrant.setDatabaseSchemaAccess(DatabaseSchemaAccess.ALL);
+ dbGrant.setDatabaseSchema(databaseSchema);
+
+ AccessCubeGrant cubeGrant = OlapFactory.eINSTANCE.createAccessCubeGrant();
+ cubeGrant.setCube(cube);
+ cubeGrant.setCubeAccess(CubeAccess.ALL);
+
+ AccessCatalogGrant catalogGrant = CommonFactory.eINSTANCE.createAccessCatalogGrant();
+ catalogGrant.setCatalogAccess(CatalogAccess.ALL_DIMENSIONS);
+ catalogGrant.getCubeGrants().add(cubeGrant);
+ catalogGrant.getDatabaseSchemaGrants().add(dbGrant);
+
+ AccessRole role = CommonFactory.eINSTANCE.createAccessRole();
+ role.setName("role_accounting");
+ role.getAccessCatalogGrants().add(catalogGrant);
+ return role;
+ }
+
+ private AccessRole createDivisionRole(String roleName, String memberName) {
+ AccessDatabaseSchemaGrant dbGrant = DatabaseFactory.eINSTANCE.createAccessDatabaseSchemaGrant();
+ dbGrant.setDatabaseSchemaAccess(DatabaseSchemaAccess.ALL);
+ dbGrant.setDatabaseSchema(databaseSchema);
+
+ AccessMemberGrant memberGrant = OlapFactory.eINSTANCE.createAccessMemberGrant();
+ memberGrant.setMemberAccess(MemberAccess.ALL);
+ memberGrant.setMember(memberName);
+
+ AccessHierarchyGrant hierarchyGrant = OlapFactory.eINSTANCE.createAccessHierarchyGrant();
+ hierarchyGrant.setHierarchy(orgUnitHierarchy);
+ hierarchyGrant.setHierarchyAccess(HierarchyAccess.CUSTOM);
+ hierarchyGrant.setTopLevel(orgUnitLevelL1);
+ hierarchyGrant.setBottomLevel(orgUnitLevelL3);
+ hierarchyGrant.setRollupPolicy(RollupPolicy.FULL);
+ hierarchyGrant.getMemberGrants().add(memberGrant);
+
+ AccessCubeGrant cubeGrant = OlapFactory.eINSTANCE.createAccessCubeGrant();
+ cubeGrant.setCube(cube);
+ cubeGrant.setCubeAccess(CubeAccess.ALL);
+ cubeGrant.getHierarchyGrants().add(hierarchyGrant);
+
+ AccessCatalogGrant catalogGrant = CommonFactory.eINSTANCE.createAccessCatalogGrant();
+ catalogGrant.setCatalogAccess(CatalogAccess.ALL_DIMENSIONS);
+ catalogGrant.getCubeGrants().add(cubeGrant);
+ catalogGrant.getDatabaseSchemaGrants().add(dbGrant);
+
+ AccessRole role = CommonFactory.eINSTANCE.createAccessRole();
+ role.setName(roleName);
+ role.getAccessCatalogGrants().add(catalogGrant);
+ return role;
+ }
+
+ private AccessRole createReadonlyRole() {
+ AccessTableGrant denyWriteback = DatabaseFactory.eINSTANCE.createAccessTableGrant();
+ denyWriteback.setTableAccess(TableAccess.NONE);
+ denyWriteback.setTable(writebackPhysicalTable);
+
+ AccessDatabaseSchemaGrant dbGrant = DatabaseFactory.eINSTANCE.createAccessDatabaseSchemaGrant();
+ dbGrant.setDatabaseSchemaAccess(DatabaseSchemaAccess.CUSTOM);
+ dbGrant.setDatabaseSchema(databaseSchema);
+ dbGrant.getTableGrants().add(denyWriteback);
+
+ AccessCubeGrant cubeGrant = OlapFactory.eINSTANCE.createAccessCubeGrant();
+ cubeGrant.setCube(cube);
+ cubeGrant.setCubeAccess(CubeAccess.ALL);
+
+ AccessCatalogGrant catalogGrant = CommonFactory.eINSTANCE.createAccessCatalogGrant();
+ catalogGrant.setCatalogAccess(CatalogAccess.ALL_DIMENSIONS);
+ catalogGrant.getCubeGrants().add(cubeGrant);
+ catalogGrant.getDatabaseSchemaGrants().add(dbGrant);
+
+ AccessRole role = CommonFactory.eINSTANCE.createAccessRole();
+ role.setName("role_readonly");
+ role.getAccessCatalogGrants().add(catalogGrant);
+ return role;
+ }
+}
diff --git a/instance/emf/complex/accountingonecube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accountingonecube/CheckSuiteSupplier.java b/instance/emf/complex/accountingonecube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accountingonecube/CheckSuiteSupplier.java
new file mode 100644
index 000000000..7451f2e41
--- /dev/null
+++ b/instance/emf/complex/accountingonecube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accountingonecube/CheckSuiteSupplier.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.complex.accountingonecube;
+
+import org.eclipse.daanse.olap.check.model.check.CatalogCheck;
+import org.eclipse.daanse.olap.check.model.check.CubeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttribute;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttributeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseSchemaCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseTableCheck;
+import org.eclipse.daanse.olap.check.model.check.DimensionCheck;
+import org.eclipse.daanse.olap.check.model.check.HierarchyCheck;
+import org.eclipse.daanse.olap.check.model.check.LevelCheck;
+import org.eclipse.daanse.olap.check.model.check.MeasureCheck;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckFactory;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckSuite;
+import org.eclipse.daanse.olap.check.model.check.OlapConnectionCheck;
+import org.eclipse.daanse.olap.check.runtime.api.OlapCheckSuiteSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Check suite for the Accounting complex mapping. Asserts that the catalog, its
+ * single cube, all measures, dimensions/hierarchies/levels, and the full
+ * database schema (tables + column types) materialise as expected.
+ */
+@Component(service = OlapCheckSuiteSupplier.class)
+public class CheckSuiteSupplier implements OlapCheckSuiteSupplier {
+
+ private static final OlapCheckFactory factory = OlapCheckFactory.eINSTANCE;
+
+ private static final String CATALOG_NAME = "Accounting";
+ private static final String CUBE_NAME = "Accounting";
+
+ @Override
+ public OlapCheckSuite get() {
+ DimensionCheck yearDim = createDimensionCheck("Year", createHierarchyCheck("Year", createLevelCheck("Year")));
+ DimensionCheck accountDim = createDimensionCheck("Account",
+ createHierarchyCheck("Account",
+ createLevelCheck("Category"),
+ createLevelCheck("Group"),
+ createLevelCheck("Account")));
+ DimensionCheck orgUnitDim = createDimensionCheck("OrgUnit", createHierarchyCheck("OrgUnit",
+ createLevelCheck("L1"), createLevelCheck("L2"), createLevelCheck("L3")));
+
+ MeasureCheck amountIst = createMeasureCheck("AmountIst");
+ MeasureCheck amountPlan = createMeasureCheck("AmountPlan");
+ MeasureCheck comments = createMeasureCheck("Comments");
+
+ CubeCheck cubeCheck = factory.createCubeCheck();
+ cubeCheck.setName("CubeCheck-" + CUBE_NAME);
+ cubeCheck.setDescription("Check that cube '" + CUBE_NAME + "' exists with all dimensions and measures");
+ cubeCheck.setCubeName(CUBE_NAME);
+ cubeCheck.getMeasureChecks().add(amountIst);
+ cubeCheck.getMeasureChecks().add(amountPlan);
+ cubeCheck.getMeasureChecks().add(comments);
+ cubeCheck.getDimensionChecks().add(yearDim);
+ cubeCheck.getDimensionChecks().add(accountDim);
+ cubeCheck.getDimensionChecks().add(orgUnitDim);
+
+ DatabaseTableCheck bookingTable = createTableCheck("BOOKING", createColumnCheck("BOOKING_ID", "INTEGER"),
+ createColumnCheck("YEAR_KEY", "INTEGER"),
+ createColumnCheck("ACCOUNT_KEY", "VARCHAR"), createColumnCheck("ORG_UNIT_KEY", "VARCHAR"),
+ createColumnCheck("AMOUNT_IST", "INTEGER"),
+ createColumnCheck("AMOUNT_PLAN", "INTEGER"), createColumnCheck("COMMENT", "VARCHAR"));
+
+ DatabaseTableCheck bookingWbTable = createTableCheck("BOOKINGWB", createColumnCheck("ID", "VARCHAR"),
+ createColumnCheck("USER", "VARCHAR"), createColumnCheck("YEAR_KEY", "INTEGER"),
+ createColumnCheck("ACCOUNT_KEY", "VARCHAR"),
+ createColumnCheck("ORG_UNIT_KEY", "VARCHAR"),
+ createColumnCheck("AMOUNT_IST", "INTEGER"),
+ createColumnCheck("AMOUNT_PLAN", "INTEGER"), createColumnCheck("COMMENT", "VARCHAR"));
+
+ DatabaseTableCheck accountTable = createTableCheck("ACCOUNT",
+ createColumnCheck("L1_KEY", "VARCHAR"),
+ createColumnCheck("L1_NAME", "VARCHAR"),
+ createColumnCheck("L2_KEY", "VARCHAR"),
+ createColumnCheck("L2_NAME", "VARCHAR"),
+ createColumnCheck("L3_KEY", "VARCHAR"),
+ createColumnCheck("L3_NAME", "VARCHAR"));
+
+ DatabaseTableCheck yearTable = createTableCheck("YEAR", createColumnCheck("YEAR_KEY", "INTEGER"),
+ createColumnCheck("YEAR_NAME", "VARCHAR"));
+
+ DatabaseTableCheck orgUnitTable = createTableCheck("ORGUNIT", createColumnCheck("L1_KEY", "VARCHAR"),
+ createColumnCheck("L1_NAME", "VARCHAR"), createColumnCheck("L2_KEY", "VARCHAR"),
+ createColumnCheck("L2_NAME", "VARCHAR"), createColumnCheck("L3_KEY", "VARCHAR"),
+ createColumnCheck("L3_NAME", "VARCHAR"));
+
+ DatabaseSchemaCheck databaseSchemaCheck = factory.createDatabaseSchemaCheck();
+ databaseSchemaCheck.setName("Database Schema Check for " + CATALOG_NAME);
+ databaseSchemaCheck.setDescription("Database Schema Check for Accounting mapping");
+ databaseSchemaCheck.getTableChecks().add(bookingTable);
+ databaseSchemaCheck.getTableChecks().add(bookingWbTable);
+ databaseSchemaCheck.getTableChecks().add(accountTable);
+ databaseSchemaCheck.getTableChecks().add(yearTable);
+ databaseSchemaCheck.getTableChecks().add(orgUnitTable);
+
+ CatalogCheck catalogCheck = factory.createCatalogCheck();
+ catalogCheck.setName(CATALOG_NAME);
+ catalogCheck.setDescription("Check that catalog '" + CATALOG_NAME + "' exists with cube and dimensions");
+ catalogCheck.setCatalogName(CATALOG_NAME);
+ catalogCheck.getCubeChecks().add(cubeCheck);
+ catalogCheck.getDatabaseSchemaChecks().add(databaseSchemaCheck);
+
+ OlapConnectionCheck connectionCheck = factory.createOlapConnectionCheck();
+ connectionCheck.setName("Connection Check " + CATALOG_NAME);
+ connectionCheck.setDescription("Connection check for the Accounting mapping example");
+ connectionCheck.getCatalogChecks().add(catalogCheck);
+
+ OlapCheckSuite suite = factory.createOlapCheckSuite();
+ suite.setName("Accounting Example Suite");
+ suite.setDescription("Check suite for the Accounting complex mapping example");
+ suite.getConnectionChecks().add(connectionCheck);
+
+ return suite;
+ }
+
+ private MeasureCheck createMeasureCheck(String measureName) {
+ MeasureCheck measureCheck = factory.createMeasureCheck();
+ measureCheck.setName("MeasureCheck-" + measureName);
+ measureCheck.setDescription("Check that measure '" + measureName + "' exists");
+ measureCheck.setMeasureName(measureName);
+ return measureCheck;
+ }
+
+ private DimensionCheck createDimensionCheck(String dimensionName, HierarchyCheck... hierarchyChecks) {
+ DimensionCheck dimensionCheck = factory.createDimensionCheck();
+ dimensionCheck.setName("DimensionCheck for " + dimensionName);
+ dimensionCheck.setDimensionName(dimensionName);
+ for (HierarchyCheck hc : hierarchyChecks) {
+ dimensionCheck.getHierarchyChecks().add(hc);
+ }
+ return dimensionCheck;
+ }
+
+ private HierarchyCheck createHierarchyCheck(String hierarchyName, LevelCheck... levelChecks) {
+ HierarchyCheck hierarchyCheck = factory.createHierarchyCheck();
+ hierarchyCheck.setName("HierarchyCheck-" + hierarchyName);
+ hierarchyCheck.setHierarchyName(hierarchyName);
+ for (LevelCheck lc : levelChecks) {
+ hierarchyCheck.getLevelChecks().add(lc);
+ }
+ return hierarchyCheck;
+ }
+
+ private LevelCheck createLevelCheck(String levelName) {
+ LevelCheck levelCheck = factory.createLevelCheck();
+ levelCheck.setName("LevelCheck-" + levelName);
+ levelCheck.setLevelName(levelName);
+ return levelCheck;
+ }
+
+ private DatabaseColumnCheck createColumnCheck(String columnName, String columnType) {
+ DatabaseColumnAttributeCheck columnTypeCheck = factory.createDatabaseColumnAttributeCheck();
+ columnTypeCheck.setAttributeType(DatabaseColumnAttribute.TYPE);
+ columnTypeCheck.setExpectedValue(columnType);
+
+ DatabaseColumnCheck columnCheck = factory.createDatabaseColumnCheck();
+ columnCheck.setName("Database Column Check " + columnName);
+ columnCheck.setColumnName(columnName);
+ columnCheck.getColumnAttributeChecks().add(columnTypeCheck);
+ return columnCheck;
+ }
+
+ private DatabaseTableCheck createTableCheck(String tableName, DatabaseColumnCheck... columnChecks) {
+ DatabaseTableCheck tableCheck = factory.createDatabaseTableCheck();
+ tableCheck.setName("Database Table Check " + tableName);
+ tableCheck.setTableName(tableName);
+ for (DatabaseColumnCheck cc : columnChecks) {
+ tableCheck.getColumnChecks().add(cc);
+ }
+ return tableCheck;
+ }
+}
diff --git a/instance/emf/complex/accountingonecube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accountingonecube/package-info.java b/instance/emf/complex/accountingonecube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accountingonecube/package-info.java
new file mode 100644
index 000000000..62f394c62
--- /dev/null
+++ b/instance/emf/complex/accountingonecube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/complex/accountingonecube/package-info.java
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+@org.osgi.annotation.bundle.Export
+@org.osgi.annotation.versioning.Version("0.0.1")
+package org.eclipse.daanse.rolap.mapping.instance.emf.complex.accountingonecube;
diff --git a/instance/emf/complex/accountingonecube/src/main/resources/data/ACCOUNT.csv b/instance/emf/complex/accountingonecube/src/main/resources/data/ACCOUNT.csv
new file mode 100644
index 000000000..18fea91af
--- /dev/null
+++ b/instance/emf/complex/accountingonecube/src/main/resources/data/ACCOUNT.csv
@@ -0,0 +1,10 @@
+L1_KEY,L1_NAME,L2_KEY,L2_NAME,L3_KEY,L3_NAME
+VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR
+EXPENSES,Expenses,PERSONNEL,Personnel,SALARIES,Salaries
+EXPENSES,Expenses,PERSONNEL,Personnel,SOCIAL_SEC,Social Security
+EXPENSES,Expenses,RENT,Rent,OFFICE_RENT,Office Rent
+EXPENSES,Expenses,RENT,Rent,WAREHOUSE_RENT,Warehouse Rent
+EXPENSES,Expenses,TRAVEL,Travel,FLIGHTS,Flights
+EXPENSES,Expenses,TRAVEL,Travel,HOTELS,Hotels
+REVENUE,Revenue,SALES,Sales,PRODUCT_SALES,Product Sales
+REVENUE,Revenue,SERVICES,Services,CONSULTING,Consulting
diff --git a/instance/emf/complex/accountingonecube/src/main/resources/data/BOOKING.csv b/instance/emf/complex/accountingonecube/src/main/resources/data/BOOKING.csv
new file mode 100644
index 000000000..60332dedb
--- /dev/null
+++ b/instance/emf/complex/accountingonecube/src/main/resources/data/BOOKING.csv
@@ -0,0 +1,38 @@
+BOOKING_ID,YEAR_KEY,ACCOUNT_KEY,ORG_UNIT_KEY,AMOUNT_IST,AMOUNT_PLAN,COMMENT
+INTEGER,INTEGER,VARCHAR,VARCHAR,INTEGER,INTEGER,VARCHAR
+1,2025,SALARIES,DEPT_A1,52000,50000,Hired one engineer in Q1
+2,2025,OFFICE_RENT,DEPT_A1,12000,12000,Annual lease
+3,2025,FLIGHTS,DEPT_A1,3200,4000,Less travel than planned
+4,2025,PRODUCT_SALES,DEPT_A1,98000,90000,Strong Q4
+5,2025,SALARIES,DEPT_A2,48000,48000,
+6,2025,OFFICE_RENT,DEPT_A2,9000,9000,
+7,2025,HOTELS,DEPT_A2,2200,2500,
+8,2025,SOCIAL_SEC,DEPT_A2,9600,9600,
+9,2025,SALARIES,DEPT_B1,75000,72000,Includes bonus
+10,2025,WAREHOUSE_RENT,DEPT_B1,28000,28000,
+11,2025,CONSULTING,DEPT_B1,42000,40000,Brand campaign
+12,2025,PRODUCT_SALES,DEPT_B1,60000,55000,
+13,2026,SALARIES,DEPT_A1,55000,55000,
+14,2026,OFFICE_RENT,DEPT_A1,12500,12500,Rent increase
+15,2026,FLIGHTS,DEPT_A1,4200,4000,Conference travel
+16,2026,PRODUCT_SALES,DEPT_A1,105000,100000,
+17,2026,SALARIES,DEPT_A2,50000,49000,
+18,2026,OFFICE_RENT,DEPT_A2,9200,9200,
+19,2026,HOTELS,DEPT_A2,2800,2600,
+20,2026,SOCIAL_SEC,DEPT_A2,10000,9800,
+21,2026,SALARIES,DEPT_B1,78000,75000,
+22,2026,WAREHOUSE_RENT,DEPT_B1,30000,30000,New warehouse
+23,2026,CONSULTING,DEPT_B1,45000,45000,
+24,2026,PRODUCT_SALES,DEPT_B1,68000,65000,
+25,2027,SALARIES,DEPT_A1,0,58000,Plan only
+26,2027,OFFICE_RENT,DEPT_A1,0,13000,Plan only
+27,2027,FLIGHTS,DEPT_A1,0,4500,Plan only
+28,2027,PRODUCT_SALES,DEPT_A1,0,115000,Plan only
+29,2027,SALARIES,DEPT_A2,0,52000,Plan only
+30,2027,OFFICE_RENT,DEPT_A2,0,9500,Plan only
+31,2027,HOTELS,DEPT_A2,0,3000,Plan only
+32,2027,SOCIAL_SEC,DEPT_A2,0,10400,Plan only
+33,2027,SALARIES,DEPT_B1,0,80000,Plan only
+34,2027,WAREHOUSE_RENT,DEPT_B1,0,31000,Plan only
+35,2027,CONSULTING,DEPT_B1,0,48000,Plan only
+36,2027,PRODUCT_SALES,DEPT_B1,0,72000,Plan only
diff --git a/instance/emf/complex/accountingonecube/src/main/resources/data/BOOKINGWB.csv b/instance/emf/complex/accountingonecube/src/main/resources/data/BOOKINGWB.csv
new file mode 100644
index 000000000..7e5e1cf39
--- /dev/null
+++ b/instance/emf/complex/accountingonecube/src/main/resources/data/BOOKINGWB.csv
@@ -0,0 +1,3 @@
+ID,USER,YEAR_KEY,ACCOUNT_KEY,ORG_UNIT_KEY,AMOUNT_IST,AMOUNT_PLAN,COMMENT
+VARCHAR,VARCHAR,INTEGER,VARCHAR,VARCHAR,INTEGER,INTEGER,VARCHAR
+"","",0,"","",0,0,""
diff --git a/instance/emf/complex/accountingonecube/src/main/resources/data/ORGUNIT.csv b/instance/emf/complex/accountingonecube/src/main/resources/data/ORGUNIT.csv
new file mode 100644
index 000000000..8476da575
--- /dev/null
+++ b/instance/emf/complex/accountingonecube/src/main/resources/data/ORGUNIT.csv
@@ -0,0 +1,5 @@
+L1_KEY,L1_NAME,L2_KEY,L2_NAME,L3_KEY,L3_NAME
+VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR
+COMPANY,Company,DIV_A,Division A,DEPT_A1,Department A1
+COMPANY,Company,DIV_A,Division A,DEPT_A2,Department A2
+COMPANY,Company,DIV_B,Division B,DEPT_B1,Department B1
diff --git a/instance/emf/complex/accountingonecube/src/main/resources/data/YEAR.csv b/instance/emf/complex/accountingonecube/src/main/resources/data/YEAR.csv
new file mode 100644
index 000000000..a40e99015
--- /dev/null
+++ b/instance/emf/complex/accountingonecube/src/main/resources/data/YEAR.csv
@@ -0,0 +1,5 @@
+YEAR_KEY,YEAR_NAME
+INTEGER,VARCHAR
+2025,2025
+2026,2026
+2027,2027
diff --git a/instance/emf/complex/pom.xml b/instance/emf/complex/pom.xml
index d8be3bc1c..b40150a19 100644
--- a/instance/emf/complex/pom.xml
+++ b/instance/emf/complex/pom.xml
@@ -27,5 +27,7 @@
population.jena
parcel
csdl
+ accounting
+ accountingonecube
diff --git a/instance/emf/serializer/pom.xml b/instance/emf/serializer/pom.xml
index fd6f559d6..ba250561b 100644
--- a/instance/emf/serializer/pom.xml
+++ b/instance/emf/serializer/pom.xml
@@ -519,6 +519,34 @@
${project.version}
+
+ org.eclipse.daanse
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.textagg
+ ${project.version}
+
+
+
+ org.eclipse.daanse
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.parentchild
+ ${project.version}
+
+
+
+ org.eclipse.daanse
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcomment
+ ${project.version}
+
+
+
+ org.eclipse.daanse
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcommentandmultidim
+ ${project.version}
+
+
org.eclipse.daanse
@@ -661,6 +689,13 @@
${project.version}
+
+ org.eclipse.daanse
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.complex.accounting
+ ${project.version}
+
+
org.eclipse.daanse
@@ -676,6 +711,13 @@
${project.version}
+
+ org.eclipse.daanse
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.virtualcube
+ ${project.version}
+
+
diff --git a/instance/emf/serializer/test.bndrun b/instance/emf/serializer/test.bndrun
index 081b5c1af..9c34166c4 100644
--- a/instance/emf/serializer/test.bndrun
+++ b/instance/emf/serializer/test.bndrun
@@ -104,10 +104,15 @@
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.virtualcube.calculatedmember',\
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.virtualcube.dimensions',\
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.virtualcube.unvisiblereferencecubes',\
+ bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.virtualcube',\
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.inlinetable',\
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.table',\
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.view',\
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.withoutdimension',\
+ bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.textagg',\
+ bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.parentchild',\
+ bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcomment',\
+ bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcommentandmultidim',\
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.access.databaseschemagrand',\
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.access.tablegrand',\
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.access.columngrand',\
@@ -132,7 +137,8 @@
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.complex.steelwheels',\
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.complex.csdl.bikeshop',\
bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.complex.csdl.bikeaccessories',\
- bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.complex.parcel'
+ bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.complex.parcel',\
+ bnd.identity;id='org.eclipse.daanse.rolap.mapping.instance.emf.complex.accounting'
-runbundles+: \
org.eclipse.emf.common;startlevel=1,\
@@ -168,6 +174,7 @@
org.eclipse.daanse.olap.check.model.emf;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.olap.check.runtime;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.rolap.mapping.instance.api;version='[0.0.1,0.0.2)',\
+ org.eclipse.daanse.rolap.mapping.instance.emf.complex.accounting;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.rolap.mapping.instance.emf.complex.csdl.bikeaccessories;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.rolap.mapping.instance.emf.complex.csdl.bikeshop;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.rolap.mapping.instance.emf.complex.expressivenames;version='[0.0.1,0.0.2)',\
@@ -250,9 +257,14 @@
org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.virtualcube.dimensions;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.virtualcube.min;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.virtualcube.unvisiblereferencecubes;version='[0.0.1,0.0.2)',\
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcomment;version='[0.0.1,0.0.2)',\
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcommentandmultidim;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.inlinetable;version='[0.0.1,0.0.2)',\
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.parentchild;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.table;version='[0.0.1,0.0.2)',\
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.textagg;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.view;version='[0.0.1,0.0.2)',\
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.virtualcube;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.withoutdimension;version='[0.0.1,0.0.2)',\
org.eclipse.daanse.rolap.mapping.model;version='[0.0.1,0.0.2)';startlevel=30,\
org.eclipse.daanse.sql.guard.api;version='[0.0.1,0.0.2)',\
diff --git a/instance/emf/tutorial/writeback/decimalandcomment/pom.xml b/instance/emf/tutorial/writeback/decimalandcomment/pom.xml
new file mode 100644
index 000000000..8f7ef8781
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcomment/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.daanse
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback
+ ${revision}
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcomment
+
diff --git a/instance/emf/tutorial/writeback/decimalandcomment/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcomment/CatalogSupplier.java b/instance/emf/tutorial/writeback/decimalandcomment/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcomment/CatalogSupplier.java
new file mode 100644
index 000000000..1f2e4189f
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcomment/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcomment/CatalogSupplier.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcomment;
+
+import java.util.List;
+
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Column;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Schema;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Table;
+import org.eclipse.daanse.cwm.util.resource.relational.SqlSimpleTypes;
+
+import org.eclipse.daanse.rolap.mapping.instance.api.CatalogRef;
+import org.eclipse.daanse.rolap.mapping.instance.api.DocSection;
+import org.eclipse.daanse.rolap.mapping.instance.api.Kind;
+import org.eclipse.daanse.rolap.mapping.instance.api.MappingInstance;
+import org.eclipse.daanse.rolap.mapping.instance.api.Source;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescription;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescriptionSupplier;
+
+import org.eclipse.daanse.rolap.mapping.model.catalog.Catalog;
+import org.eclipse.daanse.rolap.mapping.model.catalog.CatalogFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.database.source.SourceFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.source.TableSource;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackAttribute;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackTable;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.CubeFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.MeasureGroup;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.PhysicalCube;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.MeasureFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.SumMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.TextAggMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionConnector;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.StandardDimension;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.ExplicitHierarchy;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.HierarchyFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.Level;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.LevelFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.provider.CatalogMappingSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+@Component(service = { CatalogMappingSupplier.class, TutorialDescriptionSupplier.class })
+@MappingInstance(kind = Kind.TUTORIAL, number = "2.05.07", source = Source.EMF, group = "Writeback") // NOSONAR
+public class CatalogSupplier implements CatalogMappingSupplier, TutorialDescriptionSupplier {
+
+ private static final String CUBE = "C";
+ private static final String FACT = "FACT";
+ private static final String FACTWB = "FACTWB";
+ private static final String PRODUCT = "PRODUCT";
+
+ private Catalog catalog;
+ private Schema databaseSchema;
+ private PhysicalCube cube;
+ private TableSource factSource;
+ private TableSource productSource;
+ private StandardDimension dimension;
+ private ExplicitHierarchy hierarchy;
+ private Level level;
+ private SumMeasure amountMeasure;
+ private TextAggMeasure commentsMeasure;
+ private WritebackTable writebackTable;
+
+ private static final String catalogBody = """
+ **Daanse Tutorial — Writeback with a DECIMAL amount and a text comment.**
+
+ The minimal writeback example that uses a fixed-precision
+ **decimal** numeric type (not integer) for the writeable amount
+ column, paired with a free-text comment column. One single-level
+ dimension (`Product`) supplies the slicing key.
+
+ This is the simplest realistic shape for a planning/forecasting
+ cube where users type currency amounts (which require decimal
+ precision) along with explanatory notes.
+
+ The cube exposes:
+ - **`Amount`** — `SumMeasure` over a `DECIMAL(18,2)` `AMOUNT`
+ column. Currency-formatted.
+ - **`Comments`** — `TextAggMeasure` over a `VARCHAR` `COMMENT`
+ column with separator `" | "`.
+
+ Both measures are writeable via the `FACTWB` writeback table.
+ The runtime detects the text bind type for the `Comments`
+ measure from its `ListAggAggregator`; the numeric `Amount`
+ measure follows the standard allocation path.
+ """;
+
+ private static final String databaseSchemaBody = """
+ Three tables:
+
+ - **`FACT`** — `PRODUCT` (VARCHAR), `AMOUNT` (DECIMAL),
+ `COMMENT` (VARCHAR). One row per posting.
+ - **`PRODUCT`** — `KEY` (VARCHAR), `NAME` (VARCHAR). The single
+ dimension table; the fact joins on
+ `FACT.PRODUCT = PRODUCT.KEY`.
+ - **`FACTWB`** — same dimensional + measure columns as `FACT`,
+ plus `ID` (VARCHAR, UUID written by the engine) and `USER`
+ (VARCHAR, session user).
+ """;
+
+ private static final String dimensionBody = """
+ Single dimension `Product` with one `ExplicitHierarchy` and a
+ single level. The level keys on `PRODUCT.KEY` and shows
+ `PRODUCT.NAME` as the displayed member name.
+ """;
+
+ private static final String measuresBody = """
+ - **`Amount`** — `SumMeasure(FACT.AMOUNT)`. Note that the
+ underlying column is declared `DECIMAL(18, 2)` rather than
+ `INTEGER`, so the writeback binding preserves fractional
+ precision (e.g. tax amounts, currency).
+ - **`Comments`** — `TextAggMeasure(FACT.COMMENT, separator = " | ")`.
+ On read the aggregator concatenates every matching comment;
+ on write the runtime takes the text short-path and inserts a
+ single row with the typed string.
+ """;
+
+ private static final String writebackBody = """
+ ```
+ WritebackTable("FACTWB") [database.writeback]
+ ├── WritebackAttribute(Product) → PRODUCT [database.writeback]
+ ├── WritebackMeasure("Amount") → AMOUNT [olap.cube.measure]
+ └── WritebackMeasure("Comments") → COMMENT [olap.cube.measure]
+ ```
+
+ *Numeric write.* `Cell.setValue([Measures].[Amount], 19.95, EQUAL_ALLOCATION, ...)`
+ → the runtime spreads `19.95` across the cell's leaves per the
+ allocation policy and emits the resulting rows with
+ `AMOUNT` populated as a decimal.
+
+ *Text write.* `Cell.setValue([Measures].[Comments], "promo Q3", ...)`
+ → the runtime recognises the `TextAggMeasure` target, bypasses
+ allocation, and emits exactly one row with `COMMENT = "promo Q3"`
+ and `AMOUNT = NULL`. Every writeback row gets `ID = UUID()`
+ and `USER = sessionUser`.
+ """;
+
+ private static final String cubeBody = """
+ One physical cube `C` over `FACT` with one `DimensionConnector`
+ on `Product`, one `MeasureGroup` holding `Amount` and
+ `Comments`, and the `FACTWB` writeback table.
+ """;
+
+ @Override
+ public Catalog get() {
+ if (catalog != null) {
+ return catalog;
+ }
+
+ databaseSchema = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE
+ .createSchema();
+
+ Column factProductColumn = createColumn("PRODUCT", SqlSimpleTypes.Sql99.varcharType());
+ Column factAmountColumn = createColumn("AMOUNT", SqlSimpleTypes.decimalType(18, 2));
+ Column factCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+ Table factTable = createTable(FACT, List.of(factProductColumn, factAmountColumn, factCommentColumn));
+ databaseSchema.getOwnedElement().add(factTable);
+
+ Column productKeyColumn = createColumn("KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column productNameColumn = createColumn("NAME", SqlSimpleTypes.Sql99.varcharType());
+ Table productTable = createTable(PRODUCT, List.of(productKeyColumn, productNameColumn));
+ databaseSchema.getOwnedElement().add(productTable);
+
+ Column wbProductColumn = createColumn("PRODUCT", SqlSimpleTypes.Sql99.varcharType());
+ Column wbAmountColumn = createColumn("AMOUNT", SqlSimpleTypes.decimalType(18, 2));
+ Column wbCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+ Column wbIdColumn = createColumn("ID", SqlSimpleTypes.Sql99.varcharType());
+ Column wbUserColumn = createColumn("USER", SqlSimpleTypes.Sql99.varcharType());
+ Table writebackPhysicalTable = createTable(FACTWB,
+ List.of(wbProductColumn, wbAmountColumn, wbCommentColumn, wbIdColumn, wbUserColumn));
+ databaseSchema.getOwnedElement().add(writebackPhysicalTable);
+
+ factSource = SourceFactory.eINSTANCE.createTableSource();
+ factSource.setTable(factTable);
+
+ productSource = SourceFactory.eINSTANCE.createTableSource();
+ productSource.setTable(productTable);
+
+ level = LevelFactory.eINSTANCE.createLevel();
+ level.setName("Product");
+ level.setColumn(productKeyColumn);
+ level.setNameColumn(productNameColumn);
+ level.setUniqueMembers(true);
+
+ hierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ hierarchy.setName("Product");
+ hierarchy.setHasAll(true);
+ hierarchy.setAllMemberName("All Products");
+ hierarchy.setPrimaryKey(productKeyColumn);
+ hierarchy.setSource(productSource);
+ hierarchy.getLevels().add(level);
+
+ dimension = DimensionFactory.eINSTANCE.createStandardDimension();
+ dimension.setName("Product");
+ dimension.getHierarchies().add(hierarchy);
+
+ DimensionConnector dimensionConnector = DimensionFactory.eINSTANCE.createDimensionConnector();
+ dimensionConnector.setOverrideDimensionName("Product");
+ dimensionConnector.setDimension(dimension);
+ dimensionConnector.setForeignKey(factProductColumn);
+
+ amountMeasure = MeasureFactory.eINSTANCE.createSumMeasure();
+ amountMeasure.setName("Amount");
+ amountMeasure.setColumn(factAmountColumn);
+ amountMeasure.setFormatString("#,##0.00");
+
+ commentsMeasure = MeasureFactory.eINSTANCE.createTextAggMeasure();
+ commentsMeasure.setName("Comments");
+ commentsMeasure.setColumn(factCommentColumn);
+ commentsMeasure.setSeparator(" | ");
+
+ MeasureGroup measureGroup = CubeFactory.eINSTANCE.createMeasureGroup();
+ measureGroup.getMeasures().addAll(List.of(amountMeasure, commentsMeasure));
+
+ WritebackAttribute wbProductAttribute = WritebackFactory.eINSTANCE.createWritebackAttribute();
+ wbProductAttribute.setDimensionConnector(dimensionConnector);
+ wbProductAttribute.setColumn(wbProductColumn);
+
+ WritebackMeasure wbAmountMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbAmountMeasure.setName("Amount");
+ wbAmountMeasure.setColumn(wbAmountColumn);
+
+ WritebackMeasure wbCommentsMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbCommentsMeasure.setName("Comments");
+ wbCommentsMeasure.setColumn(wbCommentColumn);
+
+ writebackTable = WritebackFactory.eINSTANCE.createWritebackTable();
+ writebackTable.setName(FACTWB);
+ writebackTable.getWritebackAttribute().add(wbProductAttribute);
+ writebackTable.getWritebackMeasure().addAll(List.of(wbAmountMeasure, wbCommentsMeasure));
+
+ cube = CubeFactory.eINSTANCE.createPhysicalCube();
+ cube.setName(CUBE);
+ cube.setSource(factSource);
+ cube.getDimensionConnectors().add(dimensionConnector);
+ cube.getMeasureGroups().add(measureGroup);
+ cube.setWritebackTable(writebackTable);
+
+ catalog = CatalogFactory.eINSTANCE.createCatalog();
+ catalog.setName("Daanse Tutorial - Writeback Decimal + Comment");
+ catalog.setDescription("Minimal writeback example with a DECIMAL amount and a text comment.");
+ catalog.getDbschemas().add(databaseSchema);
+ catalog.getCubes().add(cube);
+
+ return catalog;
+ }
+
+ @Override
+ public TutorialDescription describe() {
+ Catalog c = get();
+ return new TutorialDescription(
+ List.of(
+ new DocSection("Daanse Tutorial - Writeback Decimal + Comment",
+ catalogBody, 1, 0, 0, null, 0),
+ new DocSection("Database Schema", databaseSchemaBody, 1, 1, 0, databaseSchema, 3),
+ new DocSection("Fact Query", "Plain TableSource over FACT.",
+ 1, 2, 0, factSource, 2),
+ new DocSection("Product dimension", dimensionBody, 1, 3, 0, dimension, 0),
+ new DocSection("Measures (decimal Amount + text Comments)", measuresBody, 1, 4, 0, null, 0),
+ new DocSection("Writeback (FACTWB)", writebackBody, 1, 5, 0, writebackTable, 2),
+ new DocSection("Cube C", cubeBody, 1, 6, 0, cube, 2)),
+ List.of(new CatalogRef("catalog", () -> c)));
+ }
+
+ private static Column createColumn(String name,
+ org.eclipse.daanse.cwm.model.cwm.resource.relational.SQLSimpleType type) {
+ Column c = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createColumn();
+ c.setName(name);
+ c.setType(type);
+ return c;
+ }
+
+ private static Table createTable(String name, List columns) {
+ Table t = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createTable();
+ t.setName(name);
+ t.getFeature().addAll(columns);
+ return t;
+ }
+}
diff --git a/instance/emf/tutorial/writeback/decimalandcomment/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcomment/CheckSuiteSupplier.java b/instance/emf/tutorial/writeback/decimalandcomment/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcomment/CheckSuiteSupplier.java
new file mode 100644
index 000000000..e8ef058de
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcomment/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcomment/CheckSuiteSupplier.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcomment;
+
+import org.eclipse.daanse.olap.check.model.check.CatalogCheck;
+import org.eclipse.daanse.olap.check.model.check.CubeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttribute;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttributeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseSchemaCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseTableCheck;
+import org.eclipse.daanse.olap.check.model.check.DimensionCheck;
+import org.eclipse.daanse.olap.check.model.check.HierarchyCheck;
+import org.eclipse.daanse.olap.check.model.check.LevelCheck;
+import org.eclipse.daanse.olap.check.model.check.MeasureCheck;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckFactory;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckSuite;
+import org.eclipse.daanse.olap.check.model.check.OlapConnectionCheck;
+import org.eclipse.daanse.olap.check.runtime.api.OlapCheckSuiteSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+@Component(service = OlapCheckSuiteSupplier.class)
+public class CheckSuiteSupplier implements OlapCheckSuiteSupplier {
+
+ private static final OlapCheckFactory factory = OlapCheckFactory.eINSTANCE;
+
+ private static final String CATALOG_NAME = "Daanse Tutorial - Writeback Decimal + Comment";
+ private static final String CUBE_NAME = "C";
+
+ @Override
+ public OlapCheckSuite get() {
+ DimensionCheck dim = createDimensionCheck("Product",
+ createHierarchyCheck("Product", createLevelCheck("Product")));
+
+ MeasureCheck amount = createMeasureCheck("Amount");
+ MeasureCheck comments = createMeasureCheck("Comments");
+
+ CubeCheck cubeCheck = factory.createCubeCheck();
+ cubeCheck.setName("CubeCheck-" + CUBE_NAME);
+ cubeCheck.setDescription("Cube '" + CUBE_NAME + "' carries Amount (decimal) + Comments (text)");
+ cubeCheck.setCubeName(CUBE_NAME);
+ cubeCheck.getMeasureChecks().add(amount);
+ cubeCheck.getMeasureChecks().add(comments);
+ cubeCheck.getDimensionChecks().add(dim);
+
+ DatabaseTableCheck factTable = createTableCheck("FACT",
+ createColumnCheck("PRODUCT", "VARCHAR"),
+ createColumnCheck("AMOUNT", "DECIMAL"),
+ createColumnCheck("COMMENT", "VARCHAR"));
+ DatabaseTableCheck productTable = createTableCheck("PRODUCT",
+ createColumnCheck("KEY", "VARCHAR"),
+ createColumnCheck("NAME", "VARCHAR"));
+ DatabaseTableCheck wbTable = createTableCheck("FACTWB",
+ createColumnCheck("PRODUCT", "VARCHAR"),
+ createColumnCheck("AMOUNT", "DECIMAL"),
+ createColumnCheck("COMMENT", "VARCHAR"),
+ createColumnCheck("ID", "VARCHAR"),
+ createColumnCheck("USER", "VARCHAR"));
+
+ DatabaseSchemaCheck schemaCheck = factory.createDatabaseSchemaCheck();
+ schemaCheck.setName("Database Schema Check for " + CATALOG_NAME);
+ schemaCheck.setDescription("Schema check for the writeback-decimal tutorial");
+ schemaCheck.getTableChecks().add(factTable);
+ schemaCheck.getTableChecks().add(productTable);
+ schemaCheck.getTableChecks().add(wbTable);
+
+ CatalogCheck catalogCheck = factory.createCatalogCheck();
+ catalogCheck.setName(CATALOG_NAME);
+ catalogCheck.setDescription("Catalog check for the writeback-decimal tutorial");
+ catalogCheck.setCatalogName(CATALOG_NAME);
+ catalogCheck.getCubeChecks().add(cubeCheck);
+ catalogCheck.getDatabaseSchemaChecks().add(schemaCheck);
+
+ OlapConnectionCheck connectionCheck = factory.createOlapConnectionCheck();
+ connectionCheck.setName("Connection Check " + CATALOG_NAME);
+ connectionCheck.setDescription("Connection check for the writeback-decimal tutorial");
+ connectionCheck.getCatalogChecks().add(catalogCheck);
+
+ OlapCheckSuite suite = factory.createOlapCheckSuite();
+ suite.setName("Writeback Decimal Suite");
+ suite.setDescription("Check suite for the writeback-decimal tutorial");
+ suite.getConnectionChecks().add(connectionCheck);
+
+ return suite;
+ }
+
+ private MeasureCheck createMeasureCheck(String name) {
+ MeasureCheck m = factory.createMeasureCheck();
+ m.setName("MeasureCheck-" + name);
+ m.setDescription("Measure '" + name + "' must exist");
+ m.setMeasureName(name);
+ return m;
+ }
+
+ private DimensionCheck createDimensionCheck(String name, HierarchyCheck... hierarchies) {
+ DimensionCheck d = factory.createDimensionCheck();
+ d.setName("DimensionCheck for " + name);
+ d.setDimensionName(name);
+ for (HierarchyCheck h : hierarchies) {
+ d.getHierarchyChecks().add(h);
+ }
+ return d;
+ }
+
+ private HierarchyCheck createHierarchyCheck(String name, LevelCheck... levels) {
+ HierarchyCheck h = factory.createHierarchyCheck();
+ h.setName("HierarchyCheck-" + name);
+ h.setHierarchyName(name);
+ for (LevelCheck l : levels) {
+ h.getLevelChecks().add(l);
+ }
+ return h;
+ }
+
+ private LevelCheck createLevelCheck(String name) {
+ LevelCheck l = factory.createLevelCheck();
+ l.setName("LevelCheck-" + name);
+ l.setLevelName(name);
+ return l;
+ }
+
+ private DatabaseColumnCheck createColumnCheck(String name, String type) {
+ DatabaseColumnAttributeCheck attr = factory.createDatabaseColumnAttributeCheck();
+ attr.setAttributeType(DatabaseColumnAttribute.TYPE);
+ attr.setExpectedValue(type);
+
+ DatabaseColumnCheck c = factory.createDatabaseColumnCheck();
+ c.setName("Database Column Check " + name);
+ c.setColumnName(name);
+ c.getColumnAttributeChecks().add(attr);
+ return c;
+ }
+
+ private DatabaseTableCheck createTableCheck(String name, DatabaseColumnCheck... columns) {
+ DatabaseTableCheck t = factory.createDatabaseTableCheck();
+ t.setName("Database Table Check " + name);
+ t.setTableName(name);
+ for (DatabaseColumnCheck c : columns) {
+ t.getColumnChecks().add(c);
+ }
+ return t;
+ }
+}
diff --git a/instance/emf/tutorial/writeback/decimalandcomment/src/main/resources/data/FACT.csv b/instance/emf/tutorial/writeback/decimalandcomment/src/main/resources/data/FACT.csv
new file mode 100644
index 000000000..d3d71ba2d
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcomment/src/main/resources/data/FACT.csv
@@ -0,0 +1,6 @@
+PRODUCT,AMOUNT,COMMENT
+VARCHAR,DECIMAL,VARCHAR
+P1,19.95,launch promo
+P1,24.50,
+P2,99.00,bulk order
+P3,12.75,sample shipment
diff --git a/instance/emf/tutorial/writeback/decimalandcomment/src/main/resources/data/FACTWB.csv b/instance/emf/tutorial/writeback/decimalandcomment/src/main/resources/data/FACTWB.csv
new file mode 100644
index 000000000..dc41378ca
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcomment/src/main/resources/data/FACTWB.csv
@@ -0,0 +1,2 @@
+PRODUCT,AMOUNT,COMMENT,ID,USER
+VARCHAR,DECIMAL,VARCHAR,VARCHAR,VARCHAR
diff --git a/instance/emf/tutorial/writeback/decimalandcomment/src/main/resources/data/PRODUCT.csv b/instance/emf/tutorial/writeback/decimalandcomment/src/main/resources/data/PRODUCT.csv
new file mode 100644
index 000000000..6290aace4
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcomment/src/main/resources/data/PRODUCT.csv
@@ -0,0 +1,5 @@
+KEY,NAME
+VARCHAR,VARCHAR
+P1,Pen
+P2,Notebook
+P3,Eraser
diff --git a/instance/emf/tutorial/writeback/decimalandcommentandmultidim/pom.xml b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/pom.xml
new file mode 100644
index 000000000..b85bb5c97
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.daanse
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback
+ ${revision}
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcommentandmultidim
+
diff --git a/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcommentandmultidim/CatalogSupplier.java b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcommentandmultidim/CatalogSupplier.java
new file mode 100644
index 000000000..35461bd91
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcommentandmultidim/CatalogSupplier.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcommentandmultidim;
+
+import java.util.List;
+
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Column;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Schema;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Table;
+import org.eclipse.daanse.cwm.util.resource.relational.SqlSimpleTypes;
+
+import org.eclipse.daanse.rolap.mapping.instance.api.CatalogRef;
+import org.eclipse.daanse.rolap.mapping.instance.api.DocSection;
+import org.eclipse.daanse.rolap.mapping.instance.api.Kind;
+import org.eclipse.daanse.rolap.mapping.instance.api.MappingInstance;
+import org.eclipse.daanse.rolap.mapping.instance.api.Source;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescription;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescriptionSupplier;
+
+import org.eclipse.daanse.rolap.mapping.model.catalog.Catalog;
+import org.eclipse.daanse.rolap.mapping.model.catalog.CatalogFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.database.source.SourceFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.source.TableSource;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackAttribute;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackTable;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.CubeFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.MeasureGroup;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.PhysicalCube;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.MeasureFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.SumMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.TextAggMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionConnector;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.StandardDimension;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.ExplicitHierarchy;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.HierarchyFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.Level;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.LevelFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.provider.CatalogMappingSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+@Component(service = { CatalogMappingSupplier.class, TutorialDescriptionSupplier.class })
+@MappingInstance(kind = Kind.TUTORIAL, number = "2.05.08", source = Source.EMF, group = "Writeback") // NOSONAR
+public class CatalogSupplier implements CatalogMappingSupplier, TutorialDescriptionSupplier {
+
+ private static final String CUBE = "C";
+ private static final String FACT = "FACT";
+ private static final String FACTWB = "FACTWB";
+ private static final String PRODUCT = "PRODUCT";
+ private static final String REGION = "REGION";
+
+ private Catalog catalog;
+ private Schema databaseSchema;
+ private PhysicalCube cube;
+ private TableSource factSource;
+ private TableSource productSource;
+ private TableSource regionSource;
+ private StandardDimension productDimension;
+ private StandardDimension regionDimension;
+ private ExplicitHierarchy productHierarchy;
+ private ExplicitHierarchy regionHierarchy;
+ private Level productLevel;
+ private Level countryLevel;
+ private Level cityLevel;
+ private SumMeasure amountMeasure;
+ private TextAggMeasure commentsMeasure;
+ private WritebackTable writebackTable;
+
+ private static final String catalogBody = """
+ **Daanse Tutorial — Writeback with two dimensions, one of them
+ multi-level.**
+
+ Extends the single-dimension `tutorial.writeback.decimal`
+ example with a second dimension (`Region`) that has **two**
+ levels (`Country → City`). The point: writeback works the
+ same way regardless of the slicer's depth — the runtime
+ always writes the leaf-key value of every connected
+ dimension into the writeback table, no matter which level
+ the user typed at in the UI.
+
+ The cube exposes:
+ - **`Amount`** — `SumMeasure` over a `DECIMAL(18,2)` column.
+ - **`Comments`** — `TextAggMeasure` over a `VARCHAR` column.
+
+ Two `DimensionConnector`s (Product, Region) both feed
+ `WritebackAttribute`s pointing at the matching writeback
+ columns. The `Region` connector's foreign key is the
+ leaf-level `CITY` column on the fact — exactly the column
+ the writeback row will carry.
+ """;
+
+ private static final String databaseSchemaBody = """
+ Four tables:
+
+ - **`FACT`** — `PRODUCT` (VARCHAR), `CITY` (VARCHAR),
+ `AMOUNT` (DECIMAL), `COMMENT` (VARCHAR).
+ - **`PRODUCT`** — `KEY` (VARCHAR), `NAME` (VARCHAR).
+ - **`REGION`** — `COUNTRY_KEY` (VARCHAR), `COUNTRY_NAME`
+ (VARCHAR), `CITY_KEY` (VARCHAR), `CITY_NAME` (VARCHAR).
+ Single denormalised table that lists every city with its
+ owning country.
+ - **`FACTWB`** — `PRODUCT` (VARCHAR), `CITY` (VARCHAR),
+ `AMOUNT` (DECIMAL), `COMMENT` (VARCHAR), `ID` (VARCHAR,
+ UUID written by the engine), `USER` (VARCHAR, session
+ user).
+ """;
+
+ private static final String productDimensionBody = """
+ Same single-level shape as the `tutorial.writeback.decimal`
+ tutorial — `Product` dim keyed on `PRODUCT.KEY` with the
+ displayed name from `PRODUCT.NAME`.
+ """;
+
+ private static final String regionDimensionBody = """
+ `Region` is a two-level `ExplicitHierarchy` on a single
+ denormalised `REGION` table (no snowflake join):
+
+ - **`Country`** (L1) — keyed on `REGION.COUNTRY_KEY`,
+ `uniqueMembers = true`. Each country is unique.
+ - **`City`** (L2, leaf) — keyed on `REGION.CITY_KEY`,
+ `uniqueMembers = false`. Different countries can share a
+ city *name*, but the (country, city) pair is unique.
+
+ The fact joins on the leaf: `FACT.CITY = REGION.CITY_KEY`.
+ Aggregations across a country roll up its cities through the
+ standard SQL `GROUP BY` path.
+ """;
+
+ private static final String writebackBody = """
+ ```
+ WritebackTable("FACTWB") [database.writeback]
+ ├── WritebackAttribute(Product) → PRODUCT [database.writeback]
+ ├── WritebackAttribute(Region) → CITY [database.writeback]
+ ├── WritebackMeasure("Amount") → AMOUNT [olap.cube.measure]
+ └── WritebackMeasure("Comments") → COMMENT [olap.cube.measure]
+ ```
+
+ **Two-dimensional cell coordinates.** Every cell in this
+ cube has a (Product, City) coordinate. A
+ `Cell.setValue([Measures].[Amount], 99.95, ...)` issued at
+ an intermediate `Country` member is allocated across the
+ country's cities (each city gets `99.95 / N`); a write at a
+ specific city goes directly to that one leaf.
+
+ **Text writeback semantics.** The `Comments` measure follows
+ the text short-path: a single row at the cell's exact
+ (Product, City-or-Country) coordinate; intermediate-level
+ writes record the country's `COUNTRY_KEY` as the city
+ attribute (since `Region`'s writeback attribute targets the
+ `CITY` column). At read time the `ListAggAggregator`
+ re-aggregates everything through SQL.
+
+ **Both writeable measures share the same writeback table.**
+ Numeric and text writes interleave freely; each call
+ produces its own row(s) tagged with a fresh UUID `ID` and
+ the session `USER`.
+ """;
+
+ private static final String cubeBody = """
+ One physical cube `C` over `FACT` with two
+ `DimensionConnector`s (Product, Region), one `MeasureGroup`
+ holding the decimal `Amount` plus the text `Comments`
+ measure, and the `FACTWB` writeback table.
+ """;
+
+ @Override
+ public Catalog get() {
+ if (catalog != null) {
+ return catalog;
+ }
+
+ databaseSchema = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE
+ .createSchema();
+
+ Column factProductColumn = createColumn("PRODUCT", SqlSimpleTypes.Sql99.varcharType());
+ Column factCityColumn = createColumn("CITY", SqlSimpleTypes.Sql99.varcharType());
+ Column factAmountColumn = createColumn("AMOUNT", SqlSimpleTypes.decimalType(18, 2));
+ Column factCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+ Table factTable = createTable(FACT,
+ List.of(factProductColumn, factCityColumn, factAmountColumn, factCommentColumn));
+ databaseSchema.getOwnedElement().add(factTable);
+
+ Column productKeyColumn = createColumn("KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column productNameColumn = createColumn("NAME", SqlSimpleTypes.Sql99.varcharType());
+ Table productTable = createTable(PRODUCT, List.of(productKeyColumn, productNameColumn));
+ databaseSchema.getOwnedElement().add(productTable);
+
+ Column countryKeyColumn = createColumn("COUNTRY_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column countryNameColumn = createColumn("COUNTRY_NAME", SqlSimpleTypes.Sql99.varcharType());
+ Column cityKeyColumn = createColumn("CITY_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column cityNameColumn = createColumn("CITY_NAME", SqlSimpleTypes.Sql99.varcharType());
+ Table regionTable = createTable(REGION,
+ List.of(countryKeyColumn, countryNameColumn, cityKeyColumn, cityNameColumn));
+ databaseSchema.getOwnedElement().add(regionTable);
+
+ Column wbProductColumn = createColumn("PRODUCT", SqlSimpleTypes.Sql99.varcharType());
+ Column wbCityColumn = createColumn("CITY", SqlSimpleTypes.Sql99.varcharType());
+ Column wbAmountColumn = createColumn("AMOUNT", SqlSimpleTypes.decimalType(18, 2));
+ Column wbCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+ Column wbIdColumn = createColumn("ID", SqlSimpleTypes.Sql99.varcharType());
+ Column wbUserColumn = createColumn("USER", SqlSimpleTypes.Sql99.varcharType());
+ Table writebackPhysicalTable = createTable(FACTWB,
+ List.of(wbProductColumn, wbCityColumn, wbAmountColumn, wbCommentColumn, wbIdColumn, wbUserColumn));
+ databaseSchema.getOwnedElement().add(writebackPhysicalTable);
+
+ factSource = SourceFactory.eINSTANCE.createTableSource();
+ factSource.setTable(factTable);
+
+ productSource = SourceFactory.eINSTANCE.createTableSource();
+ productSource.setTable(productTable);
+
+ regionSource = SourceFactory.eINSTANCE.createTableSource();
+ regionSource.setTable(regionTable);
+
+ productLevel = LevelFactory.eINSTANCE.createLevel();
+ productLevel.setName("Product");
+ productLevel.setColumn(productKeyColumn);
+ productLevel.setNameColumn(productNameColumn);
+ productLevel.setUniqueMembers(true);
+
+ productHierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ productHierarchy.setName("Product");
+ productHierarchy.setHasAll(true);
+ productHierarchy.setAllMemberName("All Products");
+ productHierarchy.setPrimaryKey(productKeyColumn);
+ productHierarchy.setSource(productSource);
+ productHierarchy.getLevels().add(productLevel);
+
+ productDimension = DimensionFactory.eINSTANCE.createStandardDimension();
+ productDimension.setName("Product");
+ productDimension.getHierarchies().add(productHierarchy);
+
+ countryLevel = LevelFactory.eINSTANCE.createLevel();
+ countryLevel.setName("Country");
+ countryLevel.setColumn(countryKeyColumn);
+ countryLevel.setNameColumn(countryNameColumn);
+ countryLevel.setUniqueMembers(true);
+
+ cityLevel = LevelFactory.eINSTANCE.createLevel();
+ cityLevel.setName("City");
+ cityLevel.setColumn(cityKeyColumn);
+ cityLevel.setNameColumn(cityNameColumn);
+ cityLevel.setUniqueMembers(false);
+
+ regionHierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ regionHierarchy.setName("Region");
+ regionHierarchy.setHasAll(true);
+ regionHierarchy.setAllMemberName("All Regions");
+ regionHierarchy.setPrimaryKey(cityKeyColumn);
+ regionHierarchy.setSource(regionSource);
+ regionHierarchy.getLevels().addAll(List.of(countryLevel, cityLevel));
+
+ regionDimension = DimensionFactory.eINSTANCE.createStandardDimension();
+ regionDimension.setName("Region");
+ regionDimension.getHierarchies().add(regionHierarchy);
+
+ DimensionConnector productConnector = DimensionFactory.eINSTANCE.createDimensionConnector();
+ productConnector.setOverrideDimensionName("Product");
+ productConnector.setDimension(productDimension);
+ productConnector.setForeignKey(factProductColumn);
+
+ DimensionConnector regionConnector = DimensionFactory.eINSTANCE.createDimensionConnector();
+ regionConnector.setOverrideDimensionName("Region");
+ regionConnector.setDimension(regionDimension);
+ regionConnector.setForeignKey(factCityColumn);
+
+ amountMeasure = MeasureFactory.eINSTANCE.createSumMeasure();
+ amountMeasure.setName("Amount");
+ amountMeasure.setColumn(factAmountColumn);
+ amountMeasure.setFormatString("#,##0.00");
+
+ commentsMeasure = MeasureFactory.eINSTANCE.createTextAggMeasure();
+ commentsMeasure.setName("Comments");
+ commentsMeasure.setColumn(factCommentColumn);
+ commentsMeasure.setSeparator(" | ");
+
+ MeasureGroup measureGroup = CubeFactory.eINSTANCE.createMeasureGroup();
+ measureGroup.getMeasures().addAll(List.of(amountMeasure, commentsMeasure));
+
+ WritebackAttribute wbProductAttribute = WritebackFactory.eINSTANCE.createWritebackAttribute();
+ wbProductAttribute.setDimensionConnector(productConnector);
+ wbProductAttribute.setColumn(wbProductColumn);
+
+ WritebackAttribute wbRegionAttribute = WritebackFactory.eINSTANCE.createWritebackAttribute();
+ wbRegionAttribute.setDimensionConnector(regionConnector);
+ wbRegionAttribute.setColumn(wbCityColumn);
+
+ WritebackMeasure wbAmountMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbAmountMeasure.setName("Amount");
+ wbAmountMeasure.setColumn(wbAmountColumn);
+
+ WritebackMeasure wbCommentsMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbCommentsMeasure.setName("Comments");
+ wbCommentsMeasure.setColumn(wbCommentColumn);
+
+ writebackTable = WritebackFactory.eINSTANCE.createWritebackTable();
+ writebackTable.setName(FACTWB);
+ writebackTable.getWritebackAttribute().addAll(List.of(wbProductAttribute, wbRegionAttribute));
+ writebackTable.getWritebackMeasure().addAll(List.of(wbAmountMeasure, wbCommentsMeasure));
+
+ cube = CubeFactory.eINSTANCE.createPhysicalCube();
+ cube.setName(CUBE);
+ cube.setSource(factSource);
+ cube.getDimensionConnectors().addAll(List.of(productConnector, regionConnector));
+ cube.getMeasureGroups().add(measureGroup);
+ cube.setWritebackTable(writebackTable);
+
+ catalog = CatalogFactory.eINSTANCE.createCatalog();
+ catalog.setName("Daanse Tutorial - Writeback Multi-Dimension");
+ catalog.setDescription("Writeback example with two dimensions; the Region dimension has two levels.");
+ catalog.getDbschemas().add(databaseSchema);
+ catalog.getCubes().add(cube);
+
+ return catalog;
+ }
+
+ @Override
+ public TutorialDescription describe() {
+ Catalog c = get();
+ return new TutorialDescription(
+ List.of(
+ new DocSection("Daanse Tutorial - Writeback Multi-Dimension",
+ catalogBody, 1, 0, 0, null, 0),
+ new DocSection("Database Schema", databaseSchemaBody, 1, 1, 0, databaseSchema, 3),
+ new DocSection("Fact Query", "Plain TableSource over FACT.",
+ 1, 2, 0, factSource, 2),
+ new DocSection("Product dimension (1 level)", productDimensionBody,
+ 1, 3, 0, productDimension, 0),
+ new DocSection("Region dimension (2 levels)", regionDimensionBody,
+ 1, 4, 0, regionDimension, 0),
+ new DocSection("Writeback (FACTWB)", writebackBody, 1, 5, 0, writebackTable, 2),
+ new DocSection("Cube C", cubeBody, 1, 6, 0, cube, 2)),
+ List.of(new CatalogRef("catalog", () -> c)));
+ }
+
+ private static Column createColumn(String name,
+ org.eclipse.daanse.cwm.model.cwm.resource.relational.SQLSimpleType type) {
+ Column c = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createColumn();
+ c.setName(name);
+ c.setType(type);
+ return c;
+ }
+
+ private static Table createTable(String name, List columns) {
+ Table t = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createTable();
+ t.setName(name);
+ t.getFeature().addAll(columns);
+ return t;
+ }
+}
diff --git a/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcommentandmultidim/CheckSuiteSupplier.java b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcommentandmultidim/CheckSuiteSupplier.java
new file mode 100644
index 000000000..c7aa2b5b9
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/decimalandcommentandmultidim/CheckSuiteSupplier.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.decimalandcommentandmultidim;
+
+import org.eclipse.daanse.olap.check.model.check.CatalogCheck;
+import org.eclipse.daanse.olap.check.model.check.CubeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttribute;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttributeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseSchemaCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseTableCheck;
+import org.eclipse.daanse.olap.check.model.check.DimensionCheck;
+import org.eclipse.daanse.olap.check.model.check.HierarchyCheck;
+import org.eclipse.daanse.olap.check.model.check.LevelCheck;
+import org.eclipse.daanse.olap.check.model.check.MeasureCheck;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckFactory;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckSuite;
+import org.eclipse.daanse.olap.check.model.check.OlapConnectionCheck;
+import org.eclipse.daanse.olap.check.runtime.api.OlapCheckSuiteSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+@Component(service = OlapCheckSuiteSupplier.class)
+public class CheckSuiteSupplier implements OlapCheckSuiteSupplier {
+
+ private static final OlapCheckFactory factory = OlapCheckFactory.eINSTANCE;
+
+ private static final String CATALOG_NAME = "Daanse Tutorial - Writeback Multi-Dimension";
+ private static final String CUBE_NAME = "C";
+
+ @Override
+ public OlapCheckSuite get() {
+ DimensionCheck productDim = createDimensionCheck("Product",
+ createHierarchyCheck("Product", createLevelCheck("Product")));
+ DimensionCheck regionDim = createDimensionCheck("Region",
+ createHierarchyCheck("Region",
+ createLevelCheck("Country"),
+ createLevelCheck("City")));
+
+ MeasureCheck amount = createMeasureCheck("Amount");
+ MeasureCheck comments = createMeasureCheck("Comments");
+
+ CubeCheck cubeCheck = factory.createCubeCheck();
+ cubeCheck.setName("CubeCheck-" + CUBE_NAME);
+ cubeCheck.setDescription("Cube '" + CUBE_NAME + "' carries Amount/Comments + Product + Region(2L)");
+ cubeCheck.setCubeName(CUBE_NAME);
+ cubeCheck.getMeasureChecks().add(amount);
+ cubeCheck.getMeasureChecks().add(comments);
+ cubeCheck.getDimensionChecks().add(productDim);
+ cubeCheck.getDimensionChecks().add(regionDim);
+
+ DatabaseTableCheck factTable = createTableCheck("FACT",
+ createColumnCheck("PRODUCT", "VARCHAR"),
+ createColumnCheck("CITY", "VARCHAR"),
+ createColumnCheck("AMOUNT", "DECIMAL"),
+ createColumnCheck("COMMENT", "VARCHAR"));
+ DatabaseTableCheck productTable = createTableCheck("PRODUCT",
+ createColumnCheck("KEY", "VARCHAR"),
+ createColumnCheck("NAME", "VARCHAR"));
+ DatabaseTableCheck regionTable = createTableCheck("REGION",
+ createColumnCheck("COUNTRY_KEY", "VARCHAR"),
+ createColumnCheck("COUNTRY_NAME", "VARCHAR"),
+ createColumnCheck("CITY_KEY", "VARCHAR"),
+ createColumnCheck("CITY_NAME", "VARCHAR"));
+ DatabaseTableCheck wbTable = createTableCheck("FACTWB",
+ createColumnCheck("PRODUCT", "VARCHAR"),
+ createColumnCheck("CITY", "VARCHAR"),
+ createColumnCheck("AMOUNT", "DECIMAL"),
+ createColumnCheck("COMMENT", "VARCHAR"),
+ createColumnCheck("ID", "VARCHAR"),
+ createColumnCheck("USER", "VARCHAR"));
+
+ DatabaseSchemaCheck schemaCheck = factory.createDatabaseSchemaCheck();
+ schemaCheck.setName("Database Schema Check for " + CATALOG_NAME);
+ schemaCheck.setDescription("Schema check for the writeback-multidim tutorial");
+ schemaCheck.getTableChecks().add(factTable);
+ schemaCheck.getTableChecks().add(productTable);
+ schemaCheck.getTableChecks().add(regionTable);
+ schemaCheck.getTableChecks().add(wbTable);
+
+ CatalogCheck catalogCheck = factory.createCatalogCheck();
+ catalogCheck.setName(CATALOG_NAME);
+ catalogCheck.setDescription("Catalog check for the writeback-multidim tutorial");
+ catalogCheck.setCatalogName(CATALOG_NAME);
+ catalogCheck.getCubeChecks().add(cubeCheck);
+ catalogCheck.getDatabaseSchemaChecks().add(schemaCheck);
+
+ OlapConnectionCheck connectionCheck = factory.createOlapConnectionCheck();
+ connectionCheck.setName("Connection Check " + CATALOG_NAME);
+ connectionCheck.setDescription("Connection check for the writeback-multidim tutorial");
+ connectionCheck.getCatalogChecks().add(catalogCheck);
+
+ OlapCheckSuite suite = factory.createOlapCheckSuite();
+ suite.setName("Writeback Multi-Dim Suite");
+ suite.setDescription("Check suite for the writeback-multidim tutorial");
+ suite.getConnectionChecks().add(connectionCheck);
+
+ return suite;
+ }
+
+ private MeasureCheck createMeasureCheck(String name) {
+ MeasureCheck m = factory.createMeasureCheck();
+ m.setName("MeasureCheck-" + name);
+ m.setDescription("Measure '" + name + "' must exist");
+ m.setMeasureName(name);
+ return m;
+ }
+
+ private DimensionCheck createDimensionCheck(String name, HierarchyCheck... hierarchies) {
+ DimensionCheck d = factory.createDimensionCheck();
+ d.setName("DimensionCheck for " + name);
+ d.setDimensionName(name);
+ for (HierarchyCheck h : hierarchies) {
+ d.getHierarchyChecks().add(h);
+ }
+ return d;
+ }
+
+ private HierarchyCheck createHierarchyCheck(String name, LevelCheck... levels) {
+ HierarchyCheck h = factory.createHierarchyCheck();
+ h.setName("HierarchyCheck-" + name);
+ h.setHierarchyName(name);
+ for (LevelCheck l : levels) {
+ h.getLevelChecks().add(l);
+ }
+ return h;
+ }
+
+ private LevelCheck createLevelCheck(String name) {
+ LevelCheck l = factory.createLevelCheck();
+ l.setName("LevelCheck-" + name);
+ l.setLevelName(name);
+ return l;
+ }
+
+ private DatabaseColumnCheck createColumnCheck(String name, String type) {
+ DatabaseColumnAttributeCheck attr = factory.createDatabaseColumnAttributeCheck();
+ attr.setAttributeType(DatabaseColumnAttribute.TYPE);
+ attr.setExpectedValue(type);
+
+ DatabaseColumnCheck c = factory.createDatabaseColumnCheck();
+ c.setName("Database Column Check " + name);
+ c.setColumnName(name);
+ c.getColumnAttributeChecks().add(attr);
+ return c;
+ }
+
+ private DatabaseTableCheck createTableCheck(String name, DatabaseColumnCheck... columns) {
+ DatabaseTableCheck t = factory.createDatabaseTableCheck();
+ t.setName("Database Table Check " + name);
+ t.setTableName(name);
+ for (DatabaseColumnCheck c : columns) {
+ t.getColumnChecks().add(c);
+ }
+ return t;
+ }
+}
diff --git a/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/FACT.csv b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/FACT.csv
new file mode 100644
index 000000000..d3d82e3b6
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/FACT.csv
@@ -0,0 +1,7 @@
+PRODUCT,CITY,AMOUNT,COMMENT
+VARCHAR,VARCHAR,DECIMAL,VARCHAR
+P1,BER,19.95,launch promo
+P1,MUC,21.50,
+P2,BER,99.00,bulk order
+P2,VIE,42.00,winter discount
+P3,VIE,12.75,sample shipment
diff --git a/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/FACTWB.csv b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/FACTWB.csv
new file mode 100644
index 000000000..8d0b480d3
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/FACTWB.csv
@@ -0,0 +1,2 @@
+PRODUCT,CITY,AMOUNT,COMMENT,ID,USER
+VARCHAR,VARCHAR,DECIMAL,VARCHAR,VARCHAR,VARCHAR
diff --git a/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/PRODUCT.csv b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/PRODUCT.csv
new file mode 100644
index 000000000..6290aace4
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/PRODUCT.csv
@@ -0,0 +1,5 @@
+KEY,NAME
+VARCHAR,VARCHAR
+P1,Pen
+P2,Notebook
+P3,Eraser
diff --git a/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/REGION.csv b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/REGION.csv
new file mode 100644
index 000000000..c77fe1a0e
--- /dev/null
+++ b/instance/emf/tutorial/writeback/decimalandcommentandmultidim/src/main/resources/data/REGION.csv
@@ -0,0 +1,5 @@
+COUNTRY_KEY,COUNTRY_NAME,CITY_KEY,CITY_NAME
+VARCHAR,VARCHAR,VARCHAR,VARCHAR
+DE,Germany,BER,Berlin
+DE,Germany,MUC,Munich
+AT,Austria,VIE,Vienna
diff --git a/instance/emf/tutorial/writeback/inlinetable/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/inlinetable/CatalogSupplier.java b/instance/emf/tutorial/writeback/inlinetable/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/inlinetable/CatalogSupplier.java
index 443440c40..6d473cf95 100644
--- a/instance/emf/tutorial/writeback/inlinetable/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/inlinetable/CatalogSupplier.java
+++ b/instance/emf/tutorial/writeback/inlinetable/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/inlinetable/CatalogSupplier.java
@@ -42,7 +42,7 @@
import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.SumMeasure;
import org.eclipse.daanse.rolap.mapping.model.database.source.TableSource;
import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackAttribute;
-import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure;
import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackTable;
import org.osgi.service.component.annotations.Component;
import org.eclipse.daanse.rolap.mapping.instance.api.CatalogRef;
@@ -316,11 +316,11 @@ public Catalog get() {
writebackAttribute.setDimensionConnector(dimensionConnector);
writebackAttribute.setColumn(l2Column);
- WritebackMeasure writebackMeasure1 = WritebackFactory.eINSTANCE.createWritebackMeasure();
+ WritebackMeasure writebackMeasure1 = MeasureFactory.eINSTANCE.createWritebackMeasure();
writebackMeasure1.setName("Measure1");
writebackMeasure1.setColumn(valColumn);
- WritebackMeasure writebackMeasure2 = WritebackFactory.eINSTANCE.createWritebackMeasure();
+ WritebackMeasure writebackMeasure2 = MeasureFactory.eINSTANCE.createWritebackMeasure();
writebackMeasure2.setName("Measure2");
writebackMeasure2.setColumn(val1Column);
diff --git a/instance/emf/tutorial/writeback/parentchild/pom.xml b/instance/emf/tutorial/writeback/parentchild/pom.xml
new file mode 100644
index 000000000..aa3e54944
--- /dev/null
+++ b/instance/emf/tutorial/writeback/parentchild/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.daanse
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback
+ ${revision}
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.parentchild
+
diff --git a/instance/emf/tutorial/writeback/parentchild/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/parentchild/CatalogSupplier.java b/instance/emf/tutorial/writeback/parentchild/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/parentchild/CatalogSupplier.java
new file mode 100644
index 000000000..a71b49f88
--- /dev/null
+++ b/instance/emf/tutorial/writeback/parentchild/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/parentchild/CatalogSupplier.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.parentchild;
+
+import java.util.List;
+
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Column;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Schema;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Table;
+import org.eclipse.daanse.cwm.util.resource.relational.SqlSimpleTypes;
+
+import org.eclipse.daanse.rolap.mapping.instance.api.CatalogRef;
+import org.eclipse.daanse.rolap.mapping.instance.api.DocSection;
+import org.eclipse.daanse.rolap.mapping.instance.api.Kind;
+import org.eclipse.daanse.rolap.mapping.instance.api.MappingInstance;
+import org.eclipse.daanse.rolap.mapping.instance.api.Source;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescription;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescriptionSupplier;
+
+import org.eclipse.daanse.rolap.mapping.model.catalog.Catalog;
+import org.eclipse.daanse.rolap.mapping.model.catalog.CatalogFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.database.source.SourceFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.source.TableSource;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackAttribute;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackTable;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.CubeFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.MeasureGroup;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.PhysicalCube;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.MeasureFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.SumMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionConnector;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.StandardDimension;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.HierarchyFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.ParentChildHierarchy;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.Level;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.LevelFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.provider.CatalogMappingSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+@Component(service = { CatalogMappingSupplier.class, TutorialDescriptionSupplier.class })
+@MappingInstance(kind = Kind.TUTORIAL, number = "2.05.06", source = Source.EMF, group = "Writeback") // NOSONAR
+public class CatalogSupplier implements CatalogMappingSupplier, TutorialDescriptionSupplier {
+
+ private static final String CUBE = "C";
+ private static final String FACT = "FACT";
+ private static final String NODE = "NODE";
+ private static final String FACTWB = "FACTWB";
+
+ private Catalog catalog;
+ private Schema databaseSchema;
+ private PhysicalCube cube;
+ private TableSource factSource;
+ private TableSource nodeSource;
+ private StandardDimension dimension;
+ private ParentChildHierarchy hierarchy;
+ private Level level;
+ private SumMeasure valueMeasure;
+ private WritebackTable writebackTable;
+
+ private static final String catalogBody = """
+ **Daanse Tutorial — Writeback with a parent-child hierarchy.**
+
+ The minimal example showing how writeback works against a
+ `ParentChildHierarchy`. One self-referencing dimension table
+ `NODE` (KEY, NAME, PARENT_KEY) defines an arbitrary-depth tree,
+ the fact table `FACT` posts values against any node, and a
+ `FACTWB` writeback table receives user-entered adjustments.
+
+ When the user writes a value at an *intermediate* node (e.g. a
+ parent in the tree), the runtime allocates the value across the
+ descendant leaves per the chosen `AllocationPolicy` — exactly
+ the same behaviour as for a multi-level explicit hierarchy. The
+ interesting part is that for a parent-child hierarchy the set
+ of leaves is computed by walking the self-referencing
+ relationship at cube-init time, not from a fixed level depth.
+ """;
+
+ private static final String databaseSchemaBody = """
+ Three tables:
+
+ - **`FACT`** — `NODE`, `VALUE`. One row per fact posting, with
+ `NODE` referencing the leaf node and `VALUE` the numeric
+ amount.
+ - **`NODE`** — `KEY`, `NAME`, `PARENT_KEY`. The self-referencing
+ dimension table. Root rows have an empty `PARENT_KEY`
+ (matching the configured `nullParentValue = ""`). Members at
+ any depth share the same level definition.
+ - **`FACTWB`** — `NODE`, `VALUE`, `ID`, `USER`. The writeback
+ target. `ID` is filled by the engine with a UUID, `USER` with
+ the session user.
+ """;
+
+ private static final String dimensionBody = """
+ One dimension `Tree` with a single-level `ParentChildHierarchy`.
+ The level keys on the `KEY` column; the `PARENT_KEY` column
+ wires the self-reference, and `nullParentValue = ""` marks the
+ roots. The fact joins on `FACT.NODE = NODE.KEY` — at every
+ level of the tree, the same column carries the join key.
+ """;
+
+ private static final String hierarchyBody = """
+ ```
+ ParentChildHierarchy
+ ├── name = "Tree"
+ ├── hasAll = true
+ ├── allMemberName = "All Nodes"
+ ├── primaryKey = NODE.KEY
+ ├── parentColumn = NODE.PARENT_KEY
+ ├── nullParentValue = ""
+ ├── source = NodeTableSource
+ └── level("Node") with column = NODE.KEY, nameColumn = NODE.NAME
+ ```
+
+ `setUniqueMembers(true)` on the level is appropriate here
+ because every parent-child member is uniquely identified by
+ its `KEY` value, regardless of depth.
+ """;
+
+ private static final String writebackBody = """
+ The cube wires the `Value` measure into a single-measure
+ writeback table:
+
+ ```
+ WritebackTable("FACTWB") [database.writeback]
+ ├── WritebackAttribute(Tree connector) → NODE [database.writeback]
+ └── WritebackMeasure("Value") → VALUE [olap.cube.measure]
+ ```
+
+ *Behaviour at write time.* A `Cell.setValue([Measures].[Value], 100, EQUAL_ALLOCATION, ...)`
+ at any node — root, intermediate or leaf — drives the engine to:
+
+ 1. Walk the self-referencing `NODE.PARENT_KEY` chain to compute
+ the leaves of the addressed node.
+ 2. Spread the `100` value across those leaves per the chosen
+ `AllocationPolicy` — `EQUAL_ALLOCATION` gives every leaf
+ `100 / N`, `EQUAL_INCREMENT` adds `(100 - oldValue) / N` to
+ each.
+ 3. Emit one row per leaf into `FACTWB` with the leaf's `KEY` as
+ `NODE`, the allocated portion as `VALUE`, a fresh `UUID` as
+ `ID`, and the session user as `USER`.
+
+ *Behaviour at read time.* A subsequent MDX query for any node
+ sums fact rows plus writeback rows under that subtree via the
+ standard SQL aggregation path — no special handling needed,
+ because the writeback table has the same shape as the fact and
+ both are aggregated by the cube's `SumMeasure`.
+
+ *Where the measure lives.* `WritebackMeasure` is now in the
+ `olap.cube.measure` ecore package (alongside `SumMeasure`,
+ `TextAggMeasure`, …) — it is conceptually a measure that
+ happens to be paired with a database column for persistence.
+ Only the writeback *infrastructure* (`WritebackTable`,
+ `WritebackAttribute`) remains in `database.writeback`. In Java
+ the factory call is `MeasureFactory.eINSTANCE.createWritebackMeasure()`.
+ """;
+
+ private static final String cubeBody = """
+ One physical cube `C` over `FACT` with one `DimensionConnector`
+ (Tree → `FACT.NODE`), one `MeasureGroup` holding the `Value`
+ sum measure, and the `FACTWB` writeback table.
+ """;
+
+ @Override
+ public Catalog get() {
+ if (catalog != null) {
+ return catalog;
+ }
+
+ databaseSchema = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE
+ .createSchema();
+
+ Column factNodeColumn = createColumn("NODE", SqlSimpleTypes.Sql99.varcharType());
+ Column factValueColumn = createColumn("VALUE", SqlSimpleTypes.Sql99.integerType());
+ Table factTable = createTable(FACT, List.of(factNodeColumn, factValueColumn));
+ databaseSchema.getOwnedElement().add(factTable);
+
+ Column nodeKeyColumn = createColumn("KEY", SqlSimpleTypes.Sql99.varcharType());
+ Column nodeNameColumn = createColumn("NAME", SqlSimpleTypes.Sql99.varcharType());
+ Column nodeParentKeyColumn = createColumn("PARENT_KEY", SqlSimpleTypes.Sql99.varcharType());
+ Table nodeTable = createTable(NODE, List.of(nodeKeyColumn, nodeNameColumn, nodeParentKeyColumn));
+ databaseSchema.getOwnedElement().add(nodeTable);
+
+ Column wbNodeColumn = createColumn("NODE", SqlSimpleTypes.Sql99.varcharType());
+ Column wbValueColumn = createColumn("VALUE", SqlSimpleTypes.Sql99.integerType());
+ Column wbIdColumn = createColumn("ID", SqlSimpleTypes.Sql99.varcharType());
+ Column wbUserColumn = createColumn("USER", SqlSimpleTypes.Sql99.varcharType());
+ Table writebackPhysicalTable = createTable(FACTWB,
+ List.of(wbNodeColumn, wbValueColumn, wbIdColumn, wbUserColumn));
+ databaseSchema.getOwnedElement().add(writebackPhysicalTable);
+
+ factSource = SourceFactory.eINSTANCE.createTableSource();
+ factSource.setTable(factTable);
+
+ nodeSource = SourceFactory.eINSTANCE.createTableSource();
+ nodeSource.setTable(nodeTable);
+
+ level = LevelFactory.eINSTANCE.createLevel();
+ level.setName("Node");
+ level.setUniqueMembers(true);
+ level.setColumn(nodeKeyColumn);
+ level.setNameColumn(nodeNameColumn);
+
+ hierarchy = HierarchyFactory.eINSTANCE.createParentChildHierarchy();
+ hierarchy.setName("Tree");
+ hierarchy.setHasAll(true);
+ hierarchy.setAllMemberName("All Nodes");
+ hierarchy.setPrimaryKey(nodeKeyColumn);
+ hierarchy.setSource(nodeSource);
+ hierarchy.setParentColumn(nodeParentKeyColumn);
+ hierarchy.setNullParentValue("");
+ hierarchy.setLevel(level);
+
+ dimension = DimensionFactory.eINSTANCE.createStandardDimension();
+ dimension.setName("Tree");
+ dimension.getHierarchies().add(hierarchy);
+
+ DimensionConnector dimensionConnector = DimensionFactory.eINSTANCE.createDimensionConnector();
+ dimensionConnector.setOverrideDimensionName("Tree");
+ dimensionConnector.setDimension(dimension);
+ dimensionConnector.setForeignKey(factNodeColumn);
+
+ valueMeasure = MeasureFactory.eINSTANCE.createSumMeasure();
+ valueMeasure.setName("Value");
+ valueMeasure.setColumn(factValueColumn);
+ valueMeasure.setFormatString("#,##0");
+
+ MeasureGroup measureGroup = CubeFactory.eINSTANCE.createMeasureGroup();
+ measureGroup.getMeasures().add(valueMeasure);
+
+ WritebackAttribute wbTreeAttribute = WritebackFactory.eINSTANCE.createWritebackAttribute();
+ wbTreeAttribute.setDimensionConnector(dimensionConnector);
+ wbTreeAttribute.setColumn(wbNodeColumn);
+
+ WritebackMeasure wbValueMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbValueMeasure.setName("Value");
+ wbValueMeasure.setColumn(wbValueColumn);
+
+ writebackTable = WritebackFactory.eINSTANCE.createWritebackTable();
+ writebackTable.setName(FACTWB);
+ writebackTable.getWritebackAttribute().add(wbTreeAttribute);
+ writebackTable.getWritebackMeasure().add(wbValueMeasure);
+
+ cube = CubeFactory.eINSTANCE.createPhysicalCube();
+ cube.setName(CUBE);
+ cube.setSource(factSource);
+ cube.getDimensionConnectors().add(dimensionConnector);
+ cube.getMeasureGroups().add(measureGroup);
+ cube.setWritebackTable(writebackTable);
+
+ catalog = CatalogFactory.eINSTANCE.createCatalog();
+ catalog.setName("Daanse Tutorial - Writeback Parent-Child");
+ catalog.setDescription("Minimal writeback example against a ParentChildHierarchy.");
+ catalog.getDbschemas().add(databaseSchema);
+ catalog.getCubes().add(cube);
+
+ return catalog;
+ }
+
+ @Override
+ public TutorialDescription describe() {
+ Catalog c = get();
+ return new TutorialDescription(
+ List.of(
+ new DocSection("Daanse Tutorial - Writeback Parent-Child",
+ catalogBody, 1, 0, 0, null, 0),
+ new DocSection("Database Schema", databaseSchemaBody, 1, 1, 0, databaseSchema, 3),
+ new DocSection("Fact Query", "Plain TableSource over FACT.",
+ 1, 2, 0, factSource, 2),
+ new DocSection("Tree dimension", dimensionBody, 1, 3, 0, dimension, 0),
+ new DocSection("ParentChildHierarchy", hierarchyBody, 1, 4, 0, hierarchy, 0),
+ new DocSection("Writeback (FACTWB)", writebackBody, 1, 5, 0, writebackTable, 2),
+ new DocSection("Cube C", cubeBody, 1, 6, 0, cube, 2)),
+ List.of(new CatalogRef("catalog", () -> c)));
+ }
+
+ private static Column createColumn(String name,
+ org.eclipse.daanse.cwm.model.cwm.resource.relational.SQLSimpleType type) {
+ Column c = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createColumn();
+ c.setName(name);
+ c.setType(type);
+ return c;
+ }
+
+ private static Table createTable(String name, List columns) {
+ Table t = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createTable();
+ t.setName(name);
+ t.getFeature().addAll(columns);
+ return t;
+ }
+}
diff --git a/instance/emf/tutorial/writeback/parentchild/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/parentchild/CheckSuiteSupplier.java b/instance/emf/tutorial/writeback/parentchild/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/parentchild/CheckSuiteSupplier.java
new file mode 100644
index 000000000..3a37dc39f
--- /dev/null
+++ b/instance/emf/tutorial/writeback/parentchild/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/parentchild/CheckSuiteSupplier.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.parentchild;
+
+import org.eclipse.daanse.olap.check.model.check.CatalogCheck;
+import org.eclipse.daanse.olap.check.model.check.CubeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttribute;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttributeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseSchemaCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseTableCheck;
+import org.eclipse.daanse.olap.check.model.check.DimensionCheck;
+import org.eclipse.daanse.olap.check.model.check.HierarchyCheck;
+import org.eclipse.daanse.olap.check.model.check.LevelCheck;
+import org.eclipse.daanse.olap.check.model.check.MeasureCheck;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckFactory;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckSuite;
+import org.eclipse.daanse.olap.check.model.check.OlapConnectionCheck;
+import org.eclipse.daanse.olap.check.runtime.api.OlapCheckSuiteSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+@Component(service = OlapCheckSuiteSupplier.class)
+public class CheckSuiteSupplier implements OlapCheckSuiteSupplier {
+
+ private static final OlapCheckFactory factory = OlapCheckFactory.eINSTANCE;
+
+ private static final String CATALOG_NAME = "Daanse Tutorial - Writeback Parent-Child";
+ private static final String CUBE_NAME = "C";
+
+ @Override
+ public OlapCheckSuite get() {
+ DimensionCheck dim = createDimensionCheck("Tree",
+ createHierarchyCheck("Tree", createLevelCheck("Node")));
+
+ MeasureCheck value = createMeasureCheck("Value");
+
+ CubeCheck cubeCheck = factory.createCubeCheck();
+ cubeCheck.setName("CubeCheck-" + CUBE_NAME);
+ cubeCheck.setDescription("Cube '" + CUBE_NAME + "' carries one Value measure");
+ cubeCheck.setCubeName(CUBE_NAME);
+ cubeCheck.getMeasureChecks().add(value);
+ cubeCheck.getDimensionChecks().add(dim);
+
+ DatabaseTableCheck factTable = createTableCheck("FACT",
+ createColumnCheck("NODE", "VARCHAR"),
+ createColumnCheck("VALUE", "INTEGER"));
+ DatabaseTableCheck nodeTable = createTableCheck("NODE",
+ createColumnCheck("KEY", "VARCHAR"),
+ createColumnCheck("NAME", "VARCHAR"),
+ createColumnCheck("PARENT_KEY", "VARCHAR"));
+ DatabaseTableCheck wbTable = createTableCheck("FACTWB",
+ createColumnCheck("NODE", "VARCHAR"),
+ createColumnCheck("VALUE", "INTEGER"),
+ createColumnCheck("ID", "VARCHAR"),
+ createColumnCheck("USER", "VARCHAR"));
+
+ DatabaseSchemaCheck schemaCheck = factory.createDatabaseSchemaCheck();
+ schemaCheck.setName("Database Schema Check for " + CATALOG_NAME);
+ schemaCheck.setDescription("Schema check for the writeback-parentchild tutorial");
+ schemaCheck.getTableChecks().add(factTable);
+ schemaCheck.getTableChecks().add(nodeTable);
+ schemaCheck.getTableChecks().add(wbTable);
+
+ CatalogCheck catalogCheck = factory.createCatalogCheck();
+ catalogCheck.setName(CATALOG_NAME);
+ catalogCheck.setDescription("Catalog check for the writeback-parentchild tutorial");
+ catalogCheck.setCatalogName(CATALOG_NAME);
+ catalogCheck.getCubeChecks().add(cubeCheck);
+ catalogCheck.getDatabaseSchemaChecks().add(schemaCheck);
+
+ OlapConnectionCheck connectionCheck = factory.createOlapConnectionCheck();
+ connectionCheck.setName("Connection Check " + CATALOG_NAME);
+ connectionCheck.setDescription("Connection check for the writeback-parentchild tutorial");
+ connectionCheck.getCatalogChecks().add(catalogCheck);
+
+ OlapCheckSuite suite = factory.createOlapCheckSuite();
+ suite.setName("Writeback Parent-Child Suite");
+ suite.setDescription("Check suite for the writeback-parentchild tutorial");
+ suite.getConnectionChecks().add(connectionCheck);
+
+ return suite;
+ }
+
+ private MeasureCheck createMeasureCheck(String name) {
+ MeasureCheck m = factory.createMeasureCheck();
+ m.setName("MeasureCheck-" + name);
+ m.setDescription("Measure '" + name + "' must exist");
+ m.setMeasureName(name);
+ return m;
+ }
+
+ private DimensionCheck createDimensionCheck(String name, HierarchyCheck... hierarchies) {
+ DimensionCheck d = factory.createDimensionCheck();
+ d.setName("DimensionCheck for " + name);
+ d.setDimensionName(name);
+ for (HierarchyCheck h : hierarchies) {
+ d.getHierarchyChecks().add(h);
+ }
+ return d;
+ }
+
+ private HierarchyCheck createHierarchyCheck(String name, LevelCheck... levels) {
+ HierarchyCheck h = factory.createHierarchyCheck();
+ h.setName("HierarchyCheck-" + name);
+ h.setHierarchyName(name);
+ for (LevelCheck l : levels) {
+ h.getLevelChecks().add(l);
+ }
+ return h;
+ }
+
+ private LevelCheck createLevelCheck(String name) {
+ LevelCheck l = factory.createLevelCheck();
+ l.setName("LevelCheck-" + name);
+ l.setLevelName(name);
+ return l;
+ }
+
+ private DatabaseColumnCheck createColumnCheck(String name, String type) {
+ DatabaseColumnAttributeCheck attr = factory.createDatabaseColumnAttributeCheck();
+ attr.setAttributeType(DatabaseColumnAttribute.TYPE);
+ attr.setExpectedValue(type);
+
+ DatabaseColumnCheck c = factory.createDatabaseColumnCheck();
+ c.setName("Database Column Check " + name);
+ c.setColumnName(name);
+ c.getColumnAttributeChecks().add(attr);
+ return c;
+ }
+
+ private DatabaseTableCheck createTableCheck(String name, DatabaseColumnCheck... columns) {
+ DatabaseTableCheck t = factory.createDatabaseTableCheck();
+ t.setName("Database Table Check " + name);
+ t.setTableName(name);
+ for (DatabaseColumnCheck c : columns) {
+ t.getColumnChecks().add(c);
+ }
+ return t;
+ }
+}
diff --git a/instance/emf/tutorial/writeback/parentchild/src/main/resources/data/FACT.csv b/instance/emf/tutorial/writeback/parentchild/src/main/resources/data/FACT.csv
new file mode 100644
index 000000000..32eebe67f
--- /dev/null
+++ b/instance/emf/tutorial/writeback/parentchild/src/main/resources/data/FACT.csv
@@ -0,0 +1,7 @@
+NODE,VALUE
+VARCHAR,INTEGER
+PROD_A,10
+PROD_B,15
+PROD_C,20
+SERV_X,30
+SERV_Y,40
diff --git a/instance/emf/tutorial/writeback/parentchild/src/main/resources/data/FACTWB.csv b/instance/emf/tutorial/writeback/parentchild/src/main/resources/data/FACTWB.csv
new file mode 100644
index 000000000..c771adef4
--- /dev/null
+++ b/instance/emf/tutorial/writeback/parentchild/src/main/resources/data/FACTWB.csv
@@ -0,0 +1,2 @@
+NODE,VALUE,ID,USER
+VARCHAR,INTEGER,VARCHAR,VARCHAR
diff --git a/instance/emf/tutorial/writeback/parentchild/src/main/resources/data/NODE.csv b/instance/emf/tutorial/writeback/parentchild/src/main/resources/data/NODE.csv
new file mode 100644
index 000000000..5a0bd32ed
--- /dev/null
+++ b/instance/emf/tutorial/writeback/parentchild/src/main/resources/data/NODE.csv
@@ -0,0 +1,10 @@
+KEY,NAME,PARENT_KEY
+VARCHAR,VARCHAR,VARCHAR
+COMPANY,Company,
+PRODUCTS,Products,COMPANY
+SERVICES,Services,COMPANY
+PROD_A,Product A,PRODUCTS
+PROD_B,Product B,PRODUCTS
+PROD_C,Product C,PRODUCTS
+SERV_X,Service X,SERVICES
+SERV_Y,Service Y,SERVICES
diff --git a/instance/emf/tutorial/writeback/pom.xml b/instance/emf/tutorial/writeback/pom.xml
index 6776014a3..7704d7755 100644
--- a/instance/emf/tutorial/writeback/pom.xml
+++ b/instance/emf/tutorial/writeback/pom.xml
@@ -27,6 +27,11 @@
table
view
withoutdimension
+ textagg
+ parentchild
+ decimalandcomment
+ decimalandcommentandmultidim
+ virtualcube
diff --git a/instance/emf/tutorial/writeback/table/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/table/CatalogSupplier.java b/instance/emf/tutorial/writeback/table/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/table/CatalogSupplier.java
index 6bec151cc..e8f373880 100644
--- a/instance/emf/tutorial/writeback/table/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/table/CatalogSupplier.java
+++ b/instance/emf/tutorial/writeback/table/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/table/CatalogSupplier.java
@@ -35,7 +35,7 @@
import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.SumMeasure;
import org.eclipse.daanse.rolap.mapping.model.database.source.TableSource;
import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackAttribute;
-import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure;
import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackTable;
import org.osgi.service.component.annotations.Component;
import org.eclipse.daanse.rolap.mapping.instance.api.CatalogRef;
@@ -248,11 +248,11 @@ public Catalog get() {
writebackAttribute.setDimensionConnector(dimensionConnector);
writebackAttribute.setColumn(l2Column);
- WritebackMeasure writebackMeasure1 = WritebackFactory.eINSTANCE.createWritebackMeasure();
+ WritebackMeasure writebackMeasure1 = MeasureFactory.eINSTANCE.createWritebackMeasure();
writebackMeasure1.setName("Measure1");
writebackMeasure1.setColumn(valColumn);
- WritebackMeasure writebackMeasure2 = WritebackFactory.eINSTANCE.createWritebackMeasure();
+ WritebackMeasure writebackMeasure2 = MeasureFactory.eINSTANCE.createWritebackMeasure();
writebackMeasure2.setName("Measure2");
writebackMeasure2.setColumn(val1Column);
diff --git a/instance/emf/tutorial/writeback/textagg/pom.xml b/instance/emf/tutorial/writeback/textagg/pom.xml
new file mode 100644
index 000000000..6af9f4c2d
--- /dev/null
+++ b/instance/emf/tutorial/writeback/textagg/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.daanse
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback
+ ${revision}
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.textagg
+
diff --git a/instance/emf/tutorial/writeback/textagg/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/textagg/CatalogSupplier.java b/instance/emf/tutorial/writeback/textagg/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/textagg/CatalogSupplier.java
new file mode 100644
index 000000000..bc951d5f5
--- /dev/null
+++ b/instance/emf/tutorial/writeback/textagg/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/textagg/CatalogSupplier.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.textagg;
+
+import java.util.List;
+
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Column;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Schema;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Table;
+import org.eclipse.daanse.cwm.util.resource.relational.SqlSimpleTypes;
+
+import org.eclipse.daanse.rolap.mapping.instance.api.CatalogRef;
+import org.eclipse.daanse.rolap.mapping.instance.api.DocSection;
+import org.eclipse.daanse.rolap.mapping.instance.api.Kind;
+import org.eclipse.daanse.rolap.mapping.instance.api.MappingInstance;
+import org.eclipse.daanse.rolap.mapping.instance.api.Source;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescription;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescriptionSupplier;
+
+import org.eclipse.daanse.rolap.mapping.model.catalog.Catalog;
+import org.eclipse.daanse.rolap.mapping.model.catalog.CatalogFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.database.relational.OrderedColumn;
+import org.eclipse.daanse.rolap.mapping.model.database.relational.RelationalFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.source.SourceFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.source.TableSource;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackAttribute;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackTable;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.CubeFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.MeasureGroup;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.PhysicalCube;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.MeasureFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.SumMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.TextAggMeasure;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionConnector;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.StandardDimension;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.ExplicitHierarchy;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.HierarchyFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.Level;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.LevelFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.provider.CatalogMappingSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+@Component(service = { CatalogMappingSupplier.class, TutorialDescriptionSupplier.class })
+@MappingInstance(kind = Kind.TUTORIAL, number = "2.05.05", source = Source.EMF, group = "Writeback") // NOSONAR
+public class CatalogSupplier implements CatalogMappingSupplier, TutorialDescriptionSupplier {
+
+ private static final String CUBE = "C";
+ private static final String FACT = "FACT";
+ private static final String FACTWB = "FACTWB";
+ private static final String CATEGORY = "CATEGORY";
+
+ private Catalog catalog;
+ private Schema databaseSchema;
+ private PhysicalCube cube;
+ private TableSource factSource;
+ private TableSource categorySource;
+ private StandardDimension dimension;
+ private ExplicitHierarchy hierarchy;
+ private Level level;
+ private SumMeasure amountMeasure;
+ private TextAggMeasure commentsMeasure;
+ private WritebackTable writebackTable;
+
+ private static final String catalogBody = """
+ **Daanse Tutorial — Writeback with text aggregation.**
+
+ This is the minimal example demonstrating *two-kind* writeback in one
+ cube:
+
+ - a numeric `Amount` measure backed by `SumMeasure` over an `AMOUNT`
+ column, with the usual allocation-policy semantics on write-back;
+ - a text `Comments` measure backed by `TextAggMeasure` over a
+ `COMMENT` column. On read it concatenates every matching cell's
+ `COMMENT` value using the configured separator; on **write** the
+ daanse runtime appends a single row to the writeback table with
+ the typed string for the cell's exact coordinates.
+
+ The writeback table `FACTWB` carries one `WritebackAttribute` for the
+ dimension and two `WritebackMeasure`s — one numeric, one text. The
+ runtime detects the text target from its aggregator type
+ (`ListAggAggregator` ⇒ `Datatype.VARCHAR`) and short-circuits the
+ allocation flow for that path, so text values land verbatim instead
+ of being split numerically across descendants.
+ """;
+
+ private static final String databaseSchemaBody = """
+ The database schema contains two tables:
+
+ - **`FACT`** — `CATEGORY`, `AMOUNT`, `COMMENT`. One row per posting.
+ - **`CATEGORY`** — `CATEGORY`, `NAME`. The single dimension table.
+ - **`FACTWB`** — `CATEGORY`, `AMOUNT`, `COMMENT`, `ID`, `USER`. The
+ writeback target. `ID` is filled by the engine with a UUID,
+ `USER` with the session user.
+ """;
+
+ private static final String dimensionBody = """
+ Single dimension `Category` with one explicit hierarchy and one level
+ keyed on `CATEGORY`. The fact joins on `FACT.CATEGORY = CATEGORY.CATEGORY`.
+ """;
+
+ private static final String measuresBody = """
+ Two stored measures share the same fact source:
+
+ - `Amount` — `SumMeasure(AMOUNT)`. Numeric, currency-formatted.
+ - `Comments` — `TextAggMeasure(COMMENT, separator = " | ")`. The
+ read-side aggregator concatenates all matching comments at every
+ level the slice rolls up to.
+ """;
+
+ private static final String writebackBody = """
+ The cube wires both measures into the same `FACTWB` writeback table:
+
+ ```
+ WritebackTable("FACTWB") [database.writeback]
+ ├── WritebackAttribute(Category connector) → CATEGORY [database.writeback]
+ ├── WritebackMeasure("Amount") → AMOUNT [olap.cube.measure]
+ └── WritebackMeasure("Comments") → COMMENT [olap.cube.measure]
+ ```
+
+ **Where these classes live in the ecore.** `WritebackMeasure` is
+ no longer a sibling of the writeback *infrastructure*
+ (`WritebackTable`, `WritebackAttribute`). It now sits in the
+ `olap/cube/measure/` ecore package next to `SumMeasure`,
+ `TextAggMeasure` and the other base measures, because a writeback
+ measure is conceptually a measure — it names a cube measure by
+ its logical name and just happens to be paired with a database
+ column for persistence. Concretely:
+
+ - Java package:
+ `org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure`
+ (was `…database.writeback.WritebackMeasure`).
+ - Factory call:
+ `MeasureFactory.eINSTANCE.createWritebackMeasure()`
+ (was `WritebackFactory.eINSTANCE.createWritebackMeasure()`).
+ - XMI namespace prefix: `rolapmeas:WritebackMeasure`
+ (was `rolapdwriteback:WritebackMeasure`).
+
+ **Runtime behaviour.** When the client calls
+ `Cell.setValue([Measures].[Amount], 42.0, ...)` the runtime
+ allocates the numeric value across the cell's leaves (per
+ `AllocationPolicy`) and emits one or more rows with `AMOUNT`
+ populated. When the client calls
+ `Cell.setValue([Measures].[Comments], "on track", ...)` the
+ runtime recognises the `TextAggMeasure` target by its aggregator
+ (`ListAggAggregator` ⇒ `Datatype.VARCHAR`), **bypasses
+ allocation**, and emits exactly one row keyed by the cell's
+ coordinates with `COMMENT` set and `AMOUNT` left `NULL`. The
+ engine appends `ID = UUID()` and `USER = sessionUser`.
+
+ Re-running an MDX query for either measure folds the new
+ writeback row in via the normal aggregation path — `SUM` for
+ `Amount`, `TextAgg` (string concat) for `Comments`.
+ """;
+
+ private static final String cubeBody = """
+ One physical cube `C` over the `FACT` table with one
+ `DimensionConnector` and one `MeasureGroup` holding both measures,
+ referencing the `FACTWB` writeback table.
+ """;
+
+ @Override
+ public Catalog get() {
+ if (catalog != null) {
+ return catalog;
+ }
+
+ databaseSchema = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE
+ .createSchema();
+
+ Column factCategoryColumn = createColumn("CATEGORY", SqlSimpleTypes.Sql99.varcharType());
+ Column factAmountColumn = createColumn("AMOUNT", SqlSimpleTypes.Sql99.integerType());
+ Column factCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+ Table factTable = createTable(FACT, List.of(factCategoryColumn, factAmountColumn, factCommentColumn));
+ databaseSchema.getOwnedElement().add(factTable);
+
+ Column catCategoryColumn = createColumn("CATEGORY", SqlSimpleTypes.Sql99.varcharType());
+ Column catNameColumn = createColumn("NAME", SqlSimpleTypes.Sql99.varcharType());
+ Table categoryTable = createTable(CATEGORY, List.of(catCategoryColumn, catNameColumn));
+ databaseSchema.getOwnedElement().add(categoryTable);
+
+ Column wbCategoryColumn = createColumn("CATEGORY", SqlSimpleTypes.Sql99.varcharType());
+ Column wbAmountColumn = createColumn("AMOUNT", SqlSimpleTypes.Sql99.integerType());
+ Column wbCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+ Column wbIdColumn = createColumn("ID", SqlSimpleTypes.Sql99.varcharType());
+ Column wbUserColumn = createColumn("USER", SqlSimpleTypes.Sql99.varcharType());
+ Table writebackPhysicalTable = createTable(FACTWB,
+ List.of(wbCategoryColumn, wbAmountColumn, wbCommentColumn, wbIdColumn, wbUserColumn));
+ databaseSchema.getOwnedElement().add(writebackPhysicalTable);
+
+ factSource = SourceFactory.eINSTANCE.createTableSource();
+ factSource.setTable(factTable);
+
+ categorySource = SourceFactory.eINSTANCE.createTableSource();
+ categorySource.setTable(categoryTable);
+
+ level = LevelFactory.eINSTANCE.createLevel();
+ level.setName("Category");
+ level.setColumn(catCategoryColumn);
+ level.setNameColumn(catNameColumn);
+ level.setUniqueMembers(true);
+
+ hierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ hierarchy.setName("Category");
+ hierarchy.setHasAll(true);
+ hierarchy.setAllMemberName("All Categories");
+ hierarchy.setPrimaryKey(catCategoryColumn);
+ hierarchy.setSource(categorySource);
+ hierarchy.getLevels().add(level);
+
+ dimension = DimensionFactory.eINSTANCE.createStandardDimension();
+ dimension.setName("Category");
+ dimension.getHierarchies().add(hierarchy);
+
+ DimensionConnector dimensionConnector = DimensionFactory.eINSTANCE.createDimensionConnector();
+ dimensionConnector.setOverrideDimensionName("Category");
+ dimensionConnector.setDimension(dimension);
+ dimensionConnector.setForeignKey(factCategoryColumn);
+
+ amountMeasure = MeasureFactory.eINSTANCE.createSumMeasure();
+ amountMeasure.setName("Amount");
+ amountMeasure.setColumn(factAmountColumn);
+ amountMeasure.setFormatString("#,##0");
+
+ OrderedColumn commentOrderedColumn = RelationalFactory.eINSTANCE.createOrderedColumn();
+ commentOrderedColumn.setColumn(factCommentColumn);
+
+ commentsMeasure = MeasureFactory.eINSTANCE.createTextAggMeasure();
+ commentsMeasure.setName("Comments");
+ commentsMeasure.setColumn(factCommentColumn);
+ commentsMeasure.setSeparator(" | ");
+ commentsMeasure.getOrderByColumns().add(commentOrderedColumn);
+
+ MeasureGroup measureGroup = CubeFactory.eINSTANCE.createMeasureGroup();
+ measureGroup.getMeasures().addAll(List.of(amountMeasure, commentsMeasure));
+
+ WritebackAttribute wbCategoryAttribute = WritebackFactory.eINSTANCE.createWritebackAttribute();
+ wbCategoryAttribute.setDimensionConnector(dimensionConnector);
+ wbCategoryAttribute.setColumn(wbCategoryColumn);
+
+ WritebackMeasure wbAmountMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbAmountMeasure.setName("Amount");
+ wbAmountMeasure.setColumn(wbAmountColumn);
+
+ WritebackMeasure wbCommentsMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbCommentsMeasure.setName("Comments");
+ wbCommentsMeasure.setColumn(wbCommentColumn);
+
+ writebackTable = WritebackFactory.eINSTANCE.createWritebackTable();
+ writebackTable.setName(FACTWB);
+ writebackTable.getWritebackAttribute().add(wbCategoryAttribute);
+ writebackTable.getWritebackMeasure().addAll(List.of(wbAmountMeasure, wbCommentsMeasure));
+
+ cube = CubeFactory.eINSTANCE.createPhysicalCube();
+ cube.setName(CUBE);
+ cube.setSource(factSource);
+ cube.getDimensionConnectors().add(dimensionConnector);
+ cube.getMeasureGroups().add(measureGroup);
+ cube.setWritebackTable(writebackTable);
+
+ catalog = CatalogFactory.eINSTANCE.createCatalog();
+ catalog.setName("Daanse Tutorial - Writeback Text Aggregation");
+ catalog.setDescription("Minimal writeback example: numeric SumMeasure + text TextAggMeasure"
+ + " in the same writeback table.");
+ catalog.getDbschemas().add(databaseSchema);
+ catalog.getCubes().add(cube);
+
+ return catalog;
+ }
+
+ @Override
+ public TutorialDescription describe() {
+ Catalog c = get();
+ return new TutorialDescription(
+ List.of(
+ new DocSection("Daanse Tutorial - Writeback Text Aggregation",
+ catalogBody, 1, 0, 0, null, 0),
+ new DocSection("Database Schema", databaseSchemaBody, 1, 1, 0, databaseSchema, 3),
+ new DocSection("Fact Query", "Plain TableSource over the FACT table.",
+ 1, 2, 0, factSource, 2),
+ new DocSection("Category dimension", dimensionBody, 1, 3, 0, dimension, 0),
+ new DocSection("Measures (numeric + text)", measuresBody, 1, 4, 0, null, 0),
+ new DocSection("Writeback (FACTWB)", writebackBody, 1, 5, 0, writebackTable, 2),
+ new DocSection("Cube C", cubeBody, 1, 6, 0, cube, 2)),
+ List.of(new CatalogRef("catalog", () -> c)));
+ }
+
+ private static Column createColumn(String name,
+ org.eclipse.daanse.cwm.model.cwm.resource.relational.SQLSimpleType type) {
+ Column c = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createColumn();
+ c.setName(name);
+ c.setType(type);
+ return c;
+ }
+
+ private static Table createTable(String name, List columns) {
+ Table t = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createTable();
+ t.setName(name);
+ t.getFeature().addAll(columns);
+ return t;
+ }
+}
diff --git a/instance/emf/tutorial/writeback/textagg/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/textagg/CheckSuiteSupplier.java b/instance/emf/tutorial/writeback/textagg/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/textagg/CheckSuiteSupplier.java
new file mode 100644
index 000000000..0b64d8398
--- /dev/null
+++ b/instance/emf/tutorial/writeback/textagg/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/textagg/CheckSuiteSupplier.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.textagg;
+
+import org.eclipse.daanse.olap.check.model.check.CatalogCheck;
+import org.eclipse.daanse.olap.check.model.check.CubeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttribute;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttributeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseSchemaCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseTableCheck;
+import org.eclipse.daanse.olap.check.model.check.DimensionCheck;
+import org.eclipse.daanse.olap.check.model.check.HierarchyCheck;
+import org.eclipse.daanse.olap.check.model.check.LevelCheck;
+import org.eclipse.daanse.olap.check.model.check.MeasureCheck;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckFactory;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckSuite;
+import org.eclipse.daanse.olap.check.model.check.OlapConnectionCheck;
+import org.eclipse.daanse.olap.check.runtime.api.OlapCheckSuiteSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+/** Check suite for the writeback-textagg minimal tutorial. */
+@Component(service = OlapCheckSuiteSupplier.class)
+public class CheckSuiteSupplier implements OlapCheckSuiteSupplier {
+
+ private static final OlapCheckFactory factory = OlapCheckFactory.eINSTANCE;
+
+ private static final String CATALOG_NAME = "Daanse Tutorial - Writeback Text Aggregation";
+ private static final String CUBE_NAME = "C";
+
+ @Override
+ public OlapCheckSuite get() {
+ DimensionCheck dim = createDimensionCheck("Category",
+ createHierarchyCheck("Category", createLevelCheck("Category")));
+
+ MeasureCheck amount = createMeasureCheck("Amount");
+ MeasureCheck comments = createMeasureCheck("Comments");
+
+ CubeCheck cubeCheck = factory.createCubeCheck();
+ cubeCheck.setName("CubeCheck-" + CUBE_NAME);
+ cubeCheck.setDescription("Cube '" + CUBE_NAME + "' carries Amount + Comments measures");
+ cubeCheck.setCubeName(CUBE_NAME);
+ cubeCheck.getMeasureChecks().add(amount);
+ cubeCheck.getMeasureChecks().add(comments);
+ cubeCheck.getDimensionChecks().add(dim);
+
+ DatabaseTableCheck factTable = createTableCheck("FACT",
+ createColumnCheck("CATEGORY", "VARCHAR"),
+ createColumnCheck("AMOUNT", "INTEGER"),
+ createColumnCheck("COMMENT", "VARCHAR"));
+ DatabaseTableCheck categoryTable = createTableCheck("CATEGORY",
+ createColumnCheck("CATEGORY", "VARCHAR"),
+ createColumnCheck("NAME", "VARCHAR"));
+ DatabaseTableCheck wbTable = createTableCheck("FACTWB",
+ createColumnCheck("CATEGORY", "VARCHAR"),
+ createColumnCheck("AMOUNT", "INTEGER"),
+ createColumnCheck("COMMENT", "VARCHAR"),
+ createColumnCheck("ID", "VARCHAR"),
+ createColumnCheck("USER", "VARCHAR"));
+
+ DatabaseSchemaCheck schemaCheck = factory.createDatabaseSchemaCheck();
+ schemaCheck.setName("Database Schema Check for " + CATALOG_NAME);
+ schemaCheck.setDescription("Schema check for the writeback-textagg tutorial");
+ schemaCheck.getTableChecks().add(factTable);
+ schemaCheck.getTableChecks().add(categoryTable);
+ schemaCheck.getTableChecks().add(wbTable);
+
+ CatalogCheck catalogCheck = factory.createCatalogCheck();
+ catalogCheck.setName(CATALOG_NAME);
+ catalogCheck.setDescription("Catalog check for the writeback-textagg tutorial");
+ catalogCheck.setCatalogName(CATALOG_NAME);
+ catalogCheck.getCubeChecks().add(cubeCheck);
+ catalogCheck.getDatabaseSchemaChecks().add(schemaCheck);
+
+ OlapConnectionCheck connectionCheck = factory.createOlapConnectionCheck();
+ connectionCheck.setName("Connection Check " + CATALOG_NAME);
+ connectionCheck.setDescription("Connection check for the writeback-textagg tutorial");
+ connectionCheck.getCatalogChecks().add(catalogCheck);
+
+ OlapCheckSuite suite = factory.createOlapCheckSuite();
+ suite.setName("Writeback TextAgg Suite");
+ suite.setDescription("Check suite for the writeback-textagg tutorial");
+ suite.getConnectionChecks().add(connectionCheck);
+
+ return suite;
+ }
+
+ private MeasureCheck createMeasureCheck(String measureName) {
+ MeasureCheck m = factory.createMeasureCheck();
+ m.setName("MeasureCheck-" + measureName);
+ m.setDescription("Measure '" + measureName + "' must exist");
+ m.setMeasureName(measureName);
+ return m;
+ }
+
+ private DimensionCheck createDimensionCheck(String name, HierarchyCheck... hierarchies) {
+ DimensionCheck d = factory.createDimensionCheck();
+ d.setName("DimensionCheck for " + name);
+ d.setDimensionName(name);
+ for (HierarchyCheck hc : hierarchies) {
+ d.getHierarchyChecks().add(hc);
+ }
+ return d;
+ }
+
+ private HierarchyCheck createHierarchyCheck(String name, LevelCheck... levels) {
+ HierarchyCheck h = factory.createHierarchyCheck();
+ h.setName("HierarchyCheck-" + name);
+ h.setHierarchyName(name);
+ for (LevelCheck lc : levels) {
+ h.getLevelChecks().add(lc);
+ }
+ return h;
+ }
+
+ private LevelCheck createLevelCheck(String name) {
+ LevelCheck l = factory.createLevelCheck();
+ l.setName("LevelCheck-" + name);
+ l.setLevelName(name);
+ return l;
+ }
+
+ private DatabaseColumnCheck createColumnCheck(String name, String type) {
+ DatabaseColumnAttributeCheck attr = factory.createDatabaseColumnAttributeCheck();
+ attr.setAttributeType(DatabaseColumnAttribute.TYPE);
+ attr.setExpectedValue(type);
+
+ DatabaseColumnCheck c = factory.createDatabaseColumnCheck();
+ c.setName("Database Column Check " + name);
+ c.setColumnName(name);
+ c.getColumnAttributeChecks().add(attr);
+ return c;
+ }
+
+ private DatabaseTableCheck createTableCheck(String name, DatabaseColumnCheck... columns) {
+ DatabaseTableCheck t = factory.createDatabaseTableCheck();
+ t.setName("Database Table Check " + name);
+ t.setTableName(name);
+ for (DatabaseColumnCheck c : columns) {
+ t.getColumnChecks().add(c);
+ }
+ return t;
+ }
+}
diff --git a/instance/emf/tutorial/writeback/textagg/src/main/resources/data/CATEGORY.csv b/instance/emf/tutorial/writeback/textagg/src/main/resources/data/CATEGORY.csv
new file mode 100644
index 000000000..1cd91768a
--- /dev/null
+++ b/instance/emf/tutorial/writeback/textagg/src/main/resources/data/CATEGORY.csv
@@ -0,0 +1,5 @@
+CATEGORY,NAME
+VARCHAR,VARCHAR
+A,Alpha
+B,Beta
+C,Gamma
diff --git a/instance/emf/tutorial/writeback/textagg/src/main/resources/data/FACT.csv b/instance/emf/tutorial/writeback/textagg/src/main/resources/data/FACT.csv
new file mode 100644
index 000000000..f2f084c1e
--- /dev/null
+++ b/instance/emf/tutorial/writeback/textagg/src/main/resources/data/FACT.csv
@@ -0,0 +1,7 @@
+CATEGORY,AMOUNT,COMMENT
+VARCHAR,INTEGER,VARCHAR
+A,10,initial entry for A
+A,20,follow-up on A
+B,30,opening row for B
+B,40,
+C,50,only entry on C
diff --git a/instance/emf/tutorial/writeback/textagg/src/main/resources/data/FACTWB.csv b/instance/emf/tutorial/writeback/textagg/src/main/resources/data/FACTWB.csv
new file mode 100644
index 000000000..c19fe393a
--- /dev/null
+++ b/instance/emf/tutorial/writeback/textagg/src/main/resources/data/FACTWB.csv
@@ -0,0 +1,2 @@
+CATEGORY,AMOUNT,COMMENT,ID,USER
+VARCHAR,INTEGER,VARCHAR,VARCHAR,VARCHAR
diff --git a/instance/emf/tutorial/writeback/view/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/view/CatalogSupplier.java b/instance/emf/tutorial/writeback/view/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/view/CatalogSupplier.java
index 1604fe224..1086fdeee 100644
--- a/instance/emf/tutorial/writeback/view/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/view/CatalogSupplier.java
+++ b/instance/emf/tutorial/writeback/view/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/view/CatalogSupplier.java
@@ -38,7 +38,7 @@
import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.SumMeasure;
import org.eclipse.daanse.rolap.mapping.model.database.source.TableSource;
import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackAttribute;
-import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure;
import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackTable;
import org.osgi.service.component.annotations.Component;
import org.eclipse.daanse.rolap.mapping.instance.api.CatalogRef;
@@ -256,11 +256,11 @@ public Catalog get() {
writebackAttribute.setDimensionConnector(dimensionConnector);
writebackAttribute.setColumn(l2Column);
- WritebackMeasure writebackMeasure1 = WritebackFactory.eINSTANCE.createWritebackMeasure();
+ WritebackMeasure writebackMeasure1 = MeasureFactory.eINSTANCE.createWritebackMeasure();
writebackMeasure1.setName("Measure1");
writebackMeasure1.setColumn(valColumn);
- WritebackMeasure writebackMeasure2 = WritebackFactory.eINSTANCE.createWritebackMeasure();
+ WritebackMeasure writebackMeasure2 = MeasureFactory.eINSTANCE.createWritebackMeasure();
writebackMeasure2.setName("Measure2");
writebackMeasure2.setColumn(val1Column);
diff --git a/instance/emf/tutorial/writeback/virtualcube/pom.xml b/instance/emf/tutorial/writeback/virtualcube/pom.xml
new file mode 100644
index 000000000..ff800f6ff
--- /dev/null
+++ b/instance/emf/tutorial/writeback/virtualcube/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.daanse
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback
+ ${revision}
+
+ org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.virtualcube
+
diff --git a/instance/emf/tutorial/writeback/virtualcube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/virtualcube/CatalogSupplier.java b/instance/emf/tutorial/writeback/virtualcube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/virtualcube/CatalogSupplier.java
new file mode 100644
index 000000000..0d6ff3e80
--- /dev/null
+++ b/instance/emf/tutorial/writeback/virtualcube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/virtualcube/CatalogSupplier.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.virtualcube;
+
+import java.util.List;
+
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Column;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Schema;
+import org.eclipse.daanse.cwm.model.cwm.resource.relational.Table;
+import org.eclipse.daanse.cwm.util.resource.relational.SqlSimpleTypes;
+
+import org.eclipse.daanse.rolap.mapping.instance.api.CatalogRef;
+import org.eclipse.daanse.rolap.mapping.instance.api.DocSection;
+import org.eclipse.daanse.rolap.mapping.instance.api.Kind;
+import org.eclipse.daanse.rolap.mapping.instance.api.MappingInstance;
+import org.eclipse.daanse.rolap.mapping.instance.api.Source;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescription;
+import org.eclipse.daanse.rolap.mapping.instance.api.TutorialDescriptionSupplier;
+
+import org.eclipse.daanse.rolap.mapping.model.catalog.Catalog;
+import org.eclipse.daanse.rolap.mapping.model.catalog.CatalogFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.database.relational.OrderedColumn;
+import org.eclipse.daanse.rolap.mapping.model.database.relational.RelationalFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.source.SourceFactory;
+import org.eclipse.daanse.rolap.mapping.model.database.source.TableSource;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackAttribute;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure;
+import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackTable;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.CubeFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.MeasureGroup;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.PhysicalCube;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.VirtualCube;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.MeasureFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.SumMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.TextAggMeasure;
+
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionConnector;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.DimensionFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.StandardDimension;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.ExplicitHierarchy;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.HierarchyFactory;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.Level;
+import org.eclipse.daanse.rolap.mapping.model.olap.dimension.hierarchy.level.LevelFactory;
+
+import org.eclipse.daanse.rolap.mapping.model.provider.CatalogMappingSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+@Component(service = { CatalogMappingSupplier.class, TutorialDescriptionSupplier.class })
+@MappingInstance(kind = Kind.TUTORIAL, number = "2.05.06", source = Source.EMF, group = "Writeback") // NOSONAR
+public class CatalogSupplier implements CatalogMappingSupplier, TutorialDescriptionSupplier {
+
+ private static final String CUBE_N = "CN";
+ private static final String CUBE_T = "CT";
+ private static final String CUBE_V = "V";
+ private static final String FACT_N = "FACT_N";
+ private static final String FACT_T = "FACT_T";
+ private static final String FACTWB_T = "FACTWB_T";
+ private static final String CATEGORY = "CATEGORY";
+ private static final String REGION = "REGION";
+
+ private Catalog catalog;
+ private Schema databaseSchema;
+ private PhysicalCube cube1;
+ private PhysicalCube cube2;
+ private VirtualCube vCube;
+ private TableSource factNSource;
+ private TableSource factTSource;
+ private StandardDimension categoryDimension;
+ private StandardDimension regionDimension;
+ private SumMeasure amountMeasure;
+ private SumMeasure valueMeasure;
+ private TextAggMeasure commentsMeasure;
+ private WritebackTable writebackTable;
+
+ private static final String catalogBody = """
+ **Daanse Tutorial — Writeback combined through a Virtual Cube.**
+
+ Two physical cubes sharing the same dimensions, one read-only and
+ one writeback, joined through a `VirtualCube`.
+
+ - `CN` (Cube-Numeric) — fact `FACT_N`, one `SumMeasure(Amount)`,
+ **no writeback table**. Stays read-only.
+ - `CT` (Cube-Text) — fact `FACT_T`, one `TextAggMeasure(Comments)`
+ **plus a numeric `SumMeasure(Value)`**, **writeback enabled** to
+ `FACTWB_T`. Each cell update writes one row carrying either the
+ typed string in `COMMENT` or the numeric `VALUE` payload.
+ - `V` (VirtualCube) — references both physical cubes and exposes
+ `Amount`, `Value` and `Comments` together. The same `Category`
+ and `Region` dimensions show up across both cubes via per-cube
+ `DimensionConnector` instances pointing at the same shared
+ `StandardDimension`.
+
+ Why two cubes? `PhysicalCube.writebackTable` is per-cube in the
+ ecore. A `VirtualCube` has no writeback field. Mixing a measure
+ you want to write and one you don't on the same physical cube
+ entangles their fact-table union behind one writeback table —
+ cleaner to split into two cubes and combine views above them.
+ """;
+
+ private static final String databaseSchemaBody = """
+ Five tables in one schema:
+
+ - `CATEGORY(CATEGORY, NAME)` — dimension table.
+ - `REGION(REGION, NAME)` — dimension table.
+ - `FACT_N(CATEGORY, REGION, AMOUNT)` — Cube CN's facts.
+ - `FACT_T(CATEGORY, REGION, VALUE, COMMENT)` — Cube CT's facts.
+ - `FACTWB_T(CATEGORY, REGION, VALUE, COMMENT, ID, USER)` —
+ writeback target for Cube CT. Both the numeric `VALUE` column
+ and the text `COMMENT` column are writeable. `ID` is filled
+ with a UUID by the runtime, `USER` with the session user.
+ """;
+
+ private static final String dimensionsBody = """
+ Two `StandardDimension` instances at catalog scope:
+
+ - `Category` — explicit hierarchy with one level keyed on
+ `CATEGORY.CATEGORY`, name from `CATEGORY.NAME`.
+ - `Region` — explicit hierarchy with one level keyed on
+ `REGION.REGION`, name from `REGION.NAME`.
+
+ Each physical cube gets its own `DimensionConnector` per
+ dimension. All connectors `setDimension(...)` on the same
+ shared dimension instance. The `VirtualCube` aggregates all
+ four connectors so the same hierarchies surface for both
+ cubes' measures.
+ """;
+
+ private static final String cube1Body = """
+ `CN` is a plain read-only cube:
+
+ - `TableSource` over `FACT_N`.
+ - `DimensionConnector`s for `Category` (FK = `FACT_N.CATEGORY`)
+ and `Region` (FK = `FACT_N.REGION`).
+ - One `MeasureGroup` containing `Amount = SumMeasure(AMOUNT)`.
+ - No `writebackTable`. Writebacks targeting `[Measures].[Amount]`
+ are a no-op on this cube.
+ """;
+
+ private static final String cube2Body = """
+ `CT` carries the writeback measures — one numeric, one text:
+
+ - `TableSource` over `FACT_T`.
+ - `DimensionConnector`s for `Category` (FK = `FACT_T.CATEGORY`)
+ and `Region` (FK = `FACT_T.REGION`).
+ - One `MeasureGroup` containing
+ `Value = SumMeasure(VALUE)` and
+ `Comments = TextAggMeasure(COMMENT, separator = " | ")`.
+ - `WritebackTable("FACTWB_T")` with two
+ `WritebackAttribute`s (one per dimension connector) and two
+ `WritebackMeasure` entries:
+ `Value` → `FACTWB_T.VALUE` and `Comments` → `FACTWB_T.COMMENT`.
+ """;
+
+ private static final String virtualBody = """
+ `V` is a `VirtualCube` that references both physical cubes.
+ Its `dimensionConnectors` are the four connectors above (two
+ from each cube, pointing at the same shared dimensions). Its
+ `referencedMeasures` are `Amount`, `Value` and `Comments`,
+ drawn from their respective underlying cubes.
+
+ Clients querying `[V]` see one logical schema combining both
+ facts. The default measure is `Amount`.
+ """;
+
+ private static final String writebackBody = """
+ When the client writes to `([Category].[A], [Region].[N],
+ [Measures].[Comments]) = "hello"` through `[V]` (or directly
+ through `[CT]`), the runtime resolves the target measure to
+ `Comments` on cube `CT`, detects its
+ `Datatype.VARCHAR` (because `TextAggMeasure` resolves to
+ `ListAggAggregator`), and emits exactly one row into
+ `FACTWB_T` with `CATEGORY='A'`, `REGION='N'`,
+ `COMMENT='hello'`, the cell-key fields, `ID = UUID()` and
+ `USER = sessionUser`.
+
+ A numeric write to `([Category].[A], [Region].[N],
+ [Measures].[Value]) = 42` follows the same path: the runtime
+ resolves `Value` on cube `CT`, detects `Datatype.INTEGER` from
+ the `SumMeasure`, allocates the delta across matching rows and
+ emits `VALUE = 42` into `FACTWB_T` with the same cell-key
+ fields plus `ID` and `USER`.
+
+ No row is written for `[Measures].[Amount]` even via `[V]`,
+ because `Amount`'s owning cube `CN` has no writeback table.
+ """;
+
+ @Override
+ public Catalog get() {
+ if (catalog != null) {
+ return catalog;
+ }
+
+ databaseSchema = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE
+ .createSchema();
+
+ Column factNCategoryColumn = createColumn("CATEGORY", SqlSimpleTypes.Sql99.varcharType());
+ Column factNRegionColumn = createColumn("REGION", SqlSimpleTypes.Sql99.varcharType());
+ Column factNAmountColumn = createColumn("AMOUNT", SqlSimpleTypes.Sql99.integerType());
+ Table factNTable = createTable(FACT_N,
+ List.of(factNCategoryColumn, factNRegionColumn, factNAmountColumn));
+ databaseSchema.getOwnedElement().add(factNTable);
+
+ Column factTCategoryColumn = createColumn("CATEGORY", SqlSimpleTypes.Sql99.varcharType());
+ Column factTRegionColumn = createColumn("REGION", SqlSimpleTypes.Sql99.varcharType());
+ Column factTValueColumn = createColumn("VALUE", SqlSimpleTypes.Sql99.integerType());
+ Column factTCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+ Table factTTable = createTable(FACT_T,
+ List.of(factTCategoryColumn, factTRegionColumn, factTValueColumn, factTCommentColumn));
+ databaseSchema.getOwnedElement().add(factTTable);
+
+ Column catCategoryColumn = createColumn("CATEGORY", SqlSimpleTypes.Sql99.varcharType());
+ Column catNameColumn = createColumn("NAME", SqlSimpleTypes.Sql99.varcharType());
+ Table categoryTable = createTable(CATEGORY, List.of(catCategoryColumn, catNameColumn));
+ databaseSchema.getOwnedElement().add(categoryTable);
+
+ Column regRegionColumn = createColumn("REGION", SqlSimpleTypes.Sql99.varcharType());
+ Column regNameColumn = createColumn("NAME", SqlSimpleTypes.Sql99.varcharType());
+ Table regionTable = createTable(REGION, List.of(regRegionColumn, regNameColumn));
+ databaseSchema.getOwnedElement().add(regionTable);
+
+ Column wbCategoryColumn = createColumn("CATEGORY", SqlSimpleTypes.Sql99.varcharType());
+ Column wbRegionColumn = createColumn("REGION", SqlSimpleTypes.Sql99.varcharType());
+ Column wbValueColumn = createColumn("VALUE", SqlSimpleTypes.Sql99.integerType());
+ Column wbCommentColumn = createColumn("COMMENT", SqlSimpleTypes.Sql99.varcharType());
+ Column wbIdColumn = createColumn("ID", SqlSimpleTypes.Sql99.varcharType());
+ Column wbUserColumn = createColumn("USER", SqlSimpleTypes.Sql99.varcharType());
+ Table writebackPhysicalTable = createTable(FACTWB_T,
+ List.of(wbCategoryColumn, wbRegionColumn, wbValueColumn, wbCommentColumn, wbIdColumn, wbUserColumn));
+ databaseSchema.getOwnedElement().add(writebackPhysicalTable);
+
+ factNSource = SourceFactory.eINSTANCE.createTableSource();
+ factNSource.setTable(factNTable);
+
+ factTSource = SourceFactory.eINSTANCE.createTableSource();
+ factTSource.setTable(factTTable);
+
+ TableSource categorySource = SourceFactory.eINSTANCE.createTableSource();
+ categorySource.setTable(categoryTable);
+
+ TableSource regionSource = SourceFactory.eINSTANCE.createTableSource();
+ regionSource.setTable(regionTable);
+
+ Level categoryLevel = LevelFactory.eINSTANCE.createLevel();
+ categoryLevel.setName("Category");
+ categoryLevel.setColumn(catCategoryColumn);
+ categoryLevel.setNameColumn(catNameColumn);
+ categoryLevel.setUniqueMembers(true);
+
+ ExplicitHierarchy categoryHierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ categoryHierarchy.setName("Category");
+ categoryHierarchy.setHasAll(true);
+ categoryHierarchy.setAllMemberName("All Categories");
+ categoryHierarchy.setPrimaryKey(catCategoryColumn);
+ categoryHierarchy.setSource(categorySource);
+ categoryHierarchy.getLevels().add(categoryLevel);
+
+ categoryDimension = DimensionFactory.eINSTANCE.createStandardDimension();
+ categoryDimension.setName("Category");
+ categoryDimension.getHierarchies().add(categoryHierarchy);
+
+ Level regionLevel = LevelFactory.eINSTANCE.createLevel();
+ regionLevel.setName("Region");
+ regionLevel.setColumn(regRegionColumn);
+ regionLevel.setNameColumn(regNameColumn);
+ regionLevel.setUniqueMembers(true);
+
+ ExplicitHierarchy regionHierarchy = HierarchyFactory.eINSTANCE.createExplicitHierarchy();
+ regionHierarchy.setName("Region");
+ regionHierarchy.setHasAll(true);
+ regionHierarchy.setAllMemberName("All Regions");
+ regionHierarchy.setPrimaryKey(regRegionColumn);
+ regionHierarchy.setSource(regionSource);
+ regionHierarchy.getLevels().add(regionLevel);
+
+ regionDimension = DimensionFactory.eINSTANCE.createStandardDimension();
+ regionDimension.setName("Region");
+ regionDimension.getHierarchies().add(regionHierarchy);
+
+ DimensionConnector catConn1 = DimensionFactory.eINSTANCE.createDimensionConnector();
+ catConn1.setOverrideDimensionName("Category");
+ catConn1.setDimension(categoryDimension);
+ catConn1.setForeignKey(factNCategoryColumn);
+
+ DimensionConnector regConn1 = DimensionFactory.eINSTANCE.createDimensionConnector();
+ regConn1.setOverrideDimensionName("Region");
+ regConn1.setDimension(regionDimension);
+ regConn1.setForeignKey(factNRegionColumn);
+
+ DimensionConnector catConn3 = DimensionFactory.eINSTANCE.createDimensionConnector();
+ catConn3.setOverrideDimensionName("Category");
+ catConn3.setDimension(categoryDimension);
+ catConn3.setForeignKey(factNCategoryColumn);
+
+ DimensionConnector regConn3 = DimensionFactory.eINSTANCE.createDimensionConnector();
+ regConn3.setOverrideDimensionName("Region");
+ regConn3.setDimension(regionDimension);
+ regConn3.setForeignKey(factNRegionColumn);
+
+ DimensionConnector catConn2 = DimensionFactory.eINSTANCE.createDimensionConnector();
+ catConn2.setOverrideDimensionName("Category");
+ catConn2.setDimension(categoryDimension);
+ catConn2.setForeignKey(factTCategoryColumn);
+
+ DimensionConnector regConn2 = DimensionFactory.eINSTANCE.createDimensionConnector();
+ regConn2.setOverrideDimensionName("Region");
+ regConn2.setDimension(regionDimension);
+ regConn2.setForeignKey(factTRegionColumn);
+
+ amountMeasure = MeasureFactory.eINSTANCE.createSumMeasure();
+ amountMeasure.setName("Amount");
+ amountMeasure.setColumn(factNAmountColumn);
+ amountMeasure.setFormatString("#,##0");
+
+ valueMeasure = MeasureFactory.eINSTANCE.createSumMeasure();
+ valueMeasure.setName("Value");
+ valueMeasure.setColumn(factTValueColumn);
+ valueMeasure.setFormatString("#,##0");
+
+ OrderedColumn commentOrderedColumn = RelationalFactory.eINSTANCE.createOrderedColumn();
+ commentOrderedColumn.setColumn(factTCommentColumn);
+
+ commentsMeasure = MeasureFactory.eINSTANCE.createTextAggMeasure();
+ commentsMeasure.setName("Comments");
+ commentsMeasure.setColumn(factTCommentColumn);
+ commentsMeasure.setSeparator(" | ");
+ commentsMeasure.getOrderByColumns().add(commentOrderedColumn);
+
+ MeasureGroup measureGroupN = CubeFactory.eINSTANCE.createMeasureGroup();
+ measureGroupN.getMeasures().add(amountMeasure);
+
+ MeasureGroup measureGroupT = CubeFactory.eINSTANCE.createMeasureGroup();
+ measureGroupT.getMeasures().addAll(List.of(valueMeasure, commentsMeasure));
+
+ WritebackAttribute wbCategoryAttribute = WritebackFactory.eINSTANCE.createWritebackAttribute();
+ wbCategoryAttribute.setDimensionConnector(catConn2);
+ wbCategoryAttribute.setColumn(wbCategoryColumn);
+
+ WritebackAttribute wbRegionAttribute = WritebackFactory.eINSTANCE.createWritebackAttribute();
+ wbRegionAttribute.setDimensionConnector(regConn2);
+ wbRegionAttribute.setColumn(wbRegionColumn);
+
+ WritebackMeasure wbValueMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbValueMeasure.setName("Value");
+ wbValueMeasure.setColumn(wbValueColumn);
+
+ WritebackMeasure wbCommentsMeasure = MeasureFactory.eINSTANCE.createWritebackMeasure();
+ wbCommentsMeasure.setName("Comments");
+ wbCommentsMeasure.setColumn(wbCommentColumn);
+
+ writebackTable = WritebackFactory.eINSTANCE.createWritebackTable();
+ writebackTable.setName(FACTWB_T);
+ writebackTable.getWritebackAttribute().addAll(List.of(wbCategoryAttribute, wbRegionAttribute));
+ writebackTable.getWritebackMeasure().addAll(List.of(wbValueMeasure, wbCommentsMeasure));
+
+ cube1 = CubeFactory.eINSTANCE.createPhysicalCube();
+ cube1.setName(CUBE_N);
+ cube1.setSource(factNSource);
+ cube1.getDimensionConnectors().addAll(List.of(catConn1, regConn1));
+ cube1.getMeasureGroups().add(measureGroupN);
+ catConn1.setPhysicalCube(cube1);
+ regConn1.setPhysicalCube(cube1);
+
+ cube2 = CubeFactory.eINSTANCE.createPhysicalCube();
+ cube2.setName(CUBE_T);
+ cube2.setSource(factTSource);
+ cube2.getDimensionConnectors().addAll(List.of(catConn2, regConn2));
+ cube2.getMeasureGroups().add(measureGroupT);
+ cube2.setWritebackTable(writebackTable);
+ catConn2.setPhysicalCube(cube2);
+ regConn2.setPhysicalCube(cube2);
+
+ vCube = CubeFactory.eINSTANCE.createVirtualCube();
+ vCube.setName(CUBE_V);
+ vCube.setDefaultMeasure(amountMeasure);
+ vCube.getDimensionConnectors().addAll(List.of(catConn3, regConn3));
+ vCube.getReferencedMeasures().addAll(List.of(amountMeasure, valueMeasure, commentsMeasure));
+
+ catalog = CatalogFactory.eINSTANCE.createCatalog();
+ catalog.setName("Daanse Tutorial - Writeback Virtual Cube");
+ catalog.setDescription("Two physical cubes sharing Category + Region dimensions — one read-only"
+ + " (SumMeasure 'Amount'), one with text writeback (TextAggMeasure 'Comments') — combined"
+ + " through a VirtualCube. Demonstrates that writeback lives on the underlying PhysicalCube"
+ + " even when queries flow through the VirtualCube.");
+ catalog.getDbschemas().add(databaseSchema);
+ catalog.getCubes().addAll(List.of(cube1, cube2, vCube));
+
+ return catalog;
+ }
+
+ @Override
+ public TutorialDescription describe() {
+ Catalog c = get();
+ return new TutorialDescription(
+ List.of(
+ new DocSection("Daanse Tutorial - Writeback Virtual Cube",
+ catalogBody, 1, 0, 0, null, 0),
+ new DocSection("Database Schema", databaseSchemaBody, 1, 1, 0, databaseSchema, 5),
+ new DocSection("Dimensions (shared)", dimensionsBody, 1, 2, 0, categoryDimension, 0),
+ new DocSection("Cube CN (read-only, numeric)", cube1Body, 1, 3, 0, cube1, 2),
+ new DocSection("Cube CT (writeback, text)", cube2Body, 1, 4, 0, cube2, 2),
+ new DocSection("Virtual Cube V", virtualBody, 1, 5, 0, vCube, 2),
+ new DocSection("Writeback flow (FACTWB_T)", writebackBody, 1, 6, 0, writebackTable, 2)),
+ List.of(new CatalogRef("catalog", () -> c)));
+ }
+
+ private static Column createColumn(String name,
+ org.eclipse.daanse.cwm.model.cwm.resource.relational.SQLSimpleType type) {
+ Column c = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createColumn();
+ c.setName(name);
+ c.setType(type);
+ return c;
+ }
+
+ private static Table createTable(String name, List columns) {
+ Table t = org.eclipse.daanse.cwm.model.cwm.resource.relational.RelationalFactory.eINSTANCE.createTable();
+ t.setName(name);
+ t.getFeature().addAll(columns);
+ return t;
+ }
+}
diff --git a/instance/emf/tutorial/writeback/virtualcube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/virtualcube/CheckSuiteSupplier.java b/instance/emf/tutorial/writeback/virtualcube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/virtualcube/CheckSuiteSupplier.java
new file mode 100644
index 000000000..3c2d31245
--- /dev/null
+++ b/instance/emf/tutorial/writeback/virtualcube/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/virtualcube/CheckSuiteSupplier.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *
+ */
+package org.eclipse.daanse.rolap.mapping.instance.emf.tutorial.writeback.virtualcube;
+
+import org.eclipse.daanse.olap.check.model.check.CatalogCheck;
+import org.eclipse.daanse.olap.check.model.check.CubeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttribute;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnAttributeCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseColumnCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseSchemaCheck;
+import org.eclipse.daanse.olap.check.model.check.DatabaseTableCheck;
+import org.eclipse.daanse.olap.check.model.check.DimensionCheck;
+import org.eclipse.daanse.olap.check.model.check.HierarchyCheck;
+import org.eclipse.daanse.olap.check.model.check.LevelCheck;
+import org.eclipse.daanse.olap.check.model.check.MeasureCheck;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckFactory;
+import org.eclipse.daanse.olap.check.model.check.OlapCheckSuite;
+import org.eclipse.daanse.olap.check.model.check.OlapConnectionCheck;
+import org.eclipse.daanse.olap.check.runtime.api.OlapCheckSuiteSupplier;
+
+import org.osgi.service.component.annotations.Component;
+
+/** Check suite for the writeback-virtualcube tutorial. */
+@Component(service = OlapCheckSuiteSupplier.class)
+public class CheckSuiteSupplier implements OlapCheckSuiteSupplier {
+
+ private static final OlapCheckFactory factory = OlapCheckFactory.eINSTANCE;
+
+ private static final String CATALOG_NAME = "Daanse Tutorial - Writeback Virtual Cube";
+
+ @Override
+ public OlapCheckSuite get() {
+ DimensionCheck categoryDim = createDimensionCheck("Category",
+ createHierarchyCheck("Category", createLevelCheck("Category")));
+ DimensionCheck regionDim = createDimensionCheck("Region",
+ createHierarchyCheck("Region", createLevelCheck("Region")));
+
+ MeasureCheck amount = createMeasureCheck("Amount");
+ MeasureCheck value = createMeasureCheck("Value");
+ MeasureCheck comments = createMeasureCheck("Comments");
+
+ // CN: read-only cube with Amount only
+ CubeCheck cubeNCheck = factory.createCubeCheck();
+ cubeNCheck.setName("CubeCheck-CN");
+ cubeNCheck.setDescription("Cube CN carries the read-only Amount measure");
+ cubeNCheck.setCubeName("CN");
+ cubeNCheck.getMeasureChecks().add(amount);
+ cubeNCheck.getDimensionChecks().add(createDimensionCheck("Category",
+ createHierarchyCheck("Category", createLevelCheck("Category"))));
+ cubeNCheck.getDimensionChecks().add(createDimensionCheck("Region",
+ createHierarchyCheck("Region", createLevelCheck("Region"))));
+
+ // CT: writeback cube with numeric Value + text Comments
+ CubeCheck cubeTCheck = factory.createCubeCheck();
+ cubeTCheck.setName("CubeCheck-CT");
+ cubeTCheck.setDescription("Cube CT carries the numeric-writeback Value and text-writeback Comments measures");
+ cubeTCheck.setCubeName("CT");
+ cubeTCheck.getMeasureChecks().add(value);
+ cubeTCheck.getMeasureChecks().add(comments);
+ cubeTCheck.getDimensionChecks().add(createDimensionCheck("Category",
+ createHierarchyCheck("Category", createLevelCheck("Category"))));
+ cubeTCheck.getDimensionChecks().add(createDimensionCheck("Region",
+ createHierarchyCheck("Region", createLevelCheck("Region"))));
+
+ // V: VirtualCube exposing all three measures
+ CubeCheck cubeVCheck = factory.createCubeCheck();
+ cubeVCheck.setName("CubeCheck-V");
+ cubeVCheck.setDescription("Virtual Cube V references Amount, Value and Comments");
+ cubeVCheck.setCubeName("V");
+ cubeVCheck.getMeasureChecks().add(createMeasureCheck("Amount"));
+ cubeVCheck.getMeasureChecks().add(createMeasureCheck("Value"));
+ cubeVCheck.getMeasureChecks().add(createMeasureCheck("Comments"));
+ cubeVCheck.getDimensionChecks().add(categoryDim);
+ cubeVCheck.getDimensionChecks().add(regionDim);
+
+ DatabaseTableCheck factNTable = createTableCheck("FACT_N",
+ createColumnCheck("CATEGORY", "VARCHAR"),
+ createColumnCheck("REGION", "VARCHAR"),
+ createColumnCheck("AMOUNT", "INTEGER"));
+ DatabaseTableCheck factTTable = createTableCheck("FACT_T",
+ createColumnCheck("CATEGORY", "VARCHAR"),
+ createColumnCheck("REGION", "VARCHAR"),
+ createColumnCheck("VALUE", "INTEGER"),
+ createColumnCheck("COMMENT", "VARCHAR"));
+ DatabaseTableCheck categoryTable = createTableCheck("CATEGORY",
+ createColumnCheck("CATEGORY", "VARCHAR"),
+ createColumnCheck("NAME", "VARCHAR"));
+ DatabaseTableCheck regionTable = createTableCheck("REGION",
+ createColumnCheck("REGION", "VARCHAR"),
+ createColumnCheck("NAME", "VARCHAR"));
+ DatabaseTableCheck wbTable = createTableCheck("FACTWB_T",
+ createColumnCheck("CATEGORY", "VARCHAR"),
+ createColumnCheck("REGION", "VARCHAR"),
+ createColumnCheck("VALUE", "INTEGER"),
+ createColumnCheck("COMMENT", "VARCHAR"),
+ createColumnCheck("ID", "VARCHAR"),
+ createColumnCheck("USER", "VARCHAR"));
+
+ DatabaseSchemaCheck schemaCheck = factory.createDatabaseSchemaCheck();
+ schemaCheck.setName("Database Schema Check for " + CATALOG_NAME);
+ schemaCheck.setDescription("Schema check for the writeback-virtualcube tutorial");
+ schemaCheck.getTableChecks().add(factNTable);
+ schemaCheck.getTableChecks().add(factTTable);
+ schemaCheck.getTableChecks().add(categoryTable);
+ schemaCheck.getTableChecks().add(regionTable);
+ schemaCheck.getTableChecks().add(wbTable);
+
+ CatalogCheck catalogCheck = factory.createCatalogCheck();
+ catalogCheck.setName(CATALOG_NAME);
+ catalogCheck.setDescription("Catalog check for the writeback-virtualcube tutorial");
+ catalogCheck.setCatalogName(CATALOG_NAME);
+ catalogCheck.getCubeChecks().add(cubeNCheck);
+ catalogCheck.getCubeChecks().add(cubeTCheck);
+ catalogCheck.getCubeChecks().add(cubeVCheck);
+ catalogCheck.getDatabaseSchemaChecks().add(schemaCheck);
+
+ OlapConnectionCheck connectionCheck = factory.createOlapConnectionCheck();
+ connectionCheck.setName("Connection Check " + CATALOG_NAME);
+ connectionCheck.setDescription("Connection check for the writeback-virtualcube tutorial");
+ connectionCheck.getCatalogChecks().add(catalogCheck);
+
+ OlapCheckSuite suite = factory.createOlapCheckSuite();
+ suite.setName("Writeback VirtualCube Suite");
+ suite.setDescription("Check suite for the writeback-virtualcube tutorial");
+ suite.getConnectionChecks().add(connectionCheck);
+
+ return suite;
+ }
+
+ private MeasureCheck createMeasureCheck(String measureName) {
+ MeasureCheck m = factory.createMeasureCheck();
+ m.setName("MeasureCheck-" + measureName);
+ m.setDescription("Measure '" + measureName + "' must exist");
+ m.setMeasureName(measureName);
+ return m;
+ }
+
+ private DimensionCheck createDimensionCheck(String name, HierarchyCheck... hierarchies) {
+ DimensionCheck d = factory.createDimensionCheck();
+ d.setName("DimensionCheck for " + name);
+ d.setDimensionName(name);
+ for (HierarchyCheck hc : hierarchies) {
+ d.getHierarchyChecks().add(hc);
+ }
+ return d;
+ }
+
+ private HierarchyCheck createHierarchyCheck(String name, LevelCheck... levels) {
+ HierarchyCheck h = factory.createHierarchyCheck();
+ h.setName("HierarchyCheck-" + name);
+ h.setHierarchyName(name);
+ for (LevelCheck lc : levels) {
+ h.getLevelChecks().add(lc);
+ }
+ return h;
+ }
+
+ private LevelCheck createLevelCheck(String name) {
+ LevelCheck l = factory.createLevelCheck();
+ l.setName("LevelCheck-" + name);
+ l.setLevelName(name);
+ return l;
+ }
+
+ private DatabaseColumnCheck createColumnCheck(String name, String type) {
+ DatabaseColumnAttributeCheck attr = factory.createDatabaseColumnAttributeCheck();
+ attr.setAttributeType(DatabaseColumnAttribute.TYPE);
+ attr.setExpectedValue(type);
+
+ DatabaseColumnCheck c = factory.createDatabaseColumnCheck();
+ c.setName("Database Column Check " + name);
+ c.setColumnName(name);
+ c.getColumnAttributeChecks().add(attr);
+ return c;
+ }
+
+ private DatabaseTableCheck createTableCheck(String name, DatabaseColumnCheck... columns) {
+ DatabaseTableCheck t = factory.createDatabaseTableCheck();
+ t.setName("Database Table Check " + name);
+ t.setTableName(name);
+ for (DatabaseColumnCheck c : columns) {
+ t.getColumnChecks().add(c);
+ }
+ return t;
+ }
+}
diff --git a/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/CATEGORY.csv b/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/CATEGORY.csv
new file mode 100644
index 000000000..1cd91768a
--- /dev/null
+++ b/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/CATEGORY.csv
@@ -0,0 +1,5 @@
+CATEGORY,NAME
+VARCHAR,VARCHAR
+A,Alpha
+B,Beta
+C,Gamma
diff --git a/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/FACTWB_T.csv b/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/FACTWB_T.csv
new file mode 100644
index 000000000..97efb4be7
--- /dev/null
+++ b/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/FACTWB_T.csv
@@ -0,0 +1,2 @@
+CATEGORY,REGION,VALUE,COMMENT,ID,USER
+VARCHAR,VARCHAR,INTEGER,VARCHAR,VARCHAR,VARCHAR
diff --git a/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/FACT_N.csv b/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/FACT_N.csv
new file mode 100644
index 000000000..61f4dd7ed
--- /dev/null
+++ b/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/FACT_N.csv
@@ -0,0 +1,11 @@
+CATEGORY,REGION,AMOUNT
+VARCHAR,VARCHAR,INTEGER
+A,N,10
+A,S,20
+A,E,30
+B,N,40
+B,S,50
+B,E,60
+C,N,70
+C,S,80
+C,E,90
diff --git a/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/FACT_T.csv b/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/FACT_T.csv
new file mode 100644
index 000000000..e7f9c5407
--- /dev/null
+++ b/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/FACT_T.csv
@@ -0,0 +1,7 @@
+CATEGORY,REGION,VALUE,COMMENT
+VARCHAR,VARCHAR,INTEGER,VARCHAR
+A,N,100,initial entry for A/N
+A,S,150,follow-up on A/S
+B,N,200,opening row for B/N
+B,E,250,note on B/E
+C,N,300,only entry on C/N
diff --git a/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/REGION.csv b/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/REGION.csv
new file mode 100644
index 000000000..dbcc6c2ae
--- /dev/null
+++ b/instance/emf/tutorial/writeback/virtualcube/src/main/resources/data/REGION.csv
@@ -0,0 +1,5 @@
+REGION,NAME
+VARCHAR,VARCHAR
+N,North
+S,South
+E,East
diff --git a/instance/emf/tutorial/writeback/withoutdimension/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/withoutdimension/CatalogSupplier.java b/instance/emf/tutorial/writeback/withoutdimension/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/withoutdimension/CatalogSupplier.java
index 62e9a3e4b..bba4f94ce 100644
--- a/instance/emf/tutorial/writeback/withoutdimension/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/withoutdimension/CatalogSupplier.java
+++ b/instance/emf/tutorial/writeback/withoutdimension/src/main/java/org/eclipse/daanse/rolap/mapping/instance/emf/tutorial/writeback/withoutdimension/CatalogSupplier.java
@@ -28,7 +28,7 @@
import org.eclipse.daanse.rolap.mapping.model.RolapMappingFactory;
import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.SumMeasure;
import org.eclipse.daanse.rolap.mapping.model.database.source.TableSource;
-import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackMeasure;
+import org.eclipse.daanse.rolap.mapping.model.olap.cube.measure.WritebackMeasure;
import org.eclipse.daanse.rolap.mapping.model.database.writeback.WritebackTable;
import org.osgi.service.component.annotations.Component;
import org.eclipse.daanse.rolap.mapping.instance.api.CatalogRef;
@@ -134,11 +134,11 @@ public Catalog get() {
MeasureGroup measureGroup = CubeFactory.eINSTANCE.createMeasureGroup();
measureGroup.getMeasures().addAll(List.of(measure1, measure2));
- WritebackMeasure writebackMeasure1 = WritebackFactory.eINSTANCE.createWritebackMeasure();
+ WritebackMeasure writebackMeasure1 = MeasureFactory.eINSTANCE.createWritebackMeasure();
writebackMeasure1.setName("Measure1");
writebackMeasure1.setColumn(valColumn);
- WritebackMeasure writebackMeasure2 = WritebackFactory.eINSTANCE.createWritebackMeasure();
+ WritebackMeasure writebackMeasure2 = MeasureFactory.eINSTANCE.createWritebackMeasure();
writebackMeasure2.setName("Measure2");
writebackMeasure2.setColumn(val1Column);
diff --git a/model/model/rolap.mapping.ecore b/model/model/rolap.mapping.ecore
index 7f6f65009..a78f1c809 100644
--- a/model/model/rolap.mapping.ecore
+++ b/model/model/rolap.mapping.ecore
@@ -1211,22 +1211,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1238,7 +1222,7 @@
+ eType="#//olap/cube/measure/WritebackMeasure" containment="true">
@@ -1788,6 +1772,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+