Power Queryのクエリ コピーの小ネタと何故かディープ ダイブ

**Microsoft Power BI Advent Calendar 2024** の18日目の記事です。

https://qiita.com/advent-calendar/2024/powerbi


小ネタ紹介記事で済ませようと思ったら、変な方向に突っ走ってしまった。

# 小ネタ1: Power Queryエディター間のコピペ
Power BI、データフロー、ExcelのPower Queryエディター間でクエリのコピペが相互に可能。

Power BIからExcelにコピペしてみる。逆方向やデータフローとのコピペも同様。結構便利。

複数のクエリでテストしたかったので、いつものようにヘルパー クエリのやつを作成（後でやる実験のためにクエリ名を英語にしている）。

![1 Power BIでヘルパークエリ作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/9d19d698-abae-09ab-5cd7-6bcf1a0dbc9c.png)

`source` クエリをコピー（右クリックから `複製` または Ctrl+C）して、ExcelのPower Queryエディターに貼り付けてみる。すると、関連するクエリも貼り付けられる。

![02 Excelに貼り付け 関連するクエリも.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/5ad268db-13e9-c039-f805-711d6167d8ae.png)

今度は、ヘルパー クエリを含むすべてのクエリをコピーして貼り付けてみる。期待通り、グループ構造も含めてコピーされる。

![03 Excelに貼り付け すべてのクエリ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/1084c481-379f-5ee4-c536-32f2533e5b23.png)

ただし、ヘルパー クエリの便利機能である、「関数の作成…」は引き継がれないので注意。下図のとおり、「関数の作成…」がグレー アウトされておらず、カスタム関数を作成できるようになっている。

![04 関数の作成は引き継がれない.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/4ee78234-70da-a1f7-5438-3172430d5fb7.png)


「関数の作成…」については以下の記事を参考に。

https://qiita.com/k_maki/items/cab7a340ffc717ef1a4d


# 小ネタ2: Power Queryエディターからテキスト エディターへのコピペ
実はVisual Studio Codeなどのテキスト エディターに貼り付けることも可能。Visual Studio Codeを併用してコーディングする際は非常に便利。

https://marshal115.hatenablog.com/entry/2023/01/03/151607

ただし、 **逆方向（テキスト エディターからPower Queryエディター）は不可** 。

先の一つ目のパターンで、テキスト エディターにコピペすると次のようになる（ファイルパスは修正）。Power Queryエディターに貼り付けた時と同様、関連するクエリもコピペされている。

<details><summary>sourceクエリをコピペ（折りたたみ）</summary><div>

```PowerQuery:sourceクエリをコピペ
// source
let
    Source = Folder.Files("C:\Users\...\2412 Qiita Post - Copy Query in Power Query\source"),
    #"Filtered Hidden Files1" = Table.SelectRows(Source, each [Attributes]?[Hidden]? <> true),
    #"Invoke Custom Function1" = Table.AddColumn(#"Filtered Hidden Files1", "transform file", each #"transform file"([Content])),
    #"Renamed Columns1" = Table.RenameColumns(#"Invoke Custom Function1", {"Name", "Source.Name"}),
    #"Removed Other Columns1" = Table.SelectColumns(#"Renamed Columns1", {"Source.Name", "transform file"}),
    #"Expanded Table Column1" = Table.ExpandTableColumn(#"Removed Other Columns1", "transform file", Table.ColumnNames(#"transform file"(#"sample file")))
in
    #"Expanded Table Column1"

// transform file
let
    ソース = (#"parameter 1" as binary) => let
    Source = Excel.Workbook(#"parameter 1", null, true),
    Sheet1_Sheet = Source{[Item="Sheet1",Kind="Sheet"]}[Data],
    #"Promoted Headers" = Table.PromoteHeaders(Sheet1_Sheet, [PromoteAllScalars=true])
in
    #"Promoted Headers"
in
    ソース

// sample file
let
    Source = Folder.Files("C:\Users\...\2412 Qiita Post - Copy Query in Power Query\source"),
    Navigation1 = Source{0}[Content]
in
    Navigation1
```
</div></details>


同様に、クエリ全体をコピペすると次のようになる。必要なクエリはすべて含まれているものの、グループ構造がわからないのはなぜだろう？？

<details><summary>クエリ全体をコピペ（折りたたみ）</summary><div>

```PowerQuery:クエリ全体をコピペ
// sample file
let
    Source = Folder.Files("C:\Users\...\2412 Qiita Post - Copy Query in Power Query\source"),
    Navigation1 = Source{0}[Content]
in
    Navigation1

// parameter 1
#"sample file" meta [IsParameterQuery=true, BinaryIdentifier=#"sample file", Type="Binary", IsParameterQueryRequired=true]

// transform file
let
    ソース = (#"parameter 1" as binary) => let
    Source = Excel.Workbook(#"parameter 1", null, true),
    Sheet1_Sheet = Source{[Item="Sheet1",Kind="Sheet"]}[Data],
    #"Promoted Headers" = Table.PromoteHeaders(Sheet1_Sheet, [PromoteAllScalars=true])
in
    #"Promoted Headers"
in
    ソース

// transform sample file
let
    Source = Excel.Workbook(#"parameter 1", null, true),
    Sheet1_Sheet = Source{[Item="Sheet1",Kind="Sheet"]}[Data],
    #"Promoted Headers" = Table.PromoteHeaders(Sheet1_Sheet, [PromoteAllScalars=true])
in
    #"Promoted Headers"

// source
let
    Source = Folder.Files("C:\Users\...\2412 Qiita Post - Copy Query in Power Query\source"),
    #"Filtered Hidden Files1" = Table.SelectRows(Source, each [Attributes]?[Hidden]? <> true),
    #"Invoke Custom Function1" = Table.AddColumn(#"Filtered Hidden Files1", "transform file", each #"transform file"([Content])),
    #"Renamed Columns1" = Table.RenameColumns(#"Invoke Custom Function1", {"Name", "Source.Name"}),
    #"Removed Other Columns1" = Table.SelectColumns(#"Renamed Columns1", {"Source.Name", "transform file"}),
    #"Expanded Table Column1" = Table.ExpandTableColumn(#"Removed Other Columns1", "transform file", Table.ColumnNames(#"transform file"(#"sample file")))
in
    #"Expanded Table Column1"
```

</div></details>

なんかいろいろと疑問が湧いてくる。

# ディープ ダイブ
以下、公式情報による裏取りは一切していないので注意。ポエムと同じ。

## 着眼点
[『スーパーくいしん坊』](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%BC%E3%83%91%E3%83%BC%E3%81%8F%E3%81%84%E3%81%97%E3%82%93%E5%9D%8A)の主人公、香介くんの[気持ち](https://dic.nicovideo.jp/a/%E5%87%BA%E6%9D%A5%E3%82%89%E3%81%82%E3%81%A3!)になってみた。

> 香介: 「出来らあっ！」
> コック: 「いまなんていった？」
> 香介: 「クエリのコピーでディープ ダイブしてやるっていったんだよ!!」
> コック: 「こりゃあおもしろい小僧だぜ
    大勢のお客の前でケチをつけられたんだ
    こりゃあどうしてもクエリのコピーでディープ ダイブしてもらおう」
> 香介: 「え!!　クエリのコピーでディープ ダイブを!?」


以下が気になったポイント。どうしても理由が知りたい。

- Power Queryエディターどうしのコピペは双方向なのに、テキスト エディターとは一方通行？
- クエリのグループ化の情報がどうコピペされているか？
- 「関数の作成…」がなぜ途切れるのか？

## 準備
### Windowsのクリップボードの仕組み
コピペの話なので、そもそもクリップボードがどう動いているのかを知るところから始めないと。

かなり古い記事ではあるが、以下から引用。

https://atmarkit.itmedia.co.jp/ait/articles/0012/19/news005.html

> **クリップボードの動作原理**
> クリップボードは、異なるプログラムからアクセス可能な一種の共有メモリとして実装されている。データを転送するアプリケーションは、できるだけ多くのアプリケーションがデータを取得できるように、さまざまな形式のデータをクリップボードに転送する。それを取り出す側のアプリケーションは、クリップボードに格納されたさまざまな形式の中から、情報損失が最も少ないものを取り出す。

Power Queryエディターでコピーして、テキスト エディターに貼り付けたものを再度コピーしても、Power Queryエディターに貼り付けることはできなかった。

「え？同じものをコピーしているのに？？」と思ったが、上記の説明で納得。ペーストした結果が同じでも、Power Queryエディターでコピーした時と、テキスト エディターでコピーした時で、クリップボードに格納されているデータが異なることがあるということ。

コピペが一方通行になったのは、その違いによるものではないかと。

違いを確認するためには、クリップボードの中身を知る必要がある。ということで次。

　
### クリップブックの代替
先の記事で、クリップボードの中身をしるアプリとして、クリップブック（clipbrd.exe）が紹介されている。

しかし、手元のPCではクリップブックを使えなかったので（Windows 10以降は無理？）、フリーソフトのFree Clipboard Viewer 4.0を使用。

https://freeclipboardviewer.com/jp/

インストールするまでもないので、zip版で。
（だんだん沼にはまっていく…）

## コピペが双方向だったり一方通行だったりする理由
さっそく、Free Clipboard Viewerを使って、クリップボードのデータを確認していこう。

### Power Queryエディターでクエリをコピーしてみる
単純な例から調べたいので `source file` クエリをコピーしてみる。 `プレビュー` を選択している状態だと、テキスト エディターに貼り付けたときと同じようなものが見える。

![2-1 クリップボード プレビュー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/8b354c53-5da5-fdcb-1443-7b9215063f55.png)


`Microsoft Mashup Format` というそれらしいやつを選択すると、XMLっぽいやつを確認できた！

![2-2 クリップボード Mashup Format.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/6d048482-81ae-af94-28fd-52b8833e7a52.png)


XMLを整形してみると、

```XML:source file
<?xml version="1.0" encoding="utf-8"?>
<Mashup xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://schemas.microsoft.com/DataMashup">
    <Client>PBIDesktop</Client>
    <Version>2.138.184.0</Version>
    <MinVersion>1.5.3296.0</MinVersion>
    <Culture>ja-JP</Culture>
    <SafeCombine>true</SafeCombine>
    <Items>
        <Query Name="sample file">
            <Formula>
                <![CDATA[
                    let
                        Source = Folder.Files("C:\Users\...\2412 Qiita Post - Copy Query in Power Query\source"),
                        Navigation1 = Source{0}[Content]
                    in
                        Navigation1
                ]]>
            </Formula>
            <LoadToReport>false</LoadToReport>
            <IsParameterQuery>false</IsParameterQuery>
            <IsDirectQuery xsi:nil="true" />
        </Query>
    </Items>
</Mashup>
```

なるほど、テキスト エディターに貼り付けたときよりも多くの情報が含まれている。逆に、テキストエディターのをコピーしても、見えている情報しか含まれない（当たり前）。

つまり、Power Queryエディターでコピーした時と、テキスト エディターでコピーした時で、クリップボードに格納されているデータが異なっていて、後者の場合はPower Queryエディターが必要とする情報が足りないため、コピペが一方通行となっているみたい。納得。


## グループ構造がコピペされる理由
今度は、グループの構造がコピペで保存される理由を確認しよう。テキスト エディターに貼り付けたときには、グループの情報はなくなっていた。それがクリップボードだとどうなっているかを見てみる。

全部は長すぎるので、個々のクエリの中身は省略。おお、確かにグループ構造がXMLで記述されている！

```XML:全体コピー（抜粋）

<?xml version="1.0" encoding="utf-8"?>
<Mashup xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://schemas.microsoft.com/DataMashup">
    <Client>PBIDesktop</Client>
    <Version>2.138.184.0</Version>
    <MinVersion>1.5.3296.0</MinVersion>
    <Culture>ja-JP</Culture>
    <SafeCombine>true</SafeCombine>
    <Items>
        <QueryGroup Name="Transform files from folder">
            <Description />
            <Items>
                <QueryGroup Name="Helper Query">
                    <Description />
                    <Items>
                        <Query Name="parameter 1">
                            ...(省略)...
                        </Query>
                        <Query Name="sample file">
                            ...(省略)...
                        </Query>
                        <Query Name="transform file">
                            ...(省略)...
                        </Query>
                    </Items>
                </QueryGroup>
                <Query Name="transform sample file">
                    ...(省略)...
                </Query>
            </Items>
        </QueryGroup>
        <Query Name="source">
            ...(省略)...
        </Query>
    </Items>
</Mashup>

```

<details><summary>全体コピー（省略なし）（折りたたみ）</summary><div>

```XML:全体コピー（省略なし）
<?xml version="1.0" encoding="utf-8"?>
<Mashup xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://schemas.microsoft.com/DataMashup">
    <Client>PBIDesktop</Client>
    <Version>2.138.184.0</Version>
    <MinVersion>1.5.3296.0</MinVersion>
    <Culture>ja-JP</Culture>
    <SafeCombine>true</SafeCombine>
    <Items>
        <QueryGroup Name="Transform files from folder">
            <Description />
            <Items>
                <QueryGroup Name="Helper Query">
                    <Description />
                    <Items>
                        <Query Name="parameter 1">
                            <Formula><![CDATA[
                                #"sample file" meta [IsParameterQuery=true, BinaryIdentifier=#"sample file", Type="Binary", IsParameterQueryRequired=true]
                            ]]></Formula>
                            <LoadToReport>false</LoadToReport>
                            <IsParameterQuery>true</IsParameterQuery>
                            <IsDirectQuery xsi:nil="true" />
                        </Query>
                        <Query Name="sample file">
                            <Formula><![CDATA[
                                let
                                    Source = Folder.Files("C:\Users\...\2412 Qiita Post - Copy Query in Power Query\source"),
                                    Navigation1 = Source{0}[Content]
                                in
                                    Navigation1
                            ]]></Formula>
                            <LoadToReport>false</LoadToReport>
                            <IsParameterQuery>false</IsParameterQuery>
                            <IsDirectQuery xsi:nil="true" />
                        </Query>
                        <Query Name="transform file">
                            <Formula><![CDATA[
                                let
                                    ソース = (#"parameter 1" as binary) => let
                                    Source = Excel.Workbook(#"parameter 1", null, true),
                                    Sheet1_Sheet = Source{[Item="Sheet1",Kind="Sheet"]}[Data],
                                    #"Promoted Headers" = Table.PromoteHeaders(Sheet1_Sheet, [PromoteAllScalars=true])
                                in
                                    #"Promoted Headers"
                                in
                                    ソース
                            ]]></Formula>
                            <LoadToReport>true</LoadToReport>
                            <IsParameterQuery>false</IsParameterQuery>
                            <IsDirectQuery xsi:nil="true" />
                        </Query>
                    </Items>
                </QueryGroup>
                <Query Name="transform sample file">
                    <Formula><![CDATA[
                        let
                            Source = Excel.Workbook(#"parameter 1", null, true),
                            Sheet1_Sheet = Source{[Item="Sheet1",Kind="Sheet"]}[Data],
                            #"Promoted Headers" = Table.PromoteHeaders(Sheet1_Sheet, [PromoteAllScalars=true])
                        in
                            #"Promoted Headers"
                    ]]></Formula>
                    <LoadToReport>false</LoadToReport>
                    <IsParameterQuery>false</IsParameterQuery>
                    <IsDirectQuery xsi:nil="true" />
                </Query>
            </Items>
        </QueryGroup>
        <Query Name="source">
            <Formula><![CDATA[
                let
                    Source = Folder.Files("C:\Users\...\2412 Qiita Post - Copy Query in Power Query\source"),
                    #"Filtered Hidden Files1" = Table.SelectRows(Source, each [Attributes]?[Hidden]? <> true),
                    #"Invoke Custom Function1" = Table.AddColumn(#"Filtered Hidden Files1", "transform file", each #"transform file"([Content])),
                    #"Renamed Columns1" = Table.RenameColumns(#"Invoke Custom Function1", {"Name", "Source.Name"}),
                    #"Removed Other Columns1" = Table.SelectColumns(#"Renamed Columns1", {"Source.Name", "transform file"}),
                    #"Expanded Table Column1" = Table.ExpandTableColumn(#"Removed Other Columns1", "transform file", Table.ColumnNames(#"transform file"(#"sample file")))
                in
                    #"Expanded Table Column1"
            ]]></Formula>
            <LoadToReport>true</LoadToReport>
            <IsParameterQuery>false</IsParameterQuery>
            <IsDirectQuery>false</IsDirectQuery>
        </Query>
    </Items>
</Mashup>
```

</div></details>



## コピペで「関数の作成…」が途切れる理由
Power BIのpbixファイルをpbip形式で保存し、TMDLで確認する。「関数の作成...」が有効かどうかで差分が生じるファイルを探す。

結果、 `filename.SemanticMocel/definition/expressions.tmdl` というファイルで差分が生じていた。

```diff_python:expressions.tmdl（変更箇所を抜粋）
...（省略）...

expression 'transform file' =
		let
			ソース = (#"parameter 1" as binary) => let
				Source = Excel.Workbook(#"parameter 1", null, true),
				Sheet1_Sheet = Source{[Item="Sheet1",Kind="Sheet"]}[Data],
				#"Promoted Headers" = Table.PromoteHeaders(Sheet1_Sheet, [PromoteAllScalars=true])
			in
				#"Promoted Headers"
		in
			ソース
+	mAttributes: [ FunctionQueryBinding = "{""exemplarFormulaName"":""transform sample file""}" ]
	lineageTag: 2edfa38f-32d4-436b-8cec-05166b5ece78
	queryGroup: 'Transform files from folder\Helper Query'

	annotation PBI_ResultType = Function

...（省略）...
```

`mAttributes: [ FunctionQueryBinding = "{""exemplarFormulaName"":""transform sample file""}" ]` の意味は、関数クエリ `transform file` は、クエリ `transform sample file` に紐づけられている（バインドされている）ということかと。

で、この情報はクエリをコピーしたときに、クリップボードのMashup Formatには含まれていなかった。だから、関数の紐づけはコピペで切れてしまっていた。

# 感想
息切れ気味だったので、小ネタに走るつもりが、なんかやたらと細かいことを調べてしまった。。そして大して役に立つ情報ではないという。。

ま、いっか ＼(^o^)／