Power Query Documentation
Power Query Documentation
Overview
What is Power Query?
Quick Starts
Using Power Query in Power BI
Using Query Parameters in Power BI Desktop
Combining Data
Installing the PowerQuery SDK
Starting to Develop Custom Connectors
Creating your first connector - Hello World
Tutorials
Shape and combine data using Power Query Editor
Connector Development
TripPin Walkthrough
Overview
1. OData
2. REST API
3. Navigation Tables
4. Paths
5. Paging
6. Schemas
7. Advanced Schemas
8. Diagnostics
9. Test Connection
10. Folding
OAuth Tutorials
Github
MyGraph
Samples
Functional Samples
ODBC Samples
TripPin Samples
Concepts
Certification
Power Query Online Limits
Reference
Authoring Reference
Excel Data Completeness
Connectors
Connector Development Reference
Handling Authentication
Handling Data Access
ODBC Development
Overview
ODBC Extensibility Functions
Parameters for your Data Source Function
Parameters for Odbc.DataSource
Creating Your Connector
Troubleshooting and Testing
Common Problems
Handling Resource Path
Handling Paging
Handling Transformations
Static
Dynamic
Handling Schemas
Handling Status Codes
Default Behavior
Wait Retry Pattern
Handling Unit Testing
Helper Functions
Handling Documentation
Handling Navigation Tables
Handling Gateway Support
Handling Connector Signing
Resources
Power BI Documentation
M Function Reference
M Language Document
M Type Reference
What is Power Query?
3/5/2019 • 3 minutes to read
Power Query is the Microsoft Data Connectivity and Data Preparation technology that enables business users to
seamlessly access data stored in hundreds of data sources and reshape it to fit their needs, with an easy to use,
engaging and no-code user experience.
Supported data sources include a wide range of file types, databases, Microsoft Azure services and many other
third-party online services. Power Query also provides a Custom Connectors SDK so that third parties can
create their own data connectors and seamlessly plug them into Power Query.
The Power Query Editor is the primary data preparation experience, allowing users to apply over 300 different
data transformations by previewing data and selecting transformations in the user experience. These data
transformation capabilities are common across all data sources, regardless of the underlying data source
limitations.
Where to use Power Query
Power Query is natively integrated in several Microsoft products, including the following.
Microsoft Power BI Power Query enables data analysts and report authors to
connect and transform data as part of creating Power BI
reports using Power BI Desktop.
Microsoft Excel Power Query enables Excel users to import data from a wide
range of data sources into Excel for analytics and
visualizations.
Starting with Excel 2016, Power Query capabilities are natively
integrated and can be found under the “Get & Transform”
section of the Data tab in the Excel Desktop ribbon.
Excel 2010 and 2013 users can also leverage Power Query by
installing the Microsoft Power Query for Excel add-in.
Microsoft SQL Server Data Tools for Visual Studio Business Intelligence Developers can create Azure Analysis
Services and SQL Server Analysis Services tabular models using
SQL Server Data Tools for Visual Studio. Within this experience,
users can leverage Power Query to access and reshape data as
part of defining tabular models.
Microsoft Common Data Service for Apps Common Data Service (CDS) for Apps lets you securely store
and manage data that's used by business applications. Data
within CDS for Apps is stored within a set of entities. An entity
is a set of records used to store data, similar to how a table
stores data within a database.
CDS for Apps includes a base set of standard entities that
cover typical scenarios, but you can also create custom entities
specific to your organization and populate them with data
using Power Query. App makers can then use PowerApps to
build rich applications using this data.
Finding & Connecting to data is too difficult Power Query enables connectivity to a wide range (100s) of
data sources, including data of all sizes and shapes.
Experiences for data connectivity are too fragmented Consistency of experience, and parity of query capabilities over
all data sources with Power Query.
Data often needs to be reshaped before consumption Highly interactive and intuitive experience for rapidly and
iteratively building queries over any data source, any size.
Any shaping is one-off and not repeatable When using Power Query to access and transform data, users
are defining a repeatable process (query) that can be easily
refreshed in the future to get up-to-date data.
In the event that the process/query needs to be modified to
account for underlying data or schema changes, Power Query
provides users with the ability to modify existing queries using
the same interactive and intuitive experience that they used
when initially defining their queries.
Volume (data sizes), Velocity (rate of change) and Variety Power Query offers the ability to work against a subset of the
(breadth of data sources and data shapes) entire data set in order to define the required data
transformations, allowing users to easily filter down and
transform their data to a manageable size.
Power Query queries can be refreshed manually or by
leveraging schedule refresh capabilities in specific products
(such as Power BI) or even programmatically (using Excel’s
Object Model).
Power Query provides connectivity to hundreds of data
sources and over 350 different types of data transformations
for each of these sources, allowing users to work with data
from any source and in any shape.
Next steps
Next, learn how to use Power Query in Power BI Desktop.
Quickstart: Using Power Query in Power BI Desktop
Quickstart: Using Power Query in Power BI Desktop
7/2/2019 • 5 minutes to read
With Power Query in Power BI you can connect to many different data sources, transform the data into the
shape you want, and quickly be ready to create reports and insights. When using Power BI Desktop, Power Query
functionality is provided in the Power Query Editor.
Let’s get acquainted with Power Query Editor.
If you're not signed up for Power BI, you can sign up for a free trial before you begin. Also, Power BI Desktop is
free to download.
With no data connections, Power Query Editor appears as a blank pane, ready for data.
Once a query is loaded, Power Query Editor view becomes more interesting. If we connect to the following Web
data source, Power Query Editor loads information about the data, which you can then begin to shape.
https://www.bankrate.com/finance/retirement/best-places-retire-how -state-ranks.aspx
Here’s how Power Query Editor appears once a data connection is established:
1. In the ribbon, many buttons are now active to interact with the data in the query
2. In the left pane, queries are listed and available for selection, viewing, and shaping
3. In the center pane, data from the selected query is displayed and available for shaping
4. The Query Settings window appears, listing the query’s properties and applied steps
We’ll look at each of these four areas – the ribbon, the queries pane, the data view, and the Query Settings pane –
in the following sections.
To connect to data and begin the query building process, select the Get Data button. A menu appears, providing
the most common data sources.
The Transform tab provides access to common data transformation tasks, such as adding or removing columns,
changing data types, splitting columns, and other data-driven tasks. The following image shows the Transform
tab.
The Add Column tab provides additional tasks associated with adding a column, formatting column data, and
adding custom columns. The following image shows the Add Column tab.
The View tab on the ribbon is used to toggle whether certain panes or windows are displayed. It’s also used to
display the Advanced Editor. The following image shows the View tab.
It’s useful to know that many of the tasks available from the ribbon are also available by right-clicking a column, or
other data, in the center pane.
The left pane
The left pane displays the number of active queries, as well as the name of the query. When you select a query
from the left pane, its data is displayed in the center pane, where you can shape and transform the data to meet
your needs. The following image shows the left pane with multiple queries.
Next steps
In this quickstart you learned how to use Power Query Editor in Power BI Desktop, and how to connect to data
sources. To learn more, continue with the tutorial on shaping and transforming data with Power Query.
Power Query tutorial
Using Query Parameters in Power BI Desktop
8/21/2019 • 3 minutes to read
With Power Query and Power BI Desktop, you can add Query Parameters to a report and make elements of
the report dependent on those parameters. For example, you could use Query Parameters to automatically have a
report create a filter, load a data model or a data source reference, generate a measure definition, and many other
abilities. Query Parameters let users open a report, and by providing values for its Query Parameters, jump-start
creating that report with just a few clicks.
You can have one parameter, or multiple parameters for any report. Let's take a look at how to create parameters in
Power BI Desktop.
Selecting Edit Parameters brings up a window that allows you to provide a different value for the parameter.
Providing a different value and then selecting OK refreshes the report data, and any visuals, based on the new
parameter values.
Next steps
There are all sorts of things you can do with Power Query and Power BI Desktop. For more information, check out
the following resources:
Query Overview with Power BI Desktop
Data Types in Power BI Desktop
Shape and Combine Data with Power BI Desktop
Common Query Tasks in Power BI Desktop
Using templates in Power BI Desktop
Tutorial: Shape and combine data using Power Query
7/2/2019 • 15 minutes to read
With Power Query, you can connect to many different types of data sources, then shape the data to meet your
needs, enabling you to create visual reports using Power BI Desktopu that you can share with others. Shaping
data means transforming the data – such as renaming columns or tables, changing text to numbers, removing rows,
setting the first row as headers, and so on. Combining data means connecting to two or more data sources, shaping
them as needed, then consolidating them into one useful query.
In this tutorial, you'll learn to:
Shape data using Power Query Editor
Connect to a data source
Connect to another data source
Combine those data sources, and create a data model to use in reports
This tutorial demonstrates how to shape a query using Power Query Editor, technology that's incorporated into
Power BI Desktop, and learn some common data tasks.
It’s useful to know that the Power Query Editor in Power BI Desktop makes ample use of right-click menus, as
well as the ribbon. Most of what you can select in the Transform ribbon is also available by right-clicking an item
(such as a column) and choosing from the menu that appears.
If you're not signed up for Power BI, you can sign up for a free trial before you begin. Also, Power BI Desktop is free
to download.
Shape data
When you shape data in the Power Query Editor, you’re providing step-by-step instructions (that Power Query
Editor carries out for you) to adjust the data as Power Query Editor loads and presents it. The original data source is
not affected; only this particular view of the data is adjusted, or shaped.
The steps you specify (such as rename a table, transform a data type, or delete columns) are recorded by Power
Query Editor, and each time this query connects to the data source those steps are carried out so that the data is
always shaped the way you specify. This process occurs whenever you use the Power Query Editor feature of Power
BI Desktop, or for anyone who uses your shared query, such as on the Power BI service. Those steps are captured,
sequentially, in the Query Settings pane, under Applied Steps.
The following image shows the Query Settings pane for a query that has been shaped – we’ll go through each of
those steps in the next few paragraphs.
Using the retirement data from the Using Power Query in Power BI Desktop quickstart article, which we found by
connecting to a Web data source, let’s shape that data to fit our needs.
For starters, let's add a custom column to calculate rank based on all data being equal factors and compare this to
the existing column Rank. Here's the Add Column ribbon, with an arrow pointing toward the Custom Column
button, which lets you add a custom column.
In the Custom Column dialog, in New column name, enter New Rank, and in Custom column formula, enter
the following:
([Cost of living] + [Weather] + [Health care quality] + [Crime] + [Tax] + [Culture] + [Senior] + [#"Well-
being"]) / 8
Make sure the status message reads 'No syntax errors have been detected.' and click OK.
To keep column data consistent, lets transform the new column values to whole numbers. Just right-click the
column header, and select Change Type > Whole Number to change them.
If you need to choose more than one column, first select a column then hold down SHIFT, select additional
adjacent columns, and then right-click a column header to change all selected columns. You can also use the CTRL
key to choose non-adjacent columns.
You can also transform column data types from the Transform ribbon. Here’s the Transform ribbon, with an arrow
pointing toward the Data Type button, which lets you transform the current data type to another.
Note that in Query Settings, the Applied Steps reflect any shaping steps applied to the data. If I want to remove
any step from the shaping process, I simply select the X to the left of the step. In the following image, Applied
Steps reflects the steps so far: connecting to the website ( Source); selecting the table (Navigation); and while
loading the table, Power Query Editor automatically changed text-based number columns from Text to Whole
Number (Changed Type). The last two steps show our previous actions with Added Custom and Changed
Type1.
Before we can work with this query, we need to make a few changes to get its data where we want it:
Adjust the rankings by removing a column - we have decided Cost of living is a non-factor in our results. After
removing this column, we find the issue that the data remains unchanged, though it's easy to fix using Power BI
Desktop, and doing so demonstrates a cool feature of Applied Steps in Query.
Fix a few errors – since we removed a column, we need to readjust our calculations in the New Rank column.
This involves changing a formula.
Sort the data - based on the New Rank and Rank columns.
Replace data - we will highlight how to replace a specific value and the need of inserting an Applied Step.
Change the table name – that Table 0 is not a useful descriptor, but changing it is simple.
To remove the Cost of living column, simply select the column and choose the Home tab from the ribbon, then
Remove Columns as shown in the following figure.
Notice the New Rank values have not changed; this is due to the ordering of the steps. Since Power Query Editor
records the steps sequentially, yet independently of each other, you can move each Applied Step up or down in the
sequence. Just right-click any step, and Power Query Editor provides a menu that lets you do the following:
Rename, Delete, Delete Until End (remove the current step, and all subsequent steps too), Move Up, or Move
Down. Go ahead and move up the last step Removed Columns to just above the Added Custom step.
Next, select the Added Custom step. Notice the data now shows Error which we will need to address.
There are a few ways to get more information about each error. You can select the cell (without clicking on the word
Error), or click the word Error directly. If you select the cell without clicking directly on the word Error, Power
Query Editor displays the error information on the bottom of the window.
If you click the word Error directly, Query creates an Applied Step in the Query Settings pane and displays
information about the error. We do not want to go this route, so select Cancel.
To fix the errors, select the New Rank column, then display the column's data formula by opening the View ribbon
and selecting the Formula Bar checkbox.
Now you can remove the Cost of living parameter and decrement the divisor, by changing the formula to the
following:
Table.AddColumn(#"Removed Columns", "New Rank", each ([Weather] + [Health care quality] + [Crime] + [Tax] +
[Culture] + [Senior] + [#"Well-being"]) / 7)
Select the green checkmark to the left of the formula box or press Enter, and the data should be replaced by
revised values and the Added Custom step should now complete with no errors.
NOTE
You can also Remove Errors (using the ribbon or the right-click menu), which removes any rows that have errors. In this
case it would’ve removed all the rows from our data, and we didn’t want to do that – we like all our data, and want to keep it
in the table.
Now we need to sort the data based on the New Rank column. First select the last applied step, Changed Type1
to get to the most recent data. Then, select drop-down located next to the New Rank column header and select
Sort Ascending.
Notice the data is now sorted according to New Rank. However, if you look in the Rank column, you will notice
the data is not sorted properly in cases where the New Rank value is a tie. To fix this, select the New Rank column
and change the formula in the Formula Bar to the following:
Select the green checkmark to the left of the formula box or press Enter, and the rows should now be ordered in
accordance with both New Rank and Rank.
In addition, you can select an Applied Step anywhere in the list, and continue shaping the data at that point in the
sequence. Power Query Editor will automatically insert a new step directly after the currently selected Applied
Step. Let's give that a try.
First, select the Applied Step prior to adding the custom column; this would be the Removed Columns step. Here
we will replace the value of the Weather ranking in Arizona. Right-click the appropriate cell that contains Arizona's
Weather ranking and select Replace Values... from the menu that appears. Note which Applied Step is currently
selected (the step prior to the Added Custom step).
Since we're inserting a step, Power Query Editor warns us about the danger of doing so - subsequent steps could
cause the query to break. We need to be careful, and thoughtful! Since this is a tutorial, and we're highlighting a
really cool feature of Power Query Editor to demonstrate how you can create, delete, insert, and reorder steps, we'll
push ahead and select Insert.
Change the value to 51 and the data for Arizona is replaced. When you create a new Applied Step, Power Query
Editor names it based on the action - in this case, Replaced Value. When you have more than one step with the
same name in your query, Power Query Editor adds a number (in sequence) to each subsequent Applied Step to
differentiate between them.
Now select the last Applied Step, Sorted Rows, and notice the data has changed regarding Arizona's new ranking.
This is because we inserted the Replaced Value step in the right place, before the Added Custom step.
Okay that was a little involved, but it was a good example of how powerful and versatile Power Query Editor can
be.
Lastly, we want to change the name of that table to something descriptive. When we get to creating reports, it’s
especially useful to have descriptive table names, especially when we connect to multiple data sources, and they’re
all listed in the Fields pane of the Report view.
Changing the table name is easy: in the Query Settings pane, under Properties, simply type in the new name of
the table, as shown in the following image, and hit Enter. Let’s call this table RetirementStats.
Okay, we’ve shaped that data to the extent we need to. Next let’s connect to another data source, and combine data.
Combine data
That data about various states is interesting, and will be useful for building additional analysis efforts and queries.
But there’s one problem: most data out there uses a two-letter abbreviation for state codes, not the full name of the
state. We need some way to associate state names with their abbreviations.
We’re in luck: there’s another public data source that does just that, but it needs a fair amount of shaping before we
can connect it to our retirement table. Here’s the Web resource for state abbreviations:
https://en.wikipedia.org/wiki/List_of_U.S._state_abbreviations
From the Home ribbon in Power Query Editor, we select New Source > Web and type the address, select
Connect, and the Navigator shows what it found on that Web page.
We select Codes and abbreviations... because that includes the data we want, but it’s going to take quite a bit of
shaping to pare that table’s data down to what we want.
TIP
Is there a faster or easier way to accomplish the steps below? Yes, we could create a relationship between the two tables, and
shape the data based on that relationship. The following steps are still good to learn for working with tables, just know that
relationships can help you quickly use data from multiple tables.
NOTE
If Power BI accidentally imports the table headers as a row in your data table, you can select Use First Row As Headers from
the Home tab, or from the Transform tab in the ribbon, to fix your table.
Remove the bottom 26 rows – they’re all the territories, which we don’t need to include. From the Home ribbon,
select Reduce Rows > Remove Rows > Remove Bottom Rows.
Since the RetirementStats table doesn't have information for Washington DC, we need to filter it from our list.
Select the drop-down arrow beside the Region Status column, then clear the checkbox beside Federal district.
Remove a few unneeded columns – we only need the mapping of state to its official two-letter abbreviation, so
we can remove the following columns: Column1, Column3, Column4, and then Column6 through
Column11. First select Column1, then hold down the CTRL key and select the other columns to be removed
(this lets you select multiple, non-contiguous columns). From the Home tab on the ribbon, select Remove
Columns > Remove Columns.
NOTE
This is a good time to point out that the sequence of applied steps in Power Query Editor is important, and can affect how
the data is shaped. It’s also important to consider how one step may impact another subsequent step; if you remove a step
from the Applied Steps, subsequent steps may not behave as originally intended, because of the impact of the query’s
sequence of steps.
NOTE
When you resize the Power Query Editor window to make the width smaller, some ribbon items are condensed to make the
best use of visible space. When you increase the width of the Power Query Editor window, the ribbon items expand to make
the most use of the increased ribbon area.
Rename the columns, and the table itself – as usual, there are a few ways to rename a column; first select the
column, then either select Rename from the Transform tab on the ribbon, or right-click and select Rename…
from the menu that appears. The following image has arrows pointing to both options; you only need to choose
one.
Let’s rename them to State Name and State Code. To rename the table, just type the name into the Name box in
the Query Settings pane. Let’s call this table StateCodes.
Now that we’ve shaped the StateCodes table the way we want, let’s combine these two tables, or queries, into one;
since the tables we now have are a result of the queries we applied to the data, they’re often referred to as queries.
There are two primary ways of combining queries – merging and appending.
When you have one or more columns that you’d like to add to another query, you merge the queries. When you
have additional rows of data that you’d like to add to an existing query, you append the query.
In this case, we want to merge queries. To get started, from the left pane of Power Query Editor we select the query
into which we want the other query to merge, which in this case is RetirementStats. Then select Combine > Merge
Queries from the Home tab on the ribbon.
You may be prompted to set the privacy levels, to ensure the data is combined without including or transferring
data you didn't want transferred.
Next the Merge window appears, prompting us to select which table we’d like merged into the selected table, and
then, the matching columns to use for the merge. Select State from the RetirementStats table (query), then select
the StateCodes query (easy in this case, since there’s only one other query – when you connect to many data
sources, there are many queries to choose from). When we select the correct matching columns – State from
RetirementStats, and State Name from StateCodes – the Merge window looks like the following, and the OK
button is enabled.
A NewColumn is created at the end of the query, which is the contents of the table (query) that was merged with
the existing query. All columns from the merged query are condensed into the NewColumn, but you can select to
Expand the table, and include whichever columns you want.
To Expand the merged table, and select which columns to include, select the expand icon ( ). The Expand window
appears.
In this case, we only want the State Code column, so we select only that column and then select OK. We clear the
checkbox from Use original column name as prefix because we don’t need or want that; if we leave that selected,
the merged column would be named NewColumn.State Code (the original column name, or NewColumn, then
a dot, then the name of the column being brought into the query).
NOTE
Want to play around with how to bring in that NewColumn table? You can experiment a bit, and if you don’t like the results,
just delete that step from the Applied Steps list in the Query Settings pane; your query returns to the state prior to
applying that Expand step. It’s like a free do-over, which you can do as many times as you like until the expand process looks
the way you want it.
We now have a single query (table) that combined two data sources, each of which has been shaped to meet our
needs. This query can serve as a basis for lots of additional, interesting data connections – such as housing cost
statistics, demographics, or job opportunities in any state.
To apply changes and close Power Query Editor, select Close & Apply from the Home ribbon tab. The
transformed dataset appears in Power BI Desktop, ready to be used for creating reports.
Next steps
There are all sorts of things you can do with Power Query. If you're ready to create your own custom connector,
check the following article.
Creating your first connector: Hello World
Installing the Power Query SDK
7/2/2019 • 3 minutes to read
Quickstart
Note: The steps to enable extensions changed in the June 2017 version of Power BI Desktop.
1. Install the Power Query SDK from the Visual Studio Marketplace
2. Create a new Data Connector project
3. Define your connector logic
4. Build the project to produce an extension file
5. Copy the extension file into [Documents]/Power BI Desktop/Custom Connectors
6. Check the option - (Not Recommended) Allow any extension to load without validation or warning in
Power BI Desktop (under File | Options and settings | Options | Security | Data Extensions)
7. Restart Power BI Desktop
Step by Step
Creating a New Extension in Visual Studio
Installing the Power Query SDK for Visual Studio will create a new Data Connector project template in Visual
Studio.
This creates a new project containing the following files:
1. Connector definition file (.pq)
2. A query test file ( .query.pq)
3. A string resource file (resources.resx)
4. PNG files of various sizes used to create icons
Your connector definition file will start with an empty Data Source description. Please see the Data Source
Kind section later in this document for details.
Testing in Visual Studio
The Power Query SDK provides basic query execution capabilities, allowing you to test your extension without
having to switch over to Power BI Desktop. See the Query File section for more details.
Build and Deploy from Visual Studio
Building your project will produce your .pqx file.
Data Connector projects do not support custom post build steps to copy the extension file to
your [Documents]\Microsoft Power BI Desktop\Custom Connectors directory. If this is something you want to do,
you may want to use a third party visual studio extension, such as Auto Deploy.
Extension Files
PQ extensions are bundled in a zip file and given a .mez file extension. At runtime, PBI Desktop will load extensions
from the [Documents]\Microsoft Power BI Desktop\Custom Connectors.
Note: in an upcoming change the default extension will be changed from .mez to .pqx
Extension File Format
Extensions are defined within an M section document. A section document has a slightly different format from the
query document(s) generated in Power Query. Code you import from Power Query typically requires modification
to fit into a section document, but the changes are minor. Section document differences you should be aware of
include:
They begin with a section declaration (ex. section HelloWorld;)
Each expression ends with a semi-colon (ex. a = 1; or b = let c = 1 + 2 in c;)
All functions and variables are local to the section document, unless they are marked as shared. Shared
functions become visible to other queries/functions, and can be thought of as the exports for your extension (i.e.
they become callable from Power Query).
More information about M section documents can be found in the M Language specification.
Query File
In addition to the extension file, Data Connector projects can have a Query file (name.query.pq). This file can be
used to run test queries within Visual Studio. The query evaluation will automatically include your extension code,
without having to register your .pqx file, allowing you to call/test any shared functions in your extension code.
The query file can contain a single expression (ex. HelloWorld.Contents()), a let expression (such as what Power
Query would generate), or a section document.
Starting to Develop Custom Connectors
3/5/2019 • 2 minutes to read
To get you up to speed with Power Query, we've listed some of the most common questions on this page.
What software do I need to get started with the Power Query SDK?
You need to install the Power Query SDK in addition to Visual Studio. To be able to test your connectors we suggest
that you also have Power BI installed.
What can you do with a Connector?
Data Connectors allow you to create new data sources or customize and extend an existing source. Common use
cases include:
Creating a business analyst-friendly view for a REST API
Providing branding for a source that Power Query supports with an existing connector (such as an OData
service or ODBC driver)
Implementing OAuth v2 authentication flow for a SaaS offering
Exposing a limited or filtered view over your data source to improve usability
Enabling DirectQuery for a data source via ODBC driver
Data Connectors are currently only supported in Power BI Desktop.
Creating your first connector: Hello World
3/5/2019 • 2 minutes to read
[DataSource.Kind="HelloWorld", Publish="HelloWorld.Publish"]
shared HelloWorld.Contents = (optional message as text) =>
let
message = if (message <> null) then message else "Hello world"
in
message;
HelloWorld = [
Authentication = [
Implicit = []
],
Label = Extension.LoadString("DataSourceLabel")
];
HelloWorld.Publish = [
Beta = true,
ButtonText = { Extension.LoadString("FormulaTitle"), Extension.LoadString("FormulaHelp") },
SourceImage = HelloWorld.Icons,
SourceTypeImage = HelloWorld.Icons
];
HelloWorld.Icons = [
Icon16 = { Extension.Contents("HelloWorld16.png"), Extension.Contents("HelloWorld20.png"), Extension.Conten
ts("HelloWorld24.png"), Extension.Contents("HelloWorld32.png") },
Icon32 = { Extension.Contents("HelloWorld32.png"), Extension.Contents("HelloWorld40.png"), Extension.Conten
ts("HelloWorld48.png"), Extension.Contents("HelloWorld64.png") }
];
Once you built the file and copied it to the correct directory following the instructions in Installing the PowerQuery
SDK tutorial, open PowerBI. You can search for "hello" to find your connector in the Get Data dialogue.
It will bring up an authentication dialogue. Since there's no authentication options, and the function takes no
parameters, there's no further steps in these dialogues.
Press connect and it will tell you that it's a "Preview connector", since we have "Beta" set to true in the query. Since
there's no authentication, the authentication screen will present a tab for Anonymous authentication with no fields.
Press "Connect" again to finish.
Finally, the query editor will come up showing what we expect--a function that returns the text "Hello world".
For the fully implemented sample, please see the Hello World Sample in the Data Connectors sample repo.
Tutorial: Shape and combine data using Power Query
7/2/2019 • 15 minutes to read
With Power Query, you can connect to many different types of data sources, then shape the data to meet your
needs, enabling you to create visual reports using Power BI Desktopu that you can share with others. Shaping
data means transforming the data – such as renaming columns or tables, changing text to numbers, removing
rows, setting the first row as headers, and so on. Combining data means connecting to two or more data sources,
shaping them as needed, then consolidating them into one useful query.
In this tutorial, you'll learn to:
Shape data using Power Query Editor
Connect to a data source
Connect to another data source
Combine those data sources, and create a data model to use in reports
This tutorial demonstrates how to shape a query using Power Query Editor, technology that's incorporated into
Power BI Desktop, and learn some common data tasks.
It’s useful to know that the Power Query Editor in Power BI Desktop makes ample use of right-click menus, as
well as the ribbon. Most of what you can select in the Transform ribbon is also available by right-clicking an item
(such as a column) and choosing from the menu that appears.
If you're not signed up for Power BI, you can sign up for a free trial before you begin. Also, Power BI Desktop is
free to download.
Shape data
When you shape data in the Power Query Editor, you’re providing step-by-step instructions (that Power Query
Editor carries out for you) to adjust the data as Power Query Editor loads and presents it. The original data source
is not affected; only this particular view of the data is adjusted, or shaped.
The steps you specify (such as rename a table, transform a data type, or delete columns) are recorded by Power
Query Editor, and each time this query connects to the data source those steps are carried out so that the data is
always shaped the way you specify. This process occurs whenever you use the Power Query Editor feature of
Power BI Desktop, or for anyone who uses your shared query, such as on the Power BI service. Those steps are
captured, sequentially, in the Query Settings pane, under Applied Steps.
The following image shows the Query Settings pane for a query that has been shaped – we’ll go through each of
those steps in the next few paragraphs.
Using the retirement data from the Using Power Query in Power BI Desktop quickstart article, which we found by
connecting to a Web data source, let’s shape that data to fit our needs.
For starters, let's add a custom column to calculate rank based on all data being equal factors and compare this to
the existing column Rank. Here's the Add Column ribbon, with an arrow pointing toward the Custom Column
button, which lets you add a custom column.
In the Custom Column dialog, in New column name, enter New Rank, and in Custom column formula, enter
the following:
([Cost of living] + [Weather] + [Health care quality] + [Crime] + [Tax] + [Culture] + [Senior] + [#"Well-
being"]) / 8
Make sure the status message reads 'No syntax errors have been detected.' and click OK.
To keep column data consistent, lets transform the new column values to whole numbers. Just right-click the
column header, and select Change Type > Whole Number to change them.
If you need to choose more than one column, first select a column then hold down SHIFT, select additional
adjacent columns, and then right-click a column header to change all selected columns. You can also use the CTRL
key to choose non-adjacent columns.
You can also transform column data types from the Transform ribbon. Here’s the Transform ribbon, with an
arrow pointing toward the Data Type button, which lets you transform the current data type to another.
Note that in Query Settings, the Applied Steps reflect any shaping steps applied to the data. If I want to remove
any step from the shaping process, I simply select the X to the left of the step. In the following image, Applied
Steps reflects the steps so far: connecting to the website ( Source); selecting the table (Navigation); and while
loading the table, Power Query Editor automatically changed text-based number columns from Text to Whole
Number (Changed Type). The last two steps show our previous actions with Added Custom and Changed
Type1.
Before we can work with this query, we need to make a few changes to get its data where we want it:
Adjust the rankings by removing a column - we have decided Cost of living is a non-factor in our results. After
removing this column, we find the issue that the data remains unchanged, though it's easy to fix using Power BI
Desktop, and doing so demonstrates a cool feature of Applied Steps in Query.
Fix a few errors – since we removed a column, we need to readjust our calculations in the New Rank column.
This involves changing a formula.
Sort the data - based on the New Rank and Rank columns.
Replace data - we will highlight how to replace a specific value and the need of inserting an Applied Step.
Change the table name – that Table 0 is not a useful descriptor, but changing it is simple.
To remove the Cost of living column, simply select the column and choose the Home tab from the ribbon, then
Remove Columns as shown in the following figure.
Notice the New Rank values have not changed; this is due to the ordering of the steps. Since Power Query Editor
records the steps sequentially, yet independently of each other, you can move each Applied Step up or down in
the sequence. Just right-click any step, and Power Query Editor provides a menu that lets you do the following:
Rename, Delete, Delete Until End (remove the current step, and all subsequent steps too), Move Up, or Move
Down. Go ahead and move up the last step Removed Columns to just above the Added Custom step.
Next, select the Added Custom step. Notice the data now shows Error which we will need to address.
There are a few ways to get more information about each error. You can select the cell (without clicking on the
word Error), or click the word Error directly. If you select the cell without clicking directly on the word Error, Power
Query Editor displays the error information on the bottom of the window.
If you click the word Error directly, Query creates an Applied Step in the Query Settings pane and displays
information about the error. We do not want to go this route, so select Cancel.
To fix the errors, select the New Rank column, then display the column's data formula by opening the View ribbon
and selecting the Formula Bar checkbox.
Now you can remove the Cost of living parameter and decrement the divisor, by changing the formula to the
following:
Table.AddColumn(#"Removed Columns", "New Rank", each ([Weather] + [Health care quality] + [Crime] + [Tax] +
[Culture] + [Senior] + [#"Well-being"]) / 7)
Select the green checkmark to the left of the formula box or press Enter, and the data should be replaced by
revised values and the Added Custom step should now complete with no errors.
NOTE
You can also Remove Errors (using the ribbon or the right-click menu), which removes any rows that have errors. In this
case it would’ve removed all the rows from our data, and we didn’t want to do that – we like all our data, and want to keep it
in the table.
Now we need to sort the data based on the New Rank column. First select the last applied step, Changed Type1
to get to the most recent data. Then, select drop-down located next to the New Rank column header and select
Sort Ascending.
Notice the data is now sorted according to New Rank. However, if you look in the Rank column, you will notice
the data is not sorted properly in cases where the New Rank value is a tie. To fix this, select the New Rank
column and change the formula in the Formula Bar to the following:
Select the green checkmark to the left of the formula box or press Enter, and the rows should now be ordered in
accordance with both New Rank and Rank.
In addition, you can select an Applied Step anywhere in the list, and continue shaping the data at that point in the
sequence. Power Query Editor will automatically insert a new step directly after the currently selected Applied
Step. Let's give that a try.
First, select the Applied Step prior to adding the custom column; this would be the Removed Columns step. Here
we will replace the value of the Weather ranking in Arizona. Right-click the appropriate cell that contains Arizona's
Weather ranking and select Replace Values... from the menu that appears. Note which Applied Step is currently
selected (the step prior to the Added Custom step).
Since we're inserting a step, Power Query Editor warns us about the danger of doing so - subsequent steps could
cause the query to break. We need to be careful, and thoughtful! Since this is a tutorial, and we're highlighting a
really cool feature of Power Query Editor to demonstrate how you can create, delete, insert, and reorder steps,
we'll push ahead and select Insert.
Change the value to 51 and the data for Arizona is replaced. When you create a new Applied Step, Power Query
Editor names it based on the action - in this case, Replaced Value. When you have more than one step with the
same name in your query, Power Query Editor adds a number (in sequence) to each subsequent Applied Step to
differentiate between them.
Now select the last Applied Step, Sorted Rows, and notice the data has changed regarding Arizona's new ranking.
This is because we inserted the Replaced Value step in the right place, before the Added Custom step.
Okay that was a little involved, but it was a good example of how powerful and versatile Power Query Editor can
be.
Lastly, we want to change the name of that table to something descriptive. When we get to creating reports, it’s
especially useful to have descriptive table names, especially when we connect to multiple data sources, and they’re
all listed in the Fields pane of the Report view.
Changing the table name is easy: in the Query Settings pane, under Properties, simply type in the new name of
the table, as shown in the following image, and hit Enter. Let’s call this table RetirementStats.
Okay, we’ve shaped that data to the extent we need to. Next let’s connect to another data source, and combine
data.
Combine data
That data about various states is interesting, and will be useful for building additional analysis efforts and queries.
But there’s one problem: most data out there uses a two-letter abbreviation for state codes, not the full name of
the state. We need some way to associate state names with their abbreviations.
We’re in luck: there’s another public data source that does just that, but it needs a fair amount of shaping before
we can connect it to our retirement table. Here’s the Web resource for state abbreviations:
https://en.wikipedia.org/wiki/List_of_U.S._state_abbreviations
From the Home ribbon in Power Query Editor, we select New Source > Web and type the address, select
Connect, and the Navigator shows what it found on that Web page.
We select Codes and abbreviations... because that includes the data we want, but it’s going to take quite a bit of
shaping to pare that table’s data down to what we want.
TIP
Is there a faster or easier way to accomplish the steps below? Yes, we could create a relationship between the two tables,
and shape the data based on that relationship. The following steps are still good to learn for working with tables, just know
that relationships can help you quickly use data from multiple tables.
NOTE
If Power BI accidentally imports the table headers as a row in your data table, you can select Use First Row As Headers
from the Home tab, or from the Transform tab in the ribbon, to fix your table.
Remove the bottom 26 rows – they’re all the territories, which we don’t need to include. From the Home
ribbon, select Reduce Rows > Remove Rows > Remove Bottom Rows.
Since the RetirementStats table doesn't have information for Washington DC, we need to filter it from our list.
Select the drop-down arrow beside the Region Status column, then clear the checkbox beside Federal district.
Remove a few unneeded columns – we only need the mapping of state to its official two-letter abbreviation, so
we can remove the following columns: Column1, Column3, Column4, and then Column6 through
Column11. First select Column1, then hold down the CTRL key and select the other columns to be removed
(this lets you select multiple, non-contiguous columns). From the Home tab on the ribbon, select Remove
Columns > Remove Columns.
NOTE
This is a good time to point out that the sequence of applied steps in Power Query Editor is important, and can affect how
the data is shaped. It’s also important to consider how one step may impact another subsequent step; if you remove a step
from the Applied Steps, subsequent steps may not behave as originally intended, because of the impact of the query’s
sequence of steps.
NOTE
When you resize the Power Query Editor window to make the width smaller, some ribbon items are condensed to make the
best use of visible space. When you increase the width of the Power Query Editor window, the ribbon items expand to make
the most use of the increased ribbon area.
Rename the columns, and the table itself – as usual, there are a few ways to rename a column; first select the
column, then either select Rename from the Transform tab on the ribbon, or right-click and select Rename…
from the menu that appears. The following image has arrows pointing to both options; you only need to choose
one.
Let’s rename them to State Name and State Code. To rename the table, just type the name into the Name box in
the Query Settings pane. Let’s call this table StateCodes.
Now that we’ve shaped the StateCodes table the way we want, let’s combine these two tables, or queries, into one;
since the tables we now have are a result of the queries we applied to the data, they’re often referred to as queries.
There are two primary ways of combining queries – merging and appending.
When you have one or more columns that you’d like to add to another query, you merge the queries. When you
have additional rows of data that you’d like to add to an existing query, you append the query.
In this case, we want to merge queries. To get started, from the left pane of Power Query Editor we select the
query into which we want the other query to merge, which in this case is RetirementStats. Then select Combine >
Merge Queries from the Home tab on the ribbon.
You may be prompted to set the privacy levels, to ensure the data is combined without including or transferring
data you didn't want transferred.
Next the Merge window appears, prompting us to select which table we’d like merged into the selected table, and
then, the matching columns to use for the merge. Select State from the RetirementStats table (query), then select
the StateCodes query (easy in this case, since there’s only one other query – when you connect to many data
sources, there are many queries to choose from). When we select the correct matching columns – State from
RetirementStats, and State Name from StateCodes – the Merge window looks like the following, and the OK
button is enabled.
A NewColumn is created at the end of the query, which is the contents of the table (query) that was merged with
the existing query. All columns from the merged query are condensed into the NewColumn, but you can select to
Expand the table, and include whichever columns you want.
To Expand the merged table, and select which columns to include, select the expand icon ( ). The Expand window
appears.
In this case, we only want the State Code column, so we select only that column and then select OK. We clear the
checkbox from Use original column name as prefix because we don’t need or want that; if we leave that selected,
the merged column would be named NewColumn.State Code (the original column name, or NewColumn, then
a dot, then the name of the column being brought into the query).
NOTE
Want to play around with how to bring in that NewColumn table? You can experiment a bit, and if you don’t like the results,
just delete that step from the Applied Steps list in the Query Settings pane; your query returns to the state prior to
applying that Expand step. It’s like a free do-over, which you can do as many times as you like until the expand process
looks the way you want it.
We now have a single query (table) that combined two data sources, each of which has been shaped to meet our
needs. This query can serve as a basis for lots of additional, interesting data connections – such as housing cost
statistics, demographics, or job opportunities in any state.
To apply changes and close Power Query Editor, select Close & Apply from the Home ribbon tab. The
transformed dataset appears in Power BI Desktop, ready to be used for creating reports.
Next steps
There are all sorts of things you can do with Power Query. If you're ready to create your own custom connector,
check the following article.
Creating your first connector: Hello World
TripPin Tutorial
7/2/2019 • 2 minutes to read
This multi-part tutorial covers the creation of a new data source extension for Power Query. The tutorial is meant to
be done sequentially - each lesson builds on the connector created in previous lessons, incrementally adding new
capabilities to your connector.
This tutorial uses a public OData service ( TripPin) as a reference source. Although this lesson requires the use of
the M engine's OData functions, subsequent lessons will use Web.Contents, making it applicable to (most) REST
APIs.
Prerequisites
The following applications will be used throughout this tutorial:
Power BI Desktop, May 2017 release or later
Power Query SDK for Visual Studio
Fiddler - Optional, but recommended for viewing and debugging requests to your REST service
It's strongly suggested that you review:
Installing the PowerQuery SDK
Starting to Develop Custom Connectors
Creating your first connector: Hello World
Handling Data Access
Handling Authentication
Parts
PART LESSON DETAILS
This multi-part tutorial covers the creation of a new data source extension for Power Query. The tutorial is meant to
be done sequentially – each lesson builds on the connector created in previous lessons, incrementally adding new
capabilities to your connector.
In this lesson, you will:
Create a new Data Connector project using the Visual Studio SDK
Author a base function to pull data from a source
Test your connector in Visual Studio
Register your connector in Power BI Desktop
Open the TripPin.pq file and paste in the following connector definition. It contains:
A Data Source definition record for the TripPin connector
A declaration that Implicit (Anonymous) is the only authentication type for this source
A function (TripPinImpl) with an implementation that calls OData.Feed
A shared function (TripPin.Feed) that sets the parameter type to Uri.Type
A Data Source publishing record that will allow the connector to appear in the Power BI Get Data dialog
section TripPin;
[DataSource.Kind="TripPin", Publish="TripPin.Publish"]
shared TripPin.Feed = Value.ReplaceType(TripPinImpl, type function (url as Uri.Type) as any);
Open the TripPin.query.pq file. Replace the current contents with a call to your exported function and.
TripPin.Feed("https://services.odata.org/v4/TripPinService/")
The <project>.query.pq file is used to test out your extension without having to deploy it to your Power BI
Desktop's bin folder. Clicking the Start button (or pressing F5) will automatically compile your extension and
launch the M Query utility.
Running your query for the first time will result in a credential error. In Power Query, the hosting application would
convert this error into a credential prompt. In Visual Studio, you will receive a similar prompt that calls out which
data source is missing credentials and its data source path. Select shortest of the data source paths
(https://services.odata.org/) – this will apply your credential to all URLs under this path. Select the Anonymous
credential type, and click Set Credential.
Click OK to close the dialog, and then press the Start button once again. You see a query execution status dialog,
and finally a Query Result table showing the data returned from your query.
You can try out a few different OData URLs in the test file to see what how different results are returned. For
example:
1. https://services.odata.org/v4/TripPinService/Me
2. https://services.odata.org/v4/TripPinService/GetPersonWithMostFriends()
3. https://services.odata.org/v4/TripPinService/People
The TripPin.query.pq file can contain single statements, let statements, or full section documents.
let
Source = TripPin.Feed("https://services.odata.org/v4/TripPinService/"),
People = Source{[Name="People"]}[Data],
SelectColumns = Table.SelectColumns(People, {"UserName", "FirstName", "LastName"})
in
SelectColumns
Open Fiddler to capture HTTP traffic, and run the query. You should see a few different requires to
services.odata.org, generated by the mashup container process. You can see that accessing the root URL of the
service results in a 302 status and a redirect to the longer version of the URL. Following redirects is another
behavior you get “for free” from the base library functions. One thing to note if you look at the URLs is that you can
see the query folding that happened with the SelectColumns statement.
https://services.odata.org/v4/TripPinService/People?$select=UserName%2CFirstName%2CLastName
If you add more transformations to your query, you will see how they impact the generated URL.
This behavior is important to note. Even though you did not implement explicit folding logic, your connector
inherits these capabilities from the OData.Feed function. M statements are compose-able – filter contexts will
flow from one function to another, whenever possible. This is similar in concept to the way data source
functions used within your connector inherit its authentication context and credentials. In later lessons, we will
replace the use of OData.Feed, which has native folding capabilities, with Web.Contents, which does not. To get
the same level of capabilities, we will need to use the Table.View interface and implement our own explicit
folding logic.
Double click on the function name and the function invocation dialog will appear. Enter the root URL of the service
(https://services.odata.org/v4/TripPinService/), and click OK.
Since this is the first time you are accessing this data source, you will receive a prompt for credentials. Check that
the shortest URL is selected, and click Connect.
Notice that instead of getting a simple table of data, the navigator appears. This is because the OData.Feed function
returns a table with special metadata on top of it that the Power Query experience knows to display as a navigation
table. We will cover how you can create and customize your own navigation table in a future lesson.
Select the Me table, and click Edit. Notice that the columns already have types assigned (well, most of them). This is
another feature of the underlying OData.Feed function. If you watch the requests in Fiddler, you will see that we
fetch the service's $metadata document. The engine's OData implementation does this automatically to determine
the service's schema, data types, and relationships.
Conclusion
This lesson walked you through the creation of a simple connector based on the OData.Feed library function. As
you saw, very little logic is needed to enable a fully functional connector over the OData base function. Other
extensibility enabled functions, such as ODBC.DataSource, provide similar capabilities. In the next lesson, we will
replace the use of OData.Feed with a less capable function - Web.Contents. Each lesson will implement more
connector features, including paging, metadata/schema detection and query folding to the OData query syntax,
until your custom connector supports the same range of capabilities as OData.Feed.
TripPin Part 2 - Data Connector for a REST Service
7/2/2019 • 7 minutes to read
This multi-part tutorial covers the creation of a new data source extension for Power Query. The tutorial is meant to
be done sequentially – each lesson builds on the connector created in previous lessons, incrementally adding new
capabilities to your connector.
In this lesson, you will:
Create a base function that calls out to a REST API using Web.Contents
Learn how to set request headers and process a JSON response
Use Power BI Desktop to wrangle the response into a user friend format
This lesson converts the OData based connector for the TripPin service (created in the previous lesson) to a
connector that resembles something you'd create for any RESTful API. OData is a RESTful API, but one with a fixed
set of conventions. The advantage of OData is that it provides a schema, data retrieval protocol, and standard query
language. Taking away the use of OData.Feed will require us to build these capabilities into the connector ourselves.
TripPin.Feed("https://services.odata.org/v4/TripPinService/Me")
M evaluation is mostly lazy. In most cases, data values will only be retrieved/pulled when they are needed.
There are scenarios (like the /Me/BestFriend case) where a value is pulled eagerly. This tends to occur when
type information is needed for a member, and the engine has no other way to determine the type than to
retrieve the value and inspect it. Making things lazy (i.e. avoiding eager pulls) is one of the key aspects to
making an M connector performant.
Note the request headers that were sent along with the requests, and the JSON format of the response of the /Me
request.
{
"@odata.context": "https://services.odata.org/v4/TripPinService/$metadata#Me",
"UserName": "aprilcline",
"FirstName": "April",
"LastName": "Cline",
"MiddleName": null,
"Gender": "Female",
"Age": null,
"Emails": [ "[email protected]", "[email protected]" ],
"FavoriteFeature": "Feature1",
"Features": [ ],
"AddressInfo": [
{
"Address": "P.O. Box 555",
"City": {
"Name": "Lander",
"CountryRegion": "United States",
"Region": "WY"
}
}
],
"HomeAddress": null
}
When the query finishes evaluating, the M Query Output window should show the Record value for the Me
singleton.
If you compare the fields in the output window with the fields returned in the raw JSON response, you'll notice a
mismatch; the query result has additional fields ( Friends , Trips , GetFriendsTrips ) that don't appear anywhere in
the JSON response. The OData.Feed function automatically appended these fields to the record based on the
schema returned by $metadata. This is a good example of how a connector might augment and/or reformat the
response from the service to provide a better user experience.
DefaultRequestHeaders = [
#"Accept" = "application/json;odata.metadata=minimal", // column name and values only
#"OData-MaxVersion" = "4.0" // we only support v4
];
We'll change our implementation of our TripPin.Feed function so that rather than using OData.Feed, it uses
Web.Contents to make a web request, and parses the result as a JSON document.
We can now test this out in Visual Studio using the query file. The result of the /Me record now resembles the raw
JSON that we saw in the Fiddler request.
If you watch Fiddler when running the new function, you'll also notice that the evaluation now makes a single web
request, rather than three. Congratulations - you've achieved a 300% performance increase! Of course, we've now
lost all the type and schema information, but we won't focus on that part just yet.
Update your query to access some of the TripPin Entities/Tables, such as:
https://services.odata.org/v4/TripPinService/Airlines
https://services.odata.org/v4/TripPinService/Airports
https://services.odata.org/v4/TripPinService/Me/Trips
You'll notice that the paths that used to return nicely formatted tables now return a top level "value" field with an
embedded [List]. We'll need to do some transformations on the result to make it usable for BI scenarios.
let
Source = TripPin.Feed("https://services.odata.org/v4/TripPinService/Airlines"),
value = Source[value],
toTable = Table.FromList(value, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
expand = Table.ExpandRecordColumn(toTable, "Column1", {"AirlineCode", "Name"}, {"AirlineCode", "Name"})
in
expand
let
Source = TripPin.Feed("https://services.odata.org/v4/TripPinService/Airports"),
value = Source[value],
#"Converted to Table" = Table.FromList(value, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#"Expanded Column1" = Table.ExpandRecordColumn(#"Converted to Table", "Column1", {"Name", "IcaoCode",
"IataCode", "Location"}, {"Name", "IcaoCode", "IataCode", "Location"}),
#"Expanded Location" = Table.ExpandRecordColumn(#"Expanded Column1", "Location", {"Address", "Loc",
"City"}, {"Address", "Loc", "City"}),
#"Expanded City" = Table.ExpandRecordColumn(#"Expanded Location", "City", {"Name", "CountryRegion",
"Region"}, {"Name.1", "CountryRegion", "Region"}),
#"Renamed Columns" = Table.RenameColumns(#"Expanded City",{{"Name.1", "City"}}),
#"Expanded Loc" = Table.ExpandRecordColumn(#"Renamed Columns", "Loc", {"coordinates"}, {"coordinates"}),
#"Added Custom" = Table.AddColumn(#"Expanded Loc", "Latitude", each [coordinates]{1}),
#"Added Custom1" = Table.AddColumn(#"Added Custom", "Longitude", each [coordinates]{0}),
#"Removed Columns" = Table.RemoveColumns(#"Added Custom1",{"coordinates"}),
#"Changed Type" = Table.TransformColumnTypes(#"Removed Columns",{{"Name", type text}, {"IcaoCode", type
text}, {"IataCode", type text}, {"Address", type text}, {"City", type text}, {"CountryRegion", type text},
{"Region", type text}, {"Latitude", type number}, {"Longitude", type number}})
in
#"Changed Type"
You can repeat this process for additional paths under the service. Once you're ready, move onto the next step of
creating a (mock) navigation table.
If you have not set your Privacy Levels setting to "Always ignore Privacy level settings" (aka "Fast Combine") you
will see a privacy prompt.
Privacy prompts when you are combining data from multiple sources and have not yet specified a privacy level for
the source(s). Click on the Continue button and set the privacy level of the top source to Public.
Click Save and your table will appear. While this isn't a navigation table yet, it provides the basic functionality we
need to turn it into one in a subsequent lesson.
Data combination checks do not occur when accessing multiple data sources from within an extension. Since all
data source calls made from within the extension inherit the same authorization context, it is assumed they are
"safe" to combine. Your extension will always be treated as a single data source when it comes to data
combination rules. Users would still receive the regular privacy prompts when combining your source with
other M sources.
If you run Fiddler and click the Refresh Preview button in the Query Editor, you'll notice separate web requests for
each item in your navigation table. This indicates that an eager evaluation is occurring, which isn't ideal when
building navigation tables with a lot of elements. Subsequent lessons will show how to build a proper navigation
table that supports lazy evaluation.
Conclusion
This lesson showed you how to build a simple connector for a REST service. In this case, we turned an existing
OData extension into a standard REST extension (via Web.Contents), but the same concepts apply if you were
creating a new extension from scratch.
In the next lesson, we will take the queries created in this lesson using Power BI Desktop and turn them into a true
navigation table within the extension.
TripPin Part 3 - Navigation Tables
7/2/2019 • 4 minutes to read
This multi-part tutorial covers the creation of a new data source extension for Power Query. The tutorial is meant to
be done sequentially – each lesson builds on the connector created in previous lessons, incrementally adding new
capabilities to your connector.
In this lesson, you will:
Create a navigation table for a fixed set of queries
Test the navigation table in Power BI Desktop
This lesson adds a navigation table to the TripPin connector created in the previous lesson. When our connector
used the OData.Feed function (Part 1), we received the navigation table “for free”, as derived from the OData
service’s $metadata document. When we moved to the Web.Contents function (Part 2), we lost the built-in
navigation table. In this lesson, we will take a set of fixed queries we created in Power BI Desktop and add the
appropriate metadata for Power Query to popup the Navigator dialog for our data source function.
Please see the Navigation Table documentation for more information about using nav tables.
Next we will import the mock navigation table query we wrote that creates a fixed table linking to these data set
queries. Let's call it TripPinNavTable :
Finally we declare a new shared function, TripPin.Contents , that will be used as our main data source function.
We'll also remove the Publish value from TripPin.Feed so that it no longer shows up in the Get Data dialog.
[DataSource.Kind="TripPin"]
shared TripPin.Feed = Value.ReplaceType(TripPinImpl, type function (url as Uri.Type) as any);
[DataSource.Kind="TripPin", Publish="TripPin.Publish"]
shared TripPin.Contents = Value.ReplaceType(TripPinNavTable, type function (url as Uri.Type) as any);
Note: Your extension can mark multiple functions as shared , with or without associating them with a
DataSource.Kind . However, when you associate a function with a specific DataSource.Kind, each function must
have the same set of required parameters, with the same name and type. This is because the data source
function parameters are combined to make a 'key' used for looking up cached credentials.
We can test our TripPin.Contents function using our TripPin.query.pq file. Running the following test query will
give us a credential prompt, and a simple table output.
TripPin.Contents("https://services.odata.org/v4/TripPinService/")
Table.ToNavigationTable = (
table as table,
keyColumns as list,
nameColumn as text,
dataColumn as text,
itemKindColumn as text,
itemNameColumn as text,
isLeafColumn as text
) as table =>
let
tableType = Value.Type(table),
newTableType = Type.AddTableKey(tableType, keyColumns, true) meta
[
NavigationTable.NameColumn = nameColumn,
NavigationTable.DataColumn = dataColumn,
NavigationTable.ItemKindColumn = itemKindColumn,
Preview.DelayColumn = itemNameColumn,
NavigationTable.IsLeafColumn = isLeafColumn
],
navigationTable = Value.ReplaceType(table, newTableType)
in
navigationTable;
After copying this into our extension file, we will update our TripPinNavTable function to add the navigation table
fields.
TripPinNavTable = (url as text) as table =>
let
source = #table({"Name", "Data", "ItemKind", "ItemName", "IsLeaf"}, {
{ "Airlines", GetAirlinesTable(url), "Table", "Table", true },
{ "Airports", GetAirportsTable(url), "Table", "Table", true }
}),
navTable = Table.ToNavigationTable(source, {"Name"}, "Name", "Data", "ItemKind", "ItemName", "IsLeaf")
in
navTable;
Running our test query again will give us a similar result as last time - with a few more columns added.
Note: You will not see the Navigator window appear in Visual Studio. The M Query Output window will always
display the underlying table.
If we copy our extension over to our Power BI Desktop custom connector and invoke the new function from the
Get Data dialog, we will see our navigator appear.
If you right click on the root of the navigation tree and click Edit, you will see the same table as you did within
Visual Studio.
Conclusion
In this tutorial, we added a Navigation Table to our extension. Navigation Tables are a key feature that make
connectors easier to use. In this example our navigation table only has a single level, but the Power Query UI
supports displaying navigation tables that have multiple dimensions (even when they are ragged).
TripPin Part 4 - Data Source Paths
7/2/2019 • 6 minutes to read
This multi-part tutorial covers the creation of a new data source extension for Power Query. The tutorial is meant to
be done sequentially – each lesson builds on the connector created in previous lessons, incrementally adding new
capabilities to your connector.
In this lesson, you will:
Simplify the connection logic for our connector
Improve the navigation table experience
This lesson simplifies the connector built in the previous lesson by removing its required function parameters, and
improving the user experience by moving to a dynamically generated navigation table.
For an in-depth explanation of how credentials are identified, please see the Data Source Paths section of Handling
Authentication.
[DataSource.Kind="TripPin"]
shared TripPin.Feed = Value.ReplaceType(TripPinImpl, type function (url as Uri.Type) as any);
[DataSource.Kind="TripPin", Publish="TripPin.Publish"]
shared TripPin.Contents = Value.ReplaceType(TripPinNavTable, type function (url as Uri.Type) as any);
The first time we run a query that one of the functions, we receive a credential prompt with drop downs that let us
select a path and an authentication type.
If we run the same query again, with the same parameters, the M engine is able to locate the cached credentials,
and no credential prompt is shown. If we modify the url argument to our function so that the base path no longer
matches, a new credential prompt is displayed for the new path.
You can see any cached credentials on the Credentials table in the M Query Output window.
Depending on the type of change, modifying the parameters for your function will likely result in a credential error.
BaseUrl = "https://services.odata.org/v4/TripPinService/";
[DataSource.Kind="TripPin", Publish="TripPin.Publish"]
shared TripPin.Contents = () => TripPinNavTable(BaseUrl) as table;
We will keep the TripPin.Feed function, but no longer make it shared, no longer associate it with a Data Source
Kind, and simplify its declaration. From this point on, we will only use it internally within this section document.
If we update the TripPin.Contents() call in our TripPin.query.pq file and run it in Visual Studio, we will see a new
credential prompt. Note that there is now a single Data Source Path value - TripPin.
Improving the Navigation Table
In the first tutorial we used the built-in OData functions to connect to the TripPin service. This gave us a really nice
looking navigation table, based on the TripPin service document, with no additional code on our side. The
OData.Feed function automatically did the hard work for us. Since we are "roughing it" by using Web.Contents
rather than OData.Feed, we will need to recreate this navigation table ourselves.
To simplify the example, we'll only expose the three entity sets (Airlines, Airports, People), which would be
exposed as Tables in M, and skip the singleton (Me) which would be exposed as a Record. We will skip adding
the functions until a later lesson.
RootEntities = {
"Airlines",
"Airports",
"People"
};
We then update our TripPinNavTable function to build the table a column at a time. The [Data] column for each
entity is retrieved by calling TripPin.Feed with the full URL to the entity.
When dynamically building URL paths, make sure you're clear where your forward slashes (/) are! Note that
Uri.Combine uses the following rules when combining paths:
1. When the relativeUri parameter starts with a /, it will replace the entire path of the baseUri parameter
2. If the relativeUri parameter does not start with a / and the baseUri ends with a /, the path is appended
3. If the relativeUri parameter does not start with a / and the baseUri does not end with a /, the last segment of
the path is replaced
The following image shows examples of this:
Note: A disadvantage of using a generic approach to process your entities is that we lose the nice formating
and type information for our entities. We will show how to enforce schema on REST API calls in a later lesson.
Conclusion
In this tutorial, we cleaned up and simplified our connector by fixing our Data Source Path value, and moving to a
more flexible format for our navigation table. After completing these steps (or using the sample code in this
directory), the TripPin.Contents function returns a navigation table in Power BI Desktop:
TripPin Part 5 - Paging
7/2/2019 • 7 minutes to read
This multi-part tutorial covers the creation of a new data source extension for Power Query. The tutorial is meant to
be done sequentially – each lesson builds on the connector created in previous lessons, incrementally adding new
capabilities to your connector.
In this lesson, you will:
Add paging support to the connector
Many Rest APIs will return data in "pages", requiring clients to make multiple requests to stitch the results together.
Although there are some common conventions for pagination (such as RFC 5988), it generally varies from API to
API. Thankfully, TripPin is an OData service, and the OData standard defines a way of doing pagination using
odata.nextLink values returned in the body of the response.
To simplify previous iterations of the connector, the TripPin.Feed function was not 'page aware'. It simply parsed
whatever JSON was returned from the request, and formatted it as a table. Those familiar with the OData protocol
might have noticed that we made a number of incorrect assumptions on the format of the response (such as
assuming there is a value field containing an array of records). In this lesson we will improve our response
handling logic by making it page aware. Future tutorials will make the page handling logic more robust and able to
handle multiple response formats (including errors from the service).
Note: You do not need to implement your own paging logic with connectors based on OData.Feed, as it
handles it all for you automatically.
Paging Checklist
When implementing paging support, we'll need to the following things about our API:
1. How do we request the next page of data?
2. Does the paging mechanism involve calculating values, or do we extract the URL for the next page from the
response?
3. How do we know when to stop paging?
4. Are there parameters related to paging that we should be aware of? (such as "page size")
The answer to these questions will impact the way you implement your paging logic. While there is some amount
of code reuse across paging implementations (such as the use of Table.GenerateByPage, most connectors will end
up requiring custom logic.
Note: This lesson contains paging logic for an OData service, which follows a specific format. Please check the
documentation for your API to determine the changes you'll need to make in your connector to support its
paging format.
Some OData services allow clients to supply a max page size preference, but it is up to the service on whether or
not to honor it. Power Query should be able to handle responses of any size, so we won't worry about specifying a
page size preference - we can support whatever the service throws at us.
More information about Server-Driven Paging can be found in the OData specification
Testing TripPin
Before we fix our paging implementation, let's confirm the current behavior of the extension from the previous
tutorial. The following test query will retrieve the People table and add an index column to show our current row
count.
let
source = TripPin.Contents(),
data = source{[Name="People"]}[Data],
withRowCount = Table.AddIndexColumn(data, "Index")
in
withRowCount
Turn on fiddler, and run the query in Visual Studio. You'll notice that the query returns a table with 8 rows (index 0
to 7).
If we look at the body of the response from fiddler, we see that it does in fact contain an @odata.nextLink field,
indicating that there are more pages of data available.
{
"@odata.context": "https://services.odata.org/V4/TripPinService/$metadata#People",
"@odata.nextLink": "https://services.odata.org/v4/TripPinService/People?%24skiptoken=8",
"value": [
{ },
{ },
{ }
]
}
Implementing Paging for TripPin
We're going to make the following changes to our extension:
1. Import the common Table.GenerateByPage function
2. Add a GetAllPagesByNextLink function which uses Table.GenerateByPage to glue all pages together
3. Add a GetPage function that can read a single page of data
4. Add a GetNextLink function to extract the next URL from the response
5. Update TripPin.Feed to use the new page reader functions
Note: As stated earlier in this tutorial, paging logic will vary between data sources. The implementation here
tries to break up the logic into functions that should be reusable for sources that use 'next links' returned in the
response.
Table.GenerateByPage
The Table.GenerateByPage function can be used to efficiently combine multiple 'pages' of data into a single table. It
does this by repeatedly calling the function passed in as the getNextPage parameter, until it receives a null . The
function parameter must take a single argument, and return a nullable table .
getNextPage = (lastPage) as nullable table => ...
Each call to getNextPage receives the output from the previous call.
// The getNextPage function takes a single argument and is expected to return a nullable table
Table.GenerateByPage = (getNextPage as function) as table =>
let
listOfPages = List.Generate(
() => getNextPage(null), // get the first page of data
(lastPage) => lastPage <> null, // stop when the function returns null
(lastPage) => getNextPage(lastPage) // pass the previous page to the next function call
),
// concatenate the pages together
tableOfPages = Table.FromList(listOfPages, Splitter.SplitByNothing(), {"Column1"}),
firstRow = tableOfPages{0}?
in
// if we didn't get back any pages of data, return an empty table
// otherwise set the table type based on the columns of the first page
if (firstRow = null) then
Table.FromRows({})
else
Value.ReplaceType(
Table.ExpandTableColumn(tableOfPages, "Column1", Table.ColumnNames(firstRow[Column1])),
Value.Type(firstRow[Column1])
);
Implementing GetAllPagesByNextLink
The body of our GetAllPagesByNextLink function implements the getNextPage function argument for
Table.GenerateByPage . It will call the GetPage function, and retrieve the URL for the next page of data from the
NextLink field of the meta record from the previous call.
Implementing GetPage
Our GetPage function will use Web.Contents to retrieve a single page of data from the TripPin service, and
converts the response into a table. It passes the response from Web.Contents to the GetNextLink function to
extract the URL of the next page, and sets it on the meta record of the returned table (page of data).
This implementation is a slightly modified version of the TripPin.Feed call from the previous tutorials.
Implementing GetNextLink
Our GetNextLink function simply checks the body of the response for an @odata.nextLink field, and returns its
value.
// In this implementation, 'response' will be the parsed body of the response after the call to Json.Document.
// We look for the '@odata.nextLink' field and simply return null if it doesn't exist.
GetNextLink = (response) as nullable text => Record.FieldOrDefault(response, "@odata.nextLink");
If we re-run the same test query from earlier in the tutorial, we should now see the page reader in action. We
should now see we have 20 rows in the response rather than 8.
If we look at the requests in fiddler, we should now see separate requests for each page of data.
Note: You'll notice duplicate requests for the first page of data from the service, which is not ideal. The extra
request is a result of the M engine's schema checking behavior. We will ignore this issue for now and resolve it
in the next tutorial where we will apply an explict schema.
Conclusion
This lesson showed you how to implement pagination support for a Rest API. While the logic will likely vary
between APIs, the pattern established here should be reusable with minor modifications.
In the next lesson, we will look at how to apply an explicit schema to our data, going beyond the simple text and
number data types we get from Json.Document .
TripPin Part 6 - Schema
7/2/2019 • 11 minutes to read
This multi-part tutorial covers the creation of a new data source extension for Power Query. The tutorial is meant to
be done sequentially – each lesson builds on the connector created in previous lessons, incrementally adding new
capabilities to your connector.
In this lesson, you will:
Define a fixed schema for a Rest API
Dynamically set data types for columns
Enforce a table structure to avoid transformation errors due to missing columns
Hide columns from the result set
One of the big advantages of an OData service over a standard REST API is its $metadata definition. The
$metadata document describes the data found on this service, including the schema for all of its Entities (Tables)
and Fields (Columns). The OData.Feed function uses this schema definition to automatically set data type
information - so instead of getting all text and number fields (like you would from Json.Document), end users will
dates, whole numbers, times, etc., providing a better overall user experience.
Many REST APIs do not have a way to programmatically determine their schema. In these cases, you'll need to
include schema definitions within your connector. In this lesson we'll define a simple, hardcoded schema for each of
our tables, and enforce the schema on the data we read from the service.
Note: The approach described here should work for many REST services. Future lessons will build upon this
approach by recursively enforcing schemas on structured columns (record, list, table), and provide sample
implementations which can programmatically generate a schema table from CSDL or JSON Schema
documents.
Overall, enforcing a schema on the data returned by your connector has multiple benefits, such as:
1. Setting the correct data types
2. Removing columns that do not need to be shown to end users (such as internal IDs or state information)
3. Ensuring that each page of data has the same shape by adding any columns that might be missing from a
response (a common way for REST APIs to indicate a field should be null)
let
source = TripPin.Contents(),
data = source{[Name="Airlines"]}[Data]
in
data
The "@odata.*" columns are part of OData protocol, and not something we'd want or need to show to the end
users of our connector. AirlineCode and Name are the two columns we want to keep. If we look at the schema of
the table (using the handy Table.Schema function), we can see that all of the columns in the table have a data type
of Any.Type .
let
source = TripPin.Contents(),
data = source{[Name="Airlines"]}[Data]
in
Table.Schema(data)
Table.Schema returns a lot of metadata about the columns in a table, including names, positions, type information,
and many advanced properties, such as Precision, Scale, and MaxLength. Future lessons will provide design
patterns for setting these advanced properties, but for now we'll only concern ourselves with the ascribed type (
TypeName ), primitive type ( Kind ), and whether the column value might be null ( IsNullable ).
COLUMN DETAILS
Name The name of the column. This must match the name in the
results returned by the service.
Type The M data type we are going to set. This can be a primitive
type (text, number, datetime, etc), or an ascribed type
(Int64.Type, Currency.Type, etc).
The hardcoded schema table for the Airlines table will set its AirlineCode and Name columns to text , and looks
like this:
The Airports table has four fields we want to keep (including one of type record ):
Finally, the People table has seven fields, including lists ( Emails , AddressInfo ), a nullable column ( Gender ), and a
column with an ascribed type ( Concurrency ).
Note: The last step to set the table type will remove the need for the Power Query UI to infer type information
when viewing the results in the query editor. This removes the double request issue we saw at the end of the
previous tutorial.
The following helper code can be copy and pasted into your extension:
EnforceSchema.Strict = 1; // Add any missing columns, remove extra columns, set table type
EnforceSchema.IgnoreExtraColumns = 2; // Add missing columns, do not remove extra columns
EnforceSchema.IgnoreMissingColumns = 3; // Do not add or remove columns
SchemaTransformTable = (table as table, schema as table, optional enforceSchema as number) as table =>
let
// Default to EnforceSchema.Strict
_enforceSchema = if (enforceSchema <> null) then enforceSchema else EnforceSchema.Strict,
We'll also update all of the calls to these functions to make sure that we pass the schema through correctly.
Enforcing the schema
The actual schema enforcement will be done in our GetPage function.
We'll then update our TripPinNavTable function to call GetEntity , rather than making all of the calls inline. The
main advantage to this is that it will let us continue modifying our entity building code, without having to touch our
nav table logic.
let
source = TripPin.Contents(),
data = source{[Name="Airlines"]}[Data]
in
Table.Schema(data)
We now see that our Airlines table only has the two columns we defined in its schema:
If we run the same code against the People table...
let
source = TripPin.Contents(),
data = source{[Name="People"]}[Data]
in
Table.Schema(data)
We see that the ascribed type we used ( Int64.Type ) was also set correctly.
An important thing to note is that this implementation of SchemaTransformTable doesn't modify the types of list
and record columns, but the Emails and AddressInfo columns are still typed as list . This is because
Json.Document will correctly map json arrays to M lists, and json objects to M records. If you were to expand the
list or record column in Power Query, you'd see that all of the expanded columns will be of type any. Future
tutorials will improve the implementation to recursively set type information for nested complex types.
Conclusion
This tutorial provided a sample implementation for enforcing a schema on json data returned from a REST service.
While this sample uses a simple hardcoded schema table format, the approach could be expanded upon by
dynamically building a schema table definition from another source, such as json schema file, or metadata
service/endpoint exposed by the data source.
In addition to modifying column types (and values), our code is also setting the correct type information on the
table itself. Setting this type information benefits performance when running inside of Power Query, as the user
experience always attempts to infer type information to display the right UI queues to the end user, and the
inference calls can end up triggering additional calls to the underlying data APIs.
If you view the People table using the TripPin connector from the previous lesson, you'll see that all of the columns
have a 'type any' icon (even the columns that contain lists):
Running the same query with the TripPin connector from this lesson, we now see that the type information is
displayed correctly.
TripPin Part 7 - Advanced Schema with M Types
7/2/2019 • 7 minutes to read
This multi-part tutorial covers the creation of a new data source extension for Power Query. The tutorial is meant to
be done sequentially-- each lesson builds on the connector created in previous lessons, incrementally adding new
capabilities to your connector.
In this lesson, you will:
Enforce a table schema using M Types
Set types for nested records and lists
Refactor code for reuse and unit testing
In the previous lesson we defined our table schemas using a simple "Schema Table" system. This schema table
approach works for many REST APIs/Data Connectors, but services that return complete or deeply nested data
sets might benefit from the approach in this tutorial, which leverages the M type system.
This lesson will guide you through the following steps:
1. Adding unit tests
2. Defining custom M types
3. Enforcing a schema using types
4. Refactoring common code into separate files
shared TripPin.UnitTest =
[
// Put any common variables here if you only want them to be evaluated once
RootTable = TripPin.Contents(),
Airlines = RootTable{[Name="Airlines"]}[Data],
Airports = RootTable{[Name="Airports"]}[Data],
People = RootTable{[Name="People"]}[Data],
report = Facts.Summarize(facts)
][report];
Clicking run on the project will evaluate all of the Facts, and give us a report output that looks like this:
Using some principles from test-driven development, we'll add a test that currently fails, but will soon
implement/fix (by the end of this tutorial). Specifically, we'll add a test that checks one of the nested records
(Emails) we get back in the People entity.
If we run the code again, we should now see that we have a failing test.
Now we just need to implement the functionality to make this work.
A type value is a value that classifies other values. A value that is classified by a type is said to conform to
that type. The M type system consists of the following kinds of types:
Primitive types, which classify primitive values ( binary , date , datetime , datetimezone , duration , list ,
logical , null , number , record , text , time , type ) and also include a number of abstract types (
function , table , any , and none )
Record types, which classify record values based on field names and value types
List types, which classify lists using a single item base type
Function types, which classify function values based on the types of their parameters and return values
Table types, which classify table values based on column names, column types, and keys
Nullable types, which classifies the value null in addition to all the values classified by a base type
Type types, which classify values that are types
Using the raw json output we get (and/or looking up the definitions in the service's $metadata), we can define the
following record types to represent OData complex types:
LocationType = type [
Address = text,
City = CityType,
Loc = LocType
];
CityType = type [
CountryRegion = text,
Name = text,
Region = text
];
LocType = type [
#"type" = text,
coordinates = {number},
crs = CrsType
];
CrsType = type [
#"type" = text,
properties = record
];
Note how the LocationType references the CityType and LocType to represented its structured columns.
For the top level entities (that we want represented as Tables), we define table types:
We then update our SchemaTable variable (which we use as a "lookup table" for entity to type mappings) to use
these new type definitions:
The full code listing for the Table.ChangeType function can be found in the Table.ChangeType.pqm file.
Note: For flexibility, the function can be used on tables, as well as lists of records (which is how tables would be
represented in a JSON document).
We then need to update the connector code to change the schema parameter from a table to a type , and add a
call to Table.ChangeType in GetEntity .
GetPage is updated to use the list of fields from the schema (to know the names of what to expand when we get
the results), but leaves the actual schema enforcement to GetEntity .
At this point our extension almost has as much "common" code as TripPin connector code. In the future these
common functions will either be part of the built-in standard function library, or you will be able to reference them
from another extension. For now, we refactor our code in the following way:
1. Move the reusable functions to separate files (.pqm)
2. Set the Build Action property on the file to Compile to make sure it gets included in our extension file during
the build
3. Define a function to load the code using Expression.Evaluate
4. Load each of the common functions we want to use
The code to do this is included in the snippet below:
Table.ChangeType = Extension.LoadFunction("Table.ChangeType.pqm");
Table.GenerateByPage = Extension.LoadFunction("Table.GenerateByPage.pqm");
Table.ToNavigationTable = Extension.LoadFunction("Table.ToNavigationTable.pqm");
Conclusion
This tutorial made a number of improvements to the way we enforce a schema on the data we get from a REST
API. The connector is currently hard coding its schema information, which has a performance benefit at runtime,
but is unable to adapt to changes in the service's metadata overtime. Future tutorials will move to a purely dynamic
approach that will infer the schema from the service's $metadata document.
In addition to the schema changes, this tutorial added Unit Tests for our code, and refactored the common helper
functions into separate files to improve overall readability.
TripPin Part 8 - Adding Diagnostics
7/2/2019 • 8 minutes to read
This multi-part tutorial covers the creation of a new data source extension for Power Query. The tutorial is meant to
be done sequentially - each lesson builds on the connector created in previous lessons, incrementally adding new
capabilities to your connector.
In this lesson, you will:
Learn about the Diagnostics.Trace function
Use the Diagnostics helper functions to add trace information to help debug your connector
Enabling Diagnostics
Power Query users can enable trace logging by clicking the checkbox under Options | Diagnostics.
Once enabled, any subsequent queries will cause the M engine to emit trace information to log files located in a
fixed user directory.
When running M queries from within the Power Query SDK, tracing is enabled at the project level. On the project
properties page, there are three settings related to tracing:
1. Clear Log: when this is set to true , the log will be reset/cleared when you run your queries. I recommend you
keep this set to true .
2. Show Engine Traces: this setting controls the output of built-in traces from the M engine. These traces are
generally only useful to members of the Power Query team, so you'll typically want to keep this set to false .
3. Show User Traces: this setting controls trace information output by your connector. You'll want to set this to
true .
Once enabled, you'll start seeing log entries in the M Query Output window, under the Log tab.
Diagnostics.Trace
The Diagnostics.Trace function is used to write messages into the M engine's trace log.
Diagnostics.Trace = (traceLevel as number, message as text, value as any, optional delayed as nullable logical
as any) => ...
An important note: M is a functional language with lazy evaluation. When using Diagnostics.Trace , keep in
mind that the function will only be called if the expression its a part of is actually evaluated. Examples of this can
be found later in this tutorial.
The traceLevel parameter can be one of the following values (in descending order):
TraceLevel.Critical
TraceLevel.Error
TraceLevel.Warning
TraceLevel.Information
TraceLevel.Verbose
When tracing is enabled, the user can select the maximum level of messages they would like to see. All trace
messages of this level and under will be output to the log. For example, if the user selects the "Warning" level, trace
messages of TraceLevel.Warning , TraceLevel.Error , and TraceLevel.Critical would appear in the logs.
The messageparameter is the actual text that will be output to the trace file. Note that the text will not contain the
value parameter, unless you explicitly include it in the text.
The value parameter is what the function will return. When the delayed parameter is set to true , value will be
a zero parameter function which returns the actual value you are evaluating. When delayed is set to false , value
will be the actual value. An example of how this works can be found below.
Using Diagnostics.Trace in the TripPin connector
For a practical example of using Diagnostics.Trace, and the impact of the delayed parameter, update the TripPin
connector's GetSchemaForEntity function to wrap the error exception:
We can force an error during evaluation (for test purposes!) by passing an invalid entity name to the GetEntity
function. Here we change the withData line in the TripPinNavTable function, replacing [Name] with
"DoesNotExist" .
Enable tracing for your project, and run your test queries. On the Errors tab you should see the text of the error
you raised:
And on the Log tab, you should see the same message. Note that if you use different values for the message and
value parameters, these would be different.
Also note that the Action field of the log message contains the name (Data Source Kind) of your extension (in this
case, Engine/Extension/TripPin ). This makes it easier to find the messages related to your extension when there are
multiple queries involved and/or system (mashup engine) tracing is enabled.
Delayed evaluation
As an example of how the delayed parameter works, we'll make some modifications and run the queries again.
First, set the delayed value to false , but leave the value parameter as-is:
When you run the query, you'll receive an error that "We cannot convert a value of type Function to type Type", and
not the actual error you raised. This is because the call is now returning a function value, rather than the value
itself.
Next, remove the function from the value parameter:
When you run the query, you'll receive the correct error, but if you check the Log tab, there will be no messages.
This is because the error ends up being raised/evaluated during the call to Diagnostics.Trace , so the message is
never actually output.
Now that you understand the impact of the delayed parameter, be sure to reset your connector back to a
working state before proceeding.
// Diagnostics module contains multiple functions. We can take the ones we need.
Diagnostics = Extension.LoadFunction("Diagnostics.pqm");
Diagnostics.LogValue = Diagnostics[LogValue];
Diagnostics.LogFailure = Diagnostics[LogFailure];
Diagnostics.LogValue
The Diagnostics.LogValue function is a lot like Diagnostics.Trace , and can be used to output the value of what you
evaluating.
The prefix parameter is prepended to the log message. You'd use this to figure out which call output the message.
The value parameter is what the function will return, and will also be written to the trace as a text representation
of the M value. For example, if value is equal to a table with columns A and B, the log will contain the equivalent
#table representation: #table({"A", "B"}, {{"row1 A", "row1 B"}, {"row2 A", row2 B"}})
Note: Serializing M values to text can be an expensive operation. Be aware of the potential size of the values
you are outputting to the trace. Note: Most Power Query environments will truncate trace messages to a
maximum length.
As an example, we'll update the TripPin.Feed function to trace the url and schema arguments passed into the
function.
Note that we have to use the new _url and _schema values in the call to GetAllPagesByNextLink . If we used the
original function parameters, the Diagnostics.LogValue calls would never actually be evaluated, resulting in no
messages written to the trace. Functional programming is fun!
When we run our queries, we should now see new messages in the log.
Accessing url:
Schema type:
Note that we see the serialized version of the schema parameter type , rather than what you'd get when you do a
simple Text.FromValue() on a type value (which results in "type").
Diagnostics.LogFailure
The Diagnostics.LogFailure function can be used to wrap function calls, and will only write to the trace if the
function call fails (i.e. returns an error ).
Internally, Diagnostics.LogFailure adds a try operator to the function call. If the call fails, the text value is
written to the trace before returning the original error . If the function call succeeds, the result is returned without
writing anything to the trace. Since M errors don't contain a full stack trace (i.e. you typically only see the message
of the error), this can be useful when you want to pinpoint where the error was actually raised.
As a (poor) example, we'll modify the withData line of the TripPinNavTable function to force an error once again:
In the trace, we can find the resulting error message containing our text , and the original error information.
Be sure to reset your function to a working state before proceeding with the next tutorial.
Conclusion
This brief (but important!) lesson showed you how to make use of the diagnostic helper functions to log to the
Power Query trace files. When used properly, these functions are extremely useful in debugging issues within your
connector.
Note: As a connector developer, it is your responsibility to ensure that you do not log sensitive or personally
identifiable information (PII) as part of your diagnostic logging. You must also be careful to not output too
much trace information, as it can have a negative performance impact.
TripPin Part 9 - TestConnection
3/5/2019 • 4 minutes to read
This multi-part tutorial covers the creation of a new data source extension for Power Query. The tutorial is meant to
be done sequentially - each lesson builds on the connector created in previous lessons, incrementally adding new
capabilities to your connector.
In this lesson, you will:
Add a TestConnection handler
Configure the Power BI On-Premises Data Gateway (Personal mode)
Test scheduled refresh through the Power BI service
Custom Connector support was added to the April 2018 release of the Personal On-Premises Gateway. This new
(preview ) functionality allows for Scheduled Refresh of reports that make use of your custom connector.
The tutorial will cover the process of enabling your connector for refresh, and a quick walkthrough of the steps of
configuring the gateway. Specifically you will:
1. Add a TestConnection handler to your connector
2. Install the Power BI On-Premises Data Gateway in Personal mode
3. Enable Custom Connector support in the Gateway
4. Publish a workbook that uses your connector to PowerBI.com
5. Configure scheduled refresh to test your connector
Please see the Handling Gateway Support for more information on the TestConnection handler.
Background
There are three prerequisites for configuring a data source for scheduled refresh via PowerBI.com:
1. The data source is supported: This means that the target gateway environment is aware of all of the functions
contained within the query you want to refresh.
2. Credentials are provided: To present the right credential entry dialog, Power BI needs to know the support
authentication mechanism for a given data source.
3. The credentials are valid: After the user provides credentials, they are validated by calling the data source's
TestConnection handler.
The first two items are handled by registering your connector with the gateway. When the user attempts to
configure scheduled refresh in PowerBI.com, the query information is sent to your personal gateway to determine
if any data sources that aren't recognized by the Power BI service (i.e. custom ones that you created) are available
there. The third item is handled by invoking the TestConnection handler defined for your data source.
Future versions of the Power Query SDK will provide a way to validate the TestConnection handler from Visual
Studio. Currently, the only mechanism that uses TestConnection is the On-premises Data Gateway.
Download and install the Power BI On-Premises Data Gateway. When you run the installer, select the Personal
Mode.
After installation is complete, launch the gateway and sign into Power BI. The sign-in process will automatically
register your gateway with the Power BI services. Once signed in, perform the following steps:
1. Click on the Connectors tab
2. Click the switch to enable support for Custom data connectors
3. Select the directory you wish to load custom connectors from. This will usually be the same directory that you'd
use for Power BI Desktop, but the value is configurable.
4. The page should now list all extension files in your target directory
Please see the online documentation for more information about the gateway.
Note: If the dataset configuration page says that the report contains unknown data sources, your
gateway/custom connector may not be configured properly. Go to the personal gateway configuration UI and
make sure that there are no errors next to the TripPin connector. You may need to restart the gateway (on the
Service Settings page) to pick up the latest configuration.
Click on the Edit credentials link to bring up the authentication dialog, and click sign-in.
Note: If you receive an error similar to the one below ("Failed to update data source credentials"), you most
likely have an issue with your TestConnection handler.
After a successful call to TestConnection, the credentials will be accepted. You can now schedule refresh, or click on
the dataset ellipse and select "Refresh Now". You can click on the Refresh history link to view the status of the
refresh (which generally takes a few minutes to get kicked off).
Conclusion
Congratulations! You now have a production ready custom connector that supported automated refresh through
the Power BI service.
TripPin Part 10 - Query Folding (part 1)
7/2/2019 • 16 minutes to read
This multi-part tutorial covers the creation of a new data source extension for Power Query. The tutorial is meant to
be done sequentially - each lesson builds on the connector created in previous lessons, incrementally adding new
capabilities to your connector.
In this lesson, you will:
Learn the basics of query folding
Learn about the Table.View function
Replicate OData query folding handlers for
$top
$skip
$count
$select
$orderby
One of the powerful features of the M language is its ability to push transformation work to underlying data
source(s). This capability is referred to as Query Folding (other tools/technologies also refer to similar function as
Predicate Pushdown, or Query Delegation). When creating a custom connector which uses an M function with
built-in query folding capabilities, such as OData.Feed or Odbc.DataSource , your connector will automatically inherit
this capability for free. This tutorial will replicate the built-in query folding behavior for OData by implementing
function handlers for the Table.View function. This part of the tutorial will implement some of the easier handlers
to implement (i.e. the ones that don't require expression parsing and state tracking). Future tutorials will implement
more advanced query folding functionality.
To understand more about the query capabilities that an OData service might offer, please review the OData v4
URL Conventions.
Note: As stated above, the OData.Feed function will automatically provide query folding capabilities. Since the
TripPin series is treating the OData service as a regular REST API, using Web.Contents rather than OData.Feed ,
we need to implement the query folding handlers ourselves. For real world usage, it is recommended that you
use OData.Feed whenever possible.
Please see the Table.View documentation for more information about query folding in M.
Using Table.View
The Table.View function allows a custom connector to override default transformation handlers for your data
source. An implementation of Table.View will provide a function for one or more of the supported handlers. If a
handler is unimplemented, or returns an error during evaluation, the M engine will fall back to its default handler.
When a custom connector uses a function that does not support implicit query folding, such as Web.Contents ,
default transformation handlers will always be performed locally. If the REST API you are connecting to supports
query parameters as part of the query, Table.View will allow you to add optimizations that allow transformation
work to be pushed to the service.
The Table.View function has the following signature:
Table.View(table as nullable table, handlers as record) as table
Your implementation will wrap your main data source function. There are two required handlers for Table.View :
1. GetType : returns the expected table type of the query result
2. GetRows : returns the actual table result of your data source function
If you re-run the unit tests, you'll see that the behavior of your function hasn't changed. In this case your Table.View
implementation is simply passing through the call to GetEntity . Since your haven't implemented any
transformation handlers (yet!), the original url parameter remains untouched.
//
// Helper functions
//
// Retrieves the cached schema. If this is the first call
// to CalculateSchema, the table type is calculated based on
// the entity name that was passed into the function.
CalculateSchema = (state) as type =>
if (state[Schema]? = null) then
GetSchemaForEntity(entity)
else
state[Schema],
If you look at the call to , you'll see an additional wrapper function around the handlers record -
Table.View
Diagnostics.WrapHandlers . This helper function is found in the Diagnostics module (that was introduced in a
previous tutorial), and provides us with a useful way to automatically trace any errors raised by individual handlers.
The GetType and GetRows functions have been updated to make use of two new helper functions -
CalculateSchema and CaculateUrl . Right now the implementations of those functions are fairly straightforward -
you'll notice they contain parts of what was previously done by the GetEntity function.
Finally, you'll notice that we're defining an internal function ( View ) that accepts a state parameter. As we
implement more handlers, they will recursively call the internal View function, updating and passing along state
as they go.
Update the TripPinNavTable function once again, replacing the call to TripPin.SuperSimpleView with a call to the
new TripPin.View function, and re-run the unit tests. You won't see any new functionality yet, but we now have a
solid baseline for testing.
The Error on Folding Failure setting is an "all or nothing" approach. If you want to test queries that aren't
designed to fold as part of your unit tests, you'll need to add some conditional logic to enable/disable tests
accordingly.
The remaining sections of this tutorial will each add a new Table.View handler. We will be taking a Test Driven
Development (TDD ) approach, where we first add failing unit tests, and then implement the M code to resolve
them.
Each handler section below will describe the functionality provided by the handler, the OData equivalent query
syntax, the unit tests, and the implementation. Using the scaffolding code described above, each handler
implementation requires two changes:
1. Adding the handler to Table.View that will update the state record
2. Modifying CalculateUrl to retrieve the values from the state and add to the url and/or query string
parameters
Handling Table.FirstN with OnTake
The OnTake handler receives a count parameter, which is the maximum number of rows to take. In OData terms,
we can translate this to the $top query parameter.
We'll use the following unit tests:
These tests both use Table.FirstN to filter to the result set to the first X number of rows. If you run these tests with
Error on Folding Failure set to False (the default), the tests should succeed, but if you run Fiddler (or check the
trace logs), you'll see that the request we send doesn't contain any OData query parameters.
If you set Error on Folding Failure to True , they will fail with the "Please try a simpler expression." error. To fix this,
we'll define our first Table.View handler for OnTake .
The OnTake handler looks like this:
The CalculateUrl function is updated to extract the Top value from the state record, and set the right parameter
in the query string.
encodedQueryString = Uri.BuildQueryString(qsWithTop),
finalUrl = urlWithEntity & "?" & encodedQueryString
in
finalUrl
Rerunning the unit tests, we can see that the URL we are accessing now contains the $top parameter. (Note that
due to URL encoding, $top appears as %24top , but the OData service is smart enough to convert it automatically).
Handling Table.Skip with OnSkip
The OnSkip handler is a lot like OnTake. It receives a count parameter, which is the number of rows to skip from
the result set. This translates nicely to the OData $skip query parameter.
Unit tests:
// OnSkip
Fact("Fold $skip 14 on Airlines",
#table( type table [AirlineCode = text, Name = text] , {{"EK", "Emirates"}} ),
Table.Skip(Airlines, 14)
),
Fact("Fold $skip 0 and $top 1",
#table( type table [AirlineCode = text, Name = text] , {{"AA", "American Airlines"}} ),
Table.FirstN(Table.Skip(Airlines, 0), 1)
),
Implementation:
qsWithSkip =
if (state[Skip]? <> null) then
qsWithTop & [ #"$skip" = Number.ToText(state[Skip]) ]
else
qsWithTop,
// OnSelectColumns
Fact("Fold $select single column",
#table( type table [AirlineCode = text] , {{"AA"}} ),
Table.FirstN(Table.SelectColumns(Airlines, {"AirlineCode"}), 1)
),
Fact("Fold $select multiple column",
#table( type table [UserName = text, FirstName = text, LastName = text],{{"russellwhyte", "Russell",
"Whyte"}}),
Table.FirstN(Table.SelectColumns(People, {"UserName", "FirstName", "LastName"}), 1)
),
Fact("Fold $select with ignore column",
#table( type table [AirlineCode = text] , {{"AA"}} ),
Table.FirstN(Table.SelectColumns(Airlines, {"AirlineCode", "DoesNotExist"}, MissingField.Ignore), 1)
),
The first two tests select different numbers of columns with Table.SelectColumns , and include a Table.FirstN call
to simplify the test case.
Note: if the test were to simply return the column names (using Table.ColumnNames ), and not any data, the
request to the OData service will never actually be sent. this is because the call to GetType will return the
schema, which contains all of the info the M engine needs to calculate the result.
The third test uses the MissingField.Ignore option, which tells the M engine to ignore any selected columns that
don't exist in the result set. The OnSelectColumns handler does not need to worry about this option - the M engine
will handle it automatically (i.e. missing columns won't be included in the columns list).
Note: the other option for Table.SelectColumns , MissingField.UseNull , requires a connector to implement the
OnAddColumn handler. This will be done in a subsequent lesson.
// OnSort
Fact("Fold $orderby single column",
#table( type table [AirlineCode = text, Name = text], {{"TK", "Turkish Airlines"}}),
Table.FirstN(Table.Sort(Airlines, {{"AirlineCode", Order.Descending}}), 1)
),
Fact("Fold $orderby multiple column",
#table( type table [UserName = text], {{"javieralfred"}}),
Table.SelectColumns(Table.FirstN(Table.Sort(People, {{"LastName", Order.Ascending}, {"UserName",
Order.Descending}}), 1), {"UserName"})
)
Implementation:
Updates to CalculateUrl :
qsWithOrderBy =
if (state[OrderBy]? <> null) then
qsWithSelect & [ #"$orderby" = state[OrderBy] ]
else
qsWithSelect,
// GetRowCount
Fact("Fold $count", 15, Table.RowCount(Airlines)),
Since the /$count path segment returns a single value (in plain/text format), rather than a json result set, we'll also
have to add a new internal function ( TripPin.Scalar ) for making the request and handling the result.
The implementation will then use this function (if no other query parameters are found in the state ):
GetRowCount = () as number =>
if (Record.FieldCount(Record.RemoveFields(state, {"Url", "Entity", "Schema"}, MissingField.Ignore)) > 0)
then
...
else
let
newState = state & [ RowCountOnly = true ],
finalUrl = CalculateUrl(newState),
value = TripPin.Scalar(finalUrl),
converted = Number.FromText(value)
in
converted,
The CalculateUrl function is updated to append "/$count" to the URL if the RowCountOnly field is set in the state .
// Returns true if there is a folding error, or the original record (for logging purposes) if not.
Test.IsFoldingError = (tryResult as record) =>
if ( tryResult[HasError]? = true and tryResult[Error][Message] = "We couldn't fold the expression to the
data source. Please try a simpler expression.") then
true
else
tryResult;
Then we add a test that uses both Table.RowCount and Table.FirstN to force the error.
An important note here is that this test will now return an error if Error on Folding Error is set to false , because
the Table.RowCount operation will fall back to the local (default) handler. Running the tests with Error on Folding
Error set to true will cause Table.RowCount to fail, and allows the test to succeed.
Conclusion
Implementing Table.View for your connector adds a significant amount of complexity to your code. Since the M
engine can process all transformations locally, adding Table.View handlers does not enable new scenarios for your
users, but will result in more efficient processing (and potentially, happier users). One of the main advantages of the
Table.View handlers being optional is that it allows you to incrementally add new functionality without impacting
backwards compatibility for your connector.
For most connectors, an important (and basic) handler to implement is OnTake (which translates to $top in
OData), as it limits the amount of rows returned. The Power Query experience will always perform an OnTake of
1000 rows when displaying previews in the navigator and query editor, so your users might see significant
performance improvements when working with larger data sets.
In subsequent tutorials, we will look at the more advanced query handlers (such as OnSelectRows ), which require
translating M expressions.
Github Connector Sample
7/2/2019 • 7 minutes to read
The Github M extension shows how to add support for an OAuth 2.0 protocol authentication flow. You can learn
more about the specifics for Github's authentication flow on the Github Developer site.
Before you get started creating an M extension, you need to register a new app on Github, and replace the
client_id and client_secret files with the appropriate values for you app.
Note about compatibility issues in Visual Studio: The Power Query SDK uses an Internet Explorer based
control to popup OAuth dialogs. Github has deprecated its support for the version of IE used by this control, which
will prevent you from completing the permission grant for you app if run from within Visual Studio. An alternative
is to load the extension with Power BI Desktop and complete the first OAuth flow there. After your application has
been granted access to your account, subsequent logins will work fine from Visual Studio.
Note: A registered OAuth application is assigned a unique Client ID and Client Secret. The Client Secret should not
be shared. You get the Client ID and Client Secret from the Github application page. Update the files in your Data
Connector project with the Client ID ( client_id file) and Client Secret ( client_secret file).
//
// Data Source definition
//
GithubSample = [
Authentication = [
OAuth = [
StartLogin = StartLogin,
FinishLogin = FinishLogin
]
],
Label = Extension.LoadString("DataSourceLabel")
];
Step 2 - Provide details so the M engine can start the OAuth flow
The Github OAuth flow starts when you direct users to the https://Github.com/login/oauth/authorize page. For the
user to login, you need to specify a number of query parameters:
This code snippet describes how to implement a StartLogin function to start the login flow. A StartLogin function
takes a resourceUrl , state , and display value. In the function, create an AuthorizeUrl that concatenates the
Github authorize url with the following parameters:
client_id : You get the client id after you register your extension with Github from the Github application page.
scope : Set scope to " user, repo ". This sets the authorization scope (i.e. what your app wants to access) for the
user.
state : An internal value that the M engine passes in.
redirect_uri : Set to https://oauth.powerbi.com/views/oauthredirect.html
If this is the first time the user is logging in with your app (identified by its client_id value), they will see a page
that asks them to grant access to your app. Subsequent login attempts will simply ask for their credentials.
Step 3 - Convert the code received from Github into an access_token
If the user completes the authentication flow, Github redirects back to the Power BI redirect URL with a temporary
code in a code parameter, as well as the state you provided in the previous step in a state parameter. Your
FinishLogin function will extract the code from the callbackUri parameter, and then exchange it for an access
token (using the TokenMethod function).
To get a Github access token, you pass the temporary code from the Github Authorize Response. In the
TokenMethod function you formulate a POST request to Github's access_token endpoint (
https://github.com/login/oauth/access_token ). The following parameters are required for the Github endpoint:
Here are the details used parameters for the Web.Contents call.
ARGUMENT DESCRIPTION VALUE
options A record to control the behavior of this Not used in this case
function.
This code snippet describes how to implement a TokenMethod function to exchange an auth code for an access
token.
The JSON response from the service will contain an access_token field. TokenMethod method converts the JSON
response into an M record using Json.Document, and returns it to the engine.
Sample response:
{
"access_token":"e72e16c7e42f292c6912e7710c838347ae178b4a",
"scope":"user,repo",
"token_type":"bearer"
}
Step 4 - Define functions that access the Github API
The following code snippet exports two functions ( GithubSample.Contents and GithubSample.PagedTable ) by
marking them as shared , and associates them with the GithubSample Data Source Kind.
[DataSource.Kind="GithubSample", Publish="GithubSample.UI"]
shared GithubSample.Contents = Value.ReplaceType(Github.Contents, type function (url as Uri.Type) as any);
[DataSource.Kind="GithubSample"]
shared GithubSample.PagedTable = Value.ReplaceType(Github.PagedTable, type function (url as Uri.Type) as
nullable table);
The GithubSample.Contents function is also published to the UI (allowing it to appear in the Get Data dialog). The
Value.ReplaceType function is used to set the function parameter to the Url.Type ascribed type.
By associating these functions with the GithubSample data source kind, they will automatically use the credentials
that the user provided. Any M library functions that have been enabled for extensibility (such as Web.Contents) will
automatically inherit these credentials as well.
For more details on how credential and authentication works, please see Handling Authentication.
Sample URL
This connector is able to retrieve formatted data from any of the github v3 REST API endpoints. For example, the
query to pull all commits to the Data Connectors repo would look like this:
GithubSample.Contents("https://api.github.com/repos/microsoft/dataconnectors/commits")
MyGraph Connector Sample
7/2/2019 • 11 minutes to read
In this sample we will create a basic data source connector for Microsoft Graph. It is written as a walk-through that
you can follow step by step.
To access Graph, you will first need to register your own Azure Active Directory client application. If you do not
have an application ID already, you can create one through the Getting Started with Microsoft Graph site. Click the
"Universal Windows" option, and then the "Let's go" button. Follow the steps and receive an App ID. As described
in the steps below, use https://oauth.powerbi.com/views/oauthredirect.html as your redirect URI when registering
your app. Client ID value, use it to replace the existing value in the client_id file in the code sample.
let
client_id = "<your app id>",
redirect_uri = "https://oauth.powerbi.com/views/oauthredirect.html",
token_uri = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
authorize_uri = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
logout_uri = "https://login.microsoftonline.com/logout.srf"
in
logout_uri
Set the client_id with the app id you received when you registered your Graph application.
Graph has an extensive list of permission scopes that your application can request. For this sample, the app will
request all scopes that do not require admin consent. We will define two more variables – a list of the scopes we
want, and the prefix string that graph uses. We'll also add a couple of helper functions to convert the scope list into
the expected format.
scope_prefix = "https://graph.microsoft.com/",
scopes = {
"User.ReadWrite",
"Contacts.Read",
"User.ReadBasic.All",
"Calendars.ReadWrite",
"Mail.ReadWrite",
"Mail.Send",
"Contacts.ReadWrite",
"Files.ReadWrite",
"Tasks.ReadWrite",
"People.Read",
"Notes.ReadWrite.All",
"Sites.Read.All"
},
Value.IfNull = (a, b) => if a <> null then a else b,
The GetScopeString function will end up generating a scope string which looks like this:
https://graph.microsoft.com/User.ReadWrite https://graph.microsoft.com/Contacts.Read
https://graph.microsoft.com/User.ReadBasic.All ...
You will need to set several query string parameters as part of the authorization URL. We can use the
Uri.BuildQueryString function to properly encode the parameter names and values. Construct the URL by
concatenating the authorize_uri variable and query string parameters.
The full code sample is below.
let
client_id = "<your app id>",
redirect_uri = "urn:ietf:wg:oauth:2.0:oob",
token_uri = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
authorize_uri = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
logout_uri = "https://login.microsoftonline.com/logout.srf",
scope_prefix = "https://graph.microsoft.com/",
scopes = {
"User.ReadWrite",
"Contacts.Read",
"User.ReadBasic.All",
"Calendars.ReadWrite",
"Mail.ReadWrite",
"Mail.Send",
"Contacts.ReadWrite",
"Files.ReadWrite",
"Tasks.ReadWrite",
"People.Read",
"Notes.ReadWrite.All",
"Sites.Read.All"
},
Close the Advanced Query Editor to see the generated authorization URL.
Launch Fiddler and copy and paste the URL into the browser of your choice.
You will need to configure Fiddler to decrypt HTTPS traffic and skip decryption for the following
hosts: msft.sts.microsoft.com
Entering the URL should bring up the standard Azure Active Directory login page. Complete the auth flow using
your regular credentials, and then look at the fiddler trace. You'll be interested in the lines with a status of 302 and a
host value of login.microsoftonline.com.
Click on the request to /login.srf and view the Headers of the Response. Under Transport, you will find a location
header with the redirect_uri value you specified in your code, and a query string containing a very long code
value.
Extract the code value only, and paste it into your M query as a new variable. Note, the header value will also
contain a &session_state=xxxx query string value at the end – remove this part from the code. Also, be sure to
include double quotes around the value after you paste it into the advanced query editor.
To exchange the code for an auth token we will need to create a POST request to the token endpoint. We'll do this
using the Web.Contents call, and use the Uri.BuildQueryString function to format our input parameters. The code
will look like this:
tokenResponse = Web.Contents(token_uri, [
Content = Text.ToBinary(Uri.BuildQueryString([
client_id = client_id,
code = code,
scope = GetScopeString(scopes, scope_prefix),
grant_type = "authorization_code",
redirect_uri = redirect_uri])),
Headers = [
#"Content-type" = "application/x-www-form-urlencoded",
#"Accept" = "application/json"
]
]),
jsonResponse = Json.Document(tokenResponse)
When you return the jsonResponse, Power Query will likely prompt you for credentials. Choose Anonymous, and
click OK.
The authentication code returned by AAD has a short timeout – probably shorter than the time it took you to do
the previous steps. If your code has expired, you will see a response like this:
If you check the fiddler trace you will see a more detailed error message in the JSON body of the response related
to timeout. Later in this sample we'll update our code so that end users will be able to see the detailed error
messages instead of the generic 400 Bad Request error. Try the authentication process again (you will likely want
your browser to be In Private mode to avoid any stored auth info). Capture the Location header, and update your
M query with the new code value. Run the query again, and you should see a parsed record containing an
access_token value.
You now have the raw code you'll need to implement your OAuth flow. As an optional step, you can improve the
error handling of your OAuth code by using the ManualStatusHandling option to Web.Contents . This will let us
process the body of an error response (which is a json document with [error] and [error_description] fields), rather
than displaying a DataSource.Error to the user. The updated code looks like this:
tokenResponse = Web.Contents(token_uri, [
Content = Text.ToBinary(Uri.BuildQueryString([
client_id = client_id,
code = code,
scope = GetScopeString(scopes, scope_prefix),
grant_type = "authorization_code",
redirect_uri = redirect_uri])),
Headers = [
#"Content-type" = "application/x-www-form-urlencoded",
#"Accept" = "application/json"
],
ManualStatusHandling = {400}
]),
body = Json.Document(tokenResponse),
result = if (Record.HasFields(body, {"error", "error_description"})) then
error Error.Record(body[error], body[error_description], body)
else
body
Run your query again and you will receive an error (because your code was already exchanged for an auth token).
This time you should see the full detailed error message from the service, rather than a generic 400 status code.
Since our data source function has no required arguments, it acts as a Singleton data source credential type. This
means that a user will have a single credential for the data source, and that the credential is not dependent on any
of the parameters supplied to the function.
We've declared that OAuth as one of our supported credential types and provided function names for the OAuth
interface functions.
[DataSource.Kind="MyGraph", Publish="MyGraph.UI"]
MyGraph.Feed = () =>
let
source = OData.Feed("https://graph.microsoft.com/v1.0/me/", null, [ ODataVersion = 4, MoreColumns =
true ])
in
source;
//
// Data Source definition
//
MyGraph = [
Authentication = [
OAuth = [
StartLogin=StartLogin,
FinishLogin=FinishLogin,
Refresh=Refresh,
Logout=Logout
]
],
Label = "My Graph Connector"
];
//
// UI Export definition
//
MyGraph.UI = [
Beta = true,
ButtonText = { "MyGraph.Feed", "Connect to Graph" },
SourceImage = MyGraph.Icons,
SourceTypeImage = MyGraph.Icons
];
It is expected to return a record with all the fields that Power BI will need to initiate an OAuth flow.
Since our data source function has no required parameters, we won't be making use of the resourceUrl value. If
our data source function required a user supply URL or sub-domain name, then this is where it would be passed to
us. The State parameter includes a blob of state information that we're expected to include in the URL. We will not
need to use the display value at all. The body of the function will look a lot like the authorizeUrl variable you
created earlier in this sample – the main difference will be the inclusion of the state parameter (which is used to
prevent replay attacks).
StartLogin = (resourceUrl, state, display) =>
let
authorizeUrl = authorize_uri & "?" & Uri.BuildQueryString([
client_id = client_id,
redirect_uri = redirect_uri,
state = state,
scope = GetScopeString(scopes, scope_prefix),
response_type = "code",
response_mode = "query",
login = "login"
])
in
[
LoginUri = authorizeUrl,
CallbackUri = redirect_uri,
WindowHeight = 720,
WindowWidth = 1024,
Context = null
];
FinishLogin
The FinishLogin function will be called once the user has completed their OAuth flow. Its signature looks like this:
The context parameter will contain any value set in the Context field of the record returned by StartLogin .
Typically this will be a tenant ID or other identifier that was extracted from the original resource URL. The
callbackUri parameter contains the redirect value in the Location header, which we'll parse to extract the code
value. The third parameter ( state ) can be used to round-trip state information to the service – we won't need to
use it for AAD.
We will use the Uri.Parts function to break apart the callbackUri value. For the AAD auth flow, all we'll care
about is the code parameter in the query string.
If the response doesn't contain error fields, we pass the code query string parameter from the Location header
to the TokenMethod function.
The TokenMethod function converts the code to an access_token . It is not a direct part of the OAuth interface, but
it provides all the heavy lifting for the FinishLogin and Refresh functions. Its implementation is essentially the
tokenResponse logic we created earlier with one small addition – we'll use a grantType variable rather than
hardcoding the value to "authorization_code".
TokenMethod = (grantType, tokenField, code) =>
let
queryString = [
client_id = client_id,
scope = GetScopeString(scopes, scope_prefix),
grant_type = grantType,
redirect_uri = redirect_uri
],
queryWithCode = Record.AddField(queryString, tokenField, code),
tokenResponse = Web.Contents(token_uri, [
Content = Text.ToBinary(Uri.BuildQueryString(queryWithCode)),
Headers = [
#"Content-type" = "application/x-www-form-urlencoded",
#"Accept" = "application/json"
],
ManualStatusHandling = {400}
]),
body = Json.Document(tokenResponse),
result = if (Record.HasFields(body, {"error", "error_description"})) then
error Error.Record(body[error], body[error_description], body)
else
body
in
result;
Refresh
This function is called when the access_token expires – Power Query will use the refresh_token to retrieve a new
access_token . The implementation here is just a call to TokenMethod , passing in the refresh token value rather than
the code.
Logout
The last function we need to implement is Logout. The logout implementation for AAD is very simple – we just
return a fixed URL.
[DataSource.Kind="MyGraph", Publish="MyGraph.UI"]
MyGraph.Feed = () =>
let
source = OData.Feed("https://graph.microsoft.com/v1.0/me/", null, [ ODataVersion = 4, MoreColumns =
true ])
in
source;
Once your function is updated, make sure there are no syntax errors in your code (look for red squiggles). Also be
sure to update your client_id file with your own AAD app ID. If there are no errors, open the MyGraph.query.m
file.
The <project>.query.m file lets you test out your extension. You (currently) don’t get the same navigation table /
query building experience you get in Power BI Desktop, but does provide a quick way to test out your code.
A query to test your data source function would be:
MyGraph.Feed()
Select OAuth2 from the Credential Type drop down, and click Login. This will popup your OAuth flow. After
completing your flow, you should see a large blob (your token). Click the Set Credential button at the bottom of the
dialog, and close the MQuery Output window.
Run the query again. This time you should get a spinning progress dialog, and a query result window.
You can now build your project in Visual Studio to create a compiled extension file, and deploy it to your Custom
Connectors directory. Your new data source should now appear in the Get Data dialog the next time you launch
Power BI Desktop.
List of Samples
3/5/2019 • 2 minutes to read
We maintain a list of samples on the DataConnectors repo on Github. Each of the links below links to a folder in the
sample repository. Generally these folders include a readme, one or more .pq / .query.pq files, a project file for
Visual Studio, and in some cases icons. To open these in Visual Studio, make sure you've set up the SDK properly,
and run the .mproj file from the cloned or downloaded folder.
Functionality
SAMPLE DESCRIPTION LINK
Hello World This very simple sample shows the basic Github Link
structure of a connector.
Hello World with Docs Similar to the Hello World sample, this Github Link
sample shows how to add
documentation to a shared function.
Unit Testing This sample shows how you can add Github Link
simple unit testing to your .query.pq file.
OAuth
SAMPLE DESCRIPTION LINK
ODBC
SAMPLE DESCRIPTION LINK
Hive LLAP This connector sample uses the Hive Github Link
ODBC driver, and is based on the
connector template.
Direct Query for SQL This sample creates an ODBC based Github Link
custom connector that enables Direct
Query for SQL Server.
TripPin
SAMPLE DESCRIPTION LINK
We maintain a list of samples on the DataConnectors repo on Github. Each of the links below links to a folder in the
sample repository. Generally these folders include a readme, one or more .pq / .query.pq files, a project file for
Visual Studio, and in some cases icons. To open these in Visual Studio, make sure you've set up the SDK properly,
and run the .mproj file from the cloned or downloaded folder.
Functionality
SAMPLE DESCRIPTION LINK
Hello World This very simple sample shows the basic Github Link
structure of a connector.
Hello World with Docs Similar to the Hello World sample, this Github Link
sample shows how to add
documentation to a shared function.
Unit Testing This sample shows how you can add Github Link
simple unit testing to your .query.pq file.
OAuth
SAMPLE DESCRIPTION LINK
ODBC
SAMPLE DESCRIPTION LINK
Hive LLAP This connector sample uses the Hive Github Link
ODBC driver, and is based on the
connector template.
Direct Query for SQL This sample creates an ODBC based Github Link
custom connector that enables Direct
Query for SQL Server.
TripPin
SAMPLE DESCRIPTION LINK
We maintain a list of samples on the DataConnectors repo on Github. Each of the links below links to a folder in the
sample repository. Generally these folders include a readme, one or more .pq / .query.pq files, a project file for
Visual Studio, and in some cases icons. To open these in Visual Studio, make sure you've set up the SDK properly,
and run the .mproj file from the cloned or downloaded folder.
Functionality
SAMPLE DESCRIPTION LINK
Hello World This very simple sample shows the basic Github Link
structure of a connector.
Hello World with Docs Similar to the Hello World sample, this Github Link
sample shows how to add
documentation to a shared function.
Unit Testing This sample shows how you can add Github Link
simple unit testing to your .query.pq file.
OAuth
SAMPLE DESCRIPTION LINK
ODBC
SAMPLE DESCRIPTION LINK
Hive LLAP This connector sample uses the Hive Github Link
ODBC driver, and is based on the
connector template.
Direct Query for SQL This sample creates an ODBC based Github Link
custom connector that enables Direct
Query for SQL Server.
TripPin
SAMPLE DESCRIPTION LINK
When developing a custom connector, or if you've been given one by another developer or vendor, you'll notice that
they require you to lower the security settings in Power BI to use them. This is due to the fact that M is a versatile
language that (as seen in Handling Authentication) has the capacity to interact with stored credentials. This means
that we needed to give end users a way to only allow certified connectors to run.
The 'Connector Certification' program is a program in which Microsoft works with vendors to extend the data
connectivity capabilities of Power BI.
Certified connectors are:
Certified by Microsoft
Distributed by Microsoft
Maintained by the developer
Supported by the developer
We work with vendors to try to make sure that they have support in maintenance, but customer issues with the
connector itself will be directed to the developer.
We have a certain set of requirements for certification. We recognize that not every developer can meet these
requirements, and as above we're hoping to introduce a feature set that will handle their needs in short order.
Certification Requirements
Before starting
Developer must own the data source or have recorded permission from the owner of the data source to develop
a connector for it.
Developer must sign an NDA
Developer must sign a business partner agreement with our team
The business partner agreement is different from a Microsoft Partner agreement. The agreement
addresses terms of making your connector code available to us for use in the relevant products. We will
sign the agreement when we kick off the process.
Data source must not be an internal only data source
Artifacts
PBIX file
Report should contain one or more queries to test each item in their navigation table
If you don't have a set schema (as an example, databases), you should include a query for each 'type' of
table you're concerned with.
.mez file
The .mez file should follow style standards. For example, use Product.mez rather than
Product_PowerBI_Connector.mez.
Test account
The test account will be reused whenever we're troubleshooting or certifying updates, so if you have a
persistent test account it would be best to find a way to share this.
Link to external dependencies (ODBC drivers, for example).
Documentation on how to use the connector if needed
Security
If using Extension.CurrentCredentials() …
Is the usage required? If so, where do the credentials get sent to?
Are the requests guaranteed to be made via HTTPS?
You can use the HTTPS enforcement helper function.
If the credentials are sent using Web.Contents() via GET …
Can it be turned into a POST?
If GET is required, connector MUST use the CredentialQueryString record in the Web.Contents()
options record to pass in sensitive credentials
If Diagnostics.* functions are used …
Validate what is being traced - it MUST NOT
Contain PII
Contain large amounts of data
If you implemented significant tracing in development, you should attach it to a variable that checks if
tracing should be on or not, and you should turn it off before shipping.
If Expression.Evaluate() is used …
Validate where the expression is coming from / what it is (i.e. can dynamically construct calls to
Extension.CurrentCredentials() etc…
Expression should not be user provided / take user input
Expression should not be dynamic (i.e. retrieved from a web call)
Features and Style
Connector MUST use Section document format
Connector MUST have Version adornment on section
Connector MUST provide function documentation metadata
Connector MUST have TestConnection handler
Connector MUST follow naming conventions (DataSourceKind.FunctionName)
FunctionName should make sense for their domain - generally "Contents", "Tables", "Document", "Databases" …
Connector SHOULD have icons
Connector SHOULD provide a navigation table
Connector SHOULD place strings in resources.resx file
Summary
Power Query Online is integrated into a variety of Microsoft products. Since these products target different
scenarios, they may set different limits for Power Query Online usage.
Limits are enforced at the beginning of query evaluations. Once an evaluation is underway, only timeout limits are
imposed.
Limit Types
Hourly Evaluation Count: The maximum number of evaluation requests a user can issue during any 60 minute
period
Daily Evaluation Time: The net time a user can spend evaluating queries during any 24 hour period
Concurrent Evaluations: The maximum number of evaluations a user can have running at any given time
Authoring Limits
Authoring limits are the same across all products. During authoring, query evaluations return previews that may be
subsets of the data. Data is not persisted.
Hourly Evaluation Count: 1000
Daily Evaluation Time: Currently unrestricted
Per Query Timeout: 10 minutes
Refresh Limits
During refresh (either scheduled or on-demand), query evaluations return complete results. Data is typically
persisted in storage.
Under certain circumstances, users will run up against issues where Power Query fails to extract all the data from
an Excel Worksheet, or performance is severely degraded against a reasonably sized table. Both of these failures
generally resolve to the same cause: improper cell range specification.
Summary
Release State: General Availability Products: Power BI Desktop, Power BI Service (Enterprise Gateway), Dataflows
in PowerBI.com (Enterprise Gateway), Dataflows in PowerApps.com (Enterprise Gateway), Excel Authentication
Types Supported: Database (Username/Password) Note: Some capabilities may be present in one product but not
others due to deployment schedules and host-specific capabilities.
Prerequisites
In order to connect to a PostgreSQL database with Power BI Desktop, the Npgsql provider must be installed on
the computer running Power BI Desktop.
To install the Npgsql provider, go to the releases page and download the relevant release. The provider architecture
(32-bit vs. 65-bit) needs to match the architecture of the product where you intent to use the connector. When
installing, make sure that you select Npgsql GAC Installation to ensure Npgsql itself is added to your machine.
Capabilities Supported
Import
DirectQuery (Power BI only, learn more)
Advanced options
Command timeout in minutes
Native SQL statement
Relationship columns
Navigate using full hierarchy
2. In the PostgreSQL dialog that appears, provide the name of the server and database. Optionally, you may
provide a command timeout and a native query (SQL statement), as well as select whether or not you want to
include relationship columns and navigate using full hierarchy. Once you are done, select Connect.
3. If the PostgreSQL database requires database user credentials, input those credentials in the dialogue when
prompted.
Troubleshooting
Your native query may throw the error:
We cannot fold on top of this native query. Please modify the native query or remove the 'EnableFolding' option.
A basic trouble shooting step is to check if the query in Value.NativeQuery() throws the same error with a limit 1
clause around it: select * from (query) _ limit 1
Handling Authentication
7/2/2019 • 7 minutes to read
Authentication Kinds
An extension can support one or more kinds of Authentication. Each authentication kind is a different type of
credential. The authentication UI displayed to end users in Power Query is driven by the type of credential(s) that
an extension supports.
The list of supported authentication types is defined as part of an extension's Data Source Kind definition. Each
Authentication value is a record with specific fields. The table below lists the expected fields for each kind. All fields
are required unless marked otherwise.
The sample below shows the Authentication record for a connector that supports OAuth, Key, Windows, Basic
(Username and Password), and anonymous credentials.
Example:
Authentication = [
OAuth = [
StartLogin = StartLogin,
FinishLogin = FinishLogin,
Refresh = Refresh,
Logout = Logout
],
Key = [],
UsernamePassword = [],
Windows = [],
Implicit = []
]
Key The API key value. Note, the key value is Key
also available in the Password field as
well. By default the mashup engine will
insert this in an Authorization header as
if this value were a basic auth password
(with no username). If this is not the
behavior you want, you must specify
the ManualCredentials = true option in
the options record.
The following is an example of accessing the current credential for an API key and using it to populate a custom
header ( x-APIKey ).
Example:
#"x-APIKey" = apiKey,
Accept = "application/vnd.api+json",
#"Content-Type" = "application/json"
],
request = Web.Contents(_url, [ Headers = headers, ManualCredentials = true ])
in
request
Note: Power Query extensions are evaluated in applications running on client machines. Data Connectors
should not use confidential secrets in their OAuth flows, as users may inspect the extension or network traffic
to learn the secret. Please see the OAuth 2.0 for Native Apps draft RFC for further details on providing flows
that do not rely on shared secrets.
In the future we plan to support data sources that require confidential secrets (using a proxy based
mechanism).
Please see the MyGraph and Github samples for more details.
Data Source Paths
The M engine identifies a data source using a combination of its Kind and Path. When a data source is encountered
during a query evaluation, the M engine will try to find matching credentials. If no credentials are found, the engine
returns an special error which results in a credential prompt in Power Query.
The Kind value comes from Data Source Kind definition.
The Path value is derived from the required parameters of your data source function. Optional parameters are not
factored into the data source path identifier. As a result, all data source functions associated with a data source kind
must have the same parameters. There is special handling for functions that have a single parameter of type
Uri.Type . See the section below for details.
You can see an example of how credentials are stored in the Data source settings dialog in Power BI Desktop. In this
dialog, the Kind is represented by an icon, and the Path value is displayed as text.
Note: If you change your data source function's required parameters during development, previously stored
credentials will no longer work (because the path values no longer match). You should delete any stored
credentials any time you change your data source function parameters. If incompatible credentials are found,
you may receive an error at runtime.
The function has a single required parameter ( message ) of type text , and will be used to calculate the data source
path. The optional parameter ( count ) would be ignored. The path would be displayed
Credential prompt:
Data source settings UI:
When a Label value is defined, the data source path value would not be shown:
Note: We currently recommend you do not include a Label for your data source if your function has required
parameters, as users will not be able to distinguish between the different credentials they have entered. We are
hoping to improve this in the future (i.e. allowing data connectors to display their own custom data source
paths).
As Uri.Type is an ascribed type rather than a primitive type in the M language, you will need to use the
Value.ReplaceType function to indicate that your text parameter should be treated as an Uri.
[DataSource.Kind="HelloWorld", Publish="HelloWorld.Publish"]
shared HelloWorld.Contents = (optional message as text) =>
let
message = if (message <> null) then message else "Hello world"
in
message;
HelloWorld = [
Authentication = [
Implicit = []
],
Label = Extension.LoadString("DataSourceLabel")
];
Properties
The following table lists the fields for your Data Source definition record.
FIELD TYPE DETAILS
Publish to UI
Similar to the (Data Source)[#data-source-kind] definition record, the Publish record provides the Power Query UI
the information it needs to expose this extension in the Get Data dialog.
Example:
HelloWorld.Publish = [
Beta = true,
ButtonText = { Extension.LoadString("FormulaTitle"), Extension.LoadString("FormulaHelp") },
SourceImage = HelloWorld.Icons,
SourceTypeImage = HelloWorld.Icons
];
HelloWorld.Icons = [
Icon16 = { Extension.Contents("HelloWorld16.png"), Extension.Contents("HelloWorld20.png"),
Extension.Contents("HelloWorld24.png"), Extension.Contents("HelloWorld32.png") },
Icon32 = { Extension.Contents("HelloWorld32.png"), Extension.Contents("HelloWorld40.png"),
Extension.Contents("HelloWorld48.png"), Extension.Contents("HelloWorld64.png") }
];
Properties
The following table lists the fields for your Publish record.
Overview
Using M's built-in Odbc.DataSource function is the recommended way to create custom connectors for data
sources that have an existing ODBC driver and/or support a SQL query syntax. Wrapping the Odbc.DataSource
function will allow your connector to inherit default query folding behavior based on the capabilities reported by
your driver. This will enable the M engine to generate SQL statements based on filters and other transformations
defined by the user within the Power Query experience, without having to provide this logic within the connector
itself.
ODBC extensions can optionally enable Direct Query mode, allowing Power BI to dynamically generate queries at
runtime without pre-caching the user's data model.
Note: Enabling Direct Query support raises the difficulty and complexity level of your connector. When Direct
Query is enabled, Power BI will prevent the M engine from compensating for operations that cannot be fully
pushed to the underlying data source.
This document builds on the concepts presented in the M Extensibility Reference, and assumes familiarity with the
creation of a basic Data Connector.
Please refer to the SqlODBC sample for most of the code examples in the sections below. Additional samples can
be found in the ODBC samples directory.
FIELD DESCRIPTION
The following table describes the options record fields that are only available via extensibility. Fields that are not
simple literal values are described in subsequent sections.
FIELD DESCRIPTION
Overriding AstVisitor
The AstVisitor field is set through the Odbc.DataSource options record. It is used to modify SQL statements
generated for specific query scenarios.
Note: Drivers that support LIMIT and OFFSET clauses (rather than TOP ) will want to provide a LimitClause
override for AstVisitor.
Constant
Providing an override for this value has been deprecated and may be removed from future implementations.
LimitClause
This field is a function that receives two Int64.Type arguments (skip, take), and returns a record with two text fields
(Text, Location).
The skip parameter is the number of rows to skip (i.e. the argument to OFFSET). If an offset is not specified, the
skip value will be null. If your driver supports LIMIT , but does not support OFFSET , the LimitClause function
should return an unimplemented error (...) when skip is greater than 0.
The take parameter is the number of rows to take (i.e. the argument to LIMIT).
The Text field of the result contains the SQL text to add to the generated query.
The Location field specifies where to insert the clause. The following table describes supported values.
AfterSelect LIMIT goes after the SELECT statement, SELECT DISTINCT LIMIT 5 a, b, c
and after any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
AfterSelectBeforeModifiers LIMIT goes after the SELECT statement, SELECT LIMIT 5 DISTINCT a, b, c
but before any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
The following code snippet provides a LimitClause implementation for a driver that expects a LIMIT clause, with an
optional OFFSET, in the following format: [OFFSET <offset> ROWS] LIMIT <row_count>
The following code snippet provides a LimitClause implementation for a driver that supports LIMIT, but not
OFFSET. Format: LIMIT <row_count> .
Overriding SqlCapabilities
FIELD DETAILS
SupportsTop A logical value which indicates the driver supports the TOP
clause to limit the number of returned rows.
Default: false
Overriding SQLColumns
SQLColumns is a function handler that receives the results of an ODBC call to SQLColumns. The source parameter
contains a table with the data type information. This override is typically used to fix up data type mismatches
between calls to SQLGetTypeInfo and SQLColumns .
For details of the format of the source table parameter, please see: https://docs.microsoft.com/en-
us/sql/odbc/reference/syntax/sqlcolumns-function
Overriding SQLGetFunctions
This field is used to override SQLFunctions values returned by an ODBC driver. It contains a record whose field
names are equal to the FunctionId constants defined for the ODBC SQLGetFunctions function. Numeric constants
for each of these fields can be found in the ODBC specification.
FIELD DETAILS
The following code snippet provides an example explicitly telling the M engine to use CAST rather than CONVERT.
SQLGetFunctions = [
SQL_CONVERT_FUNCTIONS = 0x2 /* SQL_FN_CVT_CAST */
]
Overriding SQLGetInfo
This field is used to override SQLGetInfo values returned by an ODBC driver. It contains a record whose fields are
names are equal to the InfoType constants defined for the ODBC SQLGetInfo function. Numeric constants for each
of these fields can be found in the ODBC specification. The full list of InfoTypes that are checked can be found in
the Mashup Engine trace files.
The following table contains commonly overridden SQLGetInfo properties:
FIELD DETAILS
FIELD DETAILS
The following helper function can be used to create bitmask values from a list of integer values:
Overriding SQLGetTypeInfo
SQLGetTypeInfo can be specified in two ways:
1. A fixed table value that contains the same type information as an ODBC call to SQLGetTypeInfo
2. A function that accepts a table argument, and returns a table. The argument will contain the original results of
the ODBC call to SQLGetTypeInfo . Your function implementation can modify/add to this table.
The first approach is used to completely override the values returned by the ODBC driver. The second approach is
used if you want to add to or modify these values.
For details of the format of the types table parameter and expected return value, please see:
https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlgettypeinfo-function
SQLGetTypeInfo using a static table
The following code snippet provides a static implementation for SQLGetTypeInfo.
SQLGetTypeInfo = #table(
{ "TYPE_NAME", "DATA_TYPE", "COLUMN_SIZE", "LITERAL_PREF", "LITERAL_SUFFIX", "CREATE_PARAS",
"NULLABLE", "CASE_SENSITIVE", "SEARCHABLE", "UNSIGNED_ATTRIBUTE", "FIXED_PREC_SCALE", "AUTO_UNIQUE_VALUE",
"LOCAL_TYPE_NAME", "MINIMUM_SCALE", "MAXIMUM_SCALE", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "NUM_PREC_RADIX",
"INTERNAL_PRECISION", "USER_DATA_TYPE" }, {
Once you have simple queries working, you can then try Direct Query scenarios (i.e. building reports in the Report
Views). The queries generated in Direct Query mode will be significantly more complex (i.e. use of sub-selects,
COALESCE statements, and aggregations).
Concatenation of strings in Direct Query mode
The M engine does basic type size limit validation as part of its query folding logic. If you are receiving a folding
error when trying to concatenate two strings that potentially overflow the maximum size of the underlying
database type:
1. Ensure that your database can support up-conversion to CLOB types when string concat overflow occurs
2. Set the TolerateConcatOverflow option for Odbc.DataSource to true
The DAX CONCATENATE function is currently not supported by Power Query/ODBC extensions. Extension
authors should ensure string concatenation works through the query editor by adding calculated columns (
[stringCol1] & [stringCol2] ). When the capability to fold the CONCATENATE operation is added in the future,
it should work seamlessly with existing extensions.
Enabling Direct Query for an ODBC based connector
7/2/2019 • 22 minutes to read
Overview
Using M's built-in Odbc.DataSource function is the recommended way to create custom connectors for data
sources that have an existing ODBC driver and/or support a SQL query syntax. Wrapping the Odbc.DataSource
function will allow your connector to inherit default query folding behavior based on the capabilities reported by
your driver. This will enable the M engine to generate SQL statements based on filters and other transformations
defined by the user within the Power Query experience, without having to provide this logic within the connector
itself.
ODBC extensions can optionally enable Direct Query mode, allowing Power BI to dynamically generate queries at
runtime without pre-caching the user's data model.
Note: Enabling Direct Query support raises the difficulty and complexity level of your connector. When Direct
Query is enabled, Power BI will prevent the M engine from compensating for operations that cannot be fully
pushed to the underlying data source.
This document builds on the concepts presented in the M Extensibility Reference, and assumes familiarity with the
creation of a basic Data Connector.
Please refer to the SqlODBC sample for most of the code examples in the sections below. Additional samples can
be found in the ODBC samples directory.
FIELD DESCRIPTION
The following table describes the options record fields that are only available via extensibility. Fields that are not
simple literal values are described in subsequent sections.
FIELD DESCRIPTION
Overriding AstVisitor
The AstVisitor field is set through the Odbc.DataSource options record. It is used to modify SQL statements
generated for specific query scenarios.
Note: Drivers that support LIMIT and OFFSET clauses (rather than TOP ) will want to provide a LimitClause
override for AstVisitor.
Constant
Providing an override for this value has been deprecated and may be removed from future implementations.
LimitClause
This field is a function that receives two Int64.Type arguments (skip, take), and returns a record with two text fields
(Text, Location).
The skip parameter is the number of rows to skip (i.e. the argument to OFFSET). If an offset is not specified, the
skip value will be null. If your driver supports LIMIT , but does not support OFFSET , the LimitClause function
should return an unimplemented error (...) when skip is greater than 0.
The take parameter is the number of rows to take (i.e. the argument to LIMIT).
The Text field of the result contains the SQL text to add to the generated query.
The Location field specifies where to insert the clause. The following table describes supported values.
AfterSelect LIMIT goes after the SELECT statement, SELECT DISTINCT LIMIT 5 a, b, c
and after any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
AfterSelectBeforeModifiers LIMIT goes after the SELECT statement, SELECT LIMIT 5 DISTINCT a, b, c
but before any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
The following code snippet provides a LimitClause implementation for a driver that expects a LIMIT clause, with an
optional OFFSET, in the following format: [OFFSET <offset> ROWS] LIMIT <row_count>
The following code snippet provides a LimitClause implementation for a driver that supports LIMIT, but not
OFFSET. Format: LIMIT <row_count> .
Overriding SqlCapabilities
FIELD DETAILS
SupportsTop A logical value which indicates the driver supports the TOP
clause to limit the number of returned rows.
Default: false
Overriding SQLColumns
SQLColumns is a function handler that receives the results of an ODBC call to SQLColumns. The source parameter
contains a table with the data type information. This override is typically used to fix up data type mismatches
between calls to SQLGetTypeInfo and SQLColumns .
For details of the format of the source table parameter, please see: https://docs.microsoft.com/en-
us/sql/odbc/reference/syntax/sqlcolumns-function
Overriding SQLGetFunctions
This field is used to override SQLFunctions values returned by an ODBC driver. It contains a record whose field
names are equal to the FunctionId constants defined for the ODBC SQLGetFunctions function. Numeric constants
for each of these fields can be found in the ODBC specification.
FIELD DETAILS
The following code snippet provides an example explicitly telling the M engine to use CAST rather than CONVERT.
SQLGetFunctions = [
SQL_CONVERT_FUNCTIONS = 0x2 /* SQL_FN_CVT_CAST */
]
Overriding SQLGetInfo
This field is used to override SQLGetInfo values returned by an ODBC driver. It contains a record whose fields are
names are equal to the InfoType constants defined for the ODBC SQLGetInfo function. Numeric constants for each
of these fields can be found in the ODBC specification. The full list of InfoTypes that are checked can be found in
the Mashup Engine trace files.
The following table contains commonly overridden SQLGetInfo properties:
FIELD DETAILS
FIELD DETAILS
The following helper function can be used to create bitmask values from a list of integer values:
Overriding SQLGetTypeInfo
SQLGetTypeInfo can be specified in two ways:
1. A fixed table value that contains the same type information as an ODBC call to SQLGetTypeInfo
2. A function that accepts a table argument, and returns a table. The argument will contain the original results of
the ODBC call to SQLGetTypeInfo . Your function implementation can modify/add to this table.
The first approach is used to completely override the values returned by the ODBC driver. The second approach is
used if you want to add to or modify these values.
For details of the format of the types table parameter and expected return value, please see:
https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlgettypeinfo-function
SQLGetTypeInfo using a static table
The following code snippet provides a static implementation for SQLGetTypeInfo.
SQLGetTypeInfo = #table(
{ "TYPE_NAME", "DATA_TYPE", "COLUMN_SIZE", "LITERAL_PREF", "LITERAL_SUFFIX", "CREATE_PARAS",
"NULLABLE", "CASE_SENSITIVE", "SEARCHABLE", "UNSIGNED_ATTRIBUTE", "FIXED_PREC_SCALE", "AUTO_UNIQUE_VALUE",
"LOCAL_TYPE_NAME", "MINIMUM_SCALE", "MAXIMUM_SCALE", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "NUM_PREC_RADIX",
"INTERNAL_PRECISION", "USER_DATA_TYPE" }, {
Once you have simple queries working, you can then try Direct Query scenarios (i.e. building reports in the Report
Views). The queries generated in Direct Query mode will be significantly more complex (i.e. use of sub-selects,
COALESCE statements, and aggregations).
Concatenation of strings in Direct Query mode
The M engine does basic type size limit validation as part of its query folding logic. If you are receiving a folding
error when trying to concatenate two strings that potentially overflow the maximum size of the underlying
database type:
1. Ensure that your database can support up-conversion to CLOB types when string concat overflow occurs
2. Set the TolerateConcatOverflow option for Odbc.DataSource to true
The DAX CONCATENATE function is currently not supported by Power Query/ODBC extensions. Extension
authors should ensure string concatenation works through the query editor by adding calculated columns (
[stringCol1] & [stringCol2] ). When the capability to fold the CONCATENATE operation is added in the future,
it should work seamlessly with existing extensions.
Enabling Direct Query for an ODBC based connector
7/2/2019 • 22 minutes to read
Overview
Using M's built-in Odbc.DataSource function is the recommended way to create custom connectors for data
sources that have an existing ODBC driver and/or support a SQL query syntax. Wrapping the Odbc.DataSource
function will allow your connector to inherit default query folding behavior based on the capabilities reported by
your driver. This will enable the M engine to generate SQL statements based on filters and other transformations
defined by the user within the Power Query experience, without having to provide this logic within the connector
itself.
ODBC extensions can optionally enable Direct Query mode, allowing Power BI to dynamically generate queries at
runtime without pre-caching the user's data model.
Note: Enabling Direct Query support raises the difficulty and complexity level of your connector. When Direct
Query is enabled, Power BI will prevent the M engine from compensating for operations that cannot be fully
pushed to the underlying data source.
This document builds on the concepts presented in the M Extensibility Reference, and assumes familiarity with the
creation of a basic Data Connector.
Please refer to the SqlODBC sample for most of the code examples in the sections below. Additional samples can
be found in the ODBC samples directory.
FIELD DESCRIPTION
The following table describes the options record fields that are only available via extensibility. Fields that are not
simple literal values are described in subsequent sections.
FIELD DESCRIPTION
Overriding AstVisitor
The AstVisitor field is set through the Odbc.DataSource options record. It is used to modify SQL statements
generated for specific query scenarios.
Note: Drivers that support LIMIT and OFFSET clauses (rather than TOP ) will want to provide a LimitClause
override for AstVisitor.
Constant
Providing an override for this value has been deprecated and may be removed from future implementations.
LimitClause
This field is a function that receives two Int64.Type arguments (skip, take), and returns a record with two text fields
(Text, Location).
The skip parameter is the number of rows to skip (i.e. the argument to OFFSET). If an offset is not specified, the
skip value will be null. If your driver supports LIMIT , but does not support OFFSET , the LimitClause function
should return an unimplemented error (...) when skip is greater than 0.
The take parameter is the number of rows to take (i.e. the argument to LIMIT).
The Text field of the result contains the SQL text to add to the generated query.
The Location field specifies where to insert the clause. The following table describes supported values.
AfterSelect LIMIT goes after the SELECT statement, SELECT DISTINCT LIMIT 5 a, b, c
and after any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
AfterSelectBeforeModifiers LIMIT goes after the SELECT statement, SELECT LIMIT 5 DISTINCT a, b, c
but before any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
The following code snippet provides a LimitClause implementation for a driver that expects a LIMIT clause, with an
optional OFFSET, in the following format: [OFFSET <offset> ROWS] LIMIT <row_count>
The following code snippet provides a LimitClause implementation for a driver that supports LIMIT, but not
OFFSET. Format: LIMIT <row_count> .
Overriding SqlCapabilities
FIELD DETAILS
SupportsTop A logical value which indicates the driver supports the TOP
clause to limit the number of returned rows.
Default: false
Overriding SQLColumns
SQLColumns is a function handler that receives the results of an ODBC call to SQLColumns. The source parameter
contains a table with the data type information. This override is typically used to fix up data type mismatches
between calls to SQLGetTypeInfo and SQLColumns .
For details of the format of the source table parameter, please see: https://docs.microsoft.com/en-
us/sql/odbc/reference/syntax/sqlcolumns-function
Overriding SQLGetFunctions
This field is used to override SQLFunctions values returned by an ODBC driver. It contains a record whose field
names are equal to the FunctionId constants defined for the ODBC SQLGetFunctions function. Numeric constants
for each of these fields can be found in the ODBC specification.
FIELD DETAILS
The following code snippet provides an example explicitly telling the M engine to use CAST rather than CONVERT.
SQLGetFunctions = [
SQL_CONVERT_FUNCTIONS = 0x2 /* SQL_FN_CVT_CAST */
]
Overriding SQLGetInfo
This field is used to override SQLGetInfo values returned by an ODBC driver. It contains a record whose fields are
names are equal to the InfoType constants defined for the ODBC SQLGetInfo function. Numeric constants for each
of these fields can be found in the ODBC specification. The full list of InfoTypes that are checked can be found in
the Mashup Engine trace files.
The following table contains commonly overridden SQLGetInfo properties:
FIELD DETAILS
FIELD DETAILS
The following helper function can be used to create bitmask values from a list of integer values:
Overriding SQLGetTypeInfo
SQLGetTypeInfo can be specified in two ways:
1. A fixed table value that contains the same type information as an ODBC call to SQLGetTypeInfo
2. A function that accepts a table argument, and returns a table. The argument will contain the original results of
the ODBC call to SQLGetTypeInfo . Your function implementation can modify/add to this table.
The first approach is used to completely override the values returned by the ODBC driver. The second approach is
used if you want to add to or modify these values.
For details of the format of the types table parameter and expected return value, please see:
https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlgettypeinfo-function
SQLGetTypeInfo using a static table
The following code snippet provides a static implementation for SQLGetTypeInfo.
SQLGetTypeInfo = #table(
{ "TYPE_NAME", "DATA_TYPE", "COLUMN_SIZE", "LITERAL_PREF", "LITERAL_SUFFIX", "CREATE_PARAS",
"NULLABLE", "CASE_SENSITIVE", "SEARCHABLE", "UNSIGNED_ATTRIBUTE", "FIXED_PREC_SCALE", "AUTO_UNIQUE_VALUE",
"LOCAL_TYPE_NAME", "MINIMUM_SCALE", "MAXIMUM_SCALE", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "NUM_PREC_RADIX",
"INTERNAL_PRECISION", "USER_DATA_TYPE" }, {
Once you have simple queries working, you can then try Direct Query scenarios (i.e. building reports in the Report
Views). The queries generated in Direct Query mode will be significantly more complex (i.e. use of sub-selects,
COALESCE statements, and aggregations).
Concatenation of strings in Direct Query mode
The M engine does basic type size limit validation as part of its query folding logic. If you are receiving a folding
error when trying to concatenate two strings that potentially overflow the maximum size of the underlying
database type:
1. Ensure that your database can support up-conversion to CLOB types when string concat overflow occurs
2. Set the TolerateConcatOverflow option for Odbc.DataSource to true
The DAX CONCATENATE function is currently not supported by Power Query/ODBC extensions. Extension
authors should ensure string concatenation works through the query editor by adding calculated columns (
[stringCol1] & [stringCol2] ). When the capability to fold the CONCATENATE operation is added in the future,
it should work seamlessly with existing extensions.
Enabling Direct Query for an ODBC based connector
7/2/2019 • 22 minutes to read
Overview
Using M's built-in Odbc.DataSource function is the recommended way to create custom connectors for data
sources that have an existing ODBC driver and/or support a SQL query syntax. Wrapping the Odbc.DataSource
function will allow your connector to inherit default query folding behavior based on the capabilities reported by
your driver. This will enable the M engine to generate SQL statements based on filters and other transformations
defined by the user within the Power Query experience, without having to provide this logic within the connector
itself.
ODBC extensions can optionally enable Direct Query mode, allowing Power BI to dynamically generate queries at
runtime without pre-caching the user's data model.
Note: Enabling Direct Query support raises the difficulty and complexity level of your connector. When Direct
Query is enabled, Power BI will prevent the M engine from compensating for operations that cannot be fully
pushed to the underlying data source.
This document builds on the concepts presented in the M Extensibility Reference, and assumes familiarity with the
creation of a basic Data Connector.
Please refer to the SqlODBC sample for most of the code examples in the sections below. Additional samples can
be found in the ODBC samples directory.
FIELD DESCRIPTION
The following table describes the options record fields that are only available via extensibility. Fields that are not
simple literal values are described in subsequent sections.
FIELD DESCRIPTION
Overriding AstVisitor
The AstVisitor field is set through the Odbc.DataSource options record. It is used to modify SQL statements
generated for specific query scenarios.
Note: Drivers that support LIMIT and OFFSET clauses (rather than TOP ) will want to provide a LimitClause
override for AstVisitor.
Constant
Providing an override for this value has been deprecated and may be removed from future implementations.
LimitClause
This field is a function that receives two Int64.Type arguments (skip, take), and returns a record with two text fields
(Text, Location).
The skip parameter is the number of rows to skip (i.e. the argument to OFFSET). If an offset is not specified, the
skip value will be null. If your driver supports LIMIT , but does not support OFFSET , the LimitClause function
should return an unimplemented error (...) when skip is greater than 0.
The take parameter is the number of rows to take (i.e. the argument to LIMIT).
The Text field of the result contains the SQL text to add to the generated query.
The Location field specifies where to insert the clause. The following table describes supported values.
AfterSelect LIMIT goes after the SELECT statement, SELECT DISTINCT LIMIT 5 a, b, c
and after any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
AfterSelectBeforeModifiers LIMIT goes after the SELECT statement, SELECT LIMIT 5 DISTINCT a, b, c
but before any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
The following code snippet provides a LimitClause implementation for a driver that expects a LIMIT clause, with an
optional OFFSET, in the following format: [OFFSET <offset> ROWS] LIMIT <row_count>
The following code snippet provides a LimitClause implementation for a driver that supports LIMIT, but not
OFFSET. Format: LIMIT <row_count> .
Overriding SqlCapabilities
FIELD DETAILS
SupportsTop A logical value which indicates the driver supports the TOP
clause to limit the number of returned rows.
Default: false
Overriding SQLColumns
SQLColumns is a function handler that receives the results of an ODBC call to SQLColumns. The source parameter
contains a table with the data type information. This override is typically used to fix up data type mismatches
between calls to SQLGetTypeInfo and SQLColumns .
For details of the format of the source table parameter, please see: https://docs.microsoft.com/en-
us/sql/odbc/reference/syntax/sqlcolumns-function
Overriding SQLGetFunctions
This field is used to override SQLFunctions values returned by an ODBC driver. It contains a record whose field
names are equal to the FunctionId constants defined for the ODBC SQLGetFunctions function. Numeric constants
for each of these fields can be found in the ODBC specification.
FIELD DETAILS
The following code snippet provides an example explicitly telling the M engine to use CAST rather than CONVERT.
SQLGetFunctions = [
SQL_CONVERT_FUNCTIONS = 0x2 /* SQL_FN_CVT_CAST */
]
Overriding SQLGetInfo
This field is used to override SQLGetInfo values returned by an ODBC driver. It contains a record whose fields are
names are equal to the InfoType constants defined for the ODBC SQLGetInfo function. Numeric constants for each
of these fields can be found in the ODBC specification. The full list of InfoTypes that are checked can be found in
the Mashup Engine trace files.
The following table contains commonly overridden SQLGetInfo properties:
FIELD DETAILS
FIELD DETAILS
The following helper function can be used to create bitmask values from a list of integer values:
Overriding SQLGetTypeInfo
SQLGetTypeInfo can be specified in two ways:
1. A fixed table value that contains the same type information as an ODBC call to SQLGetTypeInfo
2. A function that accepts a table argument, and returns a table. The argument will contain the original results of
the ODBC call to SQLGetTypeInfo . Your function implementation can modify/add to this table.
The first approach is used to completely override the values returned by the ODBC driver. The second approach is
used if you want to add to or modify these values.
For details of the format of the types table parameter and expected return value, please see:
https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlgettypeinfo-function
SQLGetTypeInfo using a static table
The following code snippet provides a static implementation for SQLGetTypeInfo.
SQLGetTypeInfo = #table(
{ "TYPE_NAME", "DATA_TYPE", "COLUMN_SIZE", "LITERAL_PREF", "LITERAL_SUFFIX", "CREATE_PARAS",
"NULLABLE", "CASE_SENSITIVE", "SEARCHABLE", "UNSIGNED_ATTRIBUTE", "FIXED_PREC_SCALE", "AUTO_UNIQUE_VALUE",
"LOCAL_TYPE_NAME", "MINIMUM_SCALE", "MAXIMUM_SCALE", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "NUM_PREC_RADIX",
"INTERNAL_PRECISION", "USER_DATA_TYPE" }, {
Once you have simple queries working, you can then try Direct Query scenarios (i.e. building reports in the Report
Views). The queries generated in Direct Query mode will be significantly more complex (i.e. use of sub-selects,
COALESCE statements, and aggregations).
Concatenation of strings in Direct Query mode
The M engine does basic type size limit validation as part of its query folding logic. If you are receiving a folding
error when trying to concatenate two strings that potentially overflow the maximum size of the underlying
database type:
1. Ensure that your database can support up-conversion to CLOB types when string concat overflow occurs
2. Set the TolerateConcatOverflow option for Odbc.DataSource to true
The DAX CONCATENATE function is currently not supported by Power Query/ODBC extensions. Extension
authors should ensure string concatenation works through the query editor by adding calculated columns (
[stringCol1] & [stringCol2] ). When the capability to fold the CONCATENATE operation is added in the future,
it should work seamlessly with existing extensions.
Enabling Direct Query for an ODBC based connector
7/2/2019 • 22 minutes to read
Overview
Using M's built-in Odbc.DataSource function is the recommended way to create custom connectors for data
sources that have an existing ODBC driver and/or support a SQL query syntax. Wrapping the Odbc.DataSource
function will allow your connector to inherit default query folding behavior based on the capabilities reported by
your driver. This will enable the M engine to generate SQL statements based on filters and other transformations
defined by the user within the Power Query experience, without having to provide this logic within the connector
itself.
ODBC extensions can optionally enable Direct Query mode, allowing Power BI to dynamically generate queries at
runtime without pre-caching the user's data model.
Note: Enabling Direct Query support raises the difficulty and complexity level of your connector. When Direct
Query is enabled, Power BI will prevent the M engine from compensating for operations that cannot be fully
pushed to the underlying data source.
This document builds on the concepts presented in the M Extensibility Reference, and assumes familiarity with the
creation of a basic Data Connector.
Please refer to the SqlODBC sample for most of the code examples in the sections below. Additional samples can
be found in the ODBC samples directory.
FIELD DESCRIPTION
The following table describes the options record fields that are only available via extensibility. Fields that are not
simple literal values are described in subsequent sections.
FIELD DESCRIPTION
Overriding AstVisitor
The AstVisitor field is set through the Odbc.DataSource options record. It is used to modify SQL statements
generated for specific query scenarios.
Note: Drivers that support LIMIT and OFFSET clauses (rather than TOP ) will want to provide a LimitClause
override for AstVisitor.
Constant
Providing an override for this value has been deprecated and may be removed from future implementations.
LimitClause
This field is a function that receives two Int64.Type arguments (skip, take), and returns a record with two text fields
(Text, Location).
The skip parameter is the number of rows to skip (i.e. the argument to OFFSET). If an offset is not specified, the
skip value will be null. If your driver supports LIMIT , but does not support OFFSET , the LimitClause function
should return an unimplemented error (...) when skip is greater than 0.
The take parameter is the number of rows to take (i.e. the argument to LIMIT).
The Text field of the result contains the SQL text to add to the generated query.
The Location field specifies where to insert the clause. The following table describes supported values.
AfterSelect LIMIT goes after the SELECT statement, SELECT DISTINCT LIMIT 5 a, b, c
and after any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
AfterSelectBeforeModifiers LIMIT goes after the SELECT statement, SELECT LIMIT 5 DISTINCT a, b, c
but before any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
The following code snippet provides a LimitClause implementation for a driver that expects a LIMIT clause, with an
optional OFFSET, in the following format: [OFFSET <offset> ROWS] LIMIT <row_count>
The following code snippet provides a LimitClause implementation for a driver that supports LIMIT, but not
OFFSET. Format: LIMIT <row_count> .
Overriding SqlCapabilities
FIELD DETAILS
SupportsTop A logical value which indicates the driver supports the TOP
clause to limit the number of returned rows.
Default: false
Overriding SQLColumns
SQLColumns is a function handler that receives the results of an ODBC call to SQLColumns. The source parameter
contains a table with the data type information. This override is typically used to fix up data type mismatches
between calls to SQLGetTypeInfo and SQLColumns .
For details of the format of the source table parameter, please see: https://docs.microsoft.com/en-
us/sql/odbc/reference/syntax/sqlcolumns-function
Overriding SQLGetFunctions
This field is used to override SQLFunctions values returned by an ODBC driver. It contains a record whose field
names are equal to the FunctionId constants defined for the ODBC SQLGetFunctions function. Numeric constants
for each of these fields can be found in the ODBC specification.
FIELD DETAILS
The following code snippet provides an example explicitly telling the M engine to use CAST rather than CONVERT.
SQLGetFunctions = [
SQL_CONVERT_FUNCTIONS = 0x2 /* SQL_FN_CVT_CAST */
]
Overriding SQLGetInfo
This field is used to override SQLGetInfo values returned by an ODBC driver. It contains a record whose fields are
names are equal to the InfoType constants defined for the ODBC SQLGetInfo function. Numeric constants for each
of these fields can be found in the ODBC specification. The full list of InfoTypes that are checked can be found in
the Mashup Engine trace files.
The following table contains commonly overridden SQLGetInfo properties:
FIELD DETAILS
FIELD DETAILS
The following helper function can be used to create bitmask values from a list of integer values:
Overriding SQLGetTypeInfo
SQLGetTypeInfo can be specified in two ways:
1. A fixed table value that contains the same type information as an ODBC call to SQLGetTypeInfo
2. A function that accepts a table argument, and returns a table. The argument will contain the original results of
the ODBC call to SQLGetTypeInfo . Your function implementation can modify/add to this table.
The first approach is used to completely override the values returned by the ODBC driver. The second approach is
used if you want to add to or modify these values.
For details of the format of the types table parameter and expected return value, please see:
https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlgettypeinfo-function
SQLGetTypeInfo using a static table
The following code snippet provides a static implementation for SQLGetTypeInfo.
SQLGetTypeInfo = #table(
{ "TYPE_NAME", "DATA_TYPE", "COLUMN_SIZE", "LITERAL_PREF", "LITERAL_SUFFIX", "CREATE_PARAS",
"NULLABLE", "CASE_SENSITIVE", "SEARCHABLE", "UNSIGNED_ATTRIBUTE", "FIXED_PREC_SCALE", "AUTO_UNIQUE_VALUE",
"LOCAL_TYPE_NAME", "MINIMUM_SCALE", "MAXIMUM_SCALE", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "NUM_PREC_RADIX",
"INTERNAL_PRECISION", "USER_DATA_TYPE" }, {
Once you have simple queries working, you can then try Direct Query scenarios (i.e. building reports in the Report
Views). The queries generated in Direct Query mode will be significantly more complex (i.e. use of sub-selects,
COALESCE statements, and aggregations).
Concatenation of strings in Direct Query mode
The M engine does basic type size limit validation as part of its query folding logic. If you are receiving a folding
error when trying to concatenate two strings that potentially overflow the maximum size of the underlying
database type:
1. Ensure that your database can support up-conversion to CLOB types when string concat overflow occurs
2. Set the TolerateConcatOverflow option for Odbc.DataSource to true
The DAX CONCATENATE function is currently not supported by Power Query/ODBC extensions. Extension
authors should ensure string concatenation works through the query editor by adding calculated columns (
[stringCol1] & [stringCol2] ). When the capability to fold the CONCATENATE operation is added in the future,
it should work seamlessly with existing extensions.
Enabling Direct Query for an ODBC based connector
7/2/2019 • 22 minutes to read
Overview
Using M's built-in Odbc.DataSource function is the recommended way to create custom connectors for data
sources that have an existing ODBC driver and/or support a SQL query syntax. Wrapping the Odbc.DataSource
function will allow your connector to inherit default query folding behavior based on the capabilities reported by
your driver. This will enable the M engine to generate SQL statements based on filters and other transformations
defined by the user within the Power Query experience, without having to provide this logic within the connector
itself.
ODBC extensions can optionally enable Direct Query mode, allowing Power BI to dynamically generate queries at
runtime without pre-caching the user's data model.
Note: Enabling Direct Query support raises the difficulty and complexity level of your connector. When Direct
Query is enabled, Power BI will prevent the M engine from compensating for operations that cannot be fully
pushed to the underlying data source.
This document builds on the concepts presented in the M Extensibility Reference, and assumes familiarity with the
creation of a basic Data Connector.
Please refer to the SqlODBC sample for most of the code examples in the sections below. Additional samples can
be found in the ODBC samples directory.
FIELD DESCRIPTION
The following table describes the options record fields that are only available via extensibility. Fields that are not
simple literal values are described in subsequent sections.
FIELD DESCRIPTION
Overriding AstVisitor
The AstVisitor field is set through the Odbc.DataSource options record. It is used to modify SQL statements
generated for specific query scenarios.
Note: Drivers that support LIMIT and OFFSET clauses (rather than TOP ) will want to provide a LimitClause
override for AstVisitor.
Constant
Providing an override for this value has been deprecated and may be removed from future implementations.
LimitClause
This field is a function that receives two Int64.Type arguments (skip, take), and returns a record with two text fields
(Text, Location).
The skip parameter is the number of rows to skip (i.e. the argument to OFFSET). If an offset is not specified, the
skip value will be null. If your driver supports LIMIT , but does not support OFFSET , the LimitClause function
should return an unimplemented error (...) when skip is greater than 0.
The take parameter is the number of rows to take (i.e. the argument to LIMIT).
The Text field of the result contains the SQL text to add to the generated query.
The Location field specifies where to insert the clause. The following table describes supported values.
AfterSelect LIMIT goes after the SELECT statement, SELECT DISTINCT LIMIT 5 a, b, c
and after any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
AfterSelectBeforeModifiers LIMIT goes after the SELECT statement, SELECT LIMIT 5 DISTINCT a, b, c
but before any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
The following code snippet provides a LimitClause implementation for a driver that expects a LIMIT clause, with an
optional OFFSET, in the following format: [OFFSET <offset> ROWS] LIMIT <row_count>
The following code snippet provides a LimitClause implementation for a driver that supports LIMIT, but not
OFFSET. Format: LIMIT <row_count> .
Overriding SqlCapabilities
FIELD DETAILS
SupportsTop A logical value which indicates the driver supports the TOP
clause to limit the number of returned rows.
Default: false
Overriding SQLColumns
SQLColumns is a function handler that receives the results of an ODBC call to SQLColumns. The source parameter
contains a table with the data type information. This override is typically used to fix up data type mismatches
between calls to SQLGetTypeInfo and SQLColumns .
For details of the format of the source table parameter, please see: https://docs.microsoft.com/en-
us/sql/odbc/reference/syntax/sqlcolumns-function
Overriding SQLGetFunctions
This field is used to override SQLFunctions values returned by an ODBC driver. It contains a record whose field
names are equal to the FunctionId constants defined for the ODBC SQLGetFunctions function. Numeric constants
for each of these fields can be found in the ODBC specification.
FIELD DETAILS
The following code snippet provides an example explicitly telling the M engine to use CAST rather than CONVERT.
SQLGetFunctions = [
SQL_CONVERT_FUNCTIONS = 0x2 /* SQL_FN_CVT_CAST */
]
Overriding SQLGetInfo
This field is used to override SQLGetInfo values returned by an ODBC driver. It contains a record whose fields are
names are equal to the InfoType constants defined for the ODBC SQLGetInfo function. Numeric constants for each
of these fields can be found in the ODBC specification. The full list of InfoTypes that are checked can be found in
the Mashup Engine trace files.
The following table contains commonly overridden SQLGetInfo properties:
FIELD DETAILS
FIELD DETAILS
The following helper function can be used to create bitmask values from a list of integer values:
Overriding SQLGetTypeInfo
SQLGetTypeInfo can be specified in two ways:
1. A fixed table value that contains the same type information as an ODBC call to SQLGetTypeInfo
2. A function that accepts a table argument, and returns a table. The argument will contain the original results of
the ODBC call to SQLGetTypeInfo . Your function implementation can modify/add to this table.
The first approach is used to completely override the values returned by the ODBC driver. The second approach is
used if you want to add to or modify these values.
For details of the format of the types table parameter and expected return value, please see:
https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlgettypeinfo-function
SQLGetTypeInfo using a static table
The following code snippet provides a static implementation for SQLGetTypeInfo.
SQLGetTypeInfo = #table(
{ "TYPE_NAME", "DATA_TYPE", "COLUMN_SIZE", "LITERAL_PREF", "LITERAL_SUFFIX", "CREATE_PARAS",
"NULLABLE", "CASE_SENSITIVE", "SEARCHABLE", "UNSIGNED_ATTRIBUTE", "FIXED_PREC_SCALE", "AUTO_UNIQUE_VALUE",
"LOCAL_TYPE_NAME", "MINIMUM_SCALE", "MAXIMUM_SCALE", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "NUM_PREC_RADIX",
"INTERNAL_PRECISION", "USER_DATA_TYPE" }, {
Once you have simple queries working, you can then try Direct Query scenarios (i.e. building reports in the Report
Views). The queries generated in Direct Query mode will be significantly more complex (i.e. use of sub-selects,
COALESCE statements, and aggregations).
Concatenation of strings in Direct Query mode
The M engine does basic type size limit validation as part of its query folding logic. If you are receiving a folding
error when trying to concatenate two strings that potentially overflow the maximum size of the underlying
database type:
1. Ensure that your database can support up-conversion to CLOB types when string concat overflow occurs
2. Set the TolerateConcatOverflow option for Odbc.DataSource to true
The DAX CONCATENATE function is currently not supported by Power Query/ODBC extensions. Extension
authors should ensure string concatenation works through the query editor by adding calculated columns (
[stringCol1] & [stringCol2] ). When the capability to fold the CONCATENATE operation is added in the future,
it should work seamlessly with existing extensions.
Enabling Direct Query for an ODBC based connector
7/2/2019 • 22 minutes to read
Overview
Using M's built-in Odbc.DataSource function is the recommended way to create custom connectors for data
sources that have an existing ODBC driver and/or support a SQL query syntax. Wrapping the Odbc.DataSource
function will allow your connector to inherit default query folding behavior based on the capabilities reported by
your driver. This will enable the M engine to generate SQL statements based on filters and other transformations
defined by the user within the Power Query experience, without having to provide this logic within the connector
itself.
ODBC extensions can optionally enable Direct Query mode, allowing Power BI to dynamically generate queries at
runtime without pre-caching the user's data model.
Note: Enabling Direct Query support raises the difficulty and complexity level of your connector. When Direct
Query is enabled, Power BI will prevent the M engine from compensating for operations that cannot be fully
pushed to the underlying data source.
This document builds on the concepts presented in the M Extensibility Reference, and assumes familiarity with the
creation of a basic Data Connector.
Please refer to the SqlODBC sample for most of the code examples in the sections below. Additional samples can
be found in the ODBC samples directory.
FIELD DESCRIPTION
The following table describes the options record fields that are only available via extensibility. Fields that are not
simple literal values are described in subsequent sections.
FIELD DESCRIPTION
Overriding AstVisitor
The AstVisitor field is set through the Odbc.DataSource options record. It is used to modify SQL statements
generated for specific query scenarios.
Note: Drivers that support LIMIT and OFFSET clauses (rather than TOP ) will want to provide a LimitClause
override for AstVisitor.
Constant
Providing an override for this value has been deprecated and may be removed from future implementations.
LimitClause
This field is a function that receives two Int64.Type arguments (skip, take), and returns a record with two text fields
(Text, Location).
The skip parameter is the number of rows to skip (i.e. the argument to OFFSET). If an offset is not specified, the
skip value will be null. If your driver supports LIMIT , but does not support OFFSET , the LimitClause function
should return an unimplemented error (...) when skip is greater than 0.
The take parameter is the number of rows to take (i.e. the argument to LIMIT).
The Text field of the result contains the SQL text to add to the generated query.
The Location field specifies where to insert the clause. The following table describes supported values.
AfterSelect LIMIT goes after the SELECT statement, SELECT DISTINCT LIMIT 5 a, b, c
and after any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
AfterSelectBeforeModifiers LIMIT goes after the SELECT statement, SELECT LIMIT 5 DISTINCT a, b, c
but before any modifiers (such as
DISTINCT). FROM table
WHERE a > 10
The following code snippet provides a LimitClause implementation for a driver that expects a LIMIT clause, with an
optional OFFSET, in the following format: [OFFSET <offset> ROWS] LIMIT <row_count>
The following code snippet provides a LimitClause implementation for a driver that supports LIMIT, but not
OFFSET. Format: LIMIT <row_count> .
Overriding SqlCapabilities
FIELD DETAILS
SupportsTop A logical value which indicates the driver supports the TOP
clause to limit the number of returned rows.
Default: false
Overriding SQLColumns
SQLColumns is a function handler that receives the results of an ODBC call to SQLColumns. The source parameter
contains a table with the data type information. This override is typically used to fix up data type mismatches
between calls to SQLGetTypeInfo and SQLColumns .
For details of the format of the source table parameter, please see: https://docs.microsoft.com/en-
us/sql/odbc/reference/syntax/sqlcolumns-function
Overriding SQLGetFunctions
This field is used to override SQLFunctions values returned by an ODBC driver. It contains a record whose field
names are equal to the FunctionId constants defined for the ODBC SQLGetFunctions function. Numeric constants
for each of these fields can be found in the ODBC specification.
FIELD DETAILS
The following code snippet provides an example explicitly telling the M engine to use CAST rather than CONVERT.
SQLGetFunctions = [
SQL_CONVERT_FUNCTIONS = 0x2 /* SQL_FN_CVT_CAST */
]
Overriding SQLGetInfo
This field is used to override SQLGetInfo values returned by an ODBC driver. It contains a record whose fields are
names are equal to the InfoType constants defined for the ODBC SQLGetInfo function. Numeric constants for each
of these fields can be found in the ODBC specification. The full list of InfoTypes that are checked can be found in
the Mashup Engine trace files.
The following table contains commonly overridden SQLGetInfo properties:
FIELD DETAILS
FIELD DETAILS
The following helper function can be used to create bitmask values from a list of integer values:
Overriding SQLGetTypeInfo
SQLGetTypeInfo can be specified in two ways:
1. A fixed table value that contains the same type information as an ODBC call to SQLGetTypeInfo
2. A function that accepts a table argument, and returns a table. The argument will contain the original results of
the ODBC call to SQLGetTypeInfo . Your function implementation can modify/add to this table.
The first approach is used to completely override the values returned by the ODBC driver. The second approach is
used if you want to add to or modify these values.
For details of the format of the types table parameter and expected return value, please see:
https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlgettypeinfo-function
SQLGetTypeInfo using a static table
The following code snippet provides a static implementation for SQLGetTypeInfo.
SQLGetTypeInfo = #table(
{ "TYPE_NAME", "DATA_TYPE", "COLUMN_SIZE", "LITERAL_PREF", "LITERAL_SUFFIX", "CREATE_PARAS",
"NULLABLE", "CASE_SENSITIVE", "SEARCHABLE", "UNSIGNED_ATTRIBUTE", "FIXED_PREC_SCALE", "AUTO_UNIQUE_VALUE",
"LOCAL_TYPE_NAME", "MINIMUM_SCALE", "MAXIMUM_SCALE", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "NUM_PREC_RADIX",
"INTERNAL_PRECISION", "USER_DATA_TYPE" }, {
Once you have simple queries working, you can then try Direct Query scenarios (i.e. building reports in the Report
Views). The queries generated in Direct Query mode will be significantly more complex (i.e. use of sub-selects,
COALESCE statements, and aggregations).
Concatenation of strings in Direct Query mode
The M engine does basic type size limit validation as part of its query folding logic. If you are receiving a folding
error when trying to concatenate two strings that potentially overflow the maximum size of the underlying
database type:
1. Ensure that your database can support up-conversion to CLOB types when string concat overflow occurs
2. Set the TolerateConcatOverflow option for Odbc.DataSource to true
The DAX CONCATENATE function is currently not supported by Power Query/ODBC extensions. Extension
authors should ensure string concatenation works through the query editor by adding calculated columns (
[stringCol1] & [stringCol2] ). When the capability to fold the CONCATENATE operation is added in the future,
it should work seamlessly with existing extensions.
Handling Resource Path
7/2/2019 • 2 minutes to read
The M engine identifies a data source using a combination of its Kind and Path. When a data source is encountered
during a query evaluation, the M engine will try to find matching credentials. If no credentials are found, the engine
returns a special error which results in a credential prompt in Power Query.
The Kind value comes from Data Source Kind definition.
The Path value is derived from the required parameters of your data source function(s). Optional parameters are
not factored into the data source path identifier. As a result, all data source functions associated with a data source
kind must have the same parameters. There is special handling for functions that have a single parameter of type
Uri.Type . See below for further details.
You can see an example of how credentials are stored in the Data source settings dialog in Power BI Desktop. In this
dialog, the Kind is represented by an icon, and the Path value is displayed as text.
Note: If you change your data source function's required parameters during development, previously stored
credentials will no longer work (because the path values no longer match). You should delete any stored
credentials any time you change your data source function parameters. If incompatible credentials are found,
you may receive an error at runtime.
The function has a single required parameter ( message ) of type text , and will be used to calculate the data source
path. The optional parameter ( count ) will be ignored. The path would be displayed as follows:
Credential prompt:
When a Label value is defined, the data source path value would not be shown:
Note: We currently recommend that you do not inlcude a Label for your data source if your function has
required parameters, as users will not be able to distinguish between the different credentials they have
entered. We are hoping to improve this in the future (i.e., allowing data connectors to display their own custom
data source paths).
As Uri.Type is an ascribed type rather than a primitive type in the M language, you will need to use the
Value.ReplaceType function in indicate that your text parameter should be treated as a Uri.
REST APIs typically have some mechanism to transmit large volumes of records broken up into pages of results.
Power Query has the flexibility to support many different paging mechanisms, however, since each paging
mechanism is different, some amount of modification of the below examples is likely to be necessary to fit your
situation.
Typical Patterns
The heavy lifting of compiling all page results into a single table is performed by the Table.GenerateByPage() helper
function, which can generally be used with no modification. The following code snippets describe how to
implement some common paging patterns. Regardless of pattern, you will need to understand:
1. How do we request the next page of data?
2. Does the paging mechanism involve calculating values, or do we extract the URL for the next page from the
response?
3. How do we know when to stop paging?
4. Are there parameters related to paging (such as "page size") that we should be aware of?
Handling Transformations
7/2/2019 • 3 minutes to read
For situations where the data source response is not presented in a format that Power BI can consume directly,
Power Query can be used to perform a series of transformations.
Static Transformations
In most cases, the data is presented in a consistent way by the data source: column names, data types, and
hierarchical structure are consistent for a given endpoint. In this situation it is appropriate to always apply the same
set of transformations to get the data in a format acceptable to Power BI.
An example of static transformation can be found in the TripPin Part 2 - Data Connector for a REST Service tutorial
when the data source is treated as a standard REST service:
let
Source = TripPin.Feed("https://services.odata.org/v4/TripPinService/Airlines"),
value = Source[value],
toTable = Table.FromList(value, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
expand = Table.ExpandRecordColumn(toTable, "Column1", {"AirlineCode", "Name"}, {"AirlineCode", "Name"})
in
expand
It is important to note that a sequence of static transformations of this specificity are only applicable to a single
endpoint. In the example above, this sequence of transformations will only work if "AirlineCode" and "Name" exist
in the REST endpoint response since they are hard-coded into the M code. Thus, this sequence of transformations
may not work if we try to hit the /Event endpoint.
This high level of specificity may be necessary for pushing data to a navigation table, but for more general data
access functions it is recommended that you only perform transformations that are appropriate for all endpoints.
Note: Be sure to test transformations under a variety of data circumstances. If the user doesn't have any data at
the /airlines endpoint, do your transformations result in an empty table with the correct schema? Or is an
error encountered during evaluation? See TripPin Part 7: Advanced Schema with M Types for a discussion on
unit testing.
Dynamic Transformations
More complex logic is sometimes needed to convert API responses into stable and consistent forms appropriate
for Power BI data models.
Inconsistent API Responses
Basic M control flow (if statements, HTTP status codes, try...catch blocks, etc) are typically sufficient to handle
situations where there are a handful of ways in which the API responds.
Determining Schema On-The -Fly
Some APIs are designed such that multiple pieces of information must be combined to get the correct tabular
format. Consider Smartsheet's /sheets endpoint response which contains an array of column names and an array
of data rows. The Smartsheet Connector is able to parse this response in the following way:
raw = Web.Contents(...),
columns = raw[columns],
columnTitles = List.Transform(columns, each [title]),
columnTitlesWithRowNumber = List.InsertRange(columnTitles, 0, {"RowNumber"}),
1. First deal with column header information. We pull the title record of each column into a List, prepending
with a RowNumber column that we know will always be represented as this first column.
2. Next we define a function that allows us to parse a row into a List of cell value s. We again prepend rowNumber
information.
3. Apply our RowAsList() function to each of the row s returned in the API response.
4. Convert the List to a table, specifying the column headers.
Handling Transformations
7/2/2019 • 3 minutes to read
For situations where the data source response is not presented in a format that Power BI can consume directly,
Power Query can be used to perform a series of transformations.
Static Transformations
In most cases, the data is presented in a consistent way by the data source: column names, data types, and
hierarchical structure are consistent for a given endpoint. In this situation it is appropriate to always apply the same
set of transformations to get the data in a format acceptable to Power BI.
An example of static transformation can be found in the TripPin Part 2 - Data Connector for a REST Service tutorial
when the data source is treated as a standard REST service:
let
Source = TripPin.Feed("https://services.odata.org/v4/TripPinService/Airlines"),
value = Source[value],
toTable = Table.FromList(value, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
expand = Table.ExpandRecordColumn(toTable, "Column1", {"AirlineCode", "Name"}, {"AirlineCode", "Name"})
in
expand
It is important to note that a sequence of static transformations of this specificity are only applicable to a single
endpoint. In the example above, this sequence of transformations will only work if "AirlineCode" and "Name" exist
in the REST endpoint response since they are hard-coded into the M code. Thus, this sequence of transformations
may not work if we try to hit the /Event endpoint.
This high level of specificity may be necessary for pushing data to a navigation table, but for more general data
access functions it is recommended that you only perform transformations that are appropriate for all endpoints.
Note: Be sure to test transformations under a variety of data circumstances. If the user doesn't have any data at
the /airlines endpoint, do your transformations result in an empty table with the correct schema? Or is an
error encountered during evaluation? See TripPin Part 7: Advanced Schema with M Types for a discussion on
unit testing.
Dynamic Transformations
More complex logic is sometimes needed to convert API responses into stable and consistent forms appropriate
for Power BI data models.
Inconsistent API Responses
Basic M control flow (if statements, HTTP status codes, try...catch blocks, etc) are typically sufficient to handle
situations where there are a handful of ways in which the API responds.
Determining Schema On-The -Fly
Some APIs are designed such that multiple pieces of information must be combined to get the correct tabular
format. Consider Smartsheet's /sheets endpoint response which contains an array of column names and an array
of data rows. The Smartsheet Connector is able to parse this response in the following way:
raw = Web.Contents(...),
columns = raw[columns],
columnTitles = List.Transform(columns, each [title]),
columnTitlesWithRowNumber = List.InsertRange(columnTitles, 0, {"RowNumber"}),
1. First deal with column header information. We pull the title record of each column into a List, prepending
with a RowNumber column that we know will always be represented as this first column.
2. Next we define a function that allows us to parse a row into a List of cell value s. We again prepend rowNumber
information.
3. Apply our RowAsList() function to each of the row s returned in the API response.
4. Convert the List to a table, specifying the column headers.
Handling Schema
7/2/2019 • 7 minutes to read
Depending on your data source, information about data types and column names may or may not be provided
explicitly. OData REST APIs typically handle this via the $metadata definition, and the Power Query OData.Feed
method automatically handles parsing this information and applying it to the data returned from an OData source.
Many REST APIs do not have a way to programmatically determine their schema. In these cases you will need to
include schema definition in your connector.
Consider the following code that returns a simple table from the TripPin OData sample service:
let
url = "https://services.odata.org/TripPinWebApiService/Airlines",
source = Json.Document(Web.Contents(url))[value],
asTable = Table.FromRecords(source)
in
asTable
Note: TripPin is an OData source, so realistically it would make more sense to simply use the OData.Feed
function's automatic schema handling. In this example we are treating the source as a typical REST API and
using Web.Contents to demonstrate the technique of hardcoding a schema by hand.
We can use the handy Table.Schema function to check the data type of the columns:
let
url = "https://services.odata.org/TripPinWebApiService/Airlines",
source = Json.Document(Web.Contents(url))[value],
asTable = Table.FromRecords(source)
in
Table.Schema(asTable)
Both AirlineCode and Name are of any type. Table.Schema returns a lot of metadata about the columns in a table,
including names, positions, type information, and many advanced properties such as Precision, Scale, and
MaxLength. For now we will only concern ourselves with the ascribed type ( TypeName ), primitive type ( Kind ), and
whether the column value might be null ( IsNullable ).
Defining a Simple Schema Table
Our schema table will be composed of two columns:
COLUMN DETAILS
Name The name of the column. This must match the name in the
results returned by the service.
Type The M data type we are going to set. This can be a primitive
type (text, number, datetime, etc), or an ascribed type
(Int64.Type, Currency.Type, etc).
The hardcoded schema table for the Airlines table will set its AirlineCode and Name columns to text and looks
like this:
As we look to some of the other endpoints, consider the following schema tables:
The Airports table has four fields we want to keep (including one of type record ):
The People table has seven fields, including list s ( Emails , AddressInfo ), a nullable column ( Gender ), and a
column with an ascribed type ( Concurrency ):
We will put all of these tables into a single master schema table SchemaTable :
SchemaTable = #table({"Entity", "SchemaTable"}, {
{"Airlines", Airlines},
{"Airports", Airports},
{"People", People}
})
Sophisticated Approach
The hardcoded implementation discussed above does a good job of making sure that schemas remain consistent
for simple JSON repsonses, but it is limited to parsing the first level of the response. Deeply nested data sets would
benefit from the following approach which takes advantage of M Types.
Here is a quick refresh about types in the M language from the Language Specification:
A type value is a value that classifies other values. A value that is classified by a type is said to conform to
that type. The M type system consists of the following kinds of types:
Primitive types, which classify primitive values ( binary , date , datetime , datetimezone , duration , list ,
logical , null , number , record , text , time , type ) and also include a number of abstract types (
function , table , any , and none )
Record types, which classify record values based on field names and value types
List types, which classify lists using a single item base type
Function types, which classify function values based on the types of their parameters and return values
Table types, which classify table values based on column names, column types, and keys
Nullable types, which classifies the value null in addition to all the values classified by a base type
Type types, which classify values that are types
Using the raw json output we get (and/or by looking up the definitions in the service's $metadata) we can define
the following record types to represent OData complex types:
LocationType = type [
Address = text,
City = CityType,
Loc = LocType
];
CityType = type [
CountryRegion = text,
Name = text,
Region = text
];
LocType = type [
#"type" = text,
coordinates = {number},
crs = CrsType
];
CrsType = type [
#"type" = text,
properties = record
];
Notice how LocationType references the CityType and LocType to represent its structured columns.
For the top-level entities that we want represented as Tables, we define table types:
We then update our SchemaTable variable (which we use as a lookup table for entity-to-type mappings) to use
these new type definitions:
We will rely on a common function ( Table.ChangeType ) to enforce a schema on our data, much like we used
SchemaTransformTable in the earlier exercise. Unlike SchemaTransformTable , Table.ChangeType takes an actual M
table type as an argument, and will apply our schema recursively for all nested types. Its signature is:
We then need to update the connector code to change the schema parameter from a table to a type , and add a
call to Table.ChangeType . Again, the details for doing so are very implementation-specific and thus not worth going
into in detail here. This extended TripPin connector example demonstrates how an end-to-end solution
implementing this more sophisticated approach to handling schema.
Status Code Handling with Web.Contents
7/2/2019 • 2 minutes to read
The Web.Contents function has some built in functionality for dealing with certain HTTP status codes. The default
behavior can be overridden in your extension using the ManualStatusHandling field in the options record.
Automatic retry
Web.Contents will automatically retry requests that fail with one of the following status codes:
CODE STATUS
Requests will be retried up to 3 times before failing. The engine uses an exponential back-off algorithm to
determine how long to wait until the next retry, unless the response contains a Retry-after header. When the
header is found, the engine will wait the specified number of seconds before the next retry. The minimum
supported wait time is 0.5 seconds, and the maximum value is 120 seconds.
Note: The Retry-after value must be in the delta-seconds format. The HTTP-date format is currently not
supported.
Authentication exceptions
The following status codes will result in a credentials exception, causing an authentication prompt asking the user
to provide credentials (or re-login in the cause of an expired OAuth token).
CODE STATUS
401 Unauthorized
403 Forbidden
Note: Extensions are able to use the ManualStatusHandling option with status codes 401 and 403, which is not
something that can be done in Web.Contents calls made outside of an extension context (i.e. directly from
Power Query).
Redirection
The follow status codes will result in an automatic redirect to the URI specified in the Location header. A missing
Location header will result in an error.
CODE STATUS
302 Found
Note: Only status code 307 will keep a POST request method. All other redirect status codes will result in a
switch to GET .
Wait-Retry Pattern
3/5/2019 • 2 minutes to read
In some situations a data source's behavior does not match that expected by Power Query's default HTTP code
handling. The examples below show how to work around this situation.
In this scenario we are working with a REST API that occassionally returns a 500 status code indicating an internal
server error. In these instances, we would like to wait a few seconds and retry, potentially a few times before we
give up.
ManualStatusHandling
If Web.Contents gets a 500 status code response it throws a DataSource.Error by default. We can override this
behavior by providing a list of codes as an optional argument to Web.Contents :
By specifying the status codes in this way, Power Query will continue to process the web response as normal.
However, normal response processing is often not appropriate in these cases. We need to understand that an
abnormal response code has been received and perform special logic to handle it. To determine the response code
that was returned from the web service, we can access it from the meta Record that accompanies the response:
responseCode = Value.Metadata(response)[Response.Status]
Based on whether responseCode is 200 or 500 we can either process the result as normal or follow our wait-retry
logic that we flesh out in the next section.
Note: It is recommended to use Binary.Buffer to force Power Query to cache the Web.Contents results if you
will be implementing complex logic such as the Wait-Retry pattern shown here. This prevents Power Query's
multi-threaded execution from making multiple calls with potentially inconsistent results.
Value.WaitFor
Value.WaitFor() is a standard helper function that can usually be used with no modification. It works by building a
List of retry attempts.
producer Argument
This contains the task to be (possibly) retried. It is represented as a function so that the iteration number can be
used in the producer logic. The expected behavior is that producer will return null if a retry is determined to be
necessary. If anything other than null is returned by producer , that value is in turn returned by Value.WaitFor .
delay Argument
This contains the logic to execute when between retries. It is represented as a function so that the iteration number
can be used in the delay logic. The expected behavior is that delay returns a Duration.
count Argument (optional)
A maximum number of retries can be set by providing a number to the count argument.
Putting It All Together
The following example shows how how ManualStatusHandling and Value.WaitFor can be used to implement a
delayed retry in the event of a 500 response. Wait time between retries here is shown as doubling with each try,
with a maximum of 5 retries.
let
waitForResult = Value.WaitFor(
(iteration) =>
let
result = Web.Contents(url, [ManualStatusHandling = {500}]),
buffered = Binary.Buffer(result),
status = Value.Metadata(result)[Response.Status],
actualResult = if status = 500 then null else buffered
in
actualResult,
(iteration) => #duration(0, 0, 0, Number.Power(2, iteration)),
5)
in
waitForResult,
Handling Unit Testing
3/5/2019 • 2 minutes to read
For both simple and complex connectors, adding unit tests is a best practice and highly recommended.
Unit testing is accomplished in the context of Visual Studio's Power Query SDK. Each test is defined as a Fact that
has a name, an expected value, and an actual value. In most cases, the "actual value" will be an M expression that
tests part of your expression.
Consider a very simple extension that exports three functions:
section Unittesting;
Our unit test code is made up of a number of Facts, and a bunch of common code for the unit test framework (
ValueToText , Fact , Facts , Facts.Summarize ). The following code provides an example set of Facts (please see
UnitTesting.query.pq for the common code):
section UnitTestingTests;
shared MyExtension.UnitTest =
[
// Put any common variables here if you only want them to be evaluated once
Running the sample in Visual Studio will evaluate all of the Facts and give you a visual summary of the pass rates:
Implementing unit testing early in the connector development process enables you to follow the principles of test-
driven development. Imagine that you need to write a function called Uri.GetHost that returns only the host data
from a URI. You might start by writing a test case to verify that the function appropriately performs the expected
function:
Additional tests can be written to ensure that the function appropriately handles edge cases.
An early version of the function might pass some but not all tests:
The final version of the function should pass all unit tests. This also makes it easy to ensure that future updates to
the function do not accidentally remove any of its basic functionality.
Helper Functions
7/2/2019 • 10 minutes to read
This file contains a number of helper functions commonly used in M extensions. These functions may eventually be
moved to the official M library, but for now can be copied into your extension file code. You should not mark any of
these functions as shared within your extension code.
Navigation Tables
Table.ToNavigationTable
This function adds the table type metadata needed for your extension to return a table value that Power Query can
recognize as a Navigation Tree. Please see Navigation Tables for more information.
Table.ToNavigationTable = (
table as table,
keyColumns as list,
nameColumn as text,
dataColumn as text,
itemKindColumn as text,
itemNameColumn as text,
isLeafColumn as text
) as table =>
let
tableType = Value.Type(table),
newTableType = Type.AddTableKey(tableType, keyColumns, true) meta
[
NavigationTable.NameColumn = nameColumn,
NavigationTable.DataColumn = dataColumn,
NavigationTable.ItemKindColumn = itemKindColumn,
Preview.DelayColumn = itemNameColumn,
NavigationTable.IsLeafColumn = isLeafColumn
],
navigationTable = Value.ReplaceType(table, newTableType)
in
navigationTable;
PARAMETER DETAILS
keyColumns List of column names that act as the primary key for your
navigation table
nameColumn The name of the column that should be used as the display
name in the navigator
dataColumn The name of the column that contains the Table or Function to
display
itemKindColumn The name of the column to use to determine the type of icon
to display. Valid values for the column are Table and
Function .
PARAMETER DETAILS
Example usage:
URI Manipulation
Uri.FromParts
This function constructs a full URL based on individual fields in the record. It acts as the reverse of Uri.Parts.
Uri.GetHost
This function returns the scheme, host, and default port (for HTTP/HTTPS ) for a given URL. For example,
https://bing.com/subpath/query?param=1¶m2=hello would become https://bing.com:443 .
ValidateUrlScheme
This function checks if the user entered an HTTPS url and raises an error if they don't. This is required for user
entered URLs for certified connectors.
ValidateUrlScheme = (url as text) as text => if (Uri.Parts(url)[Scheme] <> "https") then error "Url scheme must
be HTTPS" else url;
To apply it, just wrap your url parameter in your data access function.
Retrieving Data
Value.WaitFor
This function is useful when making an asynchronous HTTP request, and you need to poll the server until the
request is complete.
Value.WaitFor = (producer as function, interval as function, optional count as number) as any =>
let
list = List.Generate(
() => {0, null},
(state) => state{0} <> null and (count = null or state{0} < count),
(state) => if state{1} <> null then {null, state{1}} else {1 + state{0}, Function.InvokeAfter(() =>
producer(state{0}), interval(state{0}))},
(state) => state{1})
in
List.Last(list);
Table.GenerateByPage
This function is used when an API returns data in an incremental/paged format, which is common for many REST
APIs. The getNextPage argument is a function that takes in a single parameter, which will be the result of the
previous call to getNextPage , and should return a nullable table .
The getNextPage is called repeatedly until it returns null . The function will collate all pages into a single table.
When the result of the first call to getNextPage is null, an empty table is returned.
// The getNextPage function takes a single argument and is expected to return a nullable table
Table.GenerateByPage = (getNextPage as function) as table =>
let
listOfPages = List.Generate(
() => getNextPage(null), // get the first page of data
(lastPage) => lastPage <> null, // stop when the function returns null
(lastPage) => getNextPage(lastPage) // pass the previous page to the next function call
),
// concatenate the pages together
tableOfPages = Table.FromList(listOfPages, Splitter.SplitByNothing(), {"Column1"}),
firstRow = tableOfPages{0}?
in
// if we didn't get back any pages of data, return an empty table
// otherwise set the table type based on the columns of the first page
if (firstRow = null) then
Table.FromRows({})
else
Value.ReplaceType(
Table.ExpandTableColumn(tableOfPages, "Column1", Table.ColumnNames(firstRow[Column1])),
Value.Type(firstRow[Column1])
);
Additional notes:
The getNextPage function will need to retrieve the next page URL (or page number, or whatever other values
are used to implement the paging logic). This is generally done by adding meta values to the page before
returning it.
The columns and table type of the combined table (i.e. all pages together) are derived from the first page of data.
The getNextPage function should normalize each page of data.
The first call to getNextPage receives a null parameter.
getNextPage must return null when there are no pages left
An example of using this function can be found in the Github sample, and the TripPin paging sample.
SchemaTransformTable
EnforceSchema.Strict = 1; // Add any missing columns, remove extra columns, set table type
EnforceSchema.IgnoreExtraColumns = 2; // Add missing columns, do not remove extra columns
EnforceSchema.IgnoreMissingColumns = 3; // Do not add or remove columns
SchemaTransformTable = (table as table, schema as table, optional enforceSchema as number) as table =>
let
// Default to EnforceSchema.Strict
_enforceSchema = if (enforceSchema <> null) then enforceSchema else EnforceSchema.Strict,
Table.ChangeType
let
// table should be an actual Table.Type, or a List.Type of Records
Table.ChangeType = (table, tableType as type) as nullable table =>
// we only operate on table types
if (not Type.Is(tableType, type table)) then error "type argument should be a table type" else
// if we have a null value, just return it
// if we have a null value, just return it
if (table = null) then table else
let
columnsForType = Type.RecordFields(Type.TableRow(tableType)),
columnsAsTable = Record.ToTable(columnsForType),
schema = Table.ExpandRecordColumn(columnsAsTable, "Value", {"Type"}, {"Type"}),
previousMeta = Value.Metadata(tableType),
// If given a generic record type (no predefined fields), the original record is returned
Record.ChangeType = (record as record, recordType as type) =>
let
// record field format is [ fieldName = [ Type = type, Optional = logical], ... ]
fields = try Type.RecordFields(recordType) otherwise error "Record.ChangeType: failed to get record
fields. Is this a record type?",
fieldNames = Record.FieldNames(fields),
fieldTable = Record.ToTable(fields),
optionalFields = Table.SelectRows(fieldTable, each [Value][Optional])[Name],
requiredFields = List.Difference(fieldNames, optionalFields),
// make sure all required fields exist
withRequired = Record.SelectFields(record, requiredFields, MissingField.UseNull),
// append optional fields
withOptional = withRequired & Record.SelectFields(record, optionalFields, MissingField.Ignore),
// set types
transforms = GetTransformsForType(recordType),
withTypes = Record.TransformFields(withOptional, transforms, MissingField.Ignore),
// order the same as the record type
reorder = Record.ReorderFields(withTypes, fieldNames, MissingField.Ignore)
in
if (List.IsEmpty(fieldNames)) then record else reorder,
Power Query will automatically generate an invocation UI for you based on the arguments for your function. By
default, this UI will contain the name of your function, and an input for each of your parameters.
Similarly, evaluating the name of your function, without specifying parameters, will display information about it.
You might notice that built-in functions typically provide a better user experience, with descriptions, tooltips, and
even sample values. You can take advantage of this same mechanism by defining specific meta values on your
function type. This article describes the meta fields that are used by Power Query, and how you can make use of
them in your extensions.
Function Types
You can provide documentation for your function by defining custom type values. The process looks like this:
1. Define a type for each parameter
2. Define a type for your function
3. Add various Documentation.* fields to your types metadata record
4. Call Value.ReplaceType to ascribe the type to your shared function
You can find more information about types and metadata values in the M Language Specification.
Using this approach allows you to supply descriptions and display names for your function, as well as individual
parameters. You can also supply sample values for parameters, as well as defining a preset list of values (turning
the default text box control into a drop down).
The Power Query experience retrieves documentation from meta values on the type of your function, using a
combination of calls to Value.Type, Type.FunctionParameters, and Value.Metadata.
Function Documentation
The following table lists the Documentation fields that can be set in the metadata for your function. All fields are
optional.
Parameter Documentation
The following table lists the Documentation fields that can be set in the metadata for your function parameters. All
fields are optional.
Example
The following code snippet (and resulting dialogs) are from the HelloWorldWithDocs sample.
[DataSource.Kind="HelloWorldWithDocs", Publish="HelloWorldWithDocs.Publish"]
shared HelloWorldWithDocs.Contents = Value.ReplaceType(HelloWorldImpl, HelloWorldType);
Function info
Handling Navigation
3/5/2019 • 3 minutes to read
Navigation Tables (or nav tables) are a core part of providing a user-friendly experience for your connector. The
Power Query experience displays them to the user after they have entered any required parameters for your data
source function, and have authenticated with the data source.
Behind the scenes, a nav table is just a regular M Table value with specific metadata fields defined on its Type.
When your data source function returns a table with these fields defined, Power Query will display the navigator
dialogue. You can actually see the underlying data as a Table value by right-clicking on the root node and clicking
Edit.
Table.ToNavigationTable
You can use the Table.ToNavigationTable function to add the table type metadata needed to create a nav table.
Note: You currently need to copy and paste this function into your M extension. In the future it will likely be
moved into the M standard library.
keyColumns List of column names that act as the primary key for your
navigation table
nameColumn The name of the column that should be used as the display
name in the navigator
dataColumn The name of the column that contains the Table or Function to
display
itemKindColumn The name of the column to use to determine the type of icon
to display. See below for the list of valid values for the column.
FIELD PARAMETER
NavigationTable.NameColumn nameColumn
NavigationTable.DataColumn dataColumn
NavigationTable.ItemKindColumn itemKindColumn
NavigationTable.IsLeafColumn isLeafColumn
Preview.DelayColumn itemNameColumn
This code will result in the following Navigator display in Power BI Desktop:
This code would result in the following Navigator display in Power BI Desktop:
Test Connection
Custom Connector support is now available in both Personal and Enterprise modes of the On-Premises Data
Gateway. Both gateway modes support Import - Direct Query is only supported in Enterprise mode.
The method for implementing TestConnection functionality is likely to change prior while the Power BI Custom
Data Connector functionality is in preview.
To support scheduled refresh through the on-premises data gateway, your connector must implement a
TestConnection handler. The function is called when the user is configuring credentials for your source, and used to
ensure they are valid. The TestConnection handler is set in the Data Source Kind record, and has the following
signature:
Where dataSourcePath is the Data Source Path value for your function, and the return value is a list composed of:
1. The name of the function to call (this function must be marked as #shared , and is usually your primary data
source function)
2. One or more arguments to pass to your function
If the invocation of the function results in an error, TestConnection is considered to have failed, and the credential
will not be persisted.
Note: As stated above, the function name provided by TestConnection must be a shared member.
TripPin = [
TestConnection = (dataSourcePath) => { "TripPin.Contents" },
Authentication = [
Anonymous = []
],
Label = "TripPin"
];
DirectSQL = [
TestConnection = (dataSourcePath) =>
let
json = Json.Document(dataSourcePath),
server = json[server],
database = json[database]
in
{ "DirectSQL.Database", server, database },
Authentication = [
Windows = [],
UsernamePassword = []
],
Label = "Direct Query for SQL"
];
Handling Power Query Connector Signing
5/14/2019 • 3 minutes to read
In Power BI, the loading of custom connectors is limited by your choice of security setting. As a general rule, when
the security for loading custom connectors is set to 'Recommended', the custom connectors won't load at all, and
you have to lower it to make them load.
The exception to this is trusted, 'signed connectors'. Signed connectors are a special format of custom connector, a
.pqx instead of .mez file, which have been signed with a certificate. The signer can provide the user or the user's IT
department with a thumbprint of the signature, which can be put into the registry to securely indicate trusting a
given connector.
The following steps enable you to use a certificate (with explanation on how to generate one if you don't have one
available) and sign a custom connector with the 'MakePQX' tool.
NOTE
Note: If you need help creating a self-signed certificate to test these instructions, please see the Microsoft Documentation on
‘New-SelfSignedCertificate’ in PowerShell here.
NOTE
Note: If you need help exporting your certificate as a pfx, please see here.
OPTIONS DESCRIPTION
Commands:
COMMAND DESCRIPTION
verify Verify the signature status on a .pqx file. Return value will be
non-zero if the signature is invalid.
There are three commands in MakePQX. Use "MakePQX [command] --help" for more information about a command.
Pack
The Pack command takes a .mez file and packs it into a .pqx file, which is able to be signed. The .pqx file is also able
to support a number of capabilities that will be added in the future.
Usage: MakePQX pack [options]
Options:
OPTION DESCRIPTION
-t | --target Output file name. Defaults to the same name as the input file
Example
Sign
The Sign command signs your .pqx file with a certificate, giving it a thumbprint that can be checked for trust by
Power BI clients with the higher security setting. This takes a pqx file and returns the same pqx file, signed.
Usage: MakePQX sign [arguments] [options]
Arguments:
ARGUMENT DESCRIPTION
Options:
OPTION DESCRIPTION
Example
C:\Users\cpope\Downloads\MakePQX>MakePQX sign "C:\Users\cpope\OneDrive\Documents\Power BI Desktop\Custom
Connectors\HelloWorldSigned.pqx" --certificate ColinPopellTestCertificate.pfx --password password
Verify
The Verify command verifies that your module has been properly signed, as well as showing the Certificate status.
Usage: MakePQX verify [arguments] [options]
Arguments:
ARGUMENT DESCRIPTION
Options:
OPTION DESCRIPTION
Example
{
"SignatureStatus": "Success",
"CertificateStatus": [
{
"Issuer": "CN=Colin Popell",
"Thumbprint": "16AF59E4BE5384CD860E230ED4AED474C2A3BC69",
"Subject": "CN=Colin Popell",
"NotBefore": "2019-02-14T22:47:42-08:00",
"NotAfter": "2020-02-14T23:07:42-08:00",
"Valid": false,
"Parent": null,
"Status": "UntrustedRoot"
}
]
}