Compare commits

..

225 Commits

Author SHA1 Message Date
Simon Martín
4afa7f70d7 release 4.0.0 2017-10-04 17:06:58 +02:00
Simon Martín
5045f81fe3 Merge pull request #764 from CartoDB/express-v4.15.5
upgrade node modules to enhance security
2017-10-04 15:54:41 +02:00
Raul Ochoa
2afb6b5ac2 Regenerate yarn.lock to pickup grainstore version 2017-10-04 10:32:36 +00:00
Simon Martín
19e2515a8e Merge pull request #749 from CartoDB/dataview-factory-refactor
Dataview factory refactor
2017-10-04 12:18:55 +02:00
Raul Ochoa
5d6156a257 Add missing upgraded dep 2017-10-04 10:16:26 +00:00
Raul Ochoa
9e217d9199 Merge branch 'master' into express-v4.15.5 2017-10-04 10:16:07 +00:00
Raul Ochoa
a224a0bf91 Unify all pending changes in 4.0.0 version 2017-10-04 10:14:09 +00:00
Raul Ochoa
87bcb7ebf2 Update list of upgraded deps 2017-10-04 10:12:54 +00:00
Raul Ochoa
d06ba8b1f8 Merge remote-tracking branch 'origin/master' into express-v4.15.5 2017-10-04 10:11:08 +00:00
Simon Martín
2b37a406bc Merge pull request #763 from CartoDB/remove-list-dataview
Removes `list` dataview type
2017-10-04 12:01:42 +02:00
Simon Martín
0a507d02bc Merge branch 'master' into remove-list-dataview 2017-10-04 11:36:45 +02:00
Raul Ochoa
49fd75f0b6 Merge remote-tracking branch 'origin/master' into express-v4.15.5 2017-10-04 09:32:46 +00:00
Simon Martín
514aa53152 Merge pull request #748 from CartoDB/base-dataview-refactor
Base dataview refactor
2017-10-04 11:17:38 +02:00
Simon Martín
8fe31c45f3 fix 'this' scope with arrow function 2017-10-04 11:10:17 +02:00
Simon Martín
fe4c22d2ea Merge branch 'master' into base-dataview-refactor 2017-10-04 11:08:43 +02:00
Simon Martín
d27cce915c Merge pull request #747 from CartoDB/formula-dataview-refactor
Formula dataview refactor
2017-10-04 10:10:57 +02:00
Raul Ochoa
da832263a4 Upgrade turbo-carto, camshaft, and cartodb-psql 2017-10-03 11:46:03 +00:00
Raul Ochoa
69bd14793f Upgrade to windshaft 3.3.3
Regenerate yarn.lock file
2017-10-03 11:21:47 +00:00
Raul Ochoa
54dd15c0b0 Merge remote-tracking branch 'origin/master' into express-v4.15.5 2017-10-03 11:15:31 +00:00
Rafa de la Torre
28c8632532 Merge pull request #766 from CartoDB/update-yarn-v0.27.5
Update yarn version to v0.27.5
2017-10-03 12:54:46 +02:00
Rafa de la Torre
89172f280f Change yarn dep version range specification
In theory the caret `^` should work but as rochoa pointed out it does
not. So changing it (also for the sake of clarity).
2017-10-03 12:43:09 +02:00
Rafa de la Torre
0a7506e4b2 Update yarn version to v0.27.5 2017-10-03 12:10:43 +02:00
Simon Martín
4fd3c99531 Merge pull request #746 from CartoDB/list-dataview-refactor
List dataview refactor
2017-10-02 17:28:54 +02:00
Simon Martín
1e4c63a6dc Merge pull request #745 from CartoDB/aggregation-dataview-refactor
Aggregation dataview refactor
2017-10-02 17:24:12 +02:00
Simon Martín
742420b159 Merge pull request #744 from CartoDB/time-series-refactor
Histogram refactor
2017-10-02 17:18:21 +02:00
Rafa de la Torre
b8783a6447 Stub NEWS/package for next version 2017-10-02 14:49:01 +02:00
Rafa de la Torre
45dece65f2 Update 3.13.0 release date in NEWS.md 2017-10-02 14:45:43 +02:00
Eneko Lakasta
818cdbd99b upgrade express again (4.16.0) 2017-09-29 17:41:57 +02:00
Eneko Lakasta
75c8a73423 upgrade debug to version 3.1 2017-09-29 10:42:20 +02:00
Eneko Lakasta
649383a3df upgrade request module to version 2.83.0 2017-09-28 18:28:04 +02:00
Eneko Lakasta
52402c0333 Revert "upgrade request module"
This reverts commit cacb92b0c4.
2017-09-28 18:25:40 +02:00
Eneko Lakasta
cacb92b0c4 upgrade request module 2017-09-28 18:24:50 +02:00
Eneko Lakasta
aaf95b9223 upgrade body-parser (package.json and yarn.lock) 2017-09-28 17:41:49 +02:00
Eneko Lakasta
6149df1810 upgrade body-parser to version 1.17.2
version 1.17.2 requires a patched query string (qs) module
2017-09-28 17:37:43 +02:00
Eneko Lakasta
ff47027a51 upgrade express version to 4.15.5 2017-09-28 17:20:07 +02:00
Raul Ochoa
78b75c7a88 Removes list dataview type 2017-09-28 10:47:44 +00:00
Javier Goizueta
56d7c2c140 Merge pull request #760 from CartoDB/upgrade-camshaft-to-0.59.1
Upgrade camshsft to 0.59.1
2017-09-28 12:09:46 +02:00
Javier Goizueta
c9f0902703 Upgrade camshsft to 0.59.1
This fixes duplicate column names in aggregate-intersection analysis
2017-09-27 18:44:06 +02:00
Raul Ochoa
c960535709 Merge pull request #756 from CartoDB/upgrade-deps
Upgrades camshaft, cartodb-query-tables, and turbo-carto
2017-09-26 18:32:31 +02:00
Raul Ochoa
ac42223439 Allow 6.9.0 version 2017-09-25 15:34:16 +00:00
Raul Ochoa
1110abaa9a Merge pull request #644 from CartoDB/check-node-version
Check node version
2017-09-25 17:33:26 +02:00
Raul Ochoa
3023111896 Reuse existing log methods 2017-09-25 14:55:17 +00:00
Raul Ochoa
66380197f4 Do not explode by - token 2017-09-25 14:53:17 +00:00
Raul Ochoa
8daa4bb08a Merge remote-tracking branch 'origin/master' into check-node-version 2017-09-25 14:51:50 +00:00
Raul Ochoa
b943b09532 Merge pull request #736 from CartoDB/static-named-maps-override-bbox
Static named maps override bbox
2017-09-25 14:10:27 +02:00
Raul Ochoa
eda18726fd Merge pull request #755 from CartoDB/analyses-use-cases-tests
Analyses use cases tests
2017-09-25 14:06:44 +02:00
Raul Ochoa
d3dafc8a40 Regenerate lock file 2017-09-25 11:24:33 +00:00
Raul Ochoa
c734f43643 Upgrades camshaft, cartodb-query-tables, and turbo-carto
Better support for query variables.
2017-09-25 11:11:05 +00:00
Raul Ochoa
3134f40eac Remove advanced use cases that no longer make sense 2017-09-22 15:42:52 +00:00
Raul Ochoa
5cc31cabe2 Fix and enable some old tests related to analyses use cases 2017-09-22 15:41:58 +00:00
Simon Martín
ca63c2ef1a Merge pull request #721 from CartoDB/lzma-middleware
Implement LZMA query param inflating as middleware
2017-09-22 16:16:36 +02:00
Raul Ochoa
45698207d9 Merge remote-tracking branch 'origin/master' into static-named-maps-override-bbox 2017-09-22 11:08:12 +00:00
Raul Ochoa
33121871b0 Stubs next version 2017-09-18 12:47:37 +00:00
Raul Ochoa
f133d983e8 Release 3.12.10 2017-09-18 12:46:40 +00:00
Raul Ochoa
b6237c7bfa Merge pull request #750 from CartoDB/upgrade-windshaft
Upgrades windshaft to 3.3.2
2017-09-18 13:17:12 +02:00
Raul Ochoa
a5d9bfa0ec Upgrades windshaft to 3.3.2 2017-09-18 10:34:04 +00:00
Simon
222cfb90fd Removing 'self' vars using arrow functions 2017-09-18 12:20:59 +02:00
Simon
f63fab40ed Removing 'self' vars using arrow functions 2017-09-18 11:34:18 +02:00
Daniel García Aubert
61ea05d1c2 Do not assign a value by default for special float values counters 2017-09-15 14:51:02 +02:00
Daniel García Aubert
64c3e68303 Fix double declaration of 'result' 2017-09-15 14:48:54 +02:00
Daniel García Aubert
d4bb4edd1d Applyy extract method to check input options 2017-09-15 14:43:41 +02:00
Daniel García Aubert
419b29e609 Do not prefix with '_' template context 2017-09-15 14:43:26 +02:00
Daniel García Aubert
c7ed3d34e8 Use const instead of var to declare variables 2017-09-15 14:43:13 +02:00
Daniel García Aubert
1959a841fd Use arrow function to take advantage of bound context 2017-09-15 14:26:22 +02:00
Daniel García Aubert
ef5049f28f Use destructuring assignment to improve readability 2017-09-15 14:07:46 +02:00
Daniel García Aubert
d5d9044686 Use const keyword to declare variables 2017-09-15 14:05:23 +02:00
Daniel García Aubert
5d632d936e Use ES6 class syntax 2017-09-15 14:04:09 +02:00
Daniel García Aubert
90c4796d4e Remove empty line 2017-09-15 13:41:54 +02:00
Daniel García Aubert
ada58f6ea2 Use const keyword to declare varibles 2017-09-15 13:35:00 +02:00
Daniel García Aubert
b4ce13e429 Use object shorthand notation 2017-09-15 11:56:59 +02:00
Daniel García Aubert
11f7b38c69 Do not use dot module to build column type query 2017-09-15 11:54:56 +02:00
Daniel García Aubert
9771979b8f Missing call to super class in constructor 2017-09-15 10:59:07 +02:00
Daniel García Aubert
c00a93f414 Use destruturing assignment to format the formula result 2017-09-15 10:58:11 +02:00
Daniel García Aubert
ecbc7a28e7 Declare constants with const keyword 2017-09-15 10:49:20 +02:00
Daniel García Aubert
68dfed8b85 Use ES6 class syntax 2017-09-15 10:48:44 +02:00
Daniel García Aubert
2437288d9d Replace widget word by dataview 2017-09-15 10:37:51 +02:00
Daniel García Aubert
9c64d674b3 Do not use underscore 2017-09-14 18:02:13 +02:00
Daniel García Aubert
a4ecc18f2f Use default values for constructor's arguments 2017-09-14 17:57:24 +02:00
Daniel García Aubert
1063d81c1b Rename debug namespace 2017-09-14 17:56:40 +02:00
Daniel García Aubert
dcb9b8ec52 Rename BaseWidget by BaseDataview 2017-09-14 17:56:17 +02:00
Daniel García Aubert
dbb23bf9f0 Remove jshint's complaints 2017-09-14 17:24:13 +02:00
Daniel García Aubert
2a0b15f085 Remove prefix '_' while passing context param to templates 2017-09-14 17:22:34 +02:00
Daniel García Aubert
d0e2c9f898 Use debug module to print sql for debugging purposes 2017-09-14 17:21:21 +02:00
Daniel García Aubert
d328b534a5 Replace widget word by dataview 2017-09-14 17:19:16 +02:00
Daniel García Aubert
050e9776d1 Use const for requirements, constants and variables that are initialized once 2017-09-14 17:18:20 +02:00
Daniel García Aubert
c8ff61c531 Use ES6 class syntax 2017-09-14 17:15:43 +02:00
Daniel García Aubert
cdc56e703c Rename BaseWidget by BaseDataview 2017-09-14 17:10:51 +02:00
Daniel García Aubert
9a4794ee10 Remove dot requirement and use template strings to build list aggregation query 2017-09-14 17:09:55 +02:00
Daniel García Aubert
51907b9545 Apply extract method to condition 2017-09-14 16:56:55 +02:00
Daniel García Aubert
1f3b0beddf Fix missing parameter 2017-09-14 16:47:05 +02:00
Daniel García Aubert
38e2c040d1 Use template string to escape literals 2017-09-14 16:45:45 +02:00
Daniel García Aubert
46860541fe Apply extract method to validate input options 2017-09-14 16:42:25 +02:00
Daniel García Aubert
c2e99219ef Use ES6 goodies to refactor format method 2017-09-14 16:30:46 +02:00
Daniel García Aubert
cc2cf78264 Rename all 'widget' ocurrences by 'dataview' 2017-09-14 16:12:39 +02:00
Daniel García Aubert
746292610a Rename debug namespace 2017-09-14 16:10:23 +02:00
Daniel García Aubert
b05083bcfc Move search's templates along aggregation's templates 2017-09-14 16:09:44 +02:00
Daniel García Aubert
cd13107a4d Use ES6 let & const to declare variables 2017-09-14 16:08:12 +02:00
Daniel García Aubert
46254eaf74 rename BaseWidget by BaseDataview 2017-09-14 15:59:15 +02:00
Daniel García Aubert
086eff01a9 Use ES6 class syntax 2017-09-14 15:57:18 +02:00
Daniel García Aubert
02949003a9 Build search query in two steps 2017-09-14 15:48:54 +02:00
Daniel García Aubert
0a894da0df Remove prefix to context's properties 2017-09-14 15:27:57 +02:00
Daniel García Aubert
e2ab48bee2 Remove prefix to context's properties 2017-09-14 15:22:58 +02:00
Daniel García Aubert
132fce84c5 Remove string template in the middle 2017-09-14 15:07:30 +02:00
Daniel García Aubert
b1508af007 Remove prefix to context's props in order to share it throught all templates avoiding to duplicate passing custom params again and again 2017-09-14 13:14:12 +02:00
Daniel García Aubert
65dca454f4 Move aggragation query to its own query template 2017-09-14 12:18:03 +02:00
Daniel García Aubert
3682740f08 Build aggregation query with string templates avoiding to join all inner templates 2017-09-14 12:01:41 +02:00
Daniel García Aubert
a434015d5b Move categories CTE template out of aggregation class 2017-09-14 11:27:03 +02:00
Daniel García Aubert
2f4f719f55 Use object properties in class method instead of passing them as parameters 2017-09-13 19:42:25 +02:00
Daniel García Aubert
75645e2d7a Use string templates to build categories CTE sql 2017-09-13 19:34:09 +02:00
Daniel García Aubert
4d1a53c20f Use string templates to build error message 2017-09-13 19:27:25 +02:00
Daniel García Aubert
ee471184b9 Use default values for input params 2017-09-13 19:19:25 +02:00
Daniel García Aubert
4518b7cb6e Declare requirement with const keyword 2017-09-13 19:17:16 +02:00
Daniel García Aubert
306df5be5a Replace underscore's function by ES6 equivalents 2017-09-13 19:16:08 +02:00
Daniel García Aubert
33e8657e35 Declare constants with const keyword 2017-09-13 18:40:09 +02:00
Daniel García Aubert
6fd3388fa2 Replace dot templates by ES6 string templates 2017-09-13 18:38:54 +02:00
Daniel García Aubert
4a89ad57d7 Remove '_' as prefix for template's context properties 2017-09-12 13:05:46 +02:00
Daniel García Aubert
c0cfdad7d1 Use hasOwnProperty method to check if histogram is a time-series 2017-09-12 10:38:53 +02:00
Daniel García Aubert
8f797c3c41 Fix EOF 2017-09-12 10:16:16 +02:00
Daniel García Aubert
2576c3e7d5 Rename _shouldOverrideRange & _shouldOverrideBins methods 2017-09-12 10:14:55 +02:00
Daniel García Aubert
3a936474cf Fix bad merge with master 2017-09-11 19:48:46 +02:00
Daniel García Aubert
a98f5bf08b Merge branch 'master' into time-series-refactor 2017-09-11 19:38:21 +02:00
Daniel García Aubert
03babcb43b Simplify condition and remove unused method 2017-09-11 19:31:38 +02:00
Daniel García Aubert
9aa5a9e850 Improve comment 2017-09-11 19:26:28 +02:00
Daniel García Aubert
e3bffcd39d Use inline functions to filter desired fields of the row 2017-09-11 19:24:01 +02:00
Daniel García Aubert
5fc2b46d56 Fix bad condition 2017-09-11 19:14:11 +02:00
Daniel García Aubert
7c69240748 Use parameter default value 2017-09-11 19:10:06 +02:00
Daniel García Aubert
ee43378c68 Use arrow function 2017-09-11 19:09:05 +02:00
Daniel García Aubert
09981c2560 Extract method to check valid aggregation 2017-09-11 18:57:16 +02:00
Daniel García Aubert
fd9534797c Minor refactors 2017-09-11 18:44:14 +02:00
Daniel García Aubert
38e7e71328 Implement template method pattern to format histogram query output 2017-09-11 17:19:02 +02:00
Daniel García Aubert
271932a80d Extract condition to a method 2017-09-11 17:17:42 +02:00
Daniel García Aubert
4f33e0d794 Rename Histogram.dataview by Histogram.histogramImplementation 2017-09-11 15:34:42 +02:00
Daniel García Aubert
ec23bfc79b Rename HistogramBase by BaseHistogram 2017-09-11 13:54:46 +02:00
Daniel García Aubert
6c3fa045cd Rename HistogramBase by BaseHistogram 2017-09-11 13:53:05 +02:00
Simon
d75ee965ae changing some 'var' to 'let/const' 2017-09-11 11:48:33 +02:00
Simon
5e9b2e45c7 creating HistogramBase with the common functions of NumericHistogram and DateHistogram 2017-09-11 11:32:20 +02:00
Simon
e4a20fa954 adding forgotten return 2017-09-08 17:57:35 +02:00
Simon
a20900210d removing unneeded _isDateHistogram function 2017-09-08 17:54:25 +02:00
Simon
2650c3b3e6 removing self=this assignment 2017-09-08 17:51:02 +02:00
Simon
25ef2610aa Varible declarations to let/const 2017-09-08 17:43:10 +02:00
Simon
92f6f59e07 Fix jshint style errors 2017-09-08 16:13:23 +02:00
Simon
5e07cc2ad1 Remove unneeded condittion 2017-09-08 16:06:54 +02:00
Simon
5593d92c4b Do not choose histogram implementation until getResult() 2017-09-08 15:55:23 +02:00
Simon
29f32cb9cc Expose dataview's methods to bypass concrete overview's implementations 2017-09-08 15:53:00 +02:00
Simon
1d4935cc9a Fix undefined while destrutcuring assignment 2017-09-08 15:50:01 +02:00
Simon
f75b4312a1 Fix undefined while destructuring assignment 2017-09-08 15:49:25 +02:00
Simon
23dd143fa5 Make Histogram class as context of state pattern 2017-09-08 12:48:08 +02:00
Simon
7d42afcdb4 remove unnecessary properties of NumericHistogram 2017-09-08 12:22:24 +02:00
Simon
78b95d05d0 make private functions 2017-09-08 12:21:22 +02:00
Simon
fb753e50a2 remove getOffset function 2017-09-08 12:19:05 +02:00
Simon
c863cdd9f6 remove getAggregation function 2017-09-08 12:17:29 +02:00
Simon
a4ebce52db remove unnecessary properties in format function 2017-09-08 12:16:56 +02:00
Simon
4a00a2d673 rename buildQueryTpl by _buildQueryTpl 2017-09-08 12:15:21 +02:00
Simon
38f0e23efe rename buildNumericHistogramQueryTpl by buildQueryTpl 2017-09-08 12:10:31 +02:00
Simon
7f14785091 fix namespace for debugging 2017-09-08 12:07:49 +02:00
Simon
db969a51ad make public some private functions 2017-09-08 12:07:07 +02:00
Simon
3441ad6aa9 rename _buildDateHistogramQueryTpl by _buildQueryTpl 2017-09-08 12:04:25 +02:00
Simon
347dea8f66 naming private functions 2017-09-08 12:01:15 +02:00
Simon
a3112aa929 fix function name in recursion 2017-09-08 11:55:28 +02:00
Simon
157946cc42 Rename DateHistogram class 2017-09-08 11:54:30 +02:00
Simon
8ce25d958c fix namespace for debugging 2017-09-08 11:53:47 +02:00
Daniel García Aubert
7e099be134 Add specific implementations of histograms based on column type; still not used 2017-09-08 10:29:54 +02:00
Javier Torres
6b2e2b2241 Stub next release (package.json) 2017-09-07 16:00:27 +02:00
Javier Torres
855e5c9e4c Stub next release 2017-09-07 15:59:32 +02:00
Javier Torres
a24792f46d Release 3.12.9 2017-09-07 15:57:11 +02:00
Javier Torres
0eb57f6801 Merge pull request #743 from CartoDB/718-quantiles_turbo_carto
Do not use distinct when calculating quantiles
2017-09-07 15:55:47 +02:00
Javier Torres
f1246cb060 Bump to 3.12.9 2017-09-07 15:54:43 +02:00
Javier Torres
7dd5c5b15d Do not use distinct when calculating quantiles 2017-09-07 14:39:25 +02:00
Ivan Malagon
806c13beac Release 3.12.8 2017-09-07 10:51:23 +02:00
Ivan Malagon
69f110e037 Merge pull request #742 from CartoDB/fix-histogram-out-of-range
Fix out of range bug in date histograms
2017-09-07 10:49:49 +02:00
Ivan Malagon
d77075295e Update version and NEWS 2017-09-07 10:48:21 +02:00
Daniel García Aubert
63a7ee08d0 Avoid nested ternaries for date histograms 2017-09-06 19:15:39 +02:00
Daniel García Aubert
b63a67a5b8 Avoid nested ternaries 2017-09-06 18:33:51 +02:00
Daniel García Aubert
1ac8455dc2 Use template strings to build histogram query 2017-09-06 18:13:34 +02:00
Daniel García Aubert
9f52e58be8 Rename BaseWidget by BaseDataview 2017-09-06 16:54:08 +02:00
Daniel García Aubert
4edf18f77a Remove underscore requirement 2017-09-06 16:52:17 +02:00
Daniel García Aubert
b5d2de8edc Do not use _.omit() 2017-09-06 16:49:00 +02:00
Ivan Malagon
bd8d147a7d Fix out of range bug in date histograms 2017-09-06 16:21:01 +02:00
Daniel García Aubert
8ac041805c Use typeof !== string instead of underscore's equivalent 2017-09-06 16:20:01 +02:00
Daniel García Aubert
6e0dc8666d Use .hasOwnProperty() instead of underscore's equivalent 2017-09-06 16:14:29 +02:00
Daniel García Aubert
3e55bd2abb Make happy to jshint 2017-09-06 15:56:52 +02:00
Daniel García Aubert
da1d0550f6 Use const keyword for constants 2017-09-06 15:52:13 +02:00
Daniel García Aubert
c37ef36a61 Move parseOffset function to a class method 2017-09-06 15:49:05 +02:00
Daniel García Aubert
9e3e1cad9a Move getWidth function to a class method 2017-09-06 15:47:21 +02:00
Daniel García Aubert
e84f30488f Move getBinsCount function to a class method 2017-09-06 15:45:51 +02:00
Daniel García Aubert
49a60caffc Move getBinEnd function to a class method 2017-09-06 15:42:29 +02:00
Daniel García Aubert
392e004879 Move getBinStart and populateBinStart function to a class method 2017-09-06 15:38:23 +02:00
Daniel García Aubert
288656301b Move getOffset function to a class method 2017-09-06 15:37:28 +02:00
Daniel García Aubert
96740b82ed Move getAggregation function to a class method 2017-09-06 13:52:02 +02:00
Daniel García Aubert
1be66e1552 Use const for requirements 2017-09-06 13:47:19 +02:00
Daniel García Aubert
5ba2dfbbd6 Use ES6 class syntax 2017-09-06 13:43:54 +02:00
Daniel García Aubert
af4b3d81cd make happy to jshint 2017-09-06 11:55:40 +02:00
Daniel García Aubert
bbd42b73f2 Remove dot requirement 2017-09-06 11:44:52 +02:00
Daniel García Aubert
8e2535745e Use template string for columnCastTpl 2017-09-06 11:44:25 +02:00
Daniel García Aubert
4f75f6c07b Use template string for dateBinsQueryTpl and dateHistogramQueryTpl 2017-09-06 11:41:24 +02:00
Daniel García Aubert
0ede3013db Use template string for dateOverrideBasicsQueryTpl 2017-09-06 10:57:38 +02:00
Daniel García Aubert
0b79ac76db Use template string for dateBasicsQueryTpl 2017-09-06 10:39:17 +02:00
Daniel García Aubert
2739364193 Use template string for histogramQueryTpl 2017-09-05 15:53:21 +02:00
Daniel García Aubert
adcff54589 Use template string for nansQueryTpl 2017-09-05 15:53:06 +02:00
Daniel García Aubert
734cfa6d83 Fix undefined argument 2017-09-05 15:51:31 +02:00
Daniel García Aubert
7ea6b3e371 Use template string for infinitiesQueryTpl 2017-09-05 12:21:30 +02:00
Daniel García Aubert
f1018f3272 Use template string for nullsQueryTpl 2017-09-05 12:18:42 +02:00
Daniel García Aubert
151bdec1fd Use template string for overrideBinsQueryTpl 2017-09-05 12:16:38 +02:00
Daniel García Aubert
5d413ac1f9 Use template string for overrideBasicsQueryTpl 2017-09-05 11:26:38 +02:00
Daniel García Aubert
37b1376767 Fix bad find & replace 2017-09-05 11:26:27 +02:00
Daniel García Aubert
00741bc0a4 Use template string for basicsQueryTpl 2017-09-05 10:41:48 +02:00
Daniel García Aubert
c580600590 Extract template to filter out special numeric values 2017-09-05 10:36:18 +02:00
Daniel García Aubert
6373fe8652 Use template string for filteredQueryTpl 2017-09-04 19:01:58 +02:00
Daniel García Aubert
5ce419d863 Use template string for dateIntervalQueryTpl 2017-09-04 18:42:30 +02:00
Mario de Frutos
7be5361433 Upgrade to camshaft 0.58.1 (#739) 2017-09-01 11:31:06 +02:00
Mario de Frutos
5332fd3baa Stubs next version 2017-08-31 10:21:00 +02:00
Mario de Frutos
77f1aa7e0c Update to camshaft 0.58.0 (#737) 2017-08-31 10:12:58 +02:00
Raul Ochoa
e1990fc2f9 Use the correct fixture image 2017-08-29 13:29:39 +00:00
Raul Ochoa
ca6eb609b2 Update news 2017-08-29 13:08:14 +00:00
Raul Ochoa
91ce3a5489 Going green: allow to use bbox param, along lon, lat, and zoom
The `bbox` param was removed from the base controller, the rest kept
working as they are declared in the base, but it's better to declare
them here as well.

Fixes #735.
2017-08-29 13:05:03 +00:00
Raul Ochoa
fc0dbaaab1 Going red: Bounding box parameter ignored in static named maps 2017-08-29 13:04:20 +00:00
Mario de Frutos
03dc260104 Stubs new version 2017-08-24 12:09:51 +02:00
Mario de Frutos
d644376f88 Update to camshaft 0.57.0 (#733)
* Update to camshaft 0.57.0

* Yarn.lock with update to camshaft 0.57.0
2017-08-24 12:07:43 +02:00
Mario de Frutos
ed0bfa5f63 Stubs next version 2017-08-23 16:53:01 +02:00
Mario de Frutos
ca0b927f51 Update camshaft to 0.56.0 (#731) 2017-08-23 16:34:42 +02:00
Simon
cd27d6aa02 Stub NEWS/package for next version 2017-08-22 16:37:23 +02:00
Raul Ochoa
399561d076 Implement LZMA query param inflating as middleware
The req2params method is doing too many things,
this is an initial step to do fewer things in that method.
2017-08-04 17:30:46 +02:00
Daniel García Aubert
6a4b412cd3 Avoid lint 2017-03-27 18:27:55 +02:00
Daniel García Aubert
2374711d63 Remove reference to issue 2017-03-24 17:34:05 +01:00
Daniel García Aubert
213a3e297c Check node version 2017-03-24 17:28:23 +01:00
32 changed files with 1916 additions and 2472 deletions

View File

@@ -5,7 +5,7 @@ Make sure that you have the requirements needed. These are
- Core
- Node.js >=6.9.x
- yarn >=0.21.3
- yarn >=0.27.5 <1.0.0
- PostgreSQL >8.3.x, PostGIS >1.5.x
- Redis >2.4.0 (http://www.redis.io)
- Mapnik >3.x. See [Installing Mapnik](https://github.com/CartoDB/Windshaft#installing-mapnik).

66
NEWS.md
View File

@@ -1,5 +1,71 @@
# Changelog
## 4.0.0
Released 2017-10-04
Backward incompatible changes:
- Removes `list` dataview type.
Announcements:
- Upgrades body-parser to 1.18.2.
- Upgrades express to 4.16.0.
- Upgrades debug to 3.1.0.
- Upgrades request to 2.83.0.
- Upgrades turbo-carto to [0.20.1](https://github.com/CartoDB/turbo-carto/releases/tag/0.20.1)
- Upgrades cartodb-psql to [0.10.2](https://github.com/CartoDB/node-cartodb-psql/releases/tag/0.10.2).
- Upgrades camshaft to [0.59.2](https://github.com/CartoDB/camshaft/releases/tag/0.59.2).
- Upgrades windshaft to [3.3.3](https://github.com/CartoDB/windshaft/releases/tag/3.3.3).
- Upgrades yarn minimum version requirement to v0.27.5
## 3.13.0
Released 2017-10-02
- Upgrades camshaft, cartodb-query-tables, and turbo-carto: better support for query variables.
Bugfixes:
- Bounding box parameter ignored in static named maps #735.
- camhaft 0.59.1 fixes duplicate columns in aggregate-intersection analysis
## 3.12.10
Released 2017-09-18
- Upgrades windshaft to [3.3.2](https://github.com/CartoDB/windshaft/releases/tag/3.3.2).
## 3.12.9
Released 2017-09-07
Bug fixes:
- Do not use distinct when calculating quantiles. #743
## 3.12.8
Released 2017-09-07
Bug fixes:
- Integer out of range in date histograms. (https://github.com/CartoDB/support/issues/962)
## 3.12.7
Released 2017-09-01
- Upgrades camshaft to [0.58.1](https://github.com/CartoDB/camshaft/releases/tag/0.58.1).
## 3.12.6
Released 2017-08-31
- Upgrades camshaft to [0.58.0](https://github.com/CartoDB/camshaft/releases/tag/0.58.0).
## 3.12.5
Released 2017-08-24
- Upgrades camshaft to [0.57.0](https://github.com/CartoDB/camshaft/releases/tag/0.57.0).
## 3.12.4
Released 2017-08-23
Announcements:
- Upgrades camshaft to [0.56.0](https://github.com/CartoDB/camshaft/releases/tag/0.56.0).
## 3.12.3
Released 2017-08-22

8
app.js
View File

@@ -2,14 +2,20 @@ var http = require('http');
var https = require('https');
var path = require('path');
var fs = require('fs');
var _ = require('underscore');
var semver = require('semver');
// jshint undef:false
var log = console.log.bind(console);
var logError = console.error.bind(console);
// jshint undef:true
var nodejsVersion = process.versions.node;
if (!semver.satisfies(nodejsVersion, '>=6.9.0')) {
logError(`Node version ${nodejsVersion} is not supported, please use Node.js 6.9 or higher.`);
process.exit(1);
}
var argv = require('yargs')
.usage('Usage: $0 <environment> [options]')
.help('h')

View File

@@ -8,50 +8,13 @@ This specification describes an extension for
This extension depends on Analyses extension. It extends MapConfig with a new attribute: `dataviews`.
It makes possible to get tabular data from analysis nodes: lists, aggregated lists, aggregations, and histograms.
It makes possible to get tabular data from analysis nodes: aggregated lists, aggregations, and histograms.
## 2.1. Dataview types
### List
A list is a simple result set per row where is possible to retrieve several columns from the original layer query.
Definition
```
{
// REQUIRED
// string, `type` the list type
“type”: “list”,
// REQUIRED
// object, `options` dataview params
“options”: {
// REQUIRED
// array, `columns` to select for the list
“columns”: [“name”, “description”]
}
}
```
Expected output
```
{
"type": "list",
"rows": [
{
"{columnName1}": "val1",
"{columnName2}": 100
},
{
"{columnName1}": "val2",
"{columnName2}": 200
}
]
}
```
### Aggregation
An aggregation is very similar to a list but results are aggregated by a column and a given aggregation function.
An aggregation is a list with aggregated results by a column and a given aggregation function.
Definition
```

View File

@@ -21,10 +21,10 @@ function createTemplate(method) {
}
var methods = {
quantiles: 'CDB_QuantileBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as quantiles',
quantiles: 'CDB_QuantileBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as quantiles',
equal: 'CDB_EqualIntervalBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as equal',
jenks: 'CDB_JenksBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as jenks',
headtails: 'CDB_HeadsTailsBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as headtails'
jenks: 'CDB_JenksBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as jenks',
headtails: 'CDB_HeadsTailsBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as headtails'
};
var methodTemplates = Object.keys(methods).reduce(function(methodTemplates, methodName) {

View File

@@ -4,9 +4,6 @@ var _ = require('underscore');
var step = require('step');
var debug = require('debug')('windshaft:cartodb');
var LZMA = require('lzma').LZMA;
var lzmaWorker = new LZMA();
// Whitelist query parameters and attach format
var REQUEST_QUERY_PARAMS_WHITELIST = [
'config',
@@ -28,7 +25,7 @@ function BaseController(authApi, pgConnection) {
module.exports = BaseController;
// jshint maxcomplexity:10
// jshint maxcomplexity:8
/**
* Whitelist input and get database name & default geometry type from
* subdomain/user metadata held in CartoDB Redis
@@ -38,35 +35,6 @@ module.exports = BaseController;
BaseController.prototype.req2params = function(req, callback){
var self = this;
if ( req.query.lzma ) {
// Decode (from base64)
var lzma = new Buffer(req.query.lzma, 'base64')
.toString('binary')
.split('')
.map(function(c) {
return c.charCodeAt(0) - 128;
});
// Decompress
lzmaWorker.decompress(
lzma,
function(result) {
req.profiler.done('lzma');
try {
delete req.query.lzma;
_.extend(req.query, JSON.parse(result));
self.req2params(req, callback);
} catch (err) {
req.profiler.done('req2params');
callback(new Error('Error parsing lzma as JSON: ' + err));
}
}
);
return;
}
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
if (Array.isArray(req.context.allowedQueryParams)) {
allowedQueryParams = allowedQueryParams.concat(req.context.allowedQueryParams);

View File

@@ -32,7 +32,8 @@ NamedMapsController.prototype.register = function(app) {
this.tile.bind(this));
app.get(app.base_url_mapconfig +
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware, allowQueryParams(['layer']),
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware,
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
this.staticMap.bind(this));
};

View File

@@ -0,0 +1,30 @@
'use strict';
var LZMA = require('lzma').LZMA;
var lzmaWorker = new LZMA();
module.exports = function lzmaMiddleware(req, res, next) {
if (!req.query.hasOwnProperty('lzma')) {
return next();
}
// Decode (from base64)
var lzma = new Buffer(req.query.lzma, 'base64')
.toString('binary')
.split('')
.map(function(c) {
return c.charCodeAt(0) - 128;
});
// Decompress
lzmaWorker.decompress(lzma, function(result) {
try {
delete req.query.lzma;
Object.assign(req.query, JSON.parse(result));
next();
} catch (err) {
next(new Error('Error parsing lzma as JSON: ' + err));
}
});
};

View File

@@ -1,95 +1,178 @@
var _ = require('underscore');
var BaseWidget = require('./base');
var debug = require('debug')('windshaft:widget:aggregation');
const BaseDataview = require('./base');
const debug = require('debug')('windshaft:dataview:aggregation');
var dot = require('dot');
dot.templateSettings.strip = false;
const filteredQueryTpl = ctx => `
filtered_source AS (
SELECT *
FROM (${ctx.query}) _cdb_filtered_source
${ctx.aggregationColumn && ctx.isFloatColumn ? `
WHERE
${ctx.aggregationColumn} != 'infinity'::float
AND
${ctx.aggregationColumn} != '-infinity'::float
AND
${ctx.aggregationColumn} != 'NaN'::float` :
''
}
)
`;
var filteredQueryTpl = dot.template([
'filtered_source AS (',
' SELECT *',
' FROM ({{=it._query}}) _cdb_filtered_source',
' {{?it._aggregationColumn && it._isFloatColumn}}WHERE',
' {{=it._aggregationColumn}} != \'infinity\'::float',
' AND',
' {{=it._aggregationColumn}} != \'-infinity\'::float',
' AND',
' {{=it._aggregationColumn}} != \'NaN\'::float{{?}}',
')'
].join(' \n'));
const summaryQueryTpl = ctx => `
summary AS (
SELECT
count(1) AS count,
sum(CASE WHEN ${ctx.column} IS NULL THEN 1 ELSE 0 END) AS nulls_count
${ctx.isFloatColumn ? `,
sum(
CASE
WHEN ${ctx.aggregationColumn} = 'infinity'::float OR ${ctx.aggregationColumn} = '-infinity'::float
THEN 1
ELSE 0
END
) AS infinities_count,
sum(CASE WHEN ${ctx.aggregationColumn} = 'NaN'::float THEN 1 ELSE 0 END) AS nans_count` :
''
}
FROM (${ctx.query}) _cdb_aggregation_nulls
)
`;
var summaryQueryTpl = dot.template([
'summary AS (',
' SELECT',
' count(1) AS count,',
' sum(CASE WHEN {{=it._column}} IS NULL THEN 1 ELSE 0 END) AS nulls_count',
' {{?it._isFloatColumn}},sum(',
' CASE',
' WHEN {{=it._aggregationColumn}} = \'infinity\'::float OR {{=it._aggregationColumn}} = \'-infinity\'::float',
' THEN 1',
' ELSE 0',
' END',
' ) AS infinities_count,',
' sum(CASE WHEN {{=it._aggregationColumn}} = \'NaN\'::float THEN 1 ELSE 0 END) AS nans_count{{?}}',
' FROM ({{=it._query}}) _cdb_aggregation_nulls',
')'
].join('\n'));
const rankedCategoriesQueryTpl = ctx => `
categories AS(
SELECT
${ctx.column} AS category,
${ctx.aggregationFn} AS value,
row_number() OVER (ORDER BY ${ctx.aggregationFn} desc) as rank
FROM filtered_source
${ctx.aggregationColumn !== null ? `WHERE ${ctx.aggregationColumn} IS NOT NULL` : ''}
GROUP BY ${ctx.column}
ORDER BY 2 DESC
)
`;
var rankedCategoriesQueryTpl = dot.template([
'categories AS(',
' SELECT {{=it._column}} AS category, {{=it._aggregation}} AS value,',
' row_number() OVER (ORDER BY {{=it._aggregation}} desc) as rank',
' FROM filtered_source',
' {{?it._aggregationColumn!==null}}WHERE {{=it._aggregationColumn}} IS NOT NULL{{?}}',
' GROUP BY {{=it._column}}',
' ORDER BY 2 DESC',
')'
].join('\n'));
const categoriesSummaryMinMaxQueryTpl = () => `
categories_summary_min_max AS(
SELECT
max(value) max_val,
min(value) min_val
FROM categories
)
`;
var categoriesSummaryMinMaxQueryTpl = dot.template([
'categories_summary_min_max AS(',
' SELECT max(value) max_val, min(value) min_val',
' FROM categories',
')'
].join('\n'));
const categoriesSummaryCountQueryTpl = ctx => `
categories_summary_count AS(
SELECT count(1) AS categories_count
FROM (
SELECT ${ctx.column} AS category
FROM (${ctx.query}) _cdb_categories
GROUP BY ${ctx.column}
) _cdb_categories_count
)
`;
var categoriesSummaryCountQueryTpl = dot.template([
'categories_summary_count AS(',
' SELECT count(1) AS categories_count',
' FROM (',
' SELECT {{=it._column}} AS category',
' FROM ({{=it._query}}) _cdb_categories',
' GROUP BY {{=it._column}}',
' ) _cdb_categories_count',
')'
].join('\n'));
const specialNumericValuesColumns = () => `, nans_count, infinities_count`;
var rankedAggregationQueryTpl = dot.template([
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val,',
' count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
' WHERE rank < {{=it._limit}}',
'UNION ALL',
'SELECT \'Other\' category, {{=it._aggregationFn}}(value) as value, true as agg, nulls_count,',
' min_val, max_val, count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
' WHERE rank >= {{=it._limit}}',
'GROUP BY nulls_count, min_val, max_val, count,',
' categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}'
].join('\n'));
const rankedAggregationQueryTpl = ctx => `
SELECT
CAST(category AS text),
value,
false as agg,
nulls_count,
min_val,
max_val,
count,
categories_count
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
FROM categories, summary, categories_summary_min_max, categories_summary_count
WHERE rank < ${ctx.limit}
UNION ALL
SELECT
'Other' category,
${ctx.aggregation !== 'count' ? ctx.aggregation : 'sum'}(value) as value,
true as agg,
nulls_count,
min_val,
max_val,
count,
categories_count
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
FROM categories, summary, categories_summary_min_max, categories_summary_count
WHERE rank >= ${ctx.limit}
GROUP BY
nulls_count,
min_val,
max_val,
count,
categories_count
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
`;
var aggregationQueryTpl = dot.template([
'SELECT CAST({{=it._column}} AS text) AS category, {{=it._aggregation}} AS value, false as agg,',
' nulls_count, min_val, max_val, count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
'FROM ({{=it._query}}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count',
'GROUP BY category, nulls_count, min_val, max_val, count,',
' categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
'ORDER BY value DESC'
].join('\n'));
const aggregationQueryTpl = ctx => `
SELECT
CAST(${ctx.column} AS text) AS category,
${ctx.aggregationFn} AS value,
false as agg,
nulls_count,
min_val,
max_val,
count,
categories_count
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
FROM (${ctx.query}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count
GROUP BY
category,
nulls_count,
min_val,
max_val,
count,
categories_count
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
ORDER BY value DESC
`;
var CATEGORIES_LIMIT = 6;
const aggregationFnQueryTpl = ctx => `${ctx.aggregation}(${ctx.aggregationColumn})`;
var VALID_OPERATIONS = {
const aggregationDataviewQueryTpl = ctx => `
WITH
${filteredQueryTpl(ctx)},
${summaryQueryTpl(ctx)},
${rankedCategoriesQueryTpl(ctx)},
${categoriesSummaryMinMaxQueryTpl(ctx)},
${categoriesSummaryCountQueryTpl(ctx)}
${!!ctx.override.ownFilter ? `${aggregationQueryTpl(ctx)}` : `${rankedAggregationQueryTpl(ctx)}`}
`;
const filterCategoriesQueryTpl = ctx => `
SELECT
${ctx.column} AS category,
${ctx.value} AS value
FROM (${ctx.query}) _cdb_aggregation_search
WHERE CAST(${ctx.column} as text) ILIKE ${ctx.userQuery}
GROUP BY ${ctx.column}
`;
const searchQueryTpl = ctx => `
WITH
search_unfiltered AS (
${ctx.searchUnfiltered}
),
search_filtered AS (
${ctx.searchFiltered}
),
search_union AS (
SELECT * FROM search_unfiltered
UNION ALL
SELECT * FROM search_filtered
)
SELECT category, sum(value) AS value
FROM search_union
GROUP BY category
ORDER BY value desc
`;
const CATEGORIES_LIMIT = 6;
const VALID_OPERATIONS = {
count: [],
sum: ['aggregationColumn'],
avg: ['aggregationColumn'],
@@ -97,7 +180,7 @@ var VALID_OPERATIONS = {
max: ['aggregationColumn']
};
var TYPE = 'aggregation';
const TYPE = 'aggregation';
/**
{
@@ -108,256 +191,150 @@ var TYPE = 'aggregation';
}
}
*/
function Aggregation(query, options, queries) {
if (!_.isString(options.column)) {
throw new Error('Aggregation expects `column` in widget options');
module.exports = class Aggregation extends BaseDataview {
constructor (query, options = {}, queries = {}) {
super();
this._checkOptions(options);
this.query = query;
this.queries = queries;
this.column = options.column;
this.aggregation = options.aggregation;
this.aggregationColumn = options.aggregationColumn;
this._isFloatColumn = null;
}
if (!_.isString(options.aggregation)) {
throw new Error('Aggregation expects `aggregation` operation in widget options');
}
if (!VALID_OPERATIONS[options.aggregation]) {
throw new Error("Aggregation does not support '" + options.aggregation + "' operation");
}
var requiredOptions = VALID_OPERATIONS[options.aggregation];
var missingOptions = _.difference(requiredOptions, Object.keys(options));
if (missingOptions.length > 0) {
throw new Error(
"Aggregation '" + options.aggregation + "' is missing some options: " + missingOptions.join(',')
);
}
BaseWidget.apply(this);
this.query = query;
this.queries = queries;
this.column = options.column;
this.aggregation = options.aggregation;
this.aggregationColumn = options.aggregationColumn;
this._isFloatColumn = null;
}
Aggregation.prototype = new BaseWidget();
Aggregation.prototype.constructor = Aggregation;
module.exports = Aggregation;
Aggregation.prototype.sql = function(psql, override, callback) {
var self = this;
if (!callback) {
callback = override;
override = {};
}
if (this.aggregationColumn && this._isFloatColumn === null) {
this._isFloatColumn = false;
this.getColumnType(psql, this.aggregationColumn, this.queries.no_filters, function (err, type) {
if (!err && !!type) {
self._isFloatColumn = type.float;
}
self.sql(psql, override, callback);
});
return null;
}
var _query = this.query;
var aggregationSql;
if (!!override.ownFilter) {
aggregationSql = [
this.getCategoriesCTESql(
_query,
this.column,
this.aggregation,
this.aggregationColumn,
this._isFloatColumn
),
aggregationQueryTpl({
_isFloatColumn: this._isFloatColumn,
_query: _query,
_column: this.column,
_aggregation: this.getAggregationSql(),
_limit: CATEGORIES_LIMIT
})
].join('\n');
} else {
aggregationSql = [
this.getCategoriesCTESql(
_query,
this.column,
this.aggregation,
this.aggregationColumn,
this._isFloatColumn
),
rankedAggregationQueryTpl({
_isFloatColumn: this._isFloatColumn,
_query: _query,
_column: this.column,
_aggregationFn: this.aggregation !== 'count' ? this.aggregation : 'sum',
_limit: CATEGORIES_LIMIT
})
].join('\n');
}
debug(aggregationSql);
return callback(null, aggregationSql);
};
Aggregation.prototype.getCategoriesCTESql = function(query, column, aggregation, aggregationColumn, isFloatColumn) {
return [
"WITH",
[
filteredQueryTpl({
_isFloatColumn: isFloatColumn,
_query: this.query,
_column: this.column,
_aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
}),
summaryQueryTpl({
_isFloatColumn: isFloatColumn,
_query: query,
_column: column,
_aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
}),
rankedCategoriesQueryTpl({
_query: query,
_column: column,
_aggregation: this.getAggregationSql(),
_aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
}),
categoriesSummaryMinMaxQueryTpl({
_query: query,
_column: column
}),
categoriesSummaryCountQueryTpl({
_query: query,
_column: column
})
].join(',\n')
].join('\n');
};
var aggregationFnQueryTpl = dot.template('{{=it._aggregationFn}}({{=it._aggregationColumn}})');
Aggregation.prototype.getAggregationSql = function() {
return aggregationFnQueryTpl({
_aggregationFn: this.aggregation,
_aggregationColumn: this.aggregationColumn || 1
});
};
Aggregation.prototype.format = function(result) {
var categories = [];
var count = 0;
var nulls = 0;
var nans = 0;
var infinities = 0;
var minValue = 0;
var maxValue = 0;
var categoriesCount = 0;
if (result.rows.length) {
var firstRow = result.rows[0];
count = firstRow.count;
nulls = firstRow.nulls_count;
nans = firstRow.nans_count;
infinities = firstRow.infinities_count;
minValue = firstRow.min_val;
maxValue = firstRow.max_val;
categoriesCount = firstRow.categories_count;
result.rows.forEach(function(row) {
categories.push(_.omit(row, 'count', 'nulls_count', 'min_val',
'max_val', 'categories_count', 'nans_count', 'infinities_count'));
});
}
return {
aggregation: this.aggregation,
count: count,
nulls: nulls,
nans: nans,
infinities: infinities,
min: minValue,
max: maxValue,
categoriesCount: categoriesCount,
categories: categories
};
};
var filterCategoriesQueryTpl = dot.template([
'SELECT {{=it._column}} AS category, {{=it._value}} AS value',
'FROM ({{=it._query}}) _cdb_aggregation_search',
'WHERE CAST({{=it._column}} as text) ILIKE {{=it._userQuery}}',
'GROUP BY {{=it._column}}'
].join('\n'));
var searchQueryTpl = dot.template([
'WITH',
'search_unfiltered AS (',
' {{=it._searchUnfiltered}}',
'),',
'search_filtered AS (',
' {{=it._searchFiltered}}',
'),',
'search_union AS (',
' SELECT * FROM search_unfiltered',
' UNION ALL',
' SELECT * FROM search_filtered',
')',
'SELECT category, sum(value) AS value',
'FROM search_union',
'GROUP BY category',
'ORDER BY value desc'
].join('\n'));
Aggregation.prototype.search = function(psql, userQuery, callback) {
var self = this;
var _userQuery = psql.escapeLiteral('%' + userQuery + '%');
var _value = this.aggregation !== 'count' && this.aggregationColumn ?
this.aggregation + '(' + this.aggregationColumn + ')' : 'count(1)';
// TODO unfiltered will be wrong as filters are already applied at this point
var query = searchQueryTpl({
_searchUnfiltered: filterCategoriesQueryTpl({
_query: this.query,
_column: this.column,
_value: '0',
_userQuery: _userQuery
}),
_searchFiltered: filterCategoriesQueryTpl({
_query: this.query,
_column: this.column,
_value: _value,
_userQuery: _userQuery
})
});
psql.query(query, function(err, result) {
if (err) {
return callback(err, result);
_checkOptions (options) {
if (typeof options.column !== 'string') {
throw new Error(`Aggregation expects 'column' in dataview options`);
}
return callback(null, {type: self.getType(), categories: result.rows });
}, true); // use read-only transaction
};
if (typeof options.aggregation !== 'string') {
throw new Error(`Aggregation expects 'aggregation' operation in dataview options`);
}
Aggregation.prototype.getType = function() {
return TYPE;
};
if (!VALID_OPERATIONS[options.aggregation]) {
throw new Error(`Aggregation does not support '${options.aggregation}' operation`);
}
Aggregation.prototype.toString = function() {
return JSON.stringify({
_type: TYPE,
_query: this.query,
_column: this.column,
_aggregation: this.aggregation
});
const requiredOptions = VALID_OPERATIONS[options.aggregation];
const missingOptions = requiredOptions.filter(requiredOption => !options.hasOwnProperty(requiredOption));
if (missingOptions.length > 0) {
throw new Error(
`Aggregation '${options.aggregation}' is missing some options: ${missingOptions.join(',')}`
);
}
}
sql (psql, override, callback) {
if (!callback) {
callback = override;
override = {};
}
if (this._shouldCheckColumnType()) {
this._isFloatColumn = false;
this.getColumnType(psql, this.aggregationColumn, this.queries.no_filters, (err, type) => {
if (!err && !!type) {
this._isFloatColumn = type.float;
}
this.sql(psql, override, callback);
});
return null;
}
const aggregationSql = aggregationDataviewQueryTpl({
override: override,
query: this.query,
column: this.column,
aggregation: this.aggregation,
aggregationColumn: this.aggregation !== 'count' ? this.aggregationColumn : null,
aggregationFn: aggregationFnQueryTpl({
aggregation: this.aggregation,
aggregationColumn: this.aggregationColumn || 1
}),
isFloatColumn: this._isFloatColumn,
limit: CATEGORIES_LIMIT
});
debug(aggregationSql);
return callback(null, aggregationSql);
}
_shouldCheckColumnType () {
return this.aggregationColumn && this._isFloatColumn === null;
}
format (result) {
const {
count = 0,
nulls_count = 0,
nans_count = 0,
infinities_count = 0,
min_val = 0,
max_val = 0,
categories_count = 0
} = result.rows[0] || {};
return {
aggregation: this.aggregation,
count: count,
nulls: nulls_count,
nans: nans_count,
infinities: infinities_count,
min: min_val,
max: max_val,
categoriesCount: categories_count,
categories: result.rows.map(({ category, value, agg }) => ({ category, value, agg }))
};
}
search (psql, userQuery, callback) {
const escapedUserQuery = psql.escapeLiteral(`%${userQuery}%`);
const value = this.aggregation !== 'count' && this.aggregationColumn ?
`${this.aggregation}(${this.aggregationColumn})` :
'count(1)';
// TODO unfiltered will be wrong as filters are already applied at this point
const query = searchQueryTpl({
searchUnfiltered: filterCategoriesQueryTpl({
query: this.query,
column: this.column,
value: '0',
userQuery: escapedUserQuery
}),
searchFiltered: filterCategoriesQueryTpl({
query: this.query,
column: this.column,
value: value,
userQuery: escapedUserQuery
})
});
debug(query);
psql.query(query, (err, result) => {
if (err) {
return callback(err, result);
}
return callback(null, {type: this.getType(), categories: result.rows });
}, true); // use read-only transaction
}
getType () {
return TYPE;
}
toString () {
return JSON.stringify({
_type: TYPE,
_query: this.query,
_column: this.column,
_aggregation: this.aggregation
});
}
};

View File

@@ -1,67 +1,16 @@
var dot = require('dot');
dot.templateSettings.strip = false;
function BaseDataview() {}
module.exports = BaseDataview;
BaseDataview.prototype.getResult = function(psql, override, callback) {
var self = this;
this.sql(psql, override, function(err, query) {
if (err) {
return callback(err);
}
psql.query(query, function(err, result) {
if (err) {
return callback(err, result);
}
result = self.format(result, override);
result.type = self.getType();
return callback(null, result);
}, true); // use read-only transaction
});
};
BaseDataview.prototype.search = function(psql, userQuery, callback) {
return callback(null, this.format({ rows: [] }));
};
var FLOAT_OIDS = {
const FLOAT_OIDS = {
700: true,
701: true,
1700: true
};
var DATE_OIDS = {
const DATE_OIDS = {
1082: true,
1114: true,
1184: true
};
var columnTypeQueryTpl = dot.template(
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_column_type limit 1'
);
BaseDataview.prototype.getColumnType = function (psql, column, query, callback) {
var readOnlyTransaction = true;
var columnTypeQuery = columnTypeQueryTpl({
column: column, query: query
});
psql.query(columnTypeQuery, function(err, result) {
if (err) {
return callback(err);
}
var pgType = result.rows[0].pg_typeof;
callback(null, getPGTypeName(pgType));
}, readOnlyTransaction);
};
const columnTypeQueryTpl = ctx => `SELECT pg_typeof(${ctx.column})::oid FROM (${ctx.query}) _cdb_column_type limit 1`;
function getPGTypeName (pgType) {
return {
@@ -69,3 +18,42 @@ function getPGTypeName (pgType) {
date: DATE_OIDS.hasOwnProperty(pgType)
};
}
module.exports = class BaseDataview {
getResult (psql, override, callback) {
this.sql(psql, override, (err, query) => {
if (err) {
return callback(err);
}
psql.query(query, (err, result) => {
if (err) {
return callback(err, result);
}
result = this.format(result, override);
result.type = this.getType();
return callback(null, result);
}, true); // use read-only transaction
});
}
search (psql, userQuery, callback) {
return callback(null, this.format({ rows: [] }));
}
getColumnType (psql, column, query, callback) {
const readOnlyTransaction = true;
const columnTypeQuery = columnTypeQueryTpl({ column, query });
psql.query(columnTypeQuery, (err, result) => {
if (err) {
return callback(err);
}
const pgType = result.rows[0].pg_typeof;
callback(null, getPGTypeName(pgType));
}, readOnlyTransaction);
}
};

View File

@@ -1,18 +1,20 @@
var dataviews = require('./');
const dataviews = require('./');
var DataviewFactory = {
dataviews: Object.keys(dataviews).reduce(function(allDataviews, dataviewClassName) {
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
return allDataviews;
}, {}),
module.exports = class DataviewFactory {
static get dataviews() {
return Object.keys(dataviews).reduce((allDataviews, dataviewClassName) => {
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
return allDataviews;
}, {});
}
static getDataview (query, dataviewDefinition) {
const { type, options, sql } = dataviewDefinition;
getDataview: function(query, dataviewDefinition) {
var type = dataviewDefinition.type;
if (!this.dataviews[type]) {
throw new Error('Invalid dataview type: "' + type + '"');
}
return new this.dataviews[type](query, dataviewDefinition.options, dataviewDefinition.sql);
return new this.dataviews[type](query, options, sql);
}
};
module.exports = DataviewFactory;

View File

@@ -1,28 +1,36 @@
var _ = require('underscore');
var BaseWidget = require('./base');
var debug = require('debug')('windshaft:widget:formula');
const BaseDataview = require('./base');
const debug = require('debug')('windshaft:dataview:formula');
var dot = require('dot');
dot.templateSettings.strip = false;
const countInfinitiesQueryTpl = ctx => `
SELECT count(1) FROM (${ctx.query}) __cdb_formula_infinities
WHERE ${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float
`;
var formulaQueryTpl = dot.template([
'SELECT',
' {{=it._operation}}({{=it._column}}) AS result,',
' (SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
' {{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls',
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
' ,(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls',
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
'FROM ({{=it._query}}) _cdb_formula',
'{{?it._isFloatColumn && it._operation !== \'count\'}}WHERE',
' {{=it._column}} != \'infinity\'::float',
'AND',
' {{=it._column}} != \'-infinity\'::float',
'AND',
' {{=it._column}} != \'NaN\'::float{{?}}'
].join('\n'));
const countNansQueryTpl = ctx => `
SELECT count(1) FROM (${ctx.query}) __cdb_formula_nans
WHERE ${ctx.column} = 'NaN'::float
`;
var VALID_OPERATIONS = {
const filterOutSpecialNumericValuesTpl = ctx => `
WHERE
${ctx.column} != 'infinity'::float
AND
${ctx.column} != '-infinity'::float
AND
${ctx.column} != 'NaN'::float
`;
const formulaQueryTpl = ctx => `
SELECT
${ctx.operation}(${ctx.column}) AS result,
(SELECT count(1) FROM (${ctx.query}) _cdb_formula_nulls WHERE ${ctx.column} IS NULL) AS nulls_count
${ctx.isFloatColumn ? `,(${countInfinitiesQueryTpl(ctx)}) AS infinities_count` : ''}
${ctx.isFloatColumn ? `,(${countNansQueryTpl(ctx)}) AS nans_count` : ''}
FROM (${ctx.query}) __cdb_formula
${ctx.isFloatColumn && ctx.operation !== 'count' ? `${filterOutSpecialNumericValuesTpl(ctx)}` : ''}
`;
const VALID_OPERATIONS = {
count: true,
avg: true,
sum: true,
@@ -30,7 +38,7 @@ var VALID_OPERATIONS = {
max: true
};
var TYPE = 'formula';
const TYPE = 'formula';
/**
{
@@ -41,93 +49,90 @@ var TYPE = 'formula';
}
}
*/
function Formula(query, options, queries) {
if (!_.isString(options.operation)) {
throw new Error('Formula expects `operation` in widget options');
module.exports = class Formula extends BaseDataview {
constructor (query, options = {}, queries = {}) {
super();
this._checkOptions(options);
this.query = query;
this.queries = queries;
this.column = options.column || '1';
this.operation = options.operation;
this._isFloatColumn = null;
}
if (!VALID_OPERATIONS[options.operation]) {
throw new Error("Formula does not support '" + options.operation + "' operation");
_checkOptions (options) {
if (typeof options.operation !== 'string') {
throw new Error(`Formula expects 'operation' in dataview options`);
}
if (!VALID_OPERATIONS[options.operation]) {
throw new Error(`Formula does not support '${options.operation}' operation`);
}
if (options.operation !== 'count' && typeof options.column !== 'string') {
throw new Error(`Formula expects 'column' in dataview options`);
}
}
if (options.operation !== 'count' && !_.isString(options.column)) {
throw new Error('Formula expects `column` in widget options');
}
BaseWidget.apply(this);
sql (psql, override, callback) {
if (!callback) {
callback = override;
override = {};
}
this.query = query;
this.queries = queries;
this.column = options.column || '1';
this.operation = options.operation;
this._isFloatColumn = null;
}
if (this._isFloatColumn === null) {
this._isFloatColumn = false;
this.getColumnType(psql, this.column, this.queries.no_filters, (err, type) => {
if (!err && !!type) {
this._isFloatColumn = type.float;
}
this.sql(psql, override, callback);
});
return null;
}
Formula.prototype = new BaseWidget();
Formula.prototype.constructor = Formula;
module.exports = Formula;
Formula.prototype.sql = function(psql, override, callback) {
var self = this;
if (!callback) {
callback = override;
override = {};
}
if (this._isFloatColumn === null) {
this._isFloatColumn = false;
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
if (!err && !!type) {
self._isFloatColumn = type.float;
}
self.sql(psql, override, callback);
const formulaSql = formulaQueryTpl({
isFloatColumn: this._isFloatColumn,
query: this.query,
operation: this.operation,
column: this.column
});
return null;
debug(formulaSql);
return callback(null, formulaSql);
}
var formulaSql = formulaQueryTpl({
_isFloatColumn: this._isFloatColumn,
_query: this.query,
_operation: this.operation,
_column: this.column
});
format (res) {
const {
result = 0,
nulls_count = 0,
nans_count,
infinities_count
} = res.rows[0] || {};
debug(formulaSql);
return callback(null, formulaSql);
};
Formula.prototype.format = function(result) {
var formattedResult = {
operation: this.operation,
result: 0,
nulls: 0,
nans: 0,
infinities: 0
};
if (result.rows.length) {
formattedResult.operation = this.operation;
formattedResult.result = result.rows[0].result;
formattedResult.nulls = result.rows[0].nulls_count;
formattedResult.nans = result.rows[0].nans_count;
formattedResult.infinities = result.rows[0].infinities_count;
return {
operation: this.operation,
result,
nulls: nulls_count,
nans: nans_count,
infinities: infinities_count
};
}
return formattedResult;
};
getType () {
return TYPE;
}
Formula.prototype.getType = function() {
return TYPE;
};
Formula.prototype.toString = function() {
return JSON.stringify({
_type: TYPE,
_query: this.query,
_column: this.column,
_operation: this.operation
});
toString () {
return JSON.stringify({
_type: TYPE,
_query: this.query,
_column: this.column,
_operation: this.operation
});
}
};

View File

@@ -1,717 +1,72 @@
var _ = require('underscore');
var BaseWidget = require('./base');
var debug = require('debug')('windshaft:dataview:histogram');
const debug = require('debug')('windshaft:dataview:histogram');
const NumericHistogram = require('./histograms/numeric-histogram');
const DateHistogram = require('./histograms/date-histogram');
var dot = require('dot');
dot.templateSettings.strip = false;
const DATE_HISTOGRAM = 'DateHistogram';
const NUMERIC_HISTOGRAM = 'NumericHistogram';
var columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
module.exports = class Histogram {
constructor (query, options, queries) {
this.query = query;
this.options = options || {};
this.queries = queries;
var dateIntervalQueryTpl = dot.template([
'WITH',
'__cdb_dates AS (',
' SELECT',
' MAX({{=it.column}}::timestamp) AS __cdb_end,',
' MIN({{=it.column}}::timestamp) AS __cdb_start',
' FROM ({{=it.query}}) __cdb_source',
'),',
'__cdb_interval_in_days AS (',
' SELECT' ,
' DATE_PART(\'day\', __cdb_end - __cdb_start) AS __cdb_days',
' FROM __cdb_dates',
'),',
'__cdb_interval_in_hours AS (',
' SELECT',
' __cdb_days * 24 + DATE_PART(\'hour\', __cdb_end - __cdb_start) AS __cdb_hours',
' FROM __cdb_interval_in_days, __cdb_dates',
'),',
'__cdb_interval_in_minutes AS (',
' SELECT',
' __cdb_hours * 60 + DATE_PART(\'minute\', __cdb_end - __cdb_start) AS __cdb_minutes',
' FROM __cdb_interval_in_hours, __cdb_dates',
'),',
'__cdb_interval_in_seconds AS (',
' SELECT',
' __cdb_minutes * 60 + DATE_PART(\'second\', __cdb_end - __cdb_start) AS __cdb_seconds',
' FROM __cdb_interval_in_minutes, __cdb_dates',
')',
'SELECT',
' ROUND(__cdb_days / 365) AS year,',
' ROUND(__cdb_days / 90) AS quarter,',
' ROUND(__cdb_days / 30) AS month,',
' ROUND(__cdb_days / 7) AS week,',
' __cdb_days AS day,',
' __cdb_hours AS hour,',
' __cdb_minutes AS minute,',
' __cdb_seconds AS second',
'FROM __cdb_interval_in_days, __cdb_interval_in_hours, __cdb_interval_in_minutes, __cdb_interval_in_seconds'
].join('\n'));
var MAX_INTERVAL_VALUE = 366;
var BIN_MIN_NUMBER = 6;
var BIN_MAX_NUMBER = 48;
var filteredQueryTpl = dot.template([
'__cdb_filtered_source AS (',
' SELECT *',
' FROM ({{=it._query}}) __cdb_filtered_source_query',
' WHERE',
' {{=it._column}} IS NOT NULL',
' {{?it._isFloatColumn}}AND',
' {{=it._column}} != \'infinity\'::float',
' AND',
' {{=it._column}} != \'-infinity\'::float',
' AND',
' {{=it._column}} != \'NaN\'::float{{?}}',
')'
].join(' \n'));
var basicsQueryTpl = dot.template([
'__cdb_basics AS (',
' SELECT',
' max({{=it._column}}) AS __cdb_max_val, min({{=it._column}}) AS __cdb_min_val,',
' avg({{=it._column}}) AS __cdb_avg_val, count(1) AS __cdb_total_rows',
' FROM __cdb_filtered_source',
')'
].join(' \n'));
var overrideBasicsQueryTpl = dot.template([
'__cdb_basics AS (',
' SELECT',
' max({{=it._end}}) AS __cdb_max_val, min({{=it._start}}) AS __cdb_min_val,',
' avg({{=it._column}}) AS __cdb_avg_val, count(1) AS __cdb_total_rows',
' FROM __cdb_filtered_source',
')'
].join('\n'));
var iqrQueryTpl = dot.template([
'__cdb_iqrange AS (',
' SELECT max(quartile_max) - min(quartile_max) AS __cdb_iqr',
' FROM (',
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
' ) AS quartile',
' FROM __cdb_filtered_source) _cdb_quartiles',
' WHERE quartile = 1 or quartile = 3',
' GROUP BY quartile',
' ) __cdb_iqr',
')'
].join('\n'));
var binsQueryTpl = dot.template([
'__cdb_bins AS (',
' SELECT CASE WHEN __cdb_total_rows = 0 OR __cdb_iqr = 0',
' THEN 1',
' ELSE GREATEST(',
' LEAST({{=it._minBins}}, CAST(__cdb_total_rows AS INT)),',
' LEAST(',
' CAST(((__cdb_max_val - __cdb_min_val) / (2 * __cdb_iqr * power(__cdb_total_rows, 1/3))) AS INT),',
' {{=it._maxBins}}',
' )',
' )',
' END AS __cdb_bins_number',
' FROM __cdb_basics, __cdb_iqrange, __cdb_filtered_source',
' LIMIT 1',
')'
].join('\n'));
var overrideBinsQueryTpl = dot.template([
'__cdb_bins AS (',
' SELECT {{=it._bins}} AS __cdb_bins_number',
')'
].join('\n'));
var nullsQueryTpl = dot.template([
'__cdb_nulls AS (',
' SELECT',
' count(*) AS __cdb_nulls_count',
' FROM ({{=it._query}}) __cdb_histogram_nulls',
' WHERE {{=it._column}} IS NULL',
')'
].join('\n'));
var infinitiesQueryTpl = dot.template([
'__cdb_infinities AS (',
' SELECT',
' count(*) AS __cdb_infinities_count',
' FROM ({{=it._query}}) __cdb_infinities_query',
' WHERE',
' {{=it._column}} = \'infinity\'::float',
' OR',
' {{=it._column}} = \'-infinity\'::float',
')'
].join('\n'));
var nansQueryTpl = dot.template([
'__cdb_nans AS (',
' SELECT',
' count(*) AS __cdb_nans_count',
' FROM ({{=it._query}}) __cdb_nans_query',
' WHERE {{=it._column}} = \'NaN\'::float',
')'
].join('\n'));
var histogramQueryTpl = dot.template([
'SELECT',
' (__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,',
' __cdb_bins_number AS bins_number,',
' __cdb_nulls_count AS nulls_count,',
' {{?it._isFloatColumn}}__cdb_infinities_count AS infinities_count,',
' __cdb_nans_count AS nans_count,{{?}}',
' __cdb_avg_val AS avg_val,',
' CASE WHEN __cdb_min_val = __cdb_max_val',
' THEN 0',
' ELSE GREATEST(',
' 1,',
' LEAST(',
' WIDTH_BUCKET({{=it._column}}, __cdb_min_val, __cdb_max_val, __cdb_bins_number),',
' __cdb_bins_number',
' )',
' ) - 1',
' END AS bin,',
' min({{=it._column}})::numeric AS min,',
' max({{=it._column}})::numeric AS max,',
' avg({{=it._column}})::numeric AS avg,',
' count(*) AS freq',
'FROM __cdb_filtered_source, __cdb_basics, __cdb_nulls,',
' __cdb_bins{{?it._isFloatColumn}}, __cdb_infinities, __cdb_nans{{?}}',
'GROUP BY bin, bins_number, bin_width, nulls_count,',
' avg_val{{?it._isFloatColumn}}, infinities_count, nans_count{{?}}',
'ORDER BY bin'
].join('\n'));
var dateBasicsQueryTpl = dot.template([
'__cdb_basics AS (',
' SELECT',
' max(date_part(\'epoch\', {{=it._column}})) AS __cdb_max_val,',
' min(date_part(\'epoch\', {{=it._column}})) AS __cdb_min_val,',
' avg(date_part(\'epoch\', {{=it._column}})) AS __cdb_avg_val,',
' min(date_trunc(',
' \'{{=it._aggregation}}\', {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\'',
' )) AS __cdb_start_date,',
' max({{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\') AS __cdb_end_date,',
' count(1) AS __cdb_total_rows',
' FROM ({{=it._query}}) __cdb_basics_query',
')'
].join(' \n'));
var dateOverrideBasicsQueryTpl = dot.template([
'__cdb_basics AS (',
' SELECT',
' max({{=it._end}}) AS __cdb_max_val,',
' min({{=it._start}}) AS __cdb_min_val,',
' avg(date_part(\'epoch\', {{=it._column}})) AS __cdb_avg_val,',
' min(',
' date_trunc(',
' \'{{=it._aggregation}}\',',
' TO_TIMESTAMP({{=it._start}})::timestamp AT TIME ZONE \'{{=it._offset}}\'',
' )',
' ) AS __cdb_start_date,',
' max(',
' TO_TIMESTAMP({{=it._end}})::timestamp AT TIME ZONE \'{{=it._offset}}\'',
' ) AS __cdb_end_date,',
' count(1) AS __cdb_total_rows',
' FROM ({{=it._query}}) __cdb_basics_query',
')'
].join(' \n'));
var dateBinsQueryTpl = dot.template([
'__cdb_bins AS (',
' SELECT',
' __cdb_bins_array,',
' ARRAY_LENGTH(__cdb_bins_array, 1) AS __cdb_bins_number',
' FROM (',
' SELECT',
' ARRAY(',
' SELECT GENERATE_SERIES(',
' __cdb_start_date::timestamptz,',
' __cdb_end_date::timestamptz,',
' {{?it._aggregation==="quarter"}}\'3 month\'{{??}}\'1 {{=it._aggregation}}\'{{?}}::interval',
' )',
' ) AS __cdb_bins_array',
' FROM __cdb_basics',
' ) __cdb_bins_array_query',
')'
].join('\n'));
var dateHistogramQueryTpl = dot.template([
'SELECT',
' (__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,',
' __cdb_bins_number AS bins_number,',
' __cdb_nulls_count AS nulls_count,',
' CASE WHEN __cdb_min_val = __cdb_max_val',
' THEN 0',
' ELSE GREATEST(1, LEAST(',
' WIDTH_BUCKET(',
' {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\',',
' __cdb_bins_array',
' ),',
' __cdb_bins_number',
' )) - 1',
' END AS bin,',
' min(',
' date_part(',
' \'epoch\', ',
' date_trunc(',
' \'{{=it._aggregation}}\', {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\'',
' ) AT TIME ZONE \'{{=it._offset}}\'',
' )',
' )::numeric AS timestamp,',
' date_part(\'epoch\', __cdb_start_date)::numeric AS timestamp_start,',
' min(date_part(\'epoch\', {{=it._column}}))::numeric AS min,',
' max(date_part(\'epoch\', {{=it._column}}))::numeric AS max,',
' avg(date_part(\'epoch\', {{=it._column}}))::numeric AS avg,',
' count(*) AS freq',
'FROM ({{=it._query}}) __cdb_histogram, __cdb_basics, __cdb_bins, __cdb_nulls',
'WHERE date_part(\'epoch\', {{=it._column}}) IS NOT NULL',
'GROUP BY bin, bins_number, bin_width, nulls_count, timestamp_start',
'ORDER BY bin'
].join('\n'));
var TYPE = 'histogram';
/**
Numeric histogram:
{
type: 'histogram',
options: {
column: 'name', // column data type: numeric
bins: 10 // OPTIONAL
}
}
Time series:
{
type: 'histogram',
options: {
column: 'date', // column data type: date
aggregation: 'day' // OPTIONAL (if undefined then it'll be built as numeric)
offset: -7200 // OPTIONAL (UTC offset in seconds)
}
}
*/
function Histogram(query, options, queries) {
if (!_.isString(options.column)) {
throw new Error('Histogram expects `column` in widget options');
this.histogramImplementation = this._getHistogramImplementation();
}
this.query = query;
this.queries = queries;
this.column = options.column;
this.bins = options.bins;
this.aggregation = options.aggregation;
this.offset = options.offset;
_getHistogramImplementation (override) {
let implementation = null;
this._columnType = null;
}
Histogram.prototype = new BaseWidget();
Histogram.prototype.constructor = Histogram;
module.exports = Histogram;
Histogram.prototype.sql = function(psql, override, callback) {
var self = this;
if (!callback) {
callback = override;
override = {};
}
if (this._columnType === null) {
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
// assume numeric, will fail later
self._columnType = 'numeric';
if (!err && !!type) {
self._columnType = Object.keys(type).find(function (key) {
return type[key];
});
}
self.sql(psql, override, callback);
}, true); // use read-only transaction
return null;
}
this._buildQuery(psql, override, callback);
};
Histogram.prototype.isDateHistogram = function (override) {
return this._columnType === 'date' && (this.aggregation !== undefined || override.aggregation !== undefined);
};
Histogram.prototype._buildQuery = function (psql, override, callback) {
var filteredQuery, basicsQuery, binsQuery;
var _column = this.column;
var _query = this.query;
if (this.isDateHistogram(override)) {
return this._buildDateHistogramQuery(psql, override, callback);
}
if (this._columnType === 'date') {
_column = columnCastTpl({column: _column});
}
filteredQuery = filteredQueryTpl({
_isFloatColumn: this._columnType === 'float',
_query: _query,
_column: _column
});
if (this._shouldOverride(override)) {
debug('overriding with %j', override);
basicsQuery = overrideBasicsQueryTpl({
_query: _query,
_column: _column,
_start: getBinStart(override),
_end: getBinEnd(override)
});
binsQuery = [
overrideBinsQueryTpl({
_bins: override.bins
})
].join(',\n');
} else {
basicsQuery = basicsQueryTpl({
_query: _query,
_column: _column
});
if (this._shouldOverrideBins(override)) {
binsQuery = [
overrideBinsQueryTpl({
_bins: override.bins
})
].join(',\n');
} else {
binsQuery = [
iqrQueryTpl({
_query: _query,
_column: _column
}),
binsQueryTpl({
_query: _query,
_minBins: BIN_MIN_NUMBER,
_maxBins: BIN_MAX_NUMBER
})
].join(',\n');
}
}
var cteSql = [
filteredQuery,
basicsQuery,
binsQuery,
nullsQueryTpl({
_query: _query,
_column: _column
})
];
if (this._columnType === 'float') {
cteSql.push(
infinitiesQueryTpl({
_query: _query,
_column: _column
}),
nansQueryTpl({
_query: _query,
_column: _column
})
);
}
var histogramSql = [
"WITH",
cteSql.join(',\n'),
histogramQueryTpl({
_isFloatColumn: this._columnType === 'float',
_query: _query,
_column: _column
})
].join('\n');
debug(histogramSql);
return callback(null, histogramSql);
};
Histogram.prototype._shouldOverride = function (override) {
return override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins');
};
Histogram.prototype._shouldOverrideBins = function (override) {
return override && _.has(override, 'bins');
};
var DATE_AGGREGATIONS = {
'auto': true,
'minute': true,
'hour': true,
'day': true,
'week': true,
'month': true,
'quarter': true,
'year': true
};
Histogram.prototype._buildDateHistogramQuery = function (psql, override, callback) {
var _column = this.column;
var _query = this.query;
var _aggregation = override && override.aggregation ? override.aggregation : this.aggregation;
var _offset = override && Number.isFinite(override.offset) ? override.offset : this.offset;
if (!DATE_AGGREGATIONS.hasOwnProperty(_aggregation)) {
return callback(new Error('Invalid aggregation value. Valid ones: ' +
Object.keys(DATE_AGGREGATIONS).join(', ')
));
}
if (_aggregation === 'auto') {
this.getAutomaticAggregation(psql, function (err, aggregation) {
if (err || aggregation === 'none') {
this.aggregation = 'day';
} else {
this.aggregation = aggregation;
}
override.aggregation = this.aggregation;
this._buildDateHistogramQuery(psql, override, callback);
}.bind(this));
return null;
}
var dateBasicsQuery;
if (override && _.has(override, 'start') && _.has(override, 'end')) {
dateBasicsQuery = dateOverrideBasicsQueryTpl({
_query: _query,
_column: _column,
_aggregation: _aggregation,
_start: getBinStart(override),
_end: getBinEnd(override),
_offset: parseOffset(_offset, _aggregation)
});
} else {
dateBasicsQuery = dateBasicsQueryTpl({
_query: _query,
_column: _column,
_aggregation: _aggregation,
_offset: parseOffset(_offset, _aggregation)
});
}
var dateBinsQuery = [
dateBinsQueryTpl({
_aggregation: _aggregation
})
].join(',\n');
var nullsQuery = nullsQueryTpl({
_query: _query,
_column: _column
});
var dateHistogramQuery = dateHistogramQueryTpl({
_query: _query,
_column: _column,
_aggregation: _aggregation,
_offset: parseOffset(_offset, _aggregation)
});
var histogramSql = [
"WITH",
[
dateBasicsQuery,
dateBinsQuery,
nullsQuery
].join(',\n'),
dateHistogramQuery
].join('\n');
debug(histogramSql);
return callback(null, histogramSql);
};
Histogram.prototype.getAutomaticAggregation = function (psql, callback) {
var dateIntervalQuery = dateIntervalQueryTpl({
query: this.query,
column: this.column
});
debug(dateIntervalQuery);
psql.query(dateIntervalQuery, function (err, result) {
if (err) {
return callback(err);
switch (this._getHistogramSubtype(override)) {
case DATE_HISTOGRAM:
debug('Delegating to DateHistogram with options: %j and overriding: %j', this.options, override);
implementation = new DateHistogram(this.query, this.options, this.queries);
break;
case NUMERIC_HISTOGRAM:
debug('Delegating to NumericHistogram with options: %j and overriding: %j', this.options, override);
implementation = new NumericHistogram(this.query, this.options, this.queries);
break;
default:
throw new Error('Unsupported Histogram type');
}
var aggegations = result.rows[0];
var aggregation = Object.keys(aggegations)
.map(function (key) {
return {
name: key,
value: aggegations[key]
};
})
.reduce(function (closer, current) {
if (current.value > MAX_INTERVAL_VALUE) {
return closer;
}
return implementation;
}
var closerDiff = MAX_INTERVAL_VALUE - closer.value;
var currentDiff = MAX_INTERVAL_VALUE - current.value;
if (Number.isFinite(current.value) && closerDiff > currentDiff) {
return current;
}
return closer;
}, { name: 'none', value: -1 });
callback(null, aggregation.name);
});
};
Histogram.prototype.format = function(result, override) {
override = override || {};
var buckets = [];
var binsCount = getBinsCount(override);
var width = getWidth(override);
var binsStart = getBinStart(override);
var nulls = 0;
var infinities = 0;
var nans = 0;
var avg;
var timestampStart;
var aggregation;
var offset;
if (result.rows.length) {
var firstRow = result.rows[0];
binsCount = firstRow.bins_number;
width = firstRow.bin_width || width;
avg = firstRow.avg_val;
nulls = firstRow.nulls_count;
timestampStart = firstRow.timestamp_start;
infinities = firstRow.infinities_count;
nans = firstRow.nans_count;
binsStart = populateBinStart(override, firstRow);
if (Number.isFinite(timestampStart)) {
aggregation = getAggregation(override, this.aggregation);
offset = getOffset(override, this.offset);
_getHistogramSubtype (override) {
if(this._isDateHistogram(override)) {
return DATE_HISTOGRAM;
}
buckets = result.rows.map(function(row) {
return _.omit(
row,
'bins_number',
'bin_width',
'nulls_count',
'infinities_count',
'nans_count',
'avg_val',
'timestamp_start'
);
});
return NUMERIC_HISTOGRAM;
}
return {
aggregation: aggregation,
offset: offset,
timestamp_start: timestampStart,
bin_width: width,
bins_count: binsCount,
bins_start: binsStart,
nulls: nulls,
infinities: infinities,
nans: nans,
avg: avg,
bins: buckets
};
};
function getAggregation(override, aggregation) {
return override && override.aggregation ? override.aggregation : aggregation;
}
function getOffset(override, offset) {
if (override && override.offset) {
return override.offset;
}
if (offset) {
return offset;
}
return 0;
}
function getBinStart(override) {
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
return Math.min(override.start, override.end);
}
return override.start || 0;
}
function getBinEnd(override) {
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
return Math.max(override.start, override.end);
}
return override.end || 0;
}
function getBinsCount(override) {
return override.bins || 0;
}
function getWidth(override) {
var width = 0;
var binsCount = override.bins;
if (binsCount && Number.isFinite(override.start) && Number.isFinite(override.end)) {
width = (override.end - override.start) / binsCount;
}
return width;
}
function parseOffset(offset, aggregation) {
if (!offset) {
return '0';
}
if (aggregation === 'hour' || aggregation === 'minute') {
return '0';
}
var offsetInHours = Math.ceil(offset / 3600);
return '' + offsetInHours;
}
function populateBinStart(override, firstRow) {
var binStart;
if (firstRow.hasOwnProperty('timestamp')) {
binStart = firstRow.timestamp;
} else if (override.hasOwnProperty('start')) {
binStart = getBinStart(override);
} else {
binStart = firstRow.min;
}
return binStart;
}
Histogram.prototype.getType = function() {
return TYPE;
};
Histogram.prototype.toString = function() {
return JSON.stringify({
_type: TYPE,
_column: this.column,
_query: this.query
});
_isDateHistogram (override = {}) {
return (this.options.hasOwnProperty('aggregation') || override.hasOwnProperty('aggregation'));
}
getResult (psql, override, callback) {
this.histogramImplementation = this._getHistogramImplementation(override);
this.histogramImplementation.getResult(psql, override, callback);
}
// In order to keep previous behaviour with overviews,
// we have to expose the following methods to bypass
// the concrete overview implementation
sql (psql, override, callback) {
this.histogramImplementation.sql(psql, override, callback);
}
format (result, override) {
return this.histogramImplementation.format(result, override);
}
getType () {
return this.histogramImplementation.getType();
}
toString () {
return this.histogramImplementation.toString();
}
};

View File

@@ -0,0 +1,85 @@
const BaseDataview = require('../base');
const TYPE = 'histogram';
module.exports = class BaseHistogram extends BaseDataview {
constructor (query, options, queries) {
super();
if (typeof options.column !== 'string') {
throw new Error('Histogram expects `column` in widget options');
}
this.query = query;
this.queries = queries;
this.column = options.column;
this.bins = options.bins;
this._columnType = null;
}
sql (psql, override, callback) {
if (!callback) {
callback = override;
override = {};
}
if (this._columnType === null) {
this.getColumnType(psql, this.column, this.queries.no_filters, (err, type) => {
// assume numeric, will fail later
this._columnType = 'numeric';
if (!err && !!type) {
this._columnType = Object.keys(type).find(function (key) {
return type[key];
});
}
this.sql(psql, override, callback);
}, true); // use read-only transaction
return null;
}
return this._buildQuery(psql, override, callback);
}
format (result, override) {
const histogram = this._getSummary(result, override);
histogram.bins = this._getBuckets(result);
return histogram;
}
getType () {
return TYPE;
}
toString () {
return JSON.stringify({
_type: TYPE,
_column: this.column,
_query: this.query
});
}
_hasOverridenRange (override) {
return override && override.hasOwnProperty('start') && override.hasOwnProperty('end');
}
_getBinStart (override = {}) {
if (this._hasOverridenRange(override)) {
return Math.min(override.start, override.end);
}
return override.start || 0;
}
_getBinEnd (override = {}) {
if (this._hasOverridenRange(override)) {
return Math.max(override.start, override.end);
}
return override.end || 0;
}
_getBinsCount (override = {}) {
return override.bins || 0;
}
};

View File

@@ -0,0 +1,302 @@
const BaseHistogram = require('./base-histogram');
const debug = require('debug')('windshaft:dataview:date-histogram');
const dateIntervalQueryTpl = ctx => `
WITH
__cdb_dates AS (
SELECT
MAX(${ctx.column}::timestamp) AS __cdb_end,
MIN(${ctx.column}::timestamp) AS __cdb_start
FROM (${ctx.query}) __cdb_source
),
__cdb_interval_in_days AS (
SELECT
DATE_PART('day', __cdb_end - __cdb_start) AS __cdb_days
FROM __cdb_dates
),
__cdb_interval_in_hours AS (
SELECT
__cdb_days * 24 + DATE_PART('hour', __cdb_end - __cdb_start) AS __cdb_hours
FROM __cdb_interval_in_days, __cdb_dates
),
__cdb_interval_in_minutes AS (
SELECT
__cdb_hours * 60 + DATE_PART('minute', __cdb_end - __cdb_start) AS __cdb_minutes
FROM __cdb_interval_in_hours, __cdb_dates
),
__cdb_interval_in_seconds AS (
SELECT
__cdb_minutes * 60 + DATE_PART('second', __cdb_end - __cdb_start) AS __cdb_seconds
FROM __cdb_interval_in_minutes, __cdb_dates
)
SELECT
ROUND(__cdb_days / 365) AS year,
ROUND(__cdb_days / 90) AS quarter,
ROUND(__cdb_days / 30) AS month,
ROUND(__cdb_days / 7) AS week,
__cdb_days AS day,
__cdb_hours AS hour,
__cdb_minutes AS minute,
__cdb_seconds AS second
FROM __cdb_interval_in_days, __cdb_interval_in_hours, __cdb_interval_in_minutes, __cdb_interval_in_seconds
`;
const nullsQueryTpl = ctx => `
__cdb_nulls AS (
SELECT
count(*) AS __cdb_nulls_count
FROM (${ctx.query}) __cdb_histogram_nulls
WHERE ${ctx.column} IS NULL
)
`;
const dateBasicsQueryTpl = ctx => `
__cdb_basics AS (
SELECT
max(date_part('epoch', ${ctx.column})) AS __cdb_max_val,
min(date_part('epoch', ${ctx.column})) AS __cdb_min_val,
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
min(
date_trunc(
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
)
) AS __cdb_start_date,
max(${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}') AS __cdb_end_date,
count(1) AS __cdb_total_rows
FROM (${ctx.query}) __cdb_basics_query
)
`;
const dateOverrideBasicsQueryTpl = ctx => `
__cdb_basics AS (
SELECT
max(${ctx.end})::float AS __cdb_max_val,
min(${ctx.start})::float AS __cdb_min_val,
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
min(
date_trunc(
'${ctx.aggregation}',
TO_TIMESTAMP(${ctx.start})::timestamp AT TIME ZONE '${ctx.offset}'
)
) AS __cdb_start_date,
max(
TO_TIMESTAMP(${ctx.end})::timestamp AT TIME ZONE '${ctx.offset}'
) AS __cdb_end_date,
count(1) AS __cdb_total_rows
FROM (${ctx.query}) __cdb_basics_query
)
`;
const dateBinsQueryTpl = ctx => `
__cdb_bins AS (
SELECT
__cdb_bins_array,
ARRAY_LENGTH(__cdb_bins_array, 1) AS __cdb_bins_number
FROM (
SELECT
ARRAY(
SELECT GENERATE_SERIES(
__cdb_start_date::timestamptz,
__cdb_end_date::timestamptz,
${ctx.aggregation === 'quarter' ? `'3 month'::interval` : `'1 ${ctx.aggregation}'::interval`}
)
) AS __cdb_bins_array
FROM __cdb_basics
) __cdb_bins_array_query
)
`;
const dateHistogramQueryTpl = ctx => `
SELECT
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
__cdb_bins_number AS bins_number,
__cdb_nulls_count AS nulls_count,
CASE WHEN __cdb_min_val = __cdb_max_val
THEN 0
ELSE GREATEST(
1,
LEAST(
WIDTH_BUCKET(
${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}',
__cdb_bins_array
),
__cdb_bins_number
)
) - 1
END AS bin,
min(
date_part(
'epoch',
date_trunc(
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
) AT TIME ZONE '${ctx.offset}'
)
)::numeric AS timestamp,
date_part('epoch', __cdb_start_date)::numeric AS timestamp_start,
min(date_part('epoch', ${ctx.column}))::numeric AS min,
max(date_part('epoch', ${ctx.column}))::numeric AS max,
avg(date_part('epoch', ${ctx.column}))::numeric AS avg,
count(*) AS freq
FROM (${ctx.query}) __cdb_histogram, __cdb_basics, __cdb_bins, __cdb_nulls
WHERE date_part('epoch', ${ctx.column}) IS NOT NULL
GROUP BY bin, bins_number, bin_width, nulls_count, timestamp_start
ORDER BY bin
`;
const MAX_INTERVAL_VALUE = 366;
const DATE_AGGREGATIONS = {
'auto': true,
'minute': true,
'hour': true,
'day': true,
'week': true,
'month': true,
'quarter': true,
'year': true
};
/**
date_histogram: {
type: 'histogram',
options: {
column: 'date', // column data type: date
aggregation: 'day' // MANDATORY
offset: -7200 // OPTIONAL (UTC offset in seconds)
}
}
*/
module.exports = class DateHistogram extends BaseHistogram {
constructor (query, options, queries) {
super(query, options, queries);
this.aggregation = options.aggregation;
this.offset = options.offset;
}
_buildQueryTpl (ctx) {
return `
WITH
${this._hasOverridenRange(ctx.override) ? dateOverrideBasicsQueryTpl(ctx) : dateBasicsQueryTpl(ctx)},
${dateBinsQueryTpl(ctx)},
${nullsQueryTpl(ctx)}
${dateHistogramQueryTpl(ctx)}
`;
}
_buildQuery (psql, override, callback) {
if (!this._isValidAggregation(override)) {
return callback(new Error('Invalid aggregation value. Valid ones: ' +
Object.keys(DATE_AGGREGATIONS).join(', ')
));
}
if (this._getAggregation(override) === 'auto') {
this._getAutomaticAggregation(psql, function (err, aggregation) {
if (err || aggregation === 'none') {
this.aggregation = 'day';
} else {
this.aggregation = aggregation;
}
override.aggregation = this.aggregation;
this._buildQuery(psql, override, callback);
}.bind(this));
return null;
}
const histogramSql = this._buildQueryTpl({
override: override,
query: this.query,
column: this.column,
aggregation: this._getAggregation(override),
start: this._getBinStart(override),
end: this._getBinEnd(override),
offset: this._parseOffset(override)
});
debug(histogramSql);
return callback(null, histogramSql);
}
_isValidAggregation (override) {
return DATE_AGGREGATIONS.hasOwnProperty(this._getAggregation(override));
}
_getAutomaticAggregation (psql, callback) {
const dateIntervalQuery = dateIntervalQueryTpl({
query: this.query,
column: this.column
});
psql.query(dateIntervalQuery, function (err, result) {
if (err) {
return callback(err);
}
const aggegations = result.rows[0];
const aggregation = Object.keys(aggegations)
.map(key => ({ name: key, value: aggegations[key] }))
.reduce((closer, current) => {
if (current.value > MAX_INTERVAL_VALUE) {
return closer;
}
const closerDiff = MAX_INTERVAL_VALUE - closer.value;
const currentDiff = MAX_INTERVAL_VALUE - current.value;
if (Number.isFinite(current.value) && closerDiff > currentDiff) {
return current;
}
return closer;
}, { name: 'none', value: -1 });
callback(null, aggregation.name);
});
}
_getSummary (result, override) {
const firstRow = result.rows[0] || {};
return {
aggregation: this._getAggregation(override),
offset: this._getOffset(override),
timestamp_start: firstRow.timestamp_start,
bin_width: firstRow.bin_width,
bins_count: firstRow.bins_number,
bins_start: firstRow.timestamp,
nulls: firstRow.nulls_count,
infinities: firstRow.infinities_count,
nans: firstRow.nans_count,
avg: firstRow.avg_val
};
}
_getBuckets (result) {
return result.rows.map(({ bin, min, max, avg, freq, timestamp }) => ({ bin, min, max, avg, freq, timestamp }));
}
_getAggregation (override = {}) {
return override.aggregation ? override.aggregation : this.aggregation;
}
_getOffset (override = {}) {
return Number.isFinite(override.offset) ? override.offset : (this.offset || 0);
}
_parseOffset (override) {
if (this._shouldIgnoreOffset(override)) {
return '0';
}
const offsetInHours = Math.ceil(this._getOffset(override) / 3600);
return '' + offsetInHours;
}
_shouldIgnoreOffset (override) {
return (this._getAggregation(override) === 'hour' || this._getAggregation(override) === 'minute');
}
};

View File

@@ -0,0 +1,234 @@
const BaseHistogram = require('./base-histogram');
const debug = require('debug')('windshaft:dataview:numeric-histogram');
const columnCastTpl = ctx => `date_part('epoch', ${ctx.column})`;
const filterOutSpecialNumericValues = ctx => `
${ctx.column} != 'infinity'::float
AND
${ctx.column} != '-infinity'::float
AND
${ctx.column} != 'NaN'::float
`;
const filteredQueryTpl = ctx => `
__cdb_filtered_source AS (
SELECT *
FROM (${ctx.query}) __cdb_filtered_source_query
WHERE ${ctx.column} IS NOT NULL
${ctx.isFloatColumn ? `AND ${filterOutSpecialNumericValues(ctx)}` : ''}
)
`;
const basicsQueryTpl = ctx => `
__cdb_basics AS (
SELECT
max(${ctx.column}) AS __cdb_max_val, min(${ctx.column}) AS __cdb_min_val,
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
FROM __cdb_filtered_source
)
`;
const overrideBasicsQueryTpl = ctx => `
__cdb_basics AS (
SELECT
max(${ctx.end}) AS __cdb_max_val, min(${ctx.start}) AS __cdb_min_val,
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
FROM __cdb_filtered_source
)
`;
const iqrQueryTpl = ctx => `
__cdb_iqrange AS (
SELECT max(quartile_max) - min(quartile_max) AS __cdb_iqr
FROM (
SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (
SELECT ${ctx.column} AS _cdb_iqr_column, ntile(4) over (order by ${ctx.column}
) AS quartile
FROM __cdb_filtered_source) _cdb_quartiles
WHERE quartile = 1 or quartile = 3
GROUP BY quartile
) __cdb_iqr
)
`;
const binsQueryTpl = ctx => `
__cdb_bins AS (
SELECT
CASE WHEN __cdb_total_rows = 0 OR __cdb_iqr = 0
THEN 1
ELSE GREATEST(
LEAST(${ctx.minBins}, CAST(__cdb_total_rows AS INT)),
LEAST(
CAST(((__cdb_max_val - __cdb_min_val) / (2 * __cdb_iqr * power(__cdb_total_rows, 1/3))) AS INT),
${ctx.maxBins}
)
)
END AS __cdb_bins_number
FROM __cdb_basics, __cdb_iqrange, __cdb_filtered_source
LIMIT 1
)
`;
const overrideBinsQueryTpl = ctx => `
__cdb_bins AS (
SELECT ${ctx.override.bins} AS __cdb_bins_number
)
`;
const nullsQueryTpl = ctx => `
__cdb_nulls AS (
SELECT
count(*) AS __cdb_nulls_count
FROM (${ctx.query}) __cdb_histogram_nulls
WHERE ${ctx.column} IS NULL
)
`;
const infinitiesQueryTpl = ctx => `
__cdb_infinities AS (
SELECT
count(*) AS __cdb_infinities_count
FROM (${ctx.query}) __cdb_infinities_query
WHERE
${ctx.column} = 'infinity'::float
OR
${ctx.column} = '-infinity'::float
)
`;
const nansQueryTpl = ctx => `
__cdb_nans AS (
SELECT
count(*) AS __cdb_nans_count
FROM (${ctx.query}) __cdb_nans_query
WHERE ${ctx.column} = 'NaN'::float
)
`;
const specialNumericValuesColumnDefinitionTpl = () => `
__cdb_infinities_count AS infinities_count,
__cdb_nans_count AS nans_count
`;
const specialNumericValuesCTETpl = () => `
__cdb_infinities, __cdb_nans
`;
const specialNumericValuesColumnTpl = () => `
infinities_count, nans_count
`;
const histogramQueryTpl = ctx => `
SELECT
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
__cdb_bins_number AS bins_number,
__cdb_nulls_count AS nulls_count,
${ctx.isFloatColumn ? `${specialNumericValuesColumnDefinitionTpl()},` : ''}
__cdb_avg_val AS avg_val,
CASE WHEN __cdb_min_val = __cdb_max_val
THEN 0
ELSE GREATEST(
1,
LEAST(
WIDTH_BUCKET(${ctx.column}, __cdb_min_val, __cdb_max_val, __cdb_bins_number),
__cdb_bins_number
)
) - 1
END AS bin,
min(${ctx.column})::numeric AS min,
max(${ctx.column})::numeric AS max,
avg(${ctx.column})::numeric AS avg,
count(*) AS freq
FROM __cdb_filtered_source, __cdb_basics, __cdb_nulls, __cdb_bins
${ctx.isFloatColumn ? `, ${specialNumericValuesCTETpl()}` : ''}
GROUP BY bin, bins_number, bin_width, nulls_count, avg_val
${ctx.isFloatColumn ? `, ${specialNumericValuesColumnTpl()}` : ''}
ORDER BY bin
`;
const BIN_MIN_NUMBER = 6;
const BIN_MAX_NUMBER = 48;
/**
Numeric histogram:
{
type: 'histogram',
options: {
column: 'name', // column data type: numeric
bins: 10 // OPTIONAL
}
}
*/
module.exports = class NumericHistogram extends BaseHistogram {
constructor (query, options, queries) {
super(query, options, queries);
}
_buildQuery (psql, override, callback) {
const histogramSql = this._buildQueryTpl({
override: override,
column: this._columnType === 'date' ? columnCastTpl({ column: this.column }) : this.column,
isFloatColumn: this._columnType === 'float',
query: this.query,
start: this._getBinStart(override),
end: this._getBinEnd(override),
minBins: BIN_MIN_NUMBER,
maxBins: BIN_MAX_NUMBER,
});
debug(histogramSql);
return callback(null, histogramSql);
}
_buildQueryTpl (ctx) {
return `
WITH
${filteredQueryTpl(ctx)},
${this._hasOverridenRange(ctx.override) ? overrideBasicsQueryTpl(ctx) : basicsQueryTpl(ctx)},
${this._hasOverridenBins(ctx.override) ?
overrideBinsQueryTpl(ctx) :
`${iqrQueryTpl(ctx)}, ${binsQueryTpl(ctx)}`
},
${nullsQueryTpl(ctx)}
${ctx.isFloatColumn ? `,${infinitiesQueryTpl(ctx)}, ${nansQueryTpl(ctx)}` : ''}
${histogramQueryTpl(ctx)}
`;
}
_hasOverridenBins (override) {
return override && override.hasOwnProperty('bins');
}
_getSummary (result, override) {
const firstRow = result.rows[0] || {};
return {
bin_width: firstRow.bin_width,
bins_count: firstRow.bins_number,
bins_start: this._populateBinStart(firstRow, override),
nulls: firstRow.nulls_count,
infinities: firstRow.infinities_count,
nans: firstRow.nans_count,
avg: firstRow.avg_val,
};
}
_getBuckets (result) {
return result.rows.map(({ bin, min, max, avg, freq }) => ({ bin, min, max, avg, freq }));
}
_populateBinStart (firstRow, override = {}) {
let binStart;
if (override.hasOwnProperty('start')) {
binStart = this._getBinStart(override);
} else {
binStart = firstRow.min;
}
return binStart;
}
};

View File

@@ -1,6 +1,5 @@
module.exports = {
Aggregation: require('./aggregation'),
Formula: require('./formula'),
Histogram: require('./histogram'),
List: require('./list')
Histogram: require('./histogram')
};

View File

@@ -1,66 +0,0 @@
var dot = require('dot');
dot.templateSettings.strip = false;
var BaseWidget = require('./base');
var TYPE = 'list';
var listSqlTpl = dot.template('select {{=it._columns}} from ({{=it._query}}) as _cdb_list');
/**
{
type: 'list',
options: {
columns: ['name', 'description']
}
}
*/
function List(query, options) {
options = options || {};
if (!Array.isArray(options.columns)) {
throw new Error('List expects `columns` array in widget options');
}
BaseWidget.apply(this);
this.query = query;
this.columns = options.columns;
}
List.prototype = new BaseWidget();
List.prototype.constructor = List;
module.exports = List;
List.prototype.sql = function(psql, override, callback) {
if (!callback) {
callback = override;
}
var listSql = listSqlTpl({
_query: this.query,
_columns: this.columns.join(', ')
});
return callback(null, listSql);
};
List.prototype.format = function(result) {
return {
rows: result.rows
};
};
List.prototype.getType = function() {
return TYPE;
};
List.prototype.toString = function() {
return JSON.stringify({
_type: TYPE,
_query: this.query,
_columns: this.columns.join(', ')
});
};

View File

@@ -1,6 +1,5 @@
module.exports = {
Aggregation: require('./aggregation'),
Formula: require('./formula'),
Histogram: require('./histogram'),
List: require('./list')
Histogram: require('./histogram')
};

View File

@@ -1,11 +0,0 @@
var BaseOverviewsDataview = require('./base');
var BaseDataview = require('../list');
function List(query, options, queryRewriter, queryRewriteData, params, queries) {
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
}
List.prototype = Object.create(BaseOverviewsDataview.prototype);
List.prototype.constructor = List;
module.exports = List;

View File

@@ -4,6 +4,8 @@ var RedisPool = require('redis-mpool');
var cartodbRedis = require('cartodb-redis');
var _ = require('underscore');
var lzmaMiddleware = require('./middleware/lzma');
var controller = require('./controllers');
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
@@ -364,6 +366,8 @@ function bootstrap(opts) {
next();
});
app.use(lzmaMiddleware);
// temporary measure until we upgrade to newer version expressjs so we can check err.status
app.use(function(err, req, res, next) {
if (err) {

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "windshaft-cartodb",
"version": "3.12.3",
"version": "4.0.0",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
@@ -17,17 +17,19 @@
"Simon Tokumine <simon@vizzuality.com>",
"Javi Santana <jsantana@vizzuality.com>",
"Sandro Santilli <strk@vizzuality.com>",
"Carlos Matallín <matallo@carto.com>"
"Carlos Matallín <matallo@carto.com>",
"Daniel Garcia Aubert <dgaubert@carto.com>",
"Mario de Frutos <mario.defrutos@carto.com>"
],
"dependencies": {
"body-parser": "~1.14.0",
"camshaft": "0.55.8",
"cartodb-psql": "0.10.1",
"cartodb-query-tables": "0.2.0",
"body-parser": "^1.18.2",
"camshaft": "0.59.2",
"cartodb-psql": "0.10.2",
"cartodb-query-tables": "0.3.0",
"cartodb-redis": "0.14.0",
"debug": "~2.2.0",
"debug": "^3.1.0",
"dot": "~1.0.2",
"express": "~4.13.3",
"express": "~4.16.0",
"fastly-purge": "~1.0.1",
"log4js": "cartodb/log4js-node#cdb",
"lru-cache": "2.6.5",
@@ -35,12 +37,13 @@
"node-statsd": "~0.0.7",
"queue-async": "~1.0.7",
"redis-mpool": "0.4.1",
"request": "~2.79.0",
"request": "^2.83.0",
"semver": "~5.3.0",
"step": "~0.0.6",
"step-profiler": "~0.3.0",
"turbo-carto": "0.19.2",
"turbo-carto": "0.20.1",
"underscore": "~1.6.0",
"windshaft": "3.3.1",
"windshaft": "3.3.3",
"yargs": "~5.0.0"
},
"devDependencies": {
@@ -50,7 +53,6 @@
"moment": "~2.18.1",
"nock": "~2.11.0",
"redis": "~0.12.1",
"semver": "~1.1.4",
"strftime": "~0.8.2"
},
"scripts": {
@@ -60,6 +62,6 @@
},
"engines": {
"node": ">=6.9",
"yarn": "^0.21.3"
"yarn": ">=0.27.5 <1.0.0"
}
}

View File

@@ -5,34 +5,34 @@ var TestClient = require('../../support/test-client');
var dot = require('dot');
var debug = require('debug')('windshaft:cartodb:test');
describe('analysis-layers use cases', function() {
describe('analysis-layers use cases', function () {
var multitypeStyleTemplate = dot.template([
"#points['mapnik::geometry_type'=1] {",
" marker-fill-opacity: {{=it._opacity}};",
" marker-line-color: #FFF;",
" marker-line-width: 0.5;",
" marker-line-opacity: {{=it._opacity}};",
" marker-placement: point;",
" marker-type: ellipse;",
" marker-width: 8;",
" marker-fill: {{=it._color}};",
" marker-allow-overlap: true;",
"}",
"#lines['mapnik::geometry_type'=2] {",
" line-color: {{=it._color}};",
" line-width: 2;",
" line-opacity: {{=it._opacity}};",
"}",
"#polygons['mapnik::geometry_type'=3] {",
" polygon-fill: {{=it._color}};",
" polygon-opacity: {{=it._opacity}};",
" line-color: #FFF;",
" line-width: 0.5;",
" line-opacity: {{=it._opacity}};",
"}"
].join('\n'));
var multitypeStyleTemplate = dot.template(
`#points['mapnik::geometry_type'=1] {
marker-fill-opacity: {{=it._opacity}};
marker-line-color: #FFF;
marker-line-width: 0.5;
marker-line-opacity: {{=it._opacity}};
marker-placement: point;
marker-type: ellipse;
marker-width: 8;
marker-fill: {{=it._color}};
marker-allow-overlap: true;
}
#lines['mapnik::geometry_type'=2] {
line-color: {{=it._color}};
line-width: 2;
line-opacity: {{=it._opacity}};
}
#polygons['mapnik::geometry_type'=3] {
polygon-fill: {{=it._color}};
polygon-opacity: {{=it._opacity}};
line-color: #FFF;
line-width: 0.5;
line-opacity: {{=it._opacity}};
}`
);
function cartocss(color, opacity) {
@@ -47,18 +47,53 @@ describe('analysis-layers use cases', function() {
version: '1.5.0',
layers: layers,
dataviews: dataviews || {},
analysis: analysis || []
analyses: analysis || []
};
}
function analysisDef(analysis) {
return JSON.stringify(analysis);
}
var DEFAULT_MULTITYPE_STYLE = cartocss();
var TILE_ANALYSIS_TABLES = { z: 14, x: 8023, y: 6177 };
var pointInPolygonDef = {
id: 'a1',
type: 'point-in-polygon',
params: {
points_source: {
type: 'source',
params: {
query: 'select * from analysis_rent_listings'
}
},
polygons_source: {
type: 'buffer',
params: {
source: {
type: 'source',
params: {
query: 'select * from analysis_banks'
}
},
radius: 250
}
}
}
};
var bufferDef = {
id: 'b1',
type: 'buffer',
params: {
source: {
type: 'source',
params: {
query: 'select * from analysis_banks'
}
},
radius: 250
}
};
var useCases = [
{
desc: '1 mapnik layer',
@@ -68,7 +103,7 @@ describe('analysis-layers use cases', function() {
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
@@ -83,7 +118,7 @@ describe('analysis-layers use cases', function() {
{
type: 'cartodb',
options: {
sql: "select * from analysis_banks",
sql: 'select * from analysis_banks',
cartocss: cartocss('#2167AB'),
cartocss_version: '2.3.0'
}
@@ -91,7 +126,7 @@ describe('analysis-layers use cases', function() {
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
@@ -105,30 +140,27 @@ describe('analysis-layers use cases', function() {
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'analysis',
type: 'cartodb',
options: {
def: analysisDef({
"type": "buffer",
"params": {
"source": {
"type": "source",
"params": {
"query": "select * from analysis_banks"
}
},
"radius": 250
}
}),
cartocss: cartocss('black', 0.5)
source: {
id: 'b1'
},
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
}
])
],
{},
[
bufferDef
]
)
},
{
@@ -137,531 +169,115 @@ describe('analysis-layers use cases', function() {
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'analysis',
type: 'cartodb',
options: {
def: analysisDef({
"type": "point-in-polygon",
"params": {
"pointsSource": {
"type": "source",
"params": {
"query": "select * from analysis_rent_listings"
}
},
"polygonsSource": {
"type": "buffer",
"params": {
"source": {
"type": "source",
"params": {
"query": "select * from analysis_banks"
}
},
"radius": 250
}
}
}
}),
cartocss: cartocss('green', 1.0)
source: {
id: 'a1'
},
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
}
])
],
{},
[
pointInPolygonDef
]
)
},
{
desc: 'point-in-polygon from buffer atm-machines and rent listings + rent listings',
mapConfig: mapConfig([
{
type: 'analysis',
options: {
def: analysisDef({
"type": "point-in-polygon",
"params": {
"pointsSource": {
"type": "source",
"params": {
"query": "select * from analysis_rent_listings"
}
},
"polygonsSource": {
"type": "buffer",
"params": {
"source": {
"type": "source",
"params": {
"query": "select * from analysis_banks"
}
},
"radius": 250
}
}
}
}),
cartocss: cartocss('green', 1.0)
}
},
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
}
])
},
{
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
mapConfig: mapConfig([
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'analysis',
options: {
def: analysisDef({
"type": "buffer",
"params": {
"source": {
"type": "source",
"params": {
"query": "select * from analysis_banks"
}
},
"radius": 300
}
}),
cartocss: cartocss('magenta', 0.5)
}
},
{
type: 'analysis',
options: {
def: analysisDef({
"type": "point-in-polygon",
"params": {
"pointsSource": {
"type": "source",
"params": {
"query": "select * from analysis_rent_listings"
}
},
"polygonsSource": {
"type": "buffer",
"params": {
"source": {
"type": "source",
"params": {
"query": "select * from analysis_banks"
}
},
"radius": 300
}
}
}
}),
cartocss: cartocss('green', 1.0)
}
}
])
},
{
skip: true,
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
mapConfig: mapConfig([
{
type: 'cartodb',
options: {
"source": { id: "a" },
"cartocss": DEFAULT_MULTITYPE_STYLE,
"cartocss_version": "2.3.0"
}
},
{
type: 'cartodb',
options: {
"source": { id: "b1" },
"cartocss": cartocss('green', 1.0),
"cartocss_version": "2.3.0"
}
},
{
type: 'cartodb',
options: {
"source": { id: "b2" },
"cartocss": cartocss('magenta', 0.5),
"cartocss_version": "2.3.0"
}
}
],
[
{
id: "b2",
options: {
def: analysisDef({
"type": "count-in-polygon",
"id": "a0",
"params": {
"columnName": 'count_airbnb',
"pointsSource": {
"type": "source",
"params": {
query: "select * from analysis_rent_listings"
},
dataviews: {
price_histogram: {
type: 'histogram',
options: {
column: 'price'
}
}
}
},
"polygonsSource": {
"id": "b1",
"type": "buffer",
"params": {
"source": {
"id": "b0",
"type": "source",
"params": {
query: "select * from analysis_banks"
}
},
"radius": 250
},
dataviews: {
bank_category: {
type: 'aggregation',
options: {
column: 'bank'
}
}
}
}
},
dataviews: {
count_histogram: {
type: 'histogram',
options: {
column: 'count_airbnb'
}
}
}
}),
cartocss: cartocss('green', 1.0)
}
}
])
},
{
skip: true,
desc: 'I. Distribution centers',
mapConfig: mapConfig(
// layers
[
{
type: 'cartodb',
options: {
"source": { id: "b0" },
"cartocss": [
"#distribution_centers {",
" marker-fill-opacity: 1.0;",
" marker-line-color: #FFF;",
" marker-line-width: 0.5;",
" marker-line-opacity: 0.7;",
" marker-placement: point;",
" marker-type: ellipse;",
" marker-width: 8;",
" marker-fill: blue;",
" marker-allow-overlap: true;",
"}"
].join('\n'),
"cartocss_version": "2.3.0"
source: {
id: 'a1'
},
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'cartodb',
options: {
"source": { id: "a0" },
"cartocss": [
"#shops {",
" marker-fill-opacity: 1.0;",
" marker-line-color: #FFF;",
" marker-line-width: 0.5;",
" marker-line-opacity: 0.7;",
" marker-placement: point;",
" marker-type: ellipse;",
" marker-width: 8;",
" marker-fill: red;",
" marker-allow-overlap: true;",
"}"
].join('\n'),
"cartocss_version": "2.3.0"
}
},
{
type: 'cartodb',
options: {
"source": { id: "a1" },
"cartocss": [
"#routing {",
" line-color: ramp([routing_time], colorbrewer(Reds));",
" line-width: ramp([routing_time], 2, 8);",
" line-opacity: 1.0;",
"}"
].join('\n'),
"cartocss_version": "2.3.0"
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
}
],
// dataviews
{
distribution_center_name_category: {
source: { id: 'b0' },
type: 'aggregation',
options: {
column: 'name'
}
},
time_histogram: {
source: { id: 'a1' },
type: 'histogram',
options: {
column: 'routing_time'
}
},
distance_histogram: {
source: { id: 'a1' },
type: 'histogram',
options: {
column: 'routing_distance'
}
}
},
// analysis
{},
[
{
id: 'a1',
type: 'routing-n-to-n',
params: {
// distanceColumn: 'routing_distance',
// timeColumn: 'routing_time',
originSource: {
id: 'b0',
type: 'source',
params: {
query: 'select * from distribution_centers'
}
},
destinationSource: {
id: 'a0',
type: 'source',
params: {
query: 'select * from shops'
}
}
}
}
pointInPolygonDef
]
)
},
{
skip: true,
desc: 'II. Population analysis',
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
mapConfig: mapConfig(
// layers
[
{
type: 'cartodb',
options: {
"source": { id: "a2" },
"cartocss": [
"#count_in_polygon {",
" polygon-opacity: 1.0",
" line-color: #FFF;",
" line-width: 0.5;",
" line-opacity: 0.7",
" polygon-fill: ramp([estimated_people], colorbrewer(Reds));",
"}"
].join('\n'),
"cartocss_version": "2.3.0"
}
},
{
type: 'cartodb',
options: {
"source": { id: "a0" },
"cartocss": DEFAULT_MULTITYPE_STYLE,
"cartocss_version": "2.3.0"
}
}
],
// dataviews
{
total_population_formula: {
"source": { id: "a3" },
type: 'formula',
options: {
column: 'total_population',
operation: 'sum'
}
},
people_histogram: { // this injects a range filter at `a2` node output
"source": { id: "a2" },
type: 'histogram',
options: {
column: 'estimated_people'
}
},
subway_line_category: { // this injects a category filter at `a0` node output
"source": { id: "a0" },
type: 'aggregation',
options: {
column: 'subway_line'
}
}
},
// analysis
[
{
id: 'a3',
// this will union the polygons, produce just one polygon, and calculate the total population for it
type: 'total-population',
params: {
columnName: 'total_population',
source: {
id: 'a2',
type: 'estimated-population',
params: {
columnName: 'estimated_people',
source: {
id: 'a1',
type: 'trade-area',
params: {
source: {
"id": "a0",
"type": "source",
"params": {
query: "select * from subway_stops"
}
},
kind: 'walk',
time: 300
}
}
}
}
}
}
])
},
{
skip: true,
desc: 'III. Point in polygon',
mapConfig: mapConfig(
// layers
[
{
type: 'cartodb',
options: {
"source": { id: "a1" },
"cartocss": [
"#count_in_polygon {",
" polygon-opacity: 1.0",
" line-color: #FFF;",
" line-width: 0.5;",
" line-opacity: 0.7",
" polygon-fill: ramp([count_people], colorbrewer(Reds));",
"}"
].join('\n'),
"cartocss_version": "2.3.0"
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'cartodb',
options: {
source: {
id: 'a1'
},
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'cartodb',
options: {
source: {
id: 'b1'
},
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
}
],
// dataviews
{
age_histogram: {
"source": { id: "a0" },
type: 'histogram',
options: {
column: 'age'
}
},
income_histogram: {
"source": { id: "a0" },
type: 'histogram',
options: {
column: 'income'
}
},
gender_category: {
"source": { id: "a0" },
type: 'aggregation',
options: {
column: 'gender'
}
}
},
// analysis
{},
[
{
"id": "a1",
"type": "count-in-polygon",
"params": {
"columnName": 'count_people',
"pointsSource": {
"id": 'a0',
"type": "source",
"params": {
query: "select the_geom, age, gender, income from people"
}
},
"polygonsSource": {
"id": "b0",
"type": "source",
"params": {
query: "select * from postal_codes"
}
}
}
}
bufferDef,
pointInPolygonDef
]
)
}
];
useCases.forEach(function(useCase, imageIdx) {
useCases.forEach(function (useCase) {
if (!!useCase.skip) {
debug(JSON.stringify(useCase.mapConfig, null, 4));
return debug(JSON.stringify(useCase.mapConfig, null, 4));
}
it.skip('should implement use case: "' + useCase.desc + '"', function(done) {
it(`should implement use case: '${useCase.desc}'`, function (done) {
var testClient = new TestClient(useCase.mapConfig, 1234);
var tile = useCase.tile || TILE_ANALYSIS_TABLES;
testClient.getTile(tile.z, tile.x, tile.y, function(err, res, image) {
testClient.getTile(tile.z, tile.x, tile.y, function (err, res, image) {
assert.ok(!err, err);
image.save('/tmp/tests/' + imageIdx + '---' + useCase.desc.replace(/\s/g, '-') + '.png');
//image.save('/tmp/tests/' + imageIdx + '---' + useCase.desc.replace(/\s/g, '-') + '.png');
assert.equal(image.width(), 256);

View File

@@ -332,6 +332,21 @@ describe('histogram-dataview for date column type', function() {
});
});
it('should cast overridden start and end to float to avoid out of range errors ' + test.desc, function (done) {
var params = {
start: -2145916800,
end: 1009843199
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview(test.dataviewId, params, function (err, dataview) {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
done();
});
});
it('should return same histogram ' + test.desc, function (done) {
var params = {

View File

@@ -192,12 +192,36 @@ describe('named maps static view', function() {
}
getStaticMap({ zoom: 3 }, function(err, img) {
assert.ok(!err);
img.save('/tmp/static.png');
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), IMAGE_TOLERANCE, done);
});
});
});
it('should return override bbox', function (done) {
var view = {
bounds: {
west: 0,
south: 0,
east: 45,
north: 45
},
zoom: 4,
center: {
lng: 40,
lat: 20
}
};
templateMaps.addTemplate(username, createTemplate(view), function (err) {
if (err) {
return done(err);
}
getStaticMap({ bbox: '0,45,90,45' }, function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('override-bbox'), IMAGE_TOLERANCE, done);
});
});
});
it('should allow to select the layers to render', function (done) {
var view = {
bounds: {

View File

@@ -1,51 +0,0 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var TestClient = require('../../support/test-client');
describe('list widgets', function() {
it("should expose layer list", function(done) {
var listWidgetMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from test_table',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
names: {
type: 'list',
options: {
columns: ['name']
}
}
}
}
}
]
};
var testClient = new TestClient(listWidgetMapConfig);
testClient.getWidget('names', function(err, res) {
if (err) {
return done(err);
}
var expectedList = [
{name:"Hawai"},
{name:"El Estocolmo"},
{name:"El Rey del Tallarín"},
{name:"El Lacón"},
{name:"El Pico"}
];
assert.deepEqual(JSON.parse(res.body).rows, expectedList);
testClient.drain(done);
});
});
});

View File

@@ -1,106 +0,0 @@
require('../../../support/test_helper');
var assert = require('../../../support/assert');
var TestClient = require('../../../support/test-client');
var _ = require('underscore');
describe('widgets', function() {
describe('lists', function() {
afterEach(function(done) {
if (this.testClient) {
this.testClient.drain(done);
} else {
done();
}
});
function listsMapConfig(columns) {
return {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from test_table',
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
cartocss_version: '2.0.1',
widgets: {
places: {
type: 'list',
options: {
columns: columns || ['name', 'address']
}
}
}
}
}
]
};
}
var EXPECTED_NAMES = ['Hawai', 'El Estocolmo', 'El Rey del Tallarín', 'El Lacón', 'El Pico'];
it('can be fetched from a valid list', function(done) {
var columns = ['name', 'address'];
this.testClient = new TestClient(listsMapConfig(columns));
this.testClient.getWidget('places', function (err, res, list) {
assert.ok(!err, err);
assert.ok(list);
assert.equal(list.type, 'list');
assert.equal(list.rows.length, 5);
assert.ok(onlyHasFields(list, columns));
var names = list.rows.map(function (item) {
return item.name;
});
assert.deepEqual(names, EXPECTED_NAMES);
var expectedAddresses = [
'Calle de Pérez Galdós 9, Madrid, Spain',
'Calle de la Palma 72, Madrid, Spain',
'Plaza Conde de Toreno 2, Madrid, Spain',
'Manuel Fernández y González 8, Madrid, Spain',
'Calle Divino Pastor 12, Madrid, Spain'
];
var addresses = list.rows.map(function (item) {
return item.address;
});
assert.deepEqual(addresses, expectedAddresses);
done();
});
});
it('should fetch just one column', function(done) {
var columns = ['name'];
this.testClient = new TestClient(listsMapConfig(columns));
this.testClient.getWidget('places', function (err, res, list) {
assert.ok(!err, err);
assert.ok(list);
assert.equal(list.type, 'list');
assert.equal(list.rows.length, 5);
assert.ok(onlyHasFields(list, columns));
var names = list.rows.map(function (item) {
return item.name;
});
assert.deepEqual(names, EXPECTED_NAMES);
done();
});
});
function onlyHasFields(list, expectedFields) {
var fields = (!!list.rows[0]) ? Object.keys(list.rows[0]) : [];
return _.difference(fields, expectedFields).length === 0 &&
_.difference(expectedFields, fields).length === 0;
}
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,36 @@
var assert = require('assert');
var testHelper = require('../../support/test_helper');
var lzmaMiddleware = require('../../../lib/cartodb/middleware/lzma');
describe('lzma-middleware', function() {
it('it should extend params with decoded lzma', function(done) {
var qo = {
config: {
version: '1.3.0'
}
};
testHelper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
var req = {
headers: {
host:'localhost'
},
query: {
api_key: 'test',
lzma: data
}
};
lzmaMiddleware(req, {}, function(err) {
if ( err ) {
return done(err);
}
var query = req.query;
assert.deepEqual(qo.config, query.config);
assert.equal('test', query.api_key);
done();
});
});
});
});

View File

@@ -143,79 +143,6 @@ describe('dataviews-widgets-adapter', function() {
}
}
},
{
"input": {
"version": "1.4.0",
"layers": [
{
"type": "mapnik",
"options": {
"sql": "select * from test_table",
"cartocss": "#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }",
"cartocss_version": "2.3.0",
"widgets": {
"names": {
"type": "list",
"options": {
"columns": [
"name"
]
}
}
}
}
}
]
},
"expected": {
"version": "1.4.0",
"layers": [
{
"type": "mapnik",
"options": {
"source": {
"id": "cdb-layer-source-0"
},
"cartocss": "#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }",
"cartocss_version": "2.3.0",
// keep them for now
"widgets": {
"names": {
"type": "list",
"options": {
"columns": [
"name"
]
}
}
}
}
}
],
"analyses": [
{
"id": "cdb-layer-source-0",
"type": "source",
"params": {
"query": "select * from test_table"
}
}
],
"dataviews": {
"names": {
"source": {
"id": "cdb-layer-source-0"
},
"type": "list",
"options": {
"columns": [
"name"
]
}
}
}
}
},
{
"input": {
"version": "1.4.0",

View File

@@ -1,6 +1,6 @@
var assert = require('assert');
var _ = require('underscore');
var test_helper = require('../../support/test_helper');
require('../../support/test_helper');
var RedisPool = require('redis-mpool');
var cartodbRedis = require('cartodb-redis');
@@ -98,34 +98,31 @@ describe('req2params', function() {
});
});
it('it should extend params with decoded lzma', function(done) {
var qo = {
config: {
version: '1.3.0'
it('it should remove invalid params', function(done) {
var config = {
version: '1.3.0'
};
var req = {
headers: {
host:'localhost'
},
query: {
non_included: 'toberemoved',
api_key: 'test',
style: 'override',
config: config
}
};
test_helper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
var req = {
headers: {
host:'localhost'
},
query: {
non_included: 'toberemoved',
api_key: 'test',
style: 'override',
lzma: data
}
};
baseController.req2params(prepareRequest(req), function(err, req) {
if ( err ) {
return done(err);
}
var query = req.params;
assert.deepEqual(qo.config, query.config);
assert.equal('test', query.api_key);
assert.equal(undefined, query.non_included);
done();
});
baseController.req2params(prepareRequest(req), function(err, req) {
if (err) {
return done(err);
}
var query = req.params;
assert.deepEqual(config, query.config);
assert.equal('test', query.api_key);
assert.equal(undefined, query.non_included);
assert.equal(undefined, query.style);
done();
});
});

729
yarn.lock

File diff suppressed because it is too large Load Diff