From b07293da1417633c26221920ab698ac7cc0cc01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Gr=C3=B8nnerup?= Date: Fri, 3 Jul 2026 11:05:54 +0200 Subject: [PATCH] Update te-cli skill to 0.3.0 (align with CLI 0.6.0) Breaking-in-CLI, must-fix in skill: - Switch every example to canonical long-form verbs (te list, te remove, te move, te config list); add a callout that short aliases (ls, rm, mv) still work. - Replace --serialization te-folder with database.json (CLI-side rename to match TE3 desktop's "Folder (database.json)"). - Remove the "known gap" language around te list Relationships; list Relationships as an enumerable container. Residual gotcha (auto-assigned relationship names) preserved. - Drop the stale --source-type m pre-validation error for models with a provider data source; the CLI now accepts mixed-partition models. New capabilities added: - te query "" positional-DAX shorthand. - te macro set repeated -q/-i property pairs. - te interactive batch mode (redirected stdin) plus --no-banner, --echo, --batch / --no-batch, with a worked batch example. - New launchInteractiveMode config key (Auto / Never / Always). - --serialization tmsl documented as alias for bim (canonical) on te save, te init, and te connect --workspace-format. - Notes on te bpa run text output including a Rule ID column, and te vertipaq surfacing clear errors on unknown filters plus safe piping. --- skills/te-cli/CHANGELOG.md | 23 +++++ skills/te-cli/SKILL.md | 34 ++++---- skills/te-cli/references/command-reference.md | 84 +++++++++++-------- skills/te-cli/references/config-cicd-env.md | 18 ++-- skills/te-cli/references/gotchas.md | 15 ++-- skills/te-cli/references/pbir-cli-tandem.md | 36 ++++---- .../references/semantic-modeling-practices.md | 18 ++-- skills/te-cli/references/te2-migration.md | 3 +- skills/te-cli/references/workflows.md | 11 +-- 9 files changed, 142 insertions(+), 100 deletions(-) diff --git a/skills/te-cli/CHANGELOG.md b/skills/te-cli/CHANGELOG.md index 1052efe..2a92251 100644 --- a/skills/te-cli/CHANGELOG.md +++ b/skills/te-cli/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to the `te-cli` skill are documented in this file. +## [0.3.0] - 2026-07-02 + +Aligns the skill with CLI 0.6.0. + +### Changed + +- Rewrote every command example to use long-form canonical verbs (`te list`, `te remove`, `te move`, `te config list`), with a short callout that Unix aliases (`ls`, `rm`, `mv`) still work everywhere. `te move` also accepts `rename`. `te add` has no short alias. +- Replaced every `--serialization te-folder` occurrence with `database.json` (BREAKING CLI rename in 0.6.0). The mirror on `te connect --workspace-format` and the TE2 migration table (`-F `) both track the new spelling. +- Removed the "known gap" language for `te list Relationships`: the CLI now enumerates relationships correctly, so SKILL.md and `references/command-reference.md` list `Relationships` as an enumerable container next to `Tables`, `Measures`, etc. The gotcha entry now covers the residual issue (system-assigned relationship names) rather than the wiring gap. +- Dropped the "`--source-type m` on models with a provider data source" pre-validation error from `references/workflows.md`; the CLI now accepts mixed-partition models to match TE3 desktop. + +### Added + +- `te query ""` positional-DAX shorthand documented in the quickstart and `references/command-reference.md`; explicit `-q` still wins when both are supplied. +- `te macro set` now accepts repeated `-q -i ` pairs in one call, with a worked example in `references/command-reference.md`. +- `te interactive` batch mode (redirected stdin) and its new flags (`--no-banner`, `--echo`, `--batch` / `--no-batch`) added to `references/command-reference.md`, with a batch-mode example block. +- New `launchInteractiveMode` config key (`Auto` / `Never` / `Always`) documented in `references/config-cicd-env.md`. +- `--serialization tmsl` documented as an alias for `bim` (canonical) on `te save`, `te init`, and `te connect --workspace-format`. +- Note that `te bpa run` text output now includes a `Rule ID` column, so IDs can be copied straight into `--fix --rule `. +- Note that `te vertipaq` surfaces a clear error on unknown table/column filters (with up to 10 candidates), and its output is pipe-safe (`te vertipaq > report.txt`, `te vertipaq | less`). + +[0.3.0]: https://github.com/TabularEditor/CLI/releases/tag/skill-v0.3.0 + ## [0.2.0] - 2026-06-08 ### Changed diff --git a/skills/te-cli/SKILL.md b/skills/te-cli/SKILL.md index 8f321b6..fb66dcf 100644 --- a/skills/te-cli/SKILL.md +++ b/skills/te-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: te-cli -version: 0.2.0 +version: 0.3.0 description: Expert guidance for the cross-platform Tabular Editor CLI (the `te` binary, currently in preview) that manages Power BI / Analysis Services semantic models from the terminal on macOS, Linux, and Windows. Use when the user mentions the `te` CLI or "Tabular Editor CLI" (not the "2"), or runs a `te ` to scaffold, inspect, edit, validate, run BPA on, query, deploy, refresh, test, or migrate a semantic model. Not for the legacy Windows-only `TabularEditor.exe` (TE2). --- @@ -32,12 +32,16 @@ The `te` CLI is a single self-contained binary that loads, edits, validates, dep - First use in a session: run `te --version` and `te auth status`. If not authenticated, ask the user to run `te auth login`. - Run `te --help` and `te --help` the first time composing a command; flags are still evolving during preview. - `te connect` state is per-shell-session and does NOT survive across separate Bash tool calls (each call is a fresh shell). Pass `-m ` (and `-s`/`-d` for remote) on every command, or set `TE_SESSION=` before the first call to share state. -- MPartition path asymmetry: `te add` for an M partition uses `/`, but every other command (`te rm`, `te get`, `te ls`, `te mv`, `te set`) uses `
/Partitions/`. -- Mutations stage in memory by default. `te set`, `te add`, `te rm`, `te mv`, `te replace`, `te format`, `te script`, `te macro run`, `te incremental-refresh set/remove` need `--save` to persist (unless `interactiveEditMode` is set to `save`). +- MPartition path asymmetry: `te add` for an M partition uses `
/`, but every other command (`te remove`, `te get`, `te list`, `te move`, `te set`) uses `
/Partitions/`. +- Mutations stage in memory by default. `te set`, `te add`, `te remove`, `te move`, `te replace`, `te format`, `te script`, `te macro run`, `te incremental-refresh set/remove` need `--save` to persist (unless `interactiveEditMode` is set to `save`). - The BPA gate is ON by default for `te deploy` and `te save`. Bypass deliberately: `--skip-bpa`, `--fix-bpa`, or `bpa.onDeploy` / `bpa.onSave` config (keys are nested under `bpa.`, not flat). - In CI: pass `--non-interactive` and `--force`. `te deploy` prompts with `n` as the safe default and hangs pipelines without `--force`. - Never put secrets on the command line (visible in `ps` and shell history). Use `--auth env` with `AZURE_CLIENT_ID`/`AZURE_CLIENT_SECRET`/`AZURE_TENANT_ID`, stdin (`-`), or `--auth managed-identity`. -- Avoid destructive operations without explicit direction: `te rm`, `te mv`, `te deploy --create-only`, `te save --force`, `te connect --clear`. If a command is blocked by permissions, stop and ask. +- Avoid destructive operations without explicit direction: `te remove`, `te move`, `te deploy --create-only`, `te save --force`, `te connect --clear`. If a command is blocked by permissions, stop and ask. + +## Verb naming (canonical long-form + Unix aliases) + +Long-form verbs are canonical: `te list`, `te remove`, `te move` at the root, and `list`/`remove` in subgroups (`te macro`, `te bpa rules`, `te profile`, `te session`, `te test`, `te incremental-refresh`). Short Unix-style aliases work everywhere: `ls`, `rm`, `mv`. `te move` also accepts `rename`. `te config list` is the canonical name (previously `te config show`). `te add` has no short alias. This skill uses the long forms throughout; the aliases still work if you prefer to type them. ## Staging model (`--save` / `--stage` / `--revert`) @@ -57,13 +61,13 @@ Inside `te interactive`, `--save`, `--stage`, and `--revert` are available per c te --version && te auth status # 0. check install + auth te auth login # 1. authenticate (browser); cached te init ./my-model # 2. scaffold (PowerBI mode, TMDL, compat 1702) -te load ./model # 3. load + summary; then `te ls`, `te ls Sales` +te load ./model # 3. load + summary; then `te list`, `te list Sales` te find "Revenue" --in names -m ./model # 4. search (names | expressions | descriptions | all) te get Sales/Revenue -q expression -m ./model # 5. read a measure's DAX te bpa run --fail-on error --ci github -m ./model # 6. BPA gate te format --save -m ./model # 7. format all DAX -te query -q "EVALUATE TOPN(5, 'Sales')" -s ws -d model # 8. query -te save -o ./out --serialization tmdl -m ./model # 9. save / convert (tmdl|bim|pbip|te-folder) +te query "EVALUATE TOPN(5, 'Sales')" -s ws -d model # 8. query (positional or -q) +te save -o ./out --serialization tmdl -m ./model # 9. save / convert (tmdl|bim|pbip|database.json) te deploy ./model -s ws -d model --force --ci github # 10. deploy te refresh --type full -s ws -d model # 11. refresh ``` @@ -74,13 +78,13 @@ te refresh --type full -s ws -d model # 11. refresh The highest-frequency tasks in their most concise form. Full flags are in `references/command-reference.md`; flags are still moving in preview, so confirm with `te --help`. -1. **Summarize a model (most concise)**: `te load ./model` prints a model summary. For a structural inventory, `te ls` (tables), `te ls Measures` (every measure across the model). Relationships do not list via `te ls` (a known gap, see `references/gotchas.md`); enumerate them with `te query -q "EVALUATE INFO.VIEW.RELATIONSHIPS()"`. Add `--output-format json` for a machine-readable dump. -2. **Search the model (fastest)**: `te find "" --in names --paths-only -m ./model`. Scope `--in` to `names`, `expressions`, `descriptions`, `displayFolders`, ...; `--in expressions` walks every DAX and M expression. `--paths-only` is the fast, pipeable form. Structural lookups use wildcards (`te ls "Sales/*Amount"`). Relationships are not `te ls`-enumerable (known gap); list them with `te query -q "EVALUATE INFO.VIEW.RELATIONSHIPS()"`. +1. **Summarize a model (most concise)**: `te load ./model` prints a model summary. For a structural inventory, `te list` (tables), `te list Measures` (every measure across the model), `te list Relationships` (all relationships). Add `--output-format json` for a machine-readable dump. +2. **Search the model (fastest)**: `te find "" --in names --paths-only -m ./model`. Scope `--in` to `names`, `expressions`, `descriptions`, `displayFolders`, ...; `--in expressions` walks every DAX and M expression. `--paths-only` is the fast, pipeable form. Structural lookups use wildcards (`te list "Sales/*Amount"`). Relationships are enumerated with `te list Relationships` (or DAX `EVALUATE INFO.VIEW.RELATIONSHIPS()` for the friendly view with cross-filter direction and active flag). 3. **Query the model**: - - Inline DAX: `te query -q "EVALUATE TOPN(10, Sales)" -m ./model` + - Positional DAX: `te query "EVALUATE TOPN(10, Sales)" -m ./model` (or `-q ""`; explicit `-q` still wins) - From a `.dax` file: `te query -f query.dax -m ./model` - Save results (format picked by extension): `--output-file out.csv` (csv/tsv/json/dax); machine-readable stdout: `--output-format json`. -4. **Make a change** (stages in memory; `--save` persists): `te set Sales/Revenue -q expression -i "SUM(Sales[Amount])" --save`. Also `te add`, `te rm`, `te mv`. Read the current value first with `te get Sales/Revenue -q expression`. +4. **Make a change** (stages in memory; `--save` persists): `te set Sales/Revenue -q expression -i "SUM(Sales[Amount])" --save`. Also `te add`, `te remove`, `te move`. Read the current value first with `te get Sales/Revenue -q expression`. 5. **Make bulk changes**: - Text find/replace across the whole model: `te replace "Old" "New" --in expressions --save` (previews unless `--save`). - Arbitrary bulk logic in one pass (the model loads once, avoiding ~1-2s per-call startup): `te script -S bulk.csx --save`, or inline `echo '' | te script -e - --save`. Predefined macros: `te macro run "" --on "Sales/A,Sales/B" --save`. @@ -114,7 +118,7 @@ Driving the CLI correctly is not the same as building a good model. After `te ad | `summarizeBy` = `none` on key/ID columns | stops Power BI silently summing keys into meaningless totals | `te set Sales/ProductKey -q summarizeBy -i none --save` | | Hide foreign-key and surrogate-key columns | keys serve relationships, not visuals; keeps the field list clean | `te set Sales/ProductKey -q isHidden -i true --save` | | Mark the date table | unlocks reliable time intelligence | `te set Date -q dataCategory -i Time --save` | -| Single cross-filter direction by default | avoids ambiguous filter paths and double counting | list with `te query -q "EVALUATE INFO.VIEW.RELATIONSHIPS()"` (`te ls` cannot enumerate relationships; see gotchas), read one with `te get Relationships/` (the `->` shorthand is for `te add` only); enable bidirectional only for a deliberate bridge | +| Single cross-filter direction by default | avoids ambiguous filter paths and double counting | list with `te list Relationships` (or DAX `EVALUATE INFO.VIEW.RELATIONSHIPS()` for the friendly view), read one with `te get Relationships/` (the `->` shorthand is for `te add` only); enable bidirectional only for a deliberate bridge | | Format string on every measure | unformatted measures render raw floats | `te set "_Measures/Revenue" -q formatString -i "#,0.00" --save` | | Display folder + description on measures | a flat field pane is unusable past a few dozen measures; descriptions feed tooltips and Copilot | `te set "_Measures/Revenue" -q displayFolder -i "Revenue" --save` | | Minimal correct data types; integer surrogate keys | high-cardinality and oversized types bloat VertiPaq | `te set Sales/CustomerKey -q dataType -i int64 --save` | @@ -129,8 +133,8 @@ Full rationale, citations, and worked workflows (RLS roles, calculation groups, Ten command families. Full flags and examples in `references/command-reference.md`. - Model I/O: `te load`, `te save`, `te open`, `te init` -- Editing: `te set`, `te add`, `te rm`, `te mv`, `te replace` -- Inspection: `te ls`, `te get`, `te find`, `te diff`, `te deps` +- Editing: `te set`, `te add`, `te remove`, `te move`, `te replace` +- Inspection: `te list`, `te get`, `te find`, `te diff`, `te deps` - Analysis & quality: `te validate`, `te bpa run`, `te vertipaq`, `te format` - Execution: `te query`, `te script`, `te macro` - Deploy & refresh: `te deploy`, `te refresh`, `te incremental-refresh` @@ -155,7 +159,7 @@ For build scripts that issue many `te` calls, set `te config set bpa.onSave fals `te` owns the semantic model. Two sibling CLIs own the layers around it, and the highest-value workflows cross the boundary: -- `pbir` (the Power BI report layer): renaming or moving a model object leaves the report bound to the old `Table.Field`. Rename in the model (`te mv`, then `te replace --in expressions --save`), then repair the report bindings (`pbir fields replace`, `pbir validate --fields`). See `references/pbir-cli-tandem.md`. +- `pbir` (the Power BI report layer): renaming or moving a model object leaves the report bound to the old `Table.Field`. Rename in the model (`te move`, then `te replace --in expressions --save`), then repair the report bindings (`pbir fields replace`, `pbir validate --fields`). See `references/pbir-cli-tandem.md`. - `fab` (the Fabric / Power BI service): export a model from a workspace, edit and gate it locally with `te`, then deploy over XMLA (`te deploy`) or import it back (`fab import`). See `references/fabric-cli-tandem.md`. Gate any cross-tool refactor with `te validate` before touching the report or the service, and remember every `te` mutation stages in memory until `--save`. diff --git a/skills/te-cli/references/command-reference.md b/skills/te-cli/references/command-reference.md index d3d603f..4d32801 100644 --- a/skills/te-cli/references/command-reference.md +++ b/skills/te-cli/references/command-reference.md @@ -64,7 +64,7 @@ te connect --clear # reset ```bash te connect Finance "Revenue Model" -w ./revenue-model # remote primary, mirror to local te connect ./revenue-model -w Finance "Revenue Model" # local primary, mirror to remote -# --workspace-format # on-disk format for the mirror +# --workspace-format # on-disk format for the mirror (bim accepts tmsl alias) # --workspace-auth # auth for the remote side when primary is local ``` @@ -82,9 +82,9 @@ te connect --profile prod Backed by a formal grammar (`PathParser`); paths come in two flavors with subtly different rules: -**Object paths**; used by `te get`, `te set`, `te add`, `te rm`, `te mv`. Resolve to **one** object. Wildcards rejected. +**Object paths**; used by `te get`, `te set`, `te add`, `te remove`, `te move`. Resolve to **one** object. Wildcards rejected. -**Filter paths**; used by `te ls`, `te find`, `te deps`, `te bpa run --path`. Resolve to a **set** of objects. Wildcards allowed. +**Filter paths**; used by `te list`, `te find`, `te deps`, `te bpa run --path`. Resolve to a **set** of objects. Wildcards allowed. ### Slash-form (works on both) @@ -95,8 +95,8 @@ Backed by a formal grammar (`PathParser`); paths come in two flavors with subtly - `Measures//KPI`; KPI sub-object on a measure (resolves through the KPI wrapper) - `Roles//Members`, `Roles//TablePermissions`; role children - `Perspectives//
`; perspective membership (use `te add Perspectives/Default/Sales` to add a table) -- `Tables`, `Measures`, `Roles`, `Perspectives`, `Cultures`, `Hierarchies`, `Annotations`; model-level containers (pivot via `te ls Measures` for cross-table view) -- `Relationships` is **not** enumerable via `te ls`, despite `relationship` appearing in `te ls --type`'s help. The keyword falls through to a literal path match and errors with `No objects match path 'Relationships'`, even when relationships exist (recognized-but-empty containers say `No objects match 'X'` without the word `path`). List relationships with DAX `EVALUATE INFO.VIEW.RELATIONSHIPS()` (or `INFO.RELATIONSHIPS()` on older compat), or `te save` to TMDL and read `relationships.tmdl`. A single relationship is still addressable once you know its name: `te get Relationships/`. +- `Tables`, `Measures`, `Roles`, `Perspectives`, `Cultures`, `Hierarchies`, `Annotations`, `Relationships`; model-level containers (pivot via `te list Measures` for cross-table view, `te list Relationships` for every relationship in the model) +- A single relationship is addressable by name: `te get Relationships/`. For the friendly cross-filter-direction and active-flag view, DAX `EVALUATE INFO.VIEW.RELATIONSHIPS()` remains the richer read. Container-keyword table names (a table called `Tables`, `Roles`, etc.) resolve correctly via the path parser; the parser disambiguates by position. @@ -115,10 +115,10 @@ DAX-style quoting and bracket-suffix follow DAX conventions; doubled quote char Single `*` matches any run of characters within one segment (case-insensitive). Multi-segment globs and `?` are not supported. ```bash -te ls Sa* # tables starting with "Sa" -te ls Sales/*Amount # any child of Sales ending in "Amount" -te ls */Amount # an "Amount" column/measure across every table -te ls Roles/Re*/Members # members of every role matching Re* +te list Sa* # tables starting with "Sa" +te list Sales/*Amount # any child of Sales ending in "Amount" +te list */Amount # an "Amount" column/measure across every table +te list Roles/Re*/Members # members of every role matching Re* te bpa run --path "Sales/*" # run BPA only on objects under Sales ``` @@ -149,9 +149,9 @@ Work with every command: | Command | Purpose | Key flags | |---|---|---| | `te load ` | Load model and show summary | global `-m/-s/-d` | -| `te save` | Save / convert / persist edits | `-o, --output-path `, `--serialization tmdl\|bim\|te-folder\|pbip\|database.json`, `--force`, `--skip-bpa`, `--fix-bpa`, `--bpa-rules ` (repeatable, overrides config), `--skip-validation`, `--supporting-files` | +| `te save` | Save / convert / persist edits | `-o, --output-path `, `--serialization tmdl\|bim\|database.json\|pbip` (`bim` accepts `tmsl` as alias), `--force`, `--skip-bpa`, `--fix-bpa`, `--bpa-rules ` (repeatable, overrides config), `--skip-validation`, `--supporting-files` | | `te open ` | Open in TE3 Desktop (TE3 must be installed) | n/a | -| `te init [path]` | Create new empty model. Path is optional; falls back to global `--model` when omitted | `--compatibility-mode PowerBI\|AnalysisServices` (default `PowerBI`), `--compatibility-level ` (alias `--compat`; defaults to 1702 for PowerBI, 1500 for AnalysisServices), `--name `, `--serialization tmdl\|bim\|te-folder\|pbip` (default `tmdl`), `--force` | +| `te init [path]` | Create new empty model. Path is optional; falls back to global `--model` when omitted | `--compatibility-mode PowerBI\|AnalysisServices` (default `PowerBI`), `--compatibility-level ` (alias `--compat`; defaults to 1702 for PowerBI, 1500 for AnalysisServices), `--name `, `--serialization tmdl\|bim\|database.json\|pbip` (`bim` accepts `tmsl` as alias; default `tmdl`), `--force` | ```bash te load ./model # local TMDL folder @@ -177,8 +177,8 @@ te --model ./new.bim init # path via glob |---|---|---| | `te set ` | Set property | `-q ` (e.g. `expression`, `formatString`, `description`, `isHidden`), `-i ` (or `-` for stdin), `--save`, `--save-to ` | | `te add ` | Add object | `-t ` (`Table`, `Measure`, `Column`, `CalculatedColumn`, `CalculatedTable`, `Hierarchy`, `Role`, `Perspective`, `Culture`, `CalculationGroup`, `CalculationItem`, `MPartition`, `Partition`, `EntityPartition`, `PolicyRangePartition`, `KPI`, `NamedExpression`, ...), `-i `, `--if-not-exists` (idempotent), `--save`. Data-bound tables: `--mode import\|directquery\|directlake`, `--source sql\|lakehouse\|warehouse`, `--endpoint`, `--source-table`, `--source-database`, `--columns "Col1:Type,Col2:Type,..."`, `--partition-expression ""`, `--source-type m\|query\|calculated` | -| `te rm ` | Remove object | `--force`, `--if-exists`, `--dry-run`, `--save` | -| `te mv ` | Move/rename | `--save` | +| `te remove ` (alias: `rm`) | Remove object | `--force`, `--if-exists`, `--dry-run`, `--save` | +| `te move ` (aliases: `mv`, `rename`) | Move/rename | `--save` | | `te replace ` | Find+replace text | `--in names\|expressions\|descriptions\|displayFolders\|formatStrings\|annotations\|all`, `--regex`, `--case-sensitive`, `--save` (dry-run by default) | ```bash @@ -188,9 +188,9 @@ te add Sales/Revenue -t Measure -i "SUM(Sales[Amount])" --save te add Sales -t Table --save # empty M partition (PowerBI default) te add "Sales[ProdKey]->Product[ProdKey]" --save # relationship shorthand te add Sales/MarketingFlag -t CalculatedColumn -i "..." --if-not-exists --save -te rm Sales/OldMeasure --if-exists --save -te rm Sales/Revenue --dry-run # preview impact -te mv Sales/Revenue Finance/Revenue --save # cross-table move +te remove Sales/OldMeasure --if-exists --save +te remove Sales/Revenue --dry-run # preview impact +te move Sales/Revenue Finance/Revenue --save # cross-table move te replace "OldTable" "NewTable" --in expressions --save te replace "SUM" "SUMX" --regex --in expressions --save ``` @@ -219,18 +219,20 @@ Properties not in the list are still usable; these are the most error-prone and | Command | Purpose | Key flags | |---|---|---| -| `te ls [filter-path]` | List objects, FS-style (filter-path: wildcards allowed) | `--type `, `--paths-only`, `--no-multiline` (collapse multi-line cells; text output only) | +| `te list [filter-path]` (alias: `ls`) | List objects, FS-style (filter-path: wildcards allowed) | `--type ` (`table`, `measure`, `column`, `partition`, `role`, `relationship`, ...), `--paths-only`, `--no-multiline` (collapse multi-line cells; text output only) | | `te get ` | Get properties (object-path: no wildcards) | `-q ` (single property), `--output-format tmdl\|tmsl\|bim` (emit object as TMDL/TMSL) | | `te find ` | Search across model | `--in names\|expressions\|descriptions\|displayFolders\|formatStrings\|annotations\|all`, `--regex`, `--case-sensitive`, `--paths-only`, `--no-multiline`. **`--in expressions` walks every `IExpressionObject`**; measure DAX, calculated columns, KPI status/trend/target expressions, measure detail-rows, partition M, table-permission filters, calculation-group selection expressions | | `te diff ` | Structural diff | exit 0 identical, 2 models differ, 1 error | | `te deps [obj]` | Dependency analysis | `--unused` (no DAX refs, not in relationships/hierarchies/sort-by/variations/time roles), `--hidden` (narrow to hidden), `--deep`, `--upstream`, `--downstream`, `--max-depth ` | ```bash -te ls # tables -te ls Sales # columns + measures in Sales -te ls Sales/Measures # measures only -te ls Measures # all measures across model -te ls --type measure --paths-only # pipeable +te list # tables +te list Sales # columns + measures in Sales +te list Sales/Measures # measures only +te list Measures # all measures across model +te list Relationships # all relationships across model +te list --type measure --paths-only # pipeable +te list --type relationship # only relationships te get Sales/Revenue -q expression te get Model -q description te find "CALCULATE" --in expressions # covers DAX, calc-columns, KPI exprs, partition M, role filters, calc-group selection @@ -248,9 +250,9 @@ te deps --unused --hidden # hidden + unused | Command | Purpose | Key flags | |---|---|---| | `te validate` | Expressions + schema + TOM errors | `--ci ` (see below), `--trx `, `--no-multiline`, `--no-warnings`, `--no-antipatterns`, `--errors-only` | -| `te bpa run [model]` | Run BPA (optional positional model path) | `-r/--rules ` (repeatable; URLs supported), `--fix`, `--save`, `--save-to `, `--serialization`, `--fail-on error\|warning`, `--ci`, `--trx`, `--no-defaults`, `--no-model-rules`, `--rule ` (repeatable), `--path ` (wildcards OK: `--path "Sales/*"`), `--vpax `, `--vpa-rules`, `--allow-external-rules` (allow URL rules from model annotations), `--no-multiline` | +| `te bpa run [model]` | Run BPA (optional positional model path). Text output includes a `Rule ID` column, so IDs can be copied straight into `--fix --rule ` without a `te bpa rules list` lookup | `-r/--rules ` (repeatable; URLs supported), `--fix`, `--save`, `--save-to `, `--serialization`, `--fail-on error\|warning`, `--ci`, `--trx`, `--no-defaults`, `--no-model-rules`, `--rule ` (repeatable), `--path ` (wildcards OK: `--path "Sales/*"`), `--vpax `, `--vpa-rules`, `--allow-external-rules` (allow URL rules from model annotations), `--no-multiline` | | `te bpa rules list` | Inspect active rules | `--all` (incl. disabled+ignored), `--ignored`, `--no-multiline` | -| `te vertipaq [path]` | VertiPaq stats (optional positional object path, e.g. `Sales` or `Sales/Amount`) | `--columns`, `--relationships`, `--partitions`, `--all`, `--detail` (encoding/segments breakdown), `--fields ` (custom column set), `--export `, `--import ` (offline), `--obfuscate` (writes `.vpax.dict` sidecar), `--top `, `--stats` (DAX-queried details), `--annotate`, `--save` | +| `te vertipaq [path]` | VertiPaq stats (optional positional object path, e.g. `Sales` or `Sales/Amount`). Unknown table/column filter now exits with a clear error listing up to 10 candidates and pointing at `te list Tables`, instead of silently emitting empty results. Output is pipe-safe (`te vertipaq > report.txt`, `te vertipaq \| less`) | `--columns`, `--relationships`, `--partitions`, `--all`, `--detail` (encoding/segments breakdown), `--fields ` (custom column set), `--export `, `--import ` (offline), `--obfuscate` (writes `.vpax.dict` sidecar), `--top `, `--stats` (DAX-queried details), `--annotate`, `--save` | | `te format` | Format DAX or M | `-e ` (inline), `-p ` (single), `--lang dax\|m`, `--semicolons` (Euro), `--long` (more line breaks; default is short), `--no-space-after-function`, `-t/--type ` (disambiguate `-p` when path matches multiple), `--save`, `--save-to ` | ```bash @@ -276,21 +278,23 @@ te format -e "SUM ( Sales[Amount] )" # inline preview | Command | Purpose | Key flags | |---|---|---| -| `te query` | DAX query | `-q ` or `-f `, `--limit ` (default 100), `-o, --output-file ` (extension picks format: `.csv\|.tsv\|.json\|.dax`), `--trace`, `--cold`, `--plan`, `--runs ` (benchmark), `--no-validate` | +| `te query` | DAX query | Positional `""` or `-q ` or `-f ` (explicit `-q` wins if both are supplied), `--limit ` (default 100), `-o, --output-file ` (extension picks format: `.csv\|.tsv\|.json\|.dax`), `--trace`, `--cold`, `--plan`, `--runs ` (benchmark), `--no-validate` | | `te script` | Run C# script (TOM) | `-S ` (repeatable, `.cs`/`.csx`), `-e ` (inline, `-` = stdin), `--save`, `--save-to`, `--serialization`, `--dry-run`, `--timeout ` | -| `te macro ` | TE3 macros | `list`, `run ` (with `--on `, `--save`), `add`, `set`, `rm`, `sort` | +| `te macro ` | TE3 macros | `list`, `run ` (with `--on `, `--save`), `add`, `set` (accepts repeated `-q -i ` pairs in one call), `remove` (alias `rm`), `sort` | ```bash -te query -q "EVALUATE TOPN(5, 'Sales')" -s ws -d model +te query "EVALUATE TOPN(5, 'Sales')" -s ws -d model # positional DAX shorthand +te query -q "EVALUATE TOPN(5, 'Sales')" -s ws -d model # explicit -q form (still supported) te query -f query.dax --output-format json # global --output-format controls stdout format -te query -q "EVALUATE Sales" --output-file results.csv # writes CSV/TSV/JSON/DAX based on extension -te query -q "EVALUATE Sales" --runs 5 --cold --plan +te query "EVALUATE Sales" --output-file results.csv # writes CSV/TSV/JSON/DAX based on extension +te query "EVALUATE Sales" --runs 5 --cold --plan te script -S fix.cs --save te script -e "Info(Model.Tables.Count)" echo "Info(Model.Name);" | te script -e - te macro list te macro run "Hide all measures" te macro run "Format DAX" --on "Sales/Revenue,Sales/Margin" --save +te macro set "Format DAX" -q description -i "Formats all DAX" -q tooltip -i "Ctrl+F" # multi-pair ``` ### Deployment & Refresh @@ -341,7 +345,7 @@ te test compare (Covered above under [Authentication](#authentication) and [Connections and profiles](#connections-and-profiles).) Full subcommands: ``` -te connect [ ] [--local | -w/--workspace | --workspace-format bim|tmdl|te-folder | --workspace-auth | --force | -p/--profile | --clear] +te connect [ ] [--local | -w/--workspace | --workspace-format bim|tmdl|database.json | --workspace-auth | --force | -p/--profile | --clear] te auth login [-u ] [-p |-] [-t ] [--identity|-I] [--certificate ] [--certificate-password ] [--save] [--auth interactive|spn|env|managed-identity] te auth status te auth logout @@ -372,7 +376,7 @@ Why it matters: `te connect`, `te test use`, and `--profile` all mutate the sess | Command | Purpose | |---|---| -| `te interactive [model]` | Model-aware REPL; prompt is `te [MyModel]>` or `te>`. All subcommands work without `te` prefix. Built-ins: `help`/`?`, `status`/`pwd`, `clear`/`cls`, `exit`/`quit`/`q` | +| `te interactive [model]` | Model-aware REPL; prompt is `te [MyModel]>` or `te>`. All subcommands work without `te` prefix. Built-ins: `help`/`?`, `status`/`pwd`, `clear`/`cls`, `exit`/`quit`/`q`. Flags: `--no-banner` (suppress the intro), `--echo` (print each command before running), `--batch` / `--no-batch` (force batch or interactive semantics regardless of stdin) | | `te completion ` | Print completion script (`bash`, `zsh`, `pwsh`) | The REPL's argv splitter is bracket-aware, so DAX-style refs work without escaping the brackets; handy for paste-from-DAX-editor workflows: @@ -381,14 +385,26 @@ The REPL's argv splitter is bracket-aware, so DAX-style refs work without escapi te interactive te interactive ./model te interactive -s MyWorkspace -d MyModel -te> ls Sales -te> ls Sa* # wildcard filter-paths +te> list Sales +te> list Sa* # wildcard filter-paths te> get "Sales/Revenue" -q expression te> get [Total Sales] # lone-bracket: model-wide measure/column lookup te> get 'Sales'[Amount] # DAX-quoted form -te> ls Roles/Reader/Members # role members +te> list Roles/Reader/Members # role members te> add Perspectives/Default/Sales # add Sales table to the Default perspective te> bpa run --fail-on error te> exit ``` +**Scripted / batch mode** (redirected stdin). Piping or redirecting into `te interactive` switches the session to batch mode: it reads commands line-by-line, treats lines starting with `#` as comments, exits non-zero on the first failing command, and drops the interactive prompt. Handy for CI pipelines and editor sidecars that already know exactly which commands to run: + +```bash +te interactive ./model < script.te +te interactive ./model --no-banner --echo < script.te # cleaner logs, replay-able output +cat <<'EOF' | te interactive ./model +# add a measure, then verify +add "_Measures/Revenue" -t Measure -i "SUM(Sales[Amount])" --save +get "_Measures/Revenue" -q expression +EOF +te interactive ./model --batch < commands.te # force batch semantics even if attached to a TTY +``` diff --git a/skills/te-cli/references/config-cicd-env.md b/skills/te-cli/references/config-cicd-env.md index 45e5496..86d293a 100644 --- a/skills/te-cli/references/config-cicd-env.md +++ b/skills/te-cli/references/config-cicd-env.md @@ -6,7 +6,7 @@ Companion to the te-cli skill (SKILL.md). | Command | Purpose | |---|---| -| `te config show [--output-format json]` | Show all settings | +| `te config list [--output-format json]` | List all settings (previously `te config show`; renamed for verb consistency with `te list`) | | `te config paths` | Resolved file paths (macros, BPA rules, config) | | `te config init [--force]` | Create default config | | `te config set ` | Update setting | @@ -36,6 +36,7 @@ Companion to the te-cli skill (SKILL.md). | `formatOptions.skipSpaceAfterFunction` | bool | `false` | `SUM(x)` instead of `SUM (x)` | | `formatOptions.useSqlBiDaxFormatter` | bool | `false` | Use SQLBI's online formatter instead of the in-house one | | `interactiveEditMode` | enum | `stage` | Default for mutating commands: `stage` (in-memory only), `save` (auto-persist), `revert` (auto-roll-back). Overridden per-command by `--save`/`--stage`/`--revert` | +| `launchInteractiveMode` | enum | `Auto` | Controls whether invoking `te` with no subcommand launches the interactive REPL: `Auto` (launch on a TTY, print help when stdout is piped/redirected), `Never` (always print help), `Always` (always launch the REPL) | | `hidePreviewNotice` | bool | `false` | Suppress yellow preview banner | | `spinner` | bool | `true` | Animated progress (disable for CI) | | `debug` | bool | `false` | Debug logs to stderr | @@ -110,14 +111,14 @@ Same commands, swap `--ci github` for `--ci azdo` (or `vsts`/`azure-devops`; all - `text`: forces human-readable - `json`: always valid JSON to stdout; errors/warnings to stderr (won't contaminate) - `csv`: tabular results (only `query`, `bpa run`, `vertipaq`) -- `tmsl` (alias `bim`): emit the resolved object(s) as TMSL/BIM JSON; supported on `te get` and `te ls` -- `tmdl`: emit the resolved object as TMDL; supported on `te get` (single named object only) and `te ls` +- `bim` (alias `tmsl`): emit the resolved object(s) as BIM/TMSL JSON; supported on `te get` and `te list`. `bim` is canonical (matches the `.bim` file extension) but the two names are synonyms everywhere the flag appears. +- `tmdl`: emit the resolved object as TMDL; supported on `te get` (single named object only) and `te list` ```bash -te get Sales --output-format tmdl # Sales table as TMDL -te get "Sales/Revenue" --output-format bim # Single measure as TMSL fragment -te ls Tables --output-format bim # All tables as TMSL/BIM -te ls Measures --output-format tmdl # Every measure across the model, in TMDL +te get Sales --output-format tmdl # Sales table as TMDL +te get "Sales/Revenue" --output-format bim # Single measure as TMSL fragment +te list Tables --output-format bim # All tables as BIM/TMSL +te list Measures --output-format tmdl # Every measure across the model, in TMDL ``` **`--ci` formats** (orthogonal to `--output-format`; emits CI-system logging commands to stderr on `validate`, `bpa run`, `deploy`, `test run`, `script`): @@ -137,7 +138,7 @@ Errors and warnings are accumulated, so a non-zero exit code reflects total erro ```bash # JSON-safe pipeline -te ls --type measure --output-format json | jq -r '.[].path' +te list --type measure --output-format json | jq -r '.[].path' # Bash conditional on diff if te diff old.bim new.bim --output-format json > /dev/null; then @@ -159,4 +160,3 @@ fi | `TE_BPA_RULES` | Override path to a BPA rules file (precedence: explicit `--rules` > `TE_BPA_RULES` > `bpa.rules` config > CWD `BPARules.json`) | | `TE_BPA_CONFIG` | Override path to a `.te-bpa.json` gate-config (for deploy/save BPA gating) | | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID` | SPN credentials (used with `--auth env`) | - diff --git a/skills/te-cli/references/gotchas.md b/skills/te-cli/references/gotchas.md index 849c270..7b7c794 100644 --- a/skills/te-cli/references/gotchas.md +++ b/skills/te-cli/references/gotchas.md @@ -6,17 +6,17 @@ Sharp edges and non-obvious behavior. Companion to the te-cli skill (SKILL.md). ### Path & property-name asymmetries -- **MPartition path asymmetry**: `te add` for an MPartition uses `
/` (no `/Partitions/` segment). Every other partition command; `te rm`, `te get`, `te ls`, `te mv`, `te set`; uses `
/Partitions/`. Mixing these up errors with "Cannot add a MPartition at path … Check that -t matches the path shape." +- **MPartition path asymmetry**: `te add` for an MPartition uses `
/` (no `/Partitions/` segment). Every other partition command; `te remove`, `te get`, `te list`, `te move`, `te set`; uses `
/Partitions/`. Mixing these up errors with "Cannot add a MPartition at path … Check that -t matches the path shape." - **Partition M property: `MExpression` for `te set`, `expression` for `te get`**: `te get
/Partitions/

` displays the M as `expression`, but `te set -q expression …` errors with "Property 'expression' not found on MPartition. Did you mean: MExpression?"; use `-q MExpression -i "" --save` to update. -- **`te mv` cannot rename a partition to its parent table's name**: `te mv

/Partitions/
_m
/Partitions/
` errors with either "Destination '
/
' already exists" or "Partition '
' not found in table '
'"; the destination path `
/
` resolves to the **table object itself**, not a partition slot inside the table. Passing `-t Partition` does **not** disambiguate. Workaround: rename via the `Name` property; `te set
/Partitions/
_m -q Name -i
--save`. +- **`te move` cannot rename a partition to its parent table's name**: `te move
/Partitions/
_m
/Partitions/
` errors with either "Destination '
/
' already exists" or "Partition '
' not found in table '
'"; the destination path `
/
` resolves to the **table object itself**, not a partition slot inside the table. Passing `-t Partition` does **not** disambiguate. Workaround: rename via the `Name` property; `te set
/Partitions/
_m -q Name -i
--save`. - **Object paths use `/` as separator**, not `\` or `.`. Quote paths with spaces: `"Sales 2024"/Revenue`, `"_Measures/Total Revenue"`, `"Date/Date Hierarchy/Year"`. -- **`te ls` cannot list relationships**: both `te ls Relationships` and `te ls --type relationship` error with `Error: No objects match path 'Relationships'` even when the model has relationships (confirmed against a 23-relationship model, local TMDL and live XMLA alike). `relationship` is advertised in `te ls --type`'s help, but unlike every other container (`Tables`, `Measures`, `Roles`, `Perspectives`, `Cultures`, `Hierarchies`) the keyword is not wired to the collection, so it falls through to a literal path match. Tell: recognized-but-empty containers say `No objects match 'X'`; the broken keyword says `No objects match path 'X'` (note the word `path`). Enumerate relationships with DAX instead: `te query -q "EVALUATE INFO.VIEW.RELATIONSHIPS()"` (friendly `from ∞←1 to`, active flag, cross-filter direction), or `INFO.RELATIONSHIPS()` on older compat, or `te save` to TMDL and read `relationships.tmdl`. A single relationship is addressable by name via `te get Relationships/`, but auto-assigned names (`Relationship 2`) make DAX enumeration the practical discovery path. +- **Relationship names are often auto-assigned**: `te list Relationships` and `te list --type relationship` do enumerate every relationship, but the display name is frequently a system-generated GUID or `Relationship 2` rather than something readable. For a human-friendly view with cross-filter direction and active flag, prefer `te query "EVALUATE INFO.VIEW.RELATIONSHIPS()"` (or `INFO.RELATIONSHIPS()` on older compat). A single relationship is addressable by name via `te get Relationships/` once you know it. ### Object types and creation - **`-t DataColumn` is not accepted by `te add`**: data columns are declared at table creation via `--columns "Name:Type,..."` (auto-creates with `SourceColumn = Name`). Tune additional properties (`IsKey`, `IsHidden`, `FormatString`, `SummarizeBy`, `SortByColumn`, `Description`) afterwards with `te set`. Only `CalculatedColumn` can be added individually with `-t CalculatedColumn -i ""`. - **Workflow ordering; relationships before measures**: measures that use `RELATED()` or cross-table `CALCULATE()` are validated at save time. If the relationship doesn't exist yet, the save gate rejects with `DAX0002: Column ''[] doesn't have a relationship to any table available in the current context`. Add relationships before authoring dependent measures. If that order is impossible (scripted batch creation), use `--force` only as a last resort and run `te validate` immediately after to confirm no DAX errors remain before committing. -- **`te rm` on the last partition fails** with "Cannot remove last partition from table". Always add a replacement partition first, then remove the original. +- **`te remove` on the last partition fails** with "Cannot remove last partition from table". Always add a replacement partition first, then remove the original. - **Silent M syntax errors during partition authoring**: a typo in the partition M (unbalanced braces, an unquoted token) doesn't always raise at save time. After creating a data-bound table, sanity-check with `te get
/Partitions/
` to confirm `sourceType: M` and that the expression matches what was passed in. ### Output shapes @@ -29,12 +29,12 @@ Sharp edges and non-obvious behavior. Companion to the te-cli skill (SKILL.md). ### Behavior -- **`te ls` is filesystem-style, not workspace-style**. `te ls Sales` lists Sales' children (columns + measures), not "find Sales". Use `te find` for full-text search. -- **`--save` is opt-in for editing commands** (when `interactiveEditMode` is the default `stage`). Without it, `te set`, `te add`, `te rm`, `te mv`, `te replace`, `te format`, `te script`, `te macro run` operate in memory only and don't persist. See the Staging model section in SKILL.md for the full picture and the `save` / `revert` alternatives. +- **`te list` is filesystem-style, not workspace-style**. `te list Sales` lists Sales' children (columns + measures), not "find Sales". Use `te find` for full-text search. +- **`--save` is opt-in for editing commands** (when `interactiveEditMode` is the default `stage`). Without it, `te set`, `te add`, `te remove`, `te move`, `te replace`, `te format`, `te script`, `te macro run` operate in memory only and don't persist. See the Staging model section in SKILL.md for the full picture and the `save` / `revert` alternatives. - **`te validate` does not exercise partition M**; it checks structural/DAX validity, not whether `Table.FromRows` literals parse or SQL endpoints respond. A model with broken partitions still passes `te validate` cleanly. Verify partitions explicitly after table creation. - **`te connect` is session-scoped (per shell PID)**. Each fresh shell (each Claude Code `Bash` tool call spawns one) starts a new session without the active connection. Either set `TE_SESSION=` to share state between shells, or pass `-m ` (and `-s`/`-d` where needed) explicitly to every command. - **BPA config keys are nested under `bpa.`**. `te config set bpaOnDeploy false` will fail with "Unknown key"; the correct form is `te config set bpa.onDeploy false`. Same for `bpa.onSave`, `bpa.onMutation`, `bpa.rules`, `bpa.builtInRules`, `bpa.disabledBuiltInRuleIds`. -- **`--output-format` (stdout) vs `--serialization` (on-disk)** are different flags. The first picks how stdout is rendered (text/json/csv/tmsl/tmdl); the second picks the model file format (tmdl/bim/te-folder/pbip/database.json). Passing one when the other was meant gives a confusing error or silent wrong output. +- **`--output-format` (stdout) vs `--serialization` (on-disk)** are different flags. The first picks how stdout is rendered (text/json/csv/tmsl/tmdl); the second picks the model file format (tmdl/bim/database.json/pbip). Passing one when the other was meant gives a confusing error or silent wrong output. `--serialization tmsl` is accepted as an alias for `bim` (matching how `--output-format` already treats the two synonyms); the canonical spelling is `bim`, matching the `.bim` file extension. - **`te deploy --create-only` fails if model exists**. Use without `--create-only` to overwrite (the default), or check with a probe call first. - **`te deploy` confirmation prompt hangs CI** without `--force`. Default answer is `n` (safe), so non-interactive runs need `--force --non-interactive`. - **BPA gate on deploy/save can mask sloppy commits**. If bypassing with `--skip-bpa`, log it loudly. Prefer `--fix-bpa` or address violations. @@ -42,4 +42,3 @@ Sharp edges and non-obvious behavior. Companion to the te-cli skill (SKILL.md). - **Local SSAS / Power BI Desktop are Windows-only**: `te connect --local` and `te connect "localhost:PORT"` won't work on macOS/Linux even though the binary runs there. - **Preview banner reappears 14 days before the 2026-09-30 cutoff** regardless of `hidePreviewNotice`. Plan for the hard expiry. - **Secrets on the cmd line leak** into `ps`, shell history, CI logs. Use `--auth env` with env vars, stdin (`-`), or `--auth managed-identity`. - diff --git a/skills/te-cli/references/pbir-cli-tandem.md b/skills/te-cli/references/pbir-cli-tandem.md index 2665fd9..0424468 100644 --- a/skills/te-cli/references/pbir-cli-tandem.md +++ b/skills/te-cli/references/pbir-cli-tandem.md @@ -6,7 +6,7 @@ Two rules before every command: model edits in `te` stage in memory and need `-- ## 1. Rename a measure (model) and repair report bindings -The most common refactor. `te mv` renames the object only; it does not rewrite DAX that calls the old name, so `te replace --in expressions` is a mandatory second step. +The most common refactor. `te move` renames the object only; it does not rewrite DAX that calls the old name, so `te replace --in expressions` is a mandatory second step. ```bash # te: confirm the object and find DAX that calls it by name @@ -15,7 +15,7 @@ te find "OldRevenue" --in expressions -m ./Model.SemanticModel # measures, cal te deps "_Measures/OldRevenue" --downstream -m ./Model.SemanticModel # blast radius # te: rename the object, then fix every DAX reference to the old name -te mv "_Measures/OldRevenue" "_Measures/Revenue" --save -m ./Model.SemanticModel +te move "_Measures/OldRevenue" "_Measures/Revenue" --save -m ./Model.SemanticModel te replace "OldRevenue" "Revenue" --in expressions --save -m ./Model.SemanticModel # te: gate before touching the report @@ -36,9 +36,9 @@ Per-step purpose: ```yaml te find --in names: confirm the measure exists and get its path before touching anything -te find --in expressions: discover DAX that calls the old name by value; te mv will NOT fix these +te find --in expressions: discover DAX that calls the old name by value; te move will NOT fix these te deps --downstream: list measures/columns downstream to show the full impact -te mv: rename the TOM object (the name property only) +te move: rename the TOM object (the name property only) te replace --in expressions: rewrite every DAX call of the old name across the model; needs --save te validate --errors-only: confirm no broken DAX references remain pbir fields find: locate visuals, filters, and CF entries bound to the old reference @@ -49,12 +49,12 @@ pbir validate --fields: confirm all bindings resolve; zero broken references exp ## 2. Rename a column (model) and repair report bindings -Same shape as a measure rename, plus column-only metadata to check after `te mv`. +Same shape as a measure rename, plus column-only metadata to check after `te move`. ```bash te find "OldColumnName" --in names --paths-only -m ./Model.SemanticModel # check for same name on other tables te deps "Date/OldColumnName" --downstream -m ./Model.SemanticModel -te mv "Date/OldColumnName" "Date/NewColumnName" --save -m ./Model.SemanticModel +te move "Date/OldColumnName" "Date/NewColumnName" --save -m ./Model.SemanticModel te replace "OldColumnName" "NewColumnName" --in expressions --save -m ./Model.SemanticModel te validate -m ./Model.SemanticModel --errors-only @@ -68,7 +68,7 @@ After the rename, verify two column relationships that store the old name as a p ```bash te get Date/SomeOtherColumn -q sortByColumn -m ./Model.SemanticModel # update with te set if it broke -te ls "Date/Geography/Levels" -m ./Model.SemanticModel # hierarchy levels take a column property +te list "Date/Geography/Levels" -m ./Model.SemanticModel # hierarchy levels take a column property ``` ## 3. Rename a table (model) and repair all report bindings @@ -79,7 +79,7 @@ te ls "Date/Geography/Levels" -m ./Model.SemanticModel # hierar te find "FACT_Sales" --in names -m ./Model.SemanticModel te find "FACT_Sales" --in expressions -m ./Model.SemanticModel te replace "FACT_Sales" "Sales" --in expressions --save -m ./Model.SemanticModel -te mv FACT_Sales Sales --save -m ./Model.SemanticModel +te move FACT_Sales Sales --save -m ./Model.SemanticModel te validate -m ./Model.SemanticModel --errors-only # pbir: list fields, then replace each FACT_Sales.* binding individually @@ -93,16 +93,16 @@ pbir validate "Report.Report" --fields ```yaml te replace --in expressions: rewrites DAX text only; apostrophe-quoted refs need the quotes in the find term, e.g. te replace "'FACT_Sales'" "'Sales'" --in expressions --save te replace substring risk: if the old name is a substring of another identifier, add --case-sensitive or --regex with anchors and review the preview -relationship endpoints: te mv renames the table object; confirm relationship integrity with te validate +relationship endpoints: te move renames the table object; confirm relationship integrity with te validate ``` ## 4. Move a measure to a different table and update bindings -`te mv` across tables is the only way to change an object's table ownership. Only fully table-qualified DAX (`SourceTable[Measure]`) breaks; unqualified `[Measure]` keeps resolving model-wide. +`te move` across tables is the only way to change an object's table ownership. Only fully table-qualified DAX (`SourceTable[Measure]`) breaks; unqualified `[Measure]` keeps resolving model-wide. ```bash te deps "SourceTable/MeasureName" --downstream -m ./Model.SemanticModel -te mv "SourceTable/MeasureName" "TargetTable/MeasureName" --save -m ./Model.SemanticModel +te move "SourceTable/MeasureName" "TargetTable/MeasureName" --save -m ./Model.SemanticModel te find "SourceTable" --in expressions --paths-only -m ./Model.SemanticModel # decide if te replace is needed te validate -m ./Model.SemanticModel --errors-only @@ -111,7 +111,7 @@ pbir fields replace "Report.Report" --from "SourceTable.MeasureName" --to "Targe pbir validate "Report.Report" --fields ``` -Display folder does not move with `te mv`; reset it on the moved measure if it should match the new table's folder structure (`te set "TargetTable/MeasureName" -q displayFolder -i "" --save`). +Display folder does not move with `te move`; reset it on the moved measure if it should match the new table's folder structure (`te set "TargetTable/MeasureName" -q displayFolder -i "" --save`). ## 5. Scaffold a model, then create a thin report against it @@ -155,7 +155,7 @@ pbir report rebind "Sales.Report" --local "../Sales.SemanticModel" ## 6. Add a measure to a live model, then surface it in a bound report ```bash -te ls Measures -m ./Model.SemanticModel # check naming conventions, avoid duplicates +te list Measures -m ./Model.SemanticModel # check naming conventions, avoid duplicates te add "_Measures/Revenue YoY" -t Measure -i "DIVIDE([Revenue], CALCULATE([Revenue], SAMEPERIODLASTYEAR('Date'[Date]))) - 1" --save -m ./Model.SemanticModel te set "_Measures/Revenue YoY" -q formatString -i "0.0%" --save -m ./Model.SemanticModel te set "_Measures/Revenue YoY" -q displayFolder -i "Revenue" --save -m ./Model.SemanticModel @@ -185,15 +185,15 @@ pbir validate "Report.Report" pbir visuals bind "Report.Report/Page.Page/Visual.Visual" -r "Category:Sales.OldRegionCode" # te: delete only after the report is clean -te rm Sales/OldRegionCode --dry-run -m ./Model.SemanticModel -te rm Sales/OldRegionCode --if-exists --save -m ./Model.SemanticModel +te remove Sales/OldRegionCode --dry-run -m ./Model.SemanticModel +te remove Sales/OldRegionCode --if-exists --save -m ./Model.SemanticModel te validate -m ./Model.SemanticModel --errors-only te deploy ./Model.SemanticModel -s "MyWorkspace" -d "Sales Model" --force --non-interactive ``` ```yaml removal granularity: pbir visuals bind -r removes one role binding on one visual; prefer it over a report- or page-wide pbir fields clear, which strips bindings broadly and can leave visuals with empty roles -sort-by dependency: te rm fails if the column is another column's sortByColumn target; clear that first with te set OtherColumn -q sortByColumn -i "" --save +sort-by dependency: te remove fails if the column is another column's sortByColumn target; clear that first with te set OtherColumn -q sortByColumn -i "" --save ``` ## 8. Split a thick PBIP, edit the model, keep the report in sync @@ -230,9 +230,9 @@ pbir publish "Sales.Report" "MyWorkspace/Sales" -f # position ```yaml te owns: - - object identity (te mv / te set -q name) and DAX expression repair (te replace --in expressions) + - object identity (te move / te set -q name) and DAX expression repair (te replace --in expressions) - dependency analysis (te deps), validation (te validate), BPA (te bpa run), deploy (te deploy) - - te mv renames the object only; it does NOT rewrite DAX that calls the old name + - te move renames the object only; it does NOT rewrite DAX that calls the old name - te replace previews by default; pass --save to persist; it is literal text find-replace (use --regex / --case-sensitive for substring or quoted-name cases) - te find --in expressions covers measure DAX, calc columns, KPI expressions, partition M, role filters, and calc-group selection; it does NOT see report JSON diff --git a/skills/te-cli/references/semantic-modeling-practices.md b/skills/te-cli/references/semantic-modeling-practices.md index 3bed35c..02e2e25 100644 --- a/skills/te-cli/references/semantic-modeling-practices.md +++ b/skills/te-cli/references/semantic-modeling-practices.md @@ -14,12 +14,12 @@ Build a star: one fact table per business process at a single grain, surrounded | Practice | Why | `te` command | |---|---|---| -| Model a star, not a flat table | flat models measured up to ~2.65x larger in RAM and up to 2x slower on complex queries; where flat wins it is marginal | inspect topology first: `te ls`, `te query -q "EVALUATE INFO.VIEW.RELATIONSHIPS()"` (relationships; `te ls` cannot list them), `te deps "Sales/Revenue"` | +| Model a star, not a flat table | flat models measured up to ~2.65x larger in RAM and up to 2x slower on complex queries; where flat wins it is marginal | inspect topology first: `te list` (tables), `te list Relationships` (or DAX `EVALUATE INFO.VIEW.RELATIONSHIPS()` for the friendly cross-filter view), `te deps "Sales/Revenue"` | | One consistent grain per fact table; never join fact-to-fact (header/detail) directly | mixing grains gives wrong aggregations; fact-to-fact becomes a limited relationship (SQLBI's 1.4B-row test: ~15x CPU, 17 minutes vs seconds for distinct count) | add a shared dimension and relate both facts to it rather than to each other | | Flatten snowflake sub-dimensions into one dimension per business entity | extra hops cost CPU on every filter pass and a hierarchy cannot span tables; run-length encoding makes the duplication cheap. Keep a sub-dimension separate only when it is large and reused across parents | do the join in Power Query upstream, then relate the single dimension | | Keep degenerate dimensions (order/invoice number) as hidden fact columns | a one-column table for a high-cardinality attribute adds size and clutter for no gain | `te set Sales/OrderNumber -q isHidden -i true --save` | -| Host model measures on a dedicated hidden measure table; prefix to sort it to the top | separates calculations from data and keeps the field list clean (organizational, not an engine requirement; below ~20 measures, display folders inside the source table are equally valid) | `te add _Measures -t Table --columns "_Measures:String" --partition-expression 'let Source = #table({"_Measures"}, {{""}}) in Source' --save`, then `te set _Measures -q isHidden -i true --save`, then `te mv Sales/Revenue "_Measures/Revenue" --save` | -| Use plain business names (Customer, Sales); drop DIM_/FACT_ prefixes | a table's role comes from cardinality, not its name; prefixes degrade Q&A and Copilot interpretation (organizational, bends to warehouse naming constraints) | `te mv FACT_Sales Sales --save` | +| Host model measures on a dedicated hidden measure table; prefix to sort it to the top | separates calculations from data and keeps the field list clean (organizational, not an engine requirement; below ~20 measures, display folders inside the source table are equally valid) | `te add _Measures -t Table --columns "_Measures:String" --partition-expression 'let Source = #table({"_Measures"}, {{""}}) in Source' --save`, then `te set _Measures -q isHidden -i true --save`, then `te move Sales/Revenue "_Measures/Revenue" --save` | +| Use plain business names (Customer, Sales); drop DIM_/FACT_ prefixes | a table's role comes from cardinality, not its name; prefixes degrade Q&A and Copilot interpretation (organizational, bends to warehouse naming constraints) | `te move FACT_Sales Sales --save` | Nuance to preserve: SQLBI shows a flat model can beat star for some simple low-cardinality groupings, so "star" is the default rather than a universal absolute; when flat wins it is marginal, when it loses it is far worse. @@ -32,7 +32,7 @@ Default every relationship to one-to-many cardinality and single cross-filter di | Practice | Why | `te` command | |---|---|---| | Add the dimension-to-fact relationship before authoring measures that cross it | measures using `RELATED()` or cross-table `CALCULATE()` validate at save time and the gate rejects with `DAX0002` if the relationship is missing | `te add "Sales[ProductKey]->Product[ProductKey]" --save` | -| Keep single cross-filter direction; never set model-level bidirectional just to sync slicers | bidirectional on a model with 3+ related tables creates two propagation paths and subtly wrong numbers; scope the rare genuine need to one measure with `CROSSFILTER(..., BOTH)`. Since 2019 a visual-level "is not blank" measure filter replaces the slicer-sync use case | list relationships with `te query -q "EVALUATE INFO.VIEW.RELATIONSHIPS()"` (`te ls` cannot enumerate them; see gotchas), read one with `te get Relationships/` (the `->` shorthand is for `te add` only); change `crossFilteringBehavior` only after confirming the property name with `te set -q`, or use `te script` | +| Keep single cross-filter direction; never set model-level bidirectional just to sync slicers | bidirectional on a model with 3+ related tables creates two propagation paths and subtly wrong numbers; scope the rare genuine need to one measure with `CROSSFILTER(..., BOTH)`. Since 2019 a visual-level "is not blank" measure filter replaces the slicer-sync use case | list relationships with `te list Relationships` (or DAX `EVALUATE INFO.VIEW.RELATIONSHIPS()` for the friendly cross-filter view), read one with `te get Relationships/` (the `->` shorthand is for `te add` only); change `crossFilteringBehavior` only after confirming the property name with `te set -q`, or use `te script` | | Treat many-to-many cardinality as a last resort; bridge dimension-to-dimension via two one-to-many relationships (exactly one leg bidirectional) | one-to-many gives a regular relationship with a blank row for RI violations; many-to-many cardinality gives a limited relationship with no join index, multiple passes, and silent drops of unmatched values | model the bridge with `te add` relationships rather than a direct many-to-many | | When many-to-many is genuinely required, document that totals are non-additive | with shared membership the total counts each item once and will not equal the sum of visible rows; this is correct by design, the risk is silent misreading | add a `te set -q description` note on affected measures | | Role-playing dimensions: duplicate the physical dimension with an active relationship per role; use one shared table with inactive relationships + `USERELATIONSHIP` only when simultaneous multi-role filtering is not needed | active relationships give correct drag-and-drop results with no DAX; inactive ones force a `USERELATIONSHIP` wrapper in every measure and break Q&A. Never use `USERELATIONSHIP` in a calculated column (use `LOOKUPVALUE`) | load the dimension twice in Power Query (Order Date, Ship Date), each with its own active relationship | @@ -52,7 +52,7 @@ Column cardinality (distinct value count), not row count, is the primary driver | Prefer narrow integer surrogate keys for relationship columns | the win is a smaller dictionary for the same distinct count, NOT an encoding switch (VertiPaq always hash-encodes relationship columns); SQLBI measured a relationship dropping from 4 MB to under 50 KB | `te set Sales/CustomerKey -q dataType -i int64 --save` | | Use a Date data type (not integer YYYYMMDD) for the date key | SQLBI's 2B-row test found storage and scan essentially identical, so usability decides: Date enables native arithmetic and classic time intelligence | `te set Date/Date -q dataType -i dateTime --save` | | Use Fixed Decimal (Currency) for monetary values; avoid Double/Single for aggregated columns | VertiPaq scans segments in parallel and floating-point addition is non-associative, so Double sums can vary between runs; Fixed Decimal is a deterministic scaled integer | `te set Sales/Amount -q dataType -i decimal --save` | -| Remove unused columns before deployment, not after | every imported column costs dictionary, data-segment, and attribute-hierarchy memory; removing one 50M-value identity column cut a model from 1.3 GB to 490 MB | `te deps Sales/SomeCol --downstream` to check, then `te rm Sales/SomeCol --dry-run` and `te rm Sales/SomeCol --if-exists --save` | +| Remove unused columns before deployment, not after | every imported column costs dictionary, data-segment, and attribute-hierarchy memory; removing one 50M-value identity column cut a model from 1.3 GB to 490 MB | `te deps Sales/SomeCol --downstream` to check, then `te remove Sales/SomeCol --dry-run` and `te remove Sales/SomeCol --if-exists --save` | | Set `isAvailableInMDX` = false on high-cardinality hidden columns (keep it true for Sort By Column targets when MDX/Excel clients exist) | the per-column attribute hierarchy is pure overhead on hidden columns unreachable from MDX (one case: 1.1 GB of hierarchy) | confirm the property name first with `te set -q`, then `te set Sales/Key -q isAvailableInMDX -i false --save` (or `te script`) | | Reduce numeric precision to what analysis needs when the business agrees | fewer distinct values means smaller dictionaries; rounding loses auditability, so validate with owners and consider keeping a hidden full-precision copy | round in Power Query, then re-profile with `te vertipaq` | @@ -70,7 +70,7 @@ A model is consumer-ready and Copilot-ready only once measures and columns carry | Hide surrogate/foreign-key columns and set `summarizeBy` = none on numeric columns that should not aggregate | keys are scaffolding, not attributes; `none` removes the implicit-aggregation sigma so a key cannot be summed into nonsense, and Copilot/Q&A exclude hidden columns | `te set Sales/CustomerKey -q isHidden -i true --save` and `te set Sales/CustomerKey -q summarizeBy -i none --save` | | Apply Sort By Column to display columns with non-alphabetical order (month, weekday, fiscal period) and hide the helper | without it "Month Name" sorts April, August, December; the sort column must be 1:1 with the display column | `te set Date/MonthName -q sortByColumn -i MonthNumber --save` then `te set Date/MonthNumber -q isHidden -i true --save` | | Add descriptions to visible tables, columns, and measures; front-load the key guidance in the first 200 characters | descriptions feed Copilot/data-agent DAX (which reads the first 200 chars) and surface as service tooltips for humans | `te set "_Measures/Revenue" -q description -i "Net revenue after returns and discounts" --save` | -| Use human-readable, unique, business-aligned names; organize measures into display folders; avoid duplicate column names across tables | ambiguous or duplicated names make Copilot/Q&A misroute; folders keep a large field list navigable (purely cosmetic) | `te set "_Measures/Revenue" -q displayFolder -i "Revenue" --save`; qualify clashes as "Customer Name" vs "Store Name" with `te mv` | +| Use human-readable, unique, business-aligned names; organize measures into display folders; avoid duplicate column names across tables | ambiguous or duplicated names make Copilot/Q&A misroute; folders keep a large field list navigable (purely cosmetic) | `te set "_Measures/Revenue" -q displayFolder -i "Revenue" --save`; qualify clashes as "Customer Name" vs "Store Name" with `te move` | | Mark `isKey` on dimension primary keys, then hide them | signals the unique grain the engine builds filters from, and keeps the field list clean | `te set Product/ProductKey -q isKey -i true --save` then `te set Product/ProductKey -q isHidden -i true --save` | | Set data categories on geographic, URL, and image columns | enables map visuals, clickable links, and image rendering, and Copilot reads them for grounding | `te set Geo/City -q dataCategory -i City --save`, `te set Customer/Photo -q dataCategory -i ImageUrl --save` (confirm category values with `te get`) | @@ -90,7 +90,7 @@ Time intelligence needs one dedicated, shared Date dimension that all facts rela |---|---|---| | Build one shared Date dimension; never drive time intelligence off a fact datetime column | classic time-intelligence needs a separate date spine to apply `REMOVEFILTERS` against, and one shared table lets a single slicer drive every fact | relate each fact date to `Date[Date]` with `te add "Sales[OrderDate]->Date[Date]" --save` | | Mark the Date table (`dataCategory` = Time) and designate the date key | marking designates the date spine so time-intelligence functions (`DATESYTD`, `SAMEPERIODLASTYEAR`) know which column to clear and enumerate against, and it suppresses auto date/time; without it those functions intersect the current filter context and return silent wrong results | `te set Date -q dataCategory -i Time --save` then `te set Date/Date -q isKey -i true --save` | -| Remove auto date/time tables (`LocalDateTable_*`, `DateTableTemplate_*`) | auto date/time creates a hidden calculated table per date column, inflating size and refresh, cannot be shared, and is invisible to Excel/paginated/XMLA clients | find them with `te ls`, remove with `te rm
--save`; disable the toggle at the report/source so they do not regenerate on the next author save | +| Remove auto date/time tables (`LocalDateTable_*`, `DateTableTemplate_*`) | auto date/time creates a hidden calculated table per date column, inflating size and refresh, cannot be shared, and is invisible to Excel/paginated/XMLA clients | find them with `te list`, remove with `te remove
--save`; disable the toggle at the report/source so they do not regenerate on the next author save | | Keep blank/null dates in the fact rather than inflating the Date table; guard comparisons with `NOT ISBLANK()` | blanks on the many side add a `(Blank)` slicer row and distort totals, and comparison operators treat `BLANK` as a pre-1900 date | author guarded measures with `te add`/`te set -q expression` | | Standardize one org-wide Date definition (warehouse DimDate, dataflow, or parameterized DAX) | a shared definition prevents fiscal-year, week-numbering, and naming drift across models | import the warehouse DimDate where one exists | @@ -128,7 +128,7 @@ te set "Time Intelligence/YTD" -q ordinal -i 0 --save te set "Time Intelligence/YTD" -q formatStringDefinition -i "SELECTEDMEASUREFORMATSTRING()" --save ``` -`expression`, `ordinal`, and `formatStringDefinition` are the documented `CalculationItem` properties. Confirm the calculation-item child-path form with `te ls "Time Intelligence"` after creating the group, before scripting many items in a pipeline. +`expression`, `ordinal`, and `formatStringDefinition` are the documented `CalculationItem` properties. Confirm the calculation-item child-path form with `te list "Time Intelligence"` after creating the group, before scripting many items in a pipeline. Sources: [Calculated columns and measures (SQLBI)](https://www.sqlbi.com/articles/calculated-columns-and-measures-in-dax/), [DAX calculated columns vs Power Query (SQLBI)](https://www.sqlbi.com/articles/comparing-dax-calculated-columns-with-power-query-computed-columns/), [Data reduction - custom columns (MS Learn)](https://learn.microsoft.com/power-bi/guidance/import-modeling-data-reduction#preference-for-custom-columns), [Calculation groups (MS Learn)](https://learn.microsoft.com/analysis-services/tabular-models/calculation-groups), [Understanding calculation groups (SQLBI)](https://www.sqlbi.com/articles/understanding-calculation-groups/), [Controlling format strings in calculation groups (SQLBI)](https://www.sqlbi.com/articles/controlling-format-strings-in-calculation-groups/). @@ -139,7 +139,7 @@ Define roles, set permissions, attach filters, then validate and deploy. Prefer ```bash te add Roles/RegionManagers -t Role -m ./model --save te set Roles/RegionManagers -q modelPermission -i Read --save -# Dynamic filter: confirm the TablePermission add/path form with `te ls Roles/RegionManagers/TablePermissions` +# Dynamic filter: confirm the TablePermission add/path form with `te list Roles/RegionManagers/TablePermissions` te set Roles/RegionManagers/TablePermissions/Sales -q filterExpression -i "[Region] = USERPRINCIPALNAME()" --save te validate -m ./model te deploy ./model -s ws -d model --deploy-roles --deploy-role-members --force --ci github diff --git a/skills/te-cli/references/te2-migration.md b/skills/te-cli/references/te2-migration.md index eb02a7a..7dfce3a 100644 --- a/skills/te-cli/references/te2-migration.md +++ b/skills/te-cli/references/te2-migration.md @@ -38,7 +38,7 @@ te migrate --output-format json # machine-readable for codemods | `-T ` | `--trx ` | | `-B ` | `te save -o --serialization bim` | | `-TMDL ` | `te save -o --serialization tmdl` (default format) | -| `-F ` | `te save -o --serialization te-folder` (or `--deploy-full` after `-D`) | +| `-F ` | `te save -o --serialization database.json` (or `--deploy-full` after `-D`) | | `-Y` | `--deploy-partitions --skip-refresh-policy` | | `-W` / `-E` | (default) | | `-L ` (after `-D`) | `te auth login -u -p -t ` (prefer env vars) | @@ -49,4 +49,3 @@ te migrate --output-format json # machine-readable for codemods - `te deploy` prompts for confirmation. CI must pass `--force`. - All commands support `--output-format json` for machine-readable output. - No `start /wait` wrapper needed on Windows; it's a normal console binary. - diff --git a/skills/te-cli/references/workflows.md b/skills/te-cli/references/workflows.md index 47487a8..391d956 100644 --- a/skills/te-cli/references/workflows.md +++ b/skills/te-cli/references/workflows.md @@ -51,10 +51,11 @@ te add _Measures -t Table --columns "_Measures:String" \ **Pre-validation errors** (fail before mutation): - `--source-type calculated` paired with `-t Table` → use `-t CalculatedTable` -- `--source-type m` on a model with a provider data source → remove the DS, or use `--source-type query` - `--source-type m` on Compatibility Level < 1400 → upgrade the model - `--source-type` combined with `--mode directlake` → DL/Entity partitions are picked automatically +**Note:** `--source-type m` on a model that already has a provider data source is now supported (matches TE3 desktop's mixed-partition support); the CLI no longer rejects that combination. + **Updating an existing partition's M** after creation: `te set Sales/Partitions/Sales -q MExpression -i "" --save` (note `MExpression`, not `expression`, despite `te get` displaying the property as `expression`). ### Convert TMDL ↔ BIM ↔ PBIP @@ -93,8 +94,8 @@ te refresh --table Sales --partition "Sales.2024" --type full --dry-run > refres ```bash te deps --unused --hidden # discover candidates -te rm Sales/UnusedMeasure --dry-run # confirm impact -te rm Sales/UnusedMeasure --if-exists --save # idempotent removal +te remove Sales/UnusedMeasure --dry-run # confirm impact +te remove Sales/UnusedMeasure --if-exists --save # idempotent removal ``` ### Mirror remote workspace for local editing @@ -129,7 +130,7 @@ te test compare # detect ## Additional authoring workflows -Modeling-driven recipes (mark a date table, calculation groups, RLS roles) live in semantic-modeling-practices.md, paired with the rationale for each. The recipes below are the remaining structural-object workflows. The `te` CLI is in preview; confirm any flag or path shape below with `te --help` (or `te ls ` to see the exact child-path form) before scripting it in a pipeline. +Modeling-driven recipes (mark a date table, calculation groups, RLS roles) live in semantic-modeling-practices.md, paired with the rationale for each. The recipes below are the remaining structural-object workflows. The `te` CLI is in preview; confirm any flag or path shape below with `te --help` (or `te list ` to see the exact child-path form) before scripting it in a pipeline. ### Perspectives @@ -139,7 +140,7 @@ Perspectives are saved field-list views. Create the perspective, then add tables te add "Perspectives/Sales View" -t Perspective -m ./model --save te add "Perspectives/Sales View/Sales" -m ./model --save # add the Sales table to the perspective te add "Perspectives/Sales View/_Measures/Revenue" -m ./model --save # add a single measure -te ls "Perspectives/Sales View" # confirm membership +te list "Perspectives/Sales View" # confirm membership ``` ### Translations and cultures