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 @@ + + +
+ + + +
+ + + + +
+ + +