26

I'm looking for a way to programmatically populate a spreadsheet that filters data from another spreadsheet based on the logged in user.

I am able to do this using the query function inside a spreadsheet. BUT, unable to figure out a way to call the query function from apps script?

Can this be done? Would appreciate sample code. Thanks.

TheMaster
  • 45,448
  • 6
  • 62
  • 85
awsamar
  • 263
  • 1
  • 3
  • 6
  • 1
    Have you seen https://developers.google.com/chart/interactive/docs/querylanguage? You can make a JSON call using that to get data and then insert it to your sheet. – IMTheNachoMan Jun 21 '19 at 18:20
  • Possible duplicate of [Using Bound Google Scripts to Generate a Query Object](https://stackoverflow.com/questions/51327982/using-bound-google-scripts-to-generate-a-query-object) – TheMaster Sep 13 '19 at 11:38
  • [Search](https://stackoverflow.com/search?q=%5Bgoogle-apps-script%5D+%5Bgoogle-query-language%5D+is%3Aanswer&tab=votes) – TheMaster Sep 13 '19 at 11:38

6 Answers6

24

I do not know whether there is a restriction on that ...

function test () {
  var req = query("=QUERY(shopT!B2:E; \"select min(E) where (B=3 or B=4 and D=2) group by C, D\")");

  Logger.log(req);
}

function query(request) { 
  var sheet = sp.insertSheet();
  var r = sheet.getRange(1, 1).setFormula(request);

  var reply = sheet.getDataRange().getValues();
  sp.deleteSheet(sheet);

  return reply;
}
zizix
  • 369
  • 1
  • 2
  • 5
13

=QUERY is a spreadsheet function. If you want that functionality in Apps Script you can leverage Google's Visualization API and Query Language. I wrote a custom module for it in GAS. Here's an extract:

(function(context) {

    const Utils = (context.Utils || (context.Utils = {}));


    /**
     * Queries a spreadsheet using Google Visualization API's Datasoure Url.
     *
     * @param        {String} ssId    Spreadsheet ID.
     * @param        {String} query   Query string.
     * @param {String|Number} sheetId Sheet Id (gid if number, name if string). [OPTIONAL]
     * @param        {String} range   Range                                     [OPTIONAL]
     * @param        {Number} headers Header rows.                              [OPTIONAL]
     */
    Utils.gvizQuery = function(ssId, query, sheetId, range, headers) {
        var response = JSON.parse( UrlFetchApp
                .fetch(
                    Utilities.formatString(
                        "https://docs.google.com/spreadsheets/d/%s/gviz/tq?tq=%s%s%s%s",
                        ssId,
                        encodeURIComponent(query),
                        (typeof sheetId === "number") ? "&gid=" + sheetId :
                        (typeof sheetId === "string") ? "&sheet=" + sheetId :
                        "",
                        (typeof range === "string") ? "&range=" + range :
                        "",
                        "&headers=" + ((typeof headers === "number" && headers > 0) ? headers : "0")
                    ), 
                    {
                        "headers":{
                            "Authorization":"Bearer " + ScriptApp.getOAuthToken()
                        }
                    }
                )
                .getContentText()
                .replace("/*O_o*/\n", "") // remove JSONP wrapper
                .replace(/(google\.visualization\.Query\.setResponse\()|(\);)/gm, "") // remove JSONP wrapper
            ),
            table = response.table,
            rows;

        if (typeof headers === "number") {

            rows = table.rows.map(function(row) {
                return table.cols.reduce(
                    function(acc, col, colIndex) {
                        acc[col.label] = row.c[colIndex] && row.c[colIndex].v;
                        return acc;
                    }, 
                    {}
                );
            });

        } else {

            rows = table.rows.map(function(row) {
                return row.c.reduce(
                    function(acc, col) {
                        acc.push(col && col.v);
                        return acc;
                    },
                    []
                );
            });
        }

        return rows;

    };

    Object.freeze(Utils);

})(this);

Just drop that module into its own file in your GAS editor then you can call it as follows:

// result is an array of objects if header row is specified, otherwise it is an array of arrays
var result = Utils.gvizQuery(
    "<YOUR_SPREADSHEET_ID>", 
    "<YOUR_QUERY_STRING>", 
    <SHEET_ID_IF_NEEDED>, // can be a number (the sheetId), or the name of the sheet; if not needed, but headers are, pass in undefined
    <RANGE>, // specify range, ex: `A2:O`
    <HEADER_ROW_INDEX_IF_NEEDED> // always a number
);
TheAddonDepot
  • 8,408
  • 2
  • 20
  • 30
  • I tried calling your module using the following call: `var result = Utils.gvizQuery( ss.getId(), '=query(\'16 Jul - 20 Jul Responses\'!A1:I31, "SELECT C WHERE D = \'Not Available\'", 0)' );` I got this error: `TypeError: Cannot read property "rows" from undefined. (line 53, file "GViz")` – RouteMapper Jul 13 '18 at 16:31
  • Try this instead: `var result = Utils.gvizQuery(ss.getId(), "SELECT C WHERE D ='Not Available'", "16 Jul - 20 Jul Responses", 0);` – TheAddonDepot Jul 13 '18 at 16:38
  • That gives the same error. The line `table = response.table` is returning `undefined`. Copying and pasting that same query into the Spreadsheet works fine. – RouteMapper Jul 13 '18 at 16:39
  • 1
    Updated the call to reference the sheet name. If that doesn't work go [here](https://plus.google.com/u/0/communities/102471985047225101769) and post a link to a copy of your sheet so I can troubleshoot. – TheAddonDepot Jul 13 '18 at 16:44
  • @RouteMapper Good to hear. – TheAddonDepot Jul 13 '18 at 18:15
  • I think `out:csv`(getting the output as csv instead of json) will be easier to parse and maybe even faster. – TheMaster Jul 10 '19 at 23:01
  • how do I use the headers, do I need quotes in the select? – tofutim Apr 29 '20 at 20:58
11

No, there is no API for the Query function that allows it to be called from Google Apps Script. (There is no way to call ANY spreadsheet function in this way, in fact.)

You can get some similar functionality without writing it all yourself, though. The 2D Arrays Library includes a variety of "filter" functions that let you retrieve matching rows.

Mogsdad
  • 44,709
  • 21
  • 151
  • 275
  • This sounds like something that should do the job. Thanks! – awsamar Jul 29 '13 at 21:01
  • 1
    Mogsdad, Arrays Library useful but still not as effective as the query function. I am trying to use the visualization/charts apis to build user dashboards. In order to personalize the dashboard, I need the ability to filter the data from the spreadsheet based on the logged in user. But, the filter required comparing multiple columns which is easy in the query function '...where col5 = "&cell1&" or col6 = "&cell2&". – awsamar Jul 30 '13 at 16:33
  • 2
    Not true. See [here](https://developers.google.com/chart/interactive/docs/querylanguage#setting-the-query-in-the-data-source-url). You can use `UrlFetchApp` with access provided by `ScriptApp.getAccessToken()` – TheMaster Sep 13 '19 at 11:18
  • Fun fact: that’s not the spreadsheet query function, which was the OP’s question. It is an alternate way to get the data, though. – Mogsdad Sep 16 '19 at 23:52
  • 1
    @awsamar can't you theoretically modify one cell of a spreadsheet using google app script, and then retrieve the result rows and columns also using the google apps script? – B''H Bi'ezras -- Boruch Hashem Feb 02 '20 at 23:47
  • Debatable. One could argue that both the query function and the query js uses the same api to parse the data.(PS: I didn't really see your reply till now) – TheMaster Feb 06 '20 at 16:26
  • The Library seems to be unavailable now, at least with the Script ID provided in the documentation. – vmg Mar 02 '22 at 18:09
  • The code/Library seems to be at least available at https://script.google.com/home/projects/1r9wNWbta3ebuYL4ENAdIp4UYKmyNiWf1AqsXYzfXduRHhTZEeTxS9MhZ/edit – vmg Mar 02 '22 at 18:12
6

Perhaps, through a formula you can get something done than you need.

function testFormula() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheets()[1];
  var cell = sheet.getRange("A1");
  cell.setFormula("=QUERY('Sheet0'!A1:B5;\"SELECT A, B\"; 0)");
}
wchiquito
  • 16,177
  • 2
  • 34
  • 45
  • William, Thanks for the suggestion. I'm already using the 'Query' in a cell that gets updated through the script. However, the behaviour isn't exactly how I need. Thanks, though. – awsamar Jul 29 '13 at 21:04
3

I've managed to use Ala-SQL library.

Step 1.

Copy the library to your project: https://raw.githubusercontent.com/agershun/alasql/develop/dist/alasql.min.js

Step 2.

Read the documentation carefully and you are ready to go!

Step 3.

Create your own translator to connect sheets with the library. Please see more info in my post.

I used sql in custom functions in order to show the way to use it:

=getAlaSql(sql_text, West!A:G, East!A:G, Central!A:G)

  • sql_text use any supported syntax with "select" clause. Note: Ala-SQL also has update, insert, but not with anonymous functions.
  • select Col1 from ?... union all ... select Col1 from ? is a proper syntax. I used Col1-notation. Question marks mean tables or variables from the rest of a function.
  • West!A:G, East!A:G, Central!A:G is a list of tables. The library replaces question marks with this variables.

/*
  The code here uses http://alasql.org library:
  
  Downlaad it from here:
    https://raw.githubusercontent.com/agershun/alasql/develop/dist/alasql.min.js
  or here (not tested)
    https://cdn.jsdelivr.net/npm/alasql
    
    My sample sheet is here:
    https://docs.google.com/spreadsheets/d/1V0kHvuS0QfzgYTvkut9UkwcgK_51KV2oHDxKE6dMX7A/copy
*/

function test_AlaSqlQuery()
{
  
  var file = SpreadsheetApp.getActive();
  var sheet1 = file.getSheetByName('East');
  var range1 = sheet1.getDataRange();
  var data1 = range1.getValues();
  
  var sheet2 = file.getSheetByName('Reps');
  var range2 = sheet2.getDataRange();   
  var data2 = range2.getValues();
  
  var sql = "select a.Col1, a.Col3, reps.Col2, a.Col7 from ? a left join ? reps on reps.Col1 = a.Col3";
  var data = getAlaSql(sql, data1, data2);
  
  Logger.log(data);  

}

function getAlaSql(sql)
{
  var tables = Array.prototype.slice.call(arguments, 1);  
  var request = convertToAlaSql_(sql);
  var res = alasql(request, tables);
  //return JSON.stringify(res);
  return convertAlaSqlResultToArray_(res);
}



function test_AlaSqlSelect()
{
  var file = SpreadsheetApp.getActive();
  var sheet = file.getSheetByName('East');
  var range = sheet.getDataRange();
  var data = range.getValues();
  
  var sql = "select * from ? where Col5 > 50 and Col3 = 'Jones'"
  Logger.log(convertAlaSqlResultToArray_(getAlaSqlSelect_(data, sql)));
  /*
  [  
     [  
        Sun Jan 07 12:38:56      GMT+02:00      2018,
        East,
        Jones,
        Binder,
        60.0,
        4.99,
        299.40000000000003                                            // error: precision =(
     ],
    ...
  ]
  
  */

}


function getAlaSqlSelect_(data, sql)
{
  var request = convertToAlaSql_(sql);
  var res = alasql(request, [data]);
  // [{0=2016.0, 1=a, 2=1.0}, {0=2016.0, 1=a, 2=2.0}, {0=2018.0, 1=a, 2=4.0}, {0=2019.0, 1=a, 2=5.0}]
  return convertAlaSqlResultToArray_(res);
}


function convertToAlaSql_(string)
{
  var result = string.replace(/(Col)(\d+)/g, "[$2]");
  result = result.replace(/\[(\d+)\]/g, function(a,n){ return "["+ (+n-1) +"]"; });
  return result;
}


function convertAlaSqlResultToArray_(res)
{
  var result = [];
  var row = [];
  res.forEach
  (
  function (elt)
  {
    row = [];
    for (var key in elt) { row.push(elt[key]); }
    result.push(row);
  }  
  );
  return result;
}
Community
  • 1
  • 1
Max Makhrov
  • 17,309
  • 5
  • 55
  • 81
  • 1
    Ala-Sql is super cool, I was able to use it too, somehow latest minified version as of AlaSQL as of now 4.11 gives an error when we save it in app script, so I used not minified version and saved it as #alasql.gs and worked fine I was able to used it, You have done a wonderful job. – Jatin C Mar 16 '19 at 04:34
1

If you already have the user's information you want to use in a cell in the sheet named : "filter"

You can put this kind of formula in a sheet named : "template"

in B1, this formula : =query(DataToFilter!A1:E,"select A,B,C,D where D contains '"&A1&"'",-1)

then with this code you have in result a sheet named : "newFilter" that receive your filtered data.

var templateToCopy = ss.getSheetByName('template');
template.copyTo(ss).setName('newFilter');
var editFilter = ss.getSheetByName('newFilter');
//assuming the user information needed is in A5
editFilter.getRange('A1').setValue('=filter!A5');