Is it possible to iterate through a single API call to a SharePoint list using Power Query? - rest

Strange use case here. I have a list with approximately 18000 rows. I have specific lookup columns that show up as expected when I use the SharePoint Online list connector in Power BI. This method is horribly slow though. I stumbled upon a guide by a Power BI user Hoosier BI that outlines a way to get list items using REST API. When I use a REST API call, however, results return but the same columns I want are suspiciously absent.
I can hit the list in a web browser and see that when using https://mysharepointurl/mysharepointsite/_api/web/lists/GetByID('mylistid')/Items
all 18000 rows are accounted for but the columns I want are not there. This changes when I call the item number specifically and view field values as text
https://mysharepointurl/mysharepointsite/_api/web/lists/GetByID('mylistid')/Items(1)/FieldValuesAsText
All columns are available and expandable, including the lookup columns that were previously missing.
If using Power Query, is there a way to incrementally iterate through each item in my list and return results based on ViewFieldsAsText call?
If not, am I missing something simple here? I've taken a look at the list in SharePoint, changed the default view to include all columns I want, made sure those columns are indexed, and more that I can't recall off the top of my head.
Thoughts?
For reference, this is the M query I was using to retrieve all rows initially:
sitename = "", // if a subsite use "Site/SubSite"
listname = "BigList",
baseurl = "https:///sites/"
& sitename
& "/_api/web/lists/GetByTitle('"
& listname
& "')/",
itemcount = Json.Document(
Web.Contents(baseurl & "ItemCount", [Headers = [Accept = "application/json"]])
)[value],
skiplist = List.Numbers(0, Number.RoundUp(itemcount / 5000), 5000),
#"Converted to Table" = Table.FromList(
skiplist,
Splitter.SplitByNothing(),
null,
null,
ExtraValues.Error
),
#"Renamed Columns" = Table.RenameColumns(#"Converted to Table", {{"Column1", "Skip"}}),
#"Changed Type" = Table.TransformColumnTypes(#"Renamed Columns", {{"Skip", type text}}),
fieldselect = "&$top=5000", // all fields with no expansion
//fieldselect = "&$top=5000&$select = Id,Title,Person,Date", // list desired fields (no expansion)
//fieldselect = "&$top=5000&$select=Id,Title,Choice,LookupColumn/Title,LookupColumn/Project,LookupColumn/ProjectStatus,Date,Person/LastName,Person/FirstName,Person/EMail&$expand=LookupColumn,Person",
Custom1 = Table.AddColumn(
#"Changed Type",
"Items",
each Json.Document(
Web.Contents(
baseurl & "/items?$skipToken=Paged=TRUE%26p_ID=" & [Skip] & fieldselect,
[Headers = [Accept = "application/json"]]
)
)
),
#"Expanded Items" = Table.ExpandRecordColumn(Custom1, "Items", {"value"}, {"value"}),
#"Expanded value" = Table.ExpandListColumn(#"Expanded Items", "value")
in
#"Expanded value"```

Related

Is there a way to expand dynamically tables found in multiple columns using Power Query?

I have used the List.Accumulate() to merge mutliple tables. This is the output I've got in this simple example:
Now, I need a solution to expand all these with a formula, because in real - world I need to merge multiple tables that keep increasing in number (think Eurostat tables, for instance), and modifying the code manually wastes much time in these situations.
I have been trying to solve it, but it seems to me that the complexity of syntax easily becomes the major limitation here. For instance, If I make a new step where I nest in another List.Accumulate() the Table.ExpandTableColumns(), I need to pass inside a column name of an inner table, as a text. Fine, but to drill it down actually, I first need to pass a current column name in [] in each iteration - for instance, Column 1 - and it triggers an error if I store column names to a list because these are between "". I also experimented with TransformColumns() but didn't work either.
Does anyone know how to solve this problem whatever the approach?
See https://blog.crossjoin.co.uk/2014/05/21/expanding-all-columns-in-a-table-in-power-query/
which boils down to this function
let Source = (TableToExpand as table, optional ColumnNumber as number) =>
//https://blog.crossjoin.co.uk/2014/05/21/expanding-all-columns-in-a-table-in-power-query/
let ActualColumnNumber = if (ColumnNumber=null) then 0 else ColumnNumber,
ColumnName = Table.ColumnNames(TableToExpand){ActualColumnNumber},
ColumnContents = Table.Column(TableToExpand, ColumnName),
ColumnsToExpand = List.Distinct(List.Combine(List.Transform(ColumnContents, each if _ is table then Table.ColumnNames(_) else {}))),
NewColumnNames = List.Transform(ColumnsToExpand, each ColumnName & "." & _),
CanExpandCurrentColumn = List.Count(ColumnsToExpand)>0,
ExpandedTable = if CanExpandCurrentColumn then Table.ExpandTableColumn(TableToExpand, ColumnName, ColumnsToExpand, NewColumnNames) else TableToExpand,
NextColumnNumber = if CanExpandCurrentColumn then ActualColumnNumber else ActualColumnNumber+1,
OutputTable = if NextColumnNumber>(Table.ColumnCount(ExpandedTable)-1) then ExpandedTable else ExpandAll(ExpandedTable, NextColumnNumber)
in OutputTable
in Source
alternatively, unpivot all the table columns to get one column, then expand that value column
ColumnsToExpand = List.Distinct(List.Combine(List.Transform(Table.Column(#"PriorStepNameHere", "ValueColumnNameHere"), each if _ is table then Table.ColumnNames(_) else {}))),
#"Expanded ColumnNameHere" = Table.ExpandTableColumn(#"PriorStepNameHere", "ValueColumnNameHere",ColumnsToExpand ,ColumnsToExpand ),

How to parameterize a column for aggregation in Power BI desktop?

I have users who would like to be able to modify what columns a table aggregates by. My issue is that I seem unable to do this in Power BI. I basically want to be able to do the following in SQL:
SELECT
<OrgLevel1>,
<OrgLevel2>,
SUM([Revenue])
FROM [Data]
GROUP BY
<OrgLevel1>,
<OrgLevel2>
;
where the user can change <OrgLevel1> and/or <OrgLevel2> to be any of { "(All)", [Department], [Product] }.
The issue may be related to this post: https://community.powerbi.com/t5/Desktop/Calculated-Column-Table-Change-Dynamically-According-to-Slicer/m-p/655991#M314800
Here's a link to a workbook that illustrates this issue, TestParameterizeGroupby.pbix (hosted by Google Drive). I've also included field definitions below with screenshots. Thanks for any help.
TestParameterizeGroupby.pbix
Link: TestParameterizeGroupby.pbix (hosted by Google Drive)
Problem
[Org Level 1] and [Org Level 2] fields are not recalculating from the users' selection. Only the default values are shown.
Expected result in table
"Org Level 1", "Org Level 2", "Revenue"
"(All)", "(All)", 28
Note
The purpose is to have parameterizable organization level fields so that the report user can aggregate by all, department, product, or both in either order.
Table and column definitions
'Data' = DATATABLE(
"Department",
STRING,
"Product",
STRING,
"Revenue",
DOUBLE,
{
{"DeptA", "ProdX", 5.0},
{"DeptA", "ProdY", 6.0},
{"DeptB", "ProdX", 10.0},
{"DeptB", "ProdY", 7.0}
}
)
'Data'[Org Level 1] = SWITCH(
'Org Level 1 Parameter'[Org Level 1 Parameter Value],
0,
"(All)",
1,
[Department],
2,
[Product]
)
// Problem: [Org Level 1] and [Org Level 2] fields are not recalculating from the users' selection. Only the default values are shown.
'Org Level 1' = DATATABLE(
"Org Level 1",
STRING,
"Org Level 1 Parameter",
INTEGER,
{
{"(0) (All)", 0},
{"(1) Department", 1},
{"(2) Product", 2}
}
)
'Org Level 1 Parameter'[Org Level 1 Parameter] = GENERATESERIES(0, 2, 1)
'Org Level 1 Parameter'[Org Level 1 Parameter Value] = SELECTEDVALUE('Org Level 1 Parameter'[Org Level 1 Parameter], 1)
Table 'Org Level 1' has a 1-1 relationship with 'Org Level 1 Parameter' on column [Org Level 1 Parameter].
The user selects the value for 'Data'[Org Level 1] by selecting the value for 'Org Level 1'[Org Level 1].
Tables and columns for [Org Level 2] are defined in the same way as [Org Level 1].
Screenshots
Report view:
Data view:
Model view:
Cross-reference to post in Power BI forum:
Power BI Forum: How to parameterize a column for aggregation
One solution to this is to add two list values parameters and use their values in Power Query M code to modify the database query. Lets assume that you have a table Data with columns Department, Product and Revenue. For simplicity I will add one more column, named Dummy Column, with all rows having the same value (e.g. null). I will explain why later in this post. So the table looks like this:
Then in your report specify a query when adding this table to your model (lets assume we will import it, but in general you can do this in DirectQuery too):
Now if you look the M code you will see the above query there:
Source = Sql.Database(".", "StackOverflow", [Query=" select ....
Now define couple of parameters, that the end-user can use to select how the data should be aggregated. Lets name them Level 1 and Level 2:
The value of a parameter can be used in M by parameter name, and & is used to concatenate strings. So if there is a parameter Name with value Samuel, the expression "Hello, " & Name & "!" will be evaluated as Hello, Samuel!. The idea is to check the value of our parameters and modify the database query accordingly.
In the select part, we will replace the name of the field selected, or we will put '' (empty string) in case of <All> (I surrounded parameter values with brackets to be more easily to distinguish parameter values from database field names). So the expression should look like:
"select " & (if #"Level 1" = "<Department>" then "Department" else ..." (and so on)
Because there is a space in our parameter's name, we need to surround it with #" and ", so Level1 can be referenced simply as Level1 in the code, but Level 1 becomes #"Level 1".
The group by part is a bit trickier. We should add a comma between field names, add or not field name, or even omit the group by at all (in case both parameters are set to <All>). To simplify this, I added one dummy column, with all rows having the same value (e.g. null) and always group by this column. This way building the group by clause is way more simpler - in case the parameter value is not <All>, we should add , fieldname. So the code could look like this:
"group by DummyColumn" & (if #"Level 1" = "<Department>" then ", Department" else ..." (and so on)
So the final M code is this:
let
Source = Sql.Database(".", "StackOverflow", [Query="select#(lf) " & (if #"Level 1" = "<Department>" then "Department" else if #"Level 1" = "<Product>" then "Product" else "''") & " as [Org Level 1]#(lf) , " & (if #"Level 2" = "<Department>" then "Department" else if #"Level 2" = "<Product>" then "Product" else "''") & " as [Org Level 2]#(lf) , SUM(Revenue) as Revenue#(lf)from Data#(lf)group by DummyColumn" & (if #"Level 1" = "<Department>" then ", Department" else if #"Level 1" = "<Product>" then ", Product" else "") & (if #"Level 2" = "<Department>" then ", Department" else if #"Level 2" = "<Product>" then ", Product" else "")])
in
Source
Now the end-user can change parameter values, by clicking Edit Queries -> Edit Parameters:
And select how to group the data:
By default, Power BI Desktop will warn you first time, when particular query is executed:
If you want to turn this off, go to File -> Options and settings -> Options -> (GLOBAL) Security and make sure Require user approval for new native database queries is not selected:
When the end-user changes parameter values, the data will change too, e.g.:
Or:
And so on...
This trick works well in Power BI Desktop when every user has its own copy of the .pbix file. However, if you publish it, first changing parameter values is not very convenient (you must go to datasat's settings) and more important, changing parameter values affects all users, which are looking at this report. You can also use it to modify Table.Group statements generated by Power Query Editor, in case you want to aggregate the data in Power BI, but changing the database query is easier and more flexible.
If you want to enable this scenario for concurrent multi-users scenarios for published reports, you can use slicers and What-if parameters. Unfortunately, What-if parameters can be numeric (you can't define the list of values there), so you can use measures to "decode" the int value of the parameter and write some DAX code to perform different aggregations accordingly. It is more work, but if it is needed, it can be made too.

Conditional WHERE clause in KDB?

Full Query:
{[tier;company;ccy; startdate; enddate] select Deal_Time, Deal_Date from DEALONLINE_REMOVED where ?[company = `All; 1b; COMPANY = company], ?[tier = `All;; TIER = tier], Deal_Date within(startdate;enddate), Status = `Completed, ?[ccy = `All;1b;CCY_Pair = ccy]}
Particular Query:
where ?[company = `All; 1b; COMPANY = company], ?[tier = `All; 1b; TIER = tier],
What this query is trying to do is to get the viewstate of a dropdown.
If there dropdown selection is "All", that where clause i.e. company or tier is invalidated, and all companies or tiers are shown.
I am unsure if the query above is correct as I am getting weird charts when displaying them on KDB dashboard.
What I would recommend is to restructure your function to make use of the where clause using functional qSQL.
In your case, you need to be able to filter based on certain input, if its "All" then don't filter else filter on that input. Something like this could work.
/Define sample table
DEALONLINE_REMOVED:([]Deal_time:10#.z.p;Deal_Date:10?.z.d;Company:10?`MSFT`AAPL`GOOGL;TIER:10?`1`2`3)
/New function which joins to where clause
{[company;tier]
wc:();
if[not company=`All;wc:wc,enlist (=;`Company;enlist company)];
if[not tier=`All;wc:wc,enlist (=;`TIER;enlist tier)];
?[DEALONLINE_REMOVED;wc;0b;()]
}[`MSFT;`2]
If you replace the input with `All you will see that everything is returned.
The full functional select for your query would be as follows:
whcl:{[tier;company;ccy;startdate;enddate]
wc:(enlist (within;`Deal_Date;(enlist;startdate;enddate))),(enlist (=;`Status;enlist `Completed)),
$[tier=`All;();enlist (=;`TIER;enlist tier)],
$[company=`All;()enlist (=;`COMPANY;enlist company)],
$[ccy=`All;();enlist (=;`CCY_Pair;enlist ccy)];
?[`DEALONLINE_REMOVED;wc;0b;`Deal_Time`Deal_Date!`Deal_Time`Deal_Date]
}
The first part specifies your date range and status = `Completed in the where clause
wc:(enlist (within;`Deal_Date;(enlist;startdate;enddate))),(enlist (=;`Status;enlist `Completed)),
Next each of these conditionals checks for `All for the TIER, COMPANY and CCY_Pair column filtering. It then joins these on to the where clause when a specific TIER, COMPANY or CCY_Pair are specified. (otherwise an empty list is joined on):
$[tier=`All;();enlist (=;`TIER;enlist tier)],
$[company=`All;();enlist (=;`COMPANY;enlist company)],
$[ccy=`All;();enlist (=;`CCY_Pair;enlist ccy)];
Finally, the select statement is called in its functional form as follows, with wc as the where clause:
?[`DEALONLINE_REMOVED;wc;0b;`Deal_Time`Deal_Date!`Deal_Time`Deal_Date]

Power BI create column from nested Record values

I'm trying to create a new column from one of the records in a List type column. The value is the country that corresponds with the Latitude and Longitude fields. The information is retrieved from the Bing Map API, which got using Get Data from Web (following the tutorial here: https://sqldusty.com/2016/04/26/power-bi-and-the-bing-maps-api/).
Basically I need List.Record[1].address.countryRegion. Is it possible to make a column that holds this specific value without doing "Expand to new rows"? The issue is that some of the columns come back with France, and the number of rows increases to over 1000 but there should only be around 250.
Here is how I got to the point of having the column of Lists:
1. Get data from the web
2. Used Basic option and pasted working API request for a location with my bing maps key. Then click ok.
http://dev.virtualearth.net/REST/v1/Locations/point=40,-122?&key=BingMapsKey
3. Navigated to the Advanced editor in View > Advanced editor.
4. Made a function that uses Latitude and Longitude as input
let getCountry = (Latitude as text, Longitude as text) =>
let
Source = Json.Document(Web.Contents("http://dev.virtualearth.net/REST/v1/Locations/point="& Latitude &","& Longitude &"?&key=BingMapsKey"))
in
Source
in
getCountry
5. Renamed the function to GetCountry, then navigated to the desired table to add the column to (with Latitude and Longitude)
6. In the target table, Navigate to Add Column > Invoke Custom Function
7. Chose GetCountry from the list of functions, changed the type to column name and assigned the inputs to respective column names (latitude and longitude). Then clicked OK.
8. The column shows up on the right. I filtered out all columns besides 'resourceSets' because that has the address values.
EDIT I found a way to reduce the number of lists that are returned in the request, which is to only request the Country/Region as a query parameter:
http://dev.virtualearth.net/REST/v1/Locations/40,-122?key=BingMapsKey&includeEntityTypes=CountryRegion
This works for my needs for now, but maybe it's a good idea to keep this open to see if someone knows how to make a table from the nested table values? Thanks for all your help!
This sounds like an XY Problem to me. Let me try to clarify:
The Table.ExpandListColumn function expands records to multiple rows because there are indeed multiple rows of records returned from the API endpoint.
Unless you apply filter logic afterwards, there are no ways for the code to understand which row of records to choose.
There shouldn't be multiple rows of records returned from the API. (Find the countryRegion for a given (lat, long))
So after reading through the question, the real problem lies in the API endpoint you're using.
It should be
http://dev.virtualearth.net/REST/v1/Locations/40,-122?key=yourAPIKey
instead of
http://dev.virtualearth.net/REST/v1/Locations/point=40,-122?key=yourAPIKey
The point= is not needed. (Yes, the documentation is slightly confusing)
So you can update your GetCountry function as follows:
let getCountry = (Latitude as text, Longitude as text) =>
let
Source = Json.Document(Web.Contents("http://dev.virtualearth.net/REST/v1/Locations/"& Latitude &","& Longitude &"?key=yourAPIKey"))
in
Source
in
getCountry
(Side note: better not to expose your API Key to public :) )
As a result, there should only be one countryRegion for each place.
My query for your reference:
let
Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45Wcs/PT89JLchJrVDSUTI21zMxMjIwMACydQ2NjPQMLEwMTM2VYnWilRwLgIoUPPPSMvMyS1IVfPLzCyA6jI0NzczN4DqMDQwtLJViYwE=", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type text) meta [Serialized.Text = true]) in type table [Place = _t, Lat = _t, Long = _t]),
#"Changed Type" = Table.TransformColumnTypes(Source,{{"Place", type text}}),
#"Invoked Custom Function" = Table.AddColumn(#"Changed Type", "GetCountry", each GetCountry([Lat], [Long])),
#"Expanded GetCountry" = Table.ExpandRecordColumn(#"Invoked Custom Function", "GetCountry", {"resourceSets"}, {"GetCountry.resourceSets"}),
#"Expanded GetCountry.resourceSets" = Table.ExpandListColumn(#"Expanded GetCountry", "GetCountry.resourceSets"),
#"Expanded GetCountry.resourceSets1" = Table.ExpandRecordColumn(#"Expanded GetCountry.resourceSets", "GetCountry.resourceSets", {"resources"}, {"resources"}),
#"Expanded resources" = Table.ExpandListColumn(#"Expanded GetCountry.resourceSets1", "resources"),
#"Expanded resources1" = Table.ExpandRecordColumn(#"Expanded resources", "resources", {"address"}, {"address"}),
#"Expanded address" = Table.ExpandRecordColumn(#"Expanded resources1", "address", {"countryRegion"}, {"countryRegion"})
in
#"Expanded address"
Hope it helps :)
did you use a Parameter to filter the row you want ?
Have a look there: Get only the data I want from a db but keep structure

Drupal onChange auto populate another CCK select list

I have built a multistep form using CCK, however I have a couple of issues outstanding but, not sure if CCK can help.
In one step of the form I have 2 select boxes, the first is auto populated from the vocabulary table with the following code and all woks well.
$category_options = array();
$cat_res = db_query('select vid, name from vocabulary WHERE vid > 1 ORDER BY name ASC');
while ($cat_options = db_fetch_object($cat_res)) {
$category_options[$cat_options->vid] = $cat_options->name;
}
return $category_options;
What I would like to do is, when a user selects one item from the vocablulary list it auto populates another select box with terms from the term_data table. I have 2 issues;
1) I have added the following code to the second select list, just to make sure it works (IT DOESN'T). There a multiple terms associated with each vocabulary, but the second sql statemant only returns one result when it should return several, (SO SOMETHING WRONG HERE). For example in the term_date table there are 6 terms with the vid of 3, but I only get one added to select list.
$term_options = array();
$term_res = db_query('select vid, name from term_data WHERE vid = 3 ORDER BY name ASC'); while ($options = db_fetch_object($term_res)) {
$term_options[$options->vid] = $options->name;
}
return $term_options;
2) Can I add an onChange to the first select list to call a function to auto populate second list using CCK, or do I have to lean towards doing my entire form using the FORM API.
Any help or thoughts would be very much appreciated.
It seems to be a mistake in the query that gets terms. I tried to correct:
$term_res = db_query('select tid, name from term_data WHERE vid = 3 ORDER BY name ASC');
while ($options = db_fetch_object($term_res)) {
$term_options[$options->tid] = $options->name;
}
In your code you selected vid that is actually equal for all terms. Then you added terms names to $term_options array under the same key => so you got only 1 element.
Considering the second question: I would send the whole data structure (all vocabularies and their terms) as json to the client (insert a js script to the page from your drupal code) and implement the desired functionality with jquery.