From 258a0d276cc61c5a391a1fe3e037ad5c7db66458 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Sun, 18 Feb 2018 00:51:32 +0500 Subject: [PATCH 01/78] Add hook processRange to flot plugin. --- public/vendor/flot/jquery.flot.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index ec35fb87bd8..040eb808f48 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -632,6 +632,7 @@ Licensed under the MIT license. processRawData: [], processDatapoints: [], processOffset: [], + processRange: [], drawBackground: [], drawSeries: [], draw: [], @@ -1613,6 +1614,8 @@ Licensed under the MIT license. setRange(axis); }); + executeHooks(hooks.processRange, []); + if (showGrid) { var allocatedAxes = $.grep(axes, function (axis) { From 57013d2228cd2421ca819fbe22d307046158077b Mon Sep 17 00:00:00 2001 From: ilgizar Date: Sun, 18 Feb 2018 00:54:35 +0500 Subject: [PATCH 02/78] Share zero between Y axis. --- .../app/plugins/panel/graph/axes_editor.html | 1 + public/app/plugins/panel/graph/graph.ts | 111 ++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index 6160ef01fec..ee3654a9bbd 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -29,6 +29,7 @@ +
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 3ed8cbc1836..ade2fb80960 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -155,6 +155,116 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { } } + function processRangeHook(plot) { + var yaxis = plot.getYAxes(); + if (yaxis.length > 1 && panel.yaxes[1].shareZero) { + shareYLevel(yaxis[0].min, yaxis[0].max, yaxis[1].min, yaxis[1].max, 0); + } + } + + function shareYLevel(minLeft, maxLeft, minRight, maxRight, shareLevel) { + if (shareLevel !== 0) { + minLeft -= shareLevel; + maxLeft -= shareLevel; + minRight -= shareLevel; + maxRight -= shareLevel; + } + + // wide Y min and max using increased wideFactor + var deltaLeft = maxLeft - minLeft; + var deltaRight = maxRight - minRight; + var wideFactor = 0.25; + if (deltaLeft === 0) { + minLeft -= wideFactor; + maxLeft += wideFactor; + } + if (deltaRight === 0) { + minRight -= wideFactor; + maxRight += wideFactor; + } + + // on the opposite sides with respect to zero + if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { + if (minLeft >= 0) { + minLeft = -maxLeft; + maxRight = -minRight; + } else { + maxLeft = -minLeft; + minRight = -maxRight; + } + } else { + var limitTop = Infinity; + var limitBottom = -Infinity; + var absLeftMin = Math.abs(minLeft); + var absLeftMax = Math.abs(maxLeft); + var absRightMin = Math.abs(minRight); + var absRightMax = Math.abs(maxRight); + var upLeft = _.max([absLeftMin, absLeftMax]); + var downLeft = _.min([absLeftMin, absLeftMax]); + var upRight = _.max([absRightMin, absRightMax]); + var downRight = _.min([absRightMin, absRightMax]); + var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); + var rateLeft, rateRight, rate; + + // on the one hand with respect to zero + if (oneSide) { + rateLeft = downLeft ? upLeft / downLeft : downLeft >= 0 ? limitTop : limitBottom; + rateRight = downRight ? upRight / downRight : downRight >= 0 ? limitTop : limitBottom; + rate = _.max([rateLeft, rateRight]); + + if (rate === limitTop) { + if (maxLeft > 0) { + minLeft = 0; + minRight = 0; + } else { + maxLeft = 0; + maxRight = 0; + } + } else { + var coef = deltaLeft / deltaRight; + if ((rate === rateLeft && minLeft > 0) || (rate === rateRight && maxRight < 0)) { + maxLeft = maxRight * coef; + minRight = minLeft / coef; + } else { + minLeft = minRight * coef; + maxRight = maxLeft / coef; + } + } + } else { + rateLeft = + minLeft && maxLeft + ? minLeft < 0 ? maxLeft / minLeft : limitBottom + : minLeft < 0 || maxRight >= 0 ? limitBottom : limitTop; + rateRight = + minRight && maxRight + ? minRight < 0 ? maxRight / minRight : limitBottom + : minRight < 0 || maxLeft >= 0 ? limitBottom : limitTop; + rate = _.max([rateLeft, rateRight]); + + if (rate === rateLeft) { + minRight = + upRight === absRightMin && (absRightMin !== absRightMax || upLeft !== absLeftMin) + ? -upRight + : upRight / rate; + maxRight = upRight === absRightMax ? upRight : -upRight * rate; + } else { + minLeft = + upLeft === absLeftMin && (absLeftMin !== absLeftMax || upRight !== absRightMin) + ? -upLeft + : upLeft / rate; + maxLeft = upLeft === absLeftMax ? upLeft : -upLeft * rate; + } + } + } + + if (shareLevel !== 0) { + minLeft += shareLevel; + maxLeft += shareLevel; + minRight += shareLevel; + maxRight += shareLevel; + } + } + // Series could have different timeSteps, // let's find the smallest one so that bars are correctly rendered. // In addition, only take series which are rendered as bars for this. @@ -296,6 +406,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { hooks: { draw: [drawHook], processOffset: [processOffsetHook], + processRange: [processRangeHook], }, legend: { show: false }, series: { From 7eeb68b59088686ad3b350d0a8e839d3f41d3116 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Tue, 20 Feb 2018 16:58:49 +0500 Subject: [PATCH 03/78] Refactoring code. Change Y-Zero to Y-Level. --- .../app/plugins/panel/graph/axes_editor.html | 8 +- public/app/plugins/panel/graph/graph.ts | 148 ++++++++++-------- public/app/plugins/panel/graph/module.ts | 2 + public/vendor/flot/jquery.flot.js | 37 +++-- 4 files changed, 117 insertions(+), 78 deletions(-) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index ee3654a9bbd..a80ebd3036c 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -29,7 +29,6 @@
-
@@ -40,6 +39,13 @@
+
+ +
+ + +
+
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index ade2fb80960..95790222cac 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -157,12 +157,17 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { function processRangeHook(plot) { var yaxis = plot.getYAxes(); - if (yaxis.length > 1 && panel.yaxes[1].shareZero) { - shareYLevel(yaxis[0].min, yaxis[0].max, yaxis[1].min, yaxis[1].max, 0); + if (yaxis.length > 1 && panel.yaxes[1].shareLevel) { + shareYLevel(yaxis, parseFloat(panel.yaxes[1].shareY || 0)); } } - function shareYLevel(minLeft, maxLeft, minRight, maxRight, shareLevel) { + function shareYLevel(yaxis, shareLevel) { + var minLeft = yaxis[0].min; + var maxLeft = yaxis[0].max; + var minRight = yaxis[1].min; + var maxRight = yaxis[1].max; + if (shareLevel !== 0) { minLeft -= shareLevel; maxLeft -= shareLevel; @@ -183,76 +188,80 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { maxRight += wideFactor; } - // on the opposite sides with respect to zero - if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { - if (minLeft >= 0) { - minLeft = -maxLeft; - maxRight = -minRight; - } else { - maxLeft = -minLeft; - minRight = -maxRight; - } + // one of graphs on zero + var zero = minLeft === 0 || minRight === 0 || maxLeft === 0 || maxRight === 0; + + // on the one hand with respect to zero + var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); + + if (zero && oneSide) { + minLeft = maxLeft > 0 ? 0 : minLeft; + maxLeft = maxLeft > 0 ? maxLeft : 0; + minRight = maxRight > 0 ? 0 : minRight; + maxRight = maxRight > 0 ? maxRight : 0; } else { - var limitTop = Infinity; - var limitBottom = -Infinity; - var absLeftMin = Math.abs(minLeft); - var absLeftMax = Math.abs(maxLeft); - var absRightMin = Math.abs(minRight); - var absRightMax = Math.abs(maxRight); - var upLeft = _.max([absLeftMin, absLeftMax]); - var downLeft = _.min([absLeftMin, absLeftMax]); - var upRight = _.max([absRightMin, absRightMax]); - var downRight = _.min([absRightMin, absRightMax]); - var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); - var rateLeft, rateRight, rate; - - // on the one hand with respect to zero - if (oneSide) { - rateLeft = downLeft ? upLeft / downLeft : downLeft >= 0 ? limitTop : limitBottom; - rateRight = downRight ? upRight / downRight : downRight >= 0 ? limitTop : limitBottom; - rate = _.max([rateLeft, rateRight]); - - if (rate === limitTop) { - if (maxLeft > 0) { - minLeft = 0; - minRight = 0; - } else { - maxLeft = 0; - maxRight = 0; - } + // on the opposite sides with respect to zero + if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { + if (minLeft >= 0) { + minLeft = -maxLeft; + maxRight = -minRight; } else { - var coef = deltaLeft / deltaRight; - if ((rate === rateLeft && minLeft > 0) || (rate === rateRight && maxRight < 0)) { - maxLeft = maxRight * coef; - minRight = minLeft / coef; - } else { - minLeft = minRight * coef; - maxRight = maxLeft / coef; - } + maxLeft = -minLeft; + minRight = -maxRight; } } else { - rateLeft = - minLeft && maxLeft - ? minLeft < 0 ? maxLeft / minLeft : limitBottom - : minLeft < 0 || maxRight >= 0 ? limitBottom : limitTop; - rateRight = - minRight && maxRight - ? minRight < 0 ? maxRight / minRight : limitBottom - : minRight < 0 || maxLeft >= 0 ? limitBottom : limitTop; - rate = _.max([rateLeft, rateRight]); + // both across zero + var twoCross = minLeft <= 0 && maxLeft >= 0 && minRight <= 0 && maxRight >= 0; - if (rate === rateLeft) { - minRight = - upRight === absRightMin && (absRightMin !== absRightMax || upLeft !== absLeftMin) - ? -upRight - : upRight / rate; - maxRight = upRight === absRightMax ? upRight : -upRight * rate; + var rateLeft, rateRight, rate; + if (twoCross) { + rateLeft = minRight ? minLeft / minRight : 0; + rateRight = maxRight ? maxLeft / maxRight : 0; } else { - minLeft = - upLeft === absLeftMin && (absLeftMin !== absLeftMax || upRight !== absRightMin) - ? -upLeft - : upLeft / rate; - maxLeft = upLeft === absLeftMax ? upLeft : -upLeft * rate; + if (oneSide) { + var absLeftMin = Math.abs(minLeft); + var absLeftMax = Math.abs(maxLeft); + var absRightMin = Math.abs(minRight); + var absRightMax = Math.abs(maxRight); + var upLeft = _.max([absLeftMin, absLeftMax]); + var downLeft = _.min([absLeftMin, absLeftMax]); + var upRight = _.max([absRightMin, absRightMax]); + var downRight = _.min([absRightMin, absRightMax]); + + rateLeft = downLeft ? upLeft / downLeft : upLeft; + rateRight = downRight ? upRight / downRight : upRight; + } else { + if (minLeft > 0 || minRight > 0) { + rateLeft = maxLeft / maxRight; + rateRight = 0; + } else { + rateLeft = 0; + rateRight = minLeft / minRight; + } + } + } + rate = rateLeft > rateRight ? rateLeft : rateRight; + + if (oneSide) { + if (minLeft > 0) { + minLeft = maxLeft / rate; + minRight = maxRight / rate; + } else { + maxLeft = minLeft / rate; + maxRight = minRight / rate; + } + } else { + if (twoCross) { + minLeft = minRight ? minRight * rate : minLeft; + minRight = minLeft ? minLeft / rate : minRight; + maxLeft = maxRight ? maxRight * rate : maxLeft; + maxRight = maxLeft ? maxLeft / rate : maxRight; + } else { + minLeft = minLeft > 0 ? minRight * rate : minLeft; + minRight = minRight > 0 ? minLeft / rate : minRight; + maxLeft = maxLeft < 0 ? maxRight * rate : maxLeft; + maxRight = maxRight < 0 ? maxLeft / rate : maxRight; + } } } } @@ -263,6 +272,11 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { minRight += shareLevel; maxRight += shareLevel; } + + yaxis[0].min = minLeft; + yaxis[0].max = maxLeft; + yaxis[1].min = minRight; + yaxis[1].max = maxRight; } // Series could have different timeSteps, diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 59e72124c74..67b59997278 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -46,6 +46,8 @@ class GraphCtrl extends MetricsPanelCtrl { min: null, max: null, format: 'short', + shareLevel: false, + shareY: 0, }, ], xaxis: { diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index 040eb808f48..401198b712d 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -1622,14 +1622,24 @@ Licensed under the MIT license. return axis.show || axis.reserveSpace; }); - $.each(allocatedAxes, function (_, axis) { - // make the ticks - setupTickGeneration(axis); - setTicks(axis); - snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - measureTickLabels(axis); - }); + var snaped = false; + for (var i = 0; i < 2; i++) { + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snaped = snapRangeToTicks(axis, axis.ticks) || snaped; + // find labelWidth/Height for axis + measureTickLabels(axis); + }); + + if (snaped) { + executeHooks(hooks.processRange, []); + snaped = false; + } else { + break; + } + } // with all dimensions calculated, we can compute the // axis bounding boxes, start from the outside @@ -1646,6 +1656,7 @@ Licensed under the MIT license. }); } + plotWidth = surface.width - plotOffset.left - plotOffset.right; plotHeight = surface.height - plotOffset.bottom - plotOffset.top; @@ -1879,13 +1890,19 @@ Licensed under the MIT license. } function snapRangeToTicks(axis, ticks) { + var changed = false; if (axis.options.autoscaleMargin && ticks.length > 0) { // snap to ticks - if (axis.options.min == null) + if (axis.options.min == null) { axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) + changed = true; + } + if (axis.options.max == null && ticks.length > 1) { axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + changed = true; + } } + return changed; } function draw() { From 9e68cbea514380998d4ab5d4b9efe7926935cb05 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Thu, 22 Feb 2018 15:38:32 +0500 Subject: [PATCH 04/78] Refactoring code --- public/app/plugins/panel/graph/align_yaxes.ts | 123 ++++++++++++++++++ .../app/plugins/panel/graph/axes_editor.html | 19 +-- public/app/plugins/panel/graph/graph.ts | 122 +---------------- public/app/plugins/panel/graph/module.ts | 3 +- 4 files changed, 137 insertions(+), 130 deletions(-) create mode 100644 public/app/plugins/panel/graph/align_yaxes.ts diff --git a/public/app/plugins/panel/graph/align_yaxes.ts b/public/app/plugins/panel/graph/align_yaxes.ts new file mode 100644 index 00000000000..74fc9a063c7 --- /dev/null +++ b/public/app/plugins/panel/graph/align_yaxes.ts @@ -0,0 +1,123 @@ +import _ from 'lodash'; + +/** + * To align two Y axes by Y level + * @param yaxis data [{min: min_y1, min: max_y1}, {min: min_y2, max: max_y2}] + * @param align Y level + */ +export function alignYLevel(yaxis, alignLevel) { + var minLeft = yaxis[0].min; + var maxLeft = yaxis[0].max; + var minRight = yaxis[1].min; + var maxRight = yaxis[1].max; + + if (alignLevel !== 0) { + minLeft -= alignLevel; + maxLeft -= alignLevel; + minRight -= alignLevel; + maxRight -= alignLevel; + } + + // wide Y min and max using increased wideFactor + var deltaLeft = maxLeft - minLeft; + var deltaRight = maxRight - minRight; + var wideFactor = 0.25; + if (deltaLeft === 0) { + minLeft -= wideFactor; + maxLeft += wideFactor; + } + if (deltaRight === 0) { + minRight -= wideFactor; + maxRight += wideFactor; + } + + // one of graphs on zero + var zero = minLeft === 0 || minRight === 0 || maxLeft === 0 || maxRight === 0; + + // on the one hand with respect to zero + var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); + + if (zero && oneSide) { + minLeft = maxLeft > 0 ? 0 : minLeft; + maxLeft = maxLeft > 0 ? maxLeft : 0; + minRight = maxRight > 0 ? 0 : minRight; + maxRight = maxRight > 0 ? maxRight : 0; + } else { + // on the opposite sides with respect to zero + if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { + if (minLeft >= 0) { + minLeft = -maxLeft; + maxRight = -minRight; + } else { + maxLeft = -minLeft; + minRight = -maxRight; + } + } else { + // both across zero + var twoCross = minLeft <= 0 && maxLeft >= 0 && minRight <= 0 && maxRight >= 0; + + var rateLeft, rateRight, rate; + if (twoCross) { + rateLeft = minRight ? minLeft / minRight : 0; + rateRight = maxRight ? maxLeft / maxRight : 0; + } else { + if (oneSide) { + var absLeftMin = Math.abs(minLeft); + var absLeftMax = Math.abs(maxLeft); + var absRightMin = Math.abs(minRight); + var absRightMax = Math.abs(maxRight); + var upLeft = _.max([absLeftMin, absLeftMax]); + var downLeft = _.min([absLeftMin, absLeftMax]); + var upRight = _.max([absRightMin, absRightMax]); + var downRight = _.min([absRightMin, absRightMax]); + + rateLeft = downLeft ? upLeft / downLeft : upLeft; + rateRight = downRight ? upRight / downRight : upRight; + } else { + if (minLeft > 0 || minRight > 0) { + rateLeft = maxLeft / maxRight; + rateRight = 0; + } else { + rateLeft = 0; + rateRight = minLeft / minRight; + } + } + } + rate = rateLeft > rateRight ? rateLeft : rateRight; + + if (oneSide) { + if (minLeft > 0) { + minLeft = maxLeft / rate; + minRight = maxRight / rate; + } else { + maxLeft = minLeft / rate; + maxRight = minRight / rate; + } + } else { + if (twoCross) { + minLeft = minRight ? minRight * rate : minLeft; + minRight = minLeft ? minLeft / rate : minRight; + maxLeft = maxRight ? maxRight * rate : maxLeft; + maxRight = maxLeft ? maxLeft / rate : maxRight; + } else { + minLeft = minLeft > 0 ? minRight * rate : minLeft; + minRight = minRight > 0 ? minLeft / rate : minRight; + maxLeft = maxLeft < 0 ? maxRight * rate : maxLeft; + maxRight = maxRight < 0 ? maxLeft / rate : maxRight; + } + } + } + } + + if (alignLevel !== 0) { + minLeft += alignLevel; + maxLeft += alignLevel; + minRight += alignLevel; + maxRight += alignLevel; + } + + yaxis[0].min = minLeft; + yaxis[0].max = maxLeft; + yaxis[1].min = minRight; + yaxis[1].max = maxRight; +} diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index a80ebd3036c..7bf6756a7df 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -11,6 +11,7 @@
+
@@ -28,8 +29,15 @@
- -
+
+ +
+ + +
+ +
+
@@ -39,13 +47,6 @@
-
- -
- - -
-
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 95790222cac..3f7d1bee33c 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -18,6 +18,7 @@ import GraphTooltip from './graph_tooltip'; import { ThresholdManager } from './threshold_manager'; import { EventManager } from 'app/features/annotations/all'; import { convertValuesToHistogram, getSeriesValues } from './histogram'; +import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; /** @ngInject **/ @@ -157,128 +158,11 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { function processRangeHook(plot) { var yaxis = plot.getYAxes(); - if (yaxis.length > 1 && panel.yaxes[1].shareLevel) { - shareYLevel(yaxis, parseFloat(panel.yaxes[1].shareY || 0)); + if (yaxis.length > 1 && panel.yaxes[1].align !== null) { + alignYLevel(yaxis, parseFloat(panel.yaxes[1].align)); } } - function shareYLevel(yaxis, shareLevel) { - var minLeft = yaxis[0].min; - var maxLeft = yaxis[0].max; - var minRight = yaxis[1].min; - var maxRight = yaxis[1].max; - - if (shareLevel !== 0) { - minLeft -= shareLevel; - maxLeft -= shareLevel; - minRight -= shareLevel; - maxRight -= shareLevel; - } - - // wide Y min and max using increased wideFactor - var deltaLeft = maxLeft - minLeft; - var deltaRight = maxRight - minRight; - var wideFactor = 0.25; - if (deltaLeft === 0) { - minLeft -= wideFactor; - maxLeft += wideFactor; - } - if (deltaRight === 0) { - minRight -= wideFactor; - maxRight += wideFactor; - } - - // one of graphs on zero - var zero = minLeft === 0 || minRight === 0 || maxLeft === 0 || maxRight === 0; - - // on the one hand with respect to zero - var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); - - if (zero && oneSide) { - minLeft = maxLeft > 0 ? 0 : minLeft; - maxLeft = maxLeft > 0 ? maxLeft : 0; - minRight = maxRight > 0 ? 0 : minRight; - maxRight = maxRight > 0 ? maxRight : 0; - } else { - // on the opposite sides with respect to zero - if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { - if (minLeft >= 0) { - minLeft = -maxLeft; - maxRight = -minRight; - } else { - maxLeft = -minLeft; - minRight = -maxRight; - } - } else { - // both across zero - var twoCross = minLeft <= 0 && maxLeft >= 0 && minRight <= 0 && maxRight >= 0; - - var rateLeft, rateRight, rate; - if (twoCross) { - rateLeft = minRight ? minLeft / minRight : 0; - rateRight = maxRight ? maxLeft / maxRight : 0; - } else { - if (oneSide) { - var absLeftMin = Math.abs(minLeft); - var absLeftMax = Math.abs(maxLeft); - var absRightMin = Math.abs(minRight); - var absRightMax = Math.abs(maxRight); - var upLeft = _.max([absLeftMin, absLeftMax]); - var downLeft = _.min([absLeftMin, absLeftMax]); - var upRight = _.max([absRightMin, absRightMax]); - var downRight = _.min([absRightMin, absRightMax]); - - rateLeft = downLeft ? upLeft / downLeft : upLeft; - rateRight = downRight ? upRight / downRight : upRight; - } else { - if (minLeft > 0 || minRight > 0) { - rateLeft = maxLeft / maxRight; - rateRight = 0; - } else { - rateLeft = 0; - rateRight = minLeft / minRight; - } - } - } - rate = rateLeft > rateRight ? rateLeft : rateRight; - - if (oneSide) { - if (minLeft > 0) { - minLeft = maxLeft / rate; - minRight = maxRight / rate; - } else { - maxLeft = minLeft / rate; - maxRight = minRight / rate; - } - } else { - if (twoCross) { - minLeft = minRight ? minRight * rate : minLeft; - minRight = minLeft ? minLeft / rate : minRight; - maxLeft = maxRight ? maxRight * rate : maxLeft; - maxRight = maxLeft ? maxLeft / rate : maxRight; - } else { - minLeft = minLeft > 0 ? minRight * rate : minLeft; - minRight = minRight > 0 ? minLeft / rate : minRight; - maxLeft = maxLeft < 0 ? maxRight * rate : maxLeft; - maxRight = maxRight < 0 ? maxLeft / rate : maxRight; - } - } - } - } - - if (shareLevel !== 0) { - minLeft += shareLevel; - maxLeft += shareLevel; - minRight += shareLevel; - maxRight += shareLevel; - } - - yaxis[0].min = minLeft; - yaxis[0].max = maxLeft; - yaxis[1].min = minRight; - yaxis[1].max = maxRight; - } - // Series could have different timeSteps, // let's find the smallest one so that bars are correctly rendered. // In addition, only take series which are rendered as bars for this. diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 67b59997278..c198d118115 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -46,8 +46,7 @@ class GraphCtrl extends MetricsPanelCtrl { min: null, max: null, format: 'short', - shareLevel: false, - shareY: 0, + align: null, }, ], xaxis: { From 66590042a6b91dc3ce4e197724d0c1ef96b1bfea Mon Sep 17 00:00:00 2001 From: ilgizar Date: Thu, 22 Feb 2018 15:41:11 +0500 Subject: [PATCH 05/78] Add unit tests. --- .../plugins/panel/graph/specs/align_y.jest.ts | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 public/app/plugins/panel/graph/specs/align_y.jest.ts diff --git a/public/app/plugins/panel/graph/specs/align_y.jest.ts b/public/app/plugins/panel/graph/specs/align_y.jest.ts new file mode 100644 index 00000000000..046d02ee787 --- /dev/null +++ b/public/app/plugins/panel/graph/specs/align_y.jest.ts @@ -0,0 +1,167 @@ +import { alignYLevel } from '../align_yaxes'; + +describe('Graph Y axes aligner', function() { + let yaxes, expected; + let alignY = 0; + + describe('on the one hand with respect to zero', () => { + it('Should shrink Y axis', () => { + yaxes = [{ min: 5, max: 10 }, { min: 2, max: 3 }]; + expected = [{ min: 5, max: 10 }, { min: 1.5, max: 3 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axis', () => { + yaxes = [{ min: 2, max: 3 }, { min: 5, max: 10 }]; + expected = [{ min: 1.5, max: 3 }, { min: 5, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axis', () => { + yaxes = [{ min: -10, max: -5 }, { min: -3, max: -2 }]; + expected = [{ min: -10, max: -5 }, { min: -3, max: -1.5 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axis', () => { + yaxes = [{ min: -3, max: -2 }, { min: -10, max: -5 }]; + expected = [{ min: -3, max: -1.5 }, { min: -10, max: -5 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); + + describe('on the opposite sides with respect to zero', () => { + it('Should shrink Y axes', () => { + yaxes = [{ min: -3, max: -1 }, { min: 5, max: 10 }]; + expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: 1, max: 3 }, { min: -10, max: -5 }]; + expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); + + describe('both across zero', () => { + it('Should shrink Y axes', () => { + yaxes = [{ min: -10, max: 5 }, { min: -2, max: 3 }]; + expected = [{ min: -10, max: 15 }, { min: -2, max: 3 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -5, max: 10 }, { min: -3, max: 2 }]; + expected = [{ min: -15, max: 10 }, { min: -3, max: 2 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); + + describe('one of graphs on zero', () => { + it('Should shrink Y axes', () => { + yaxes = [{ min: 0, max: 3 }, { min: 5, max: 10 }]; + expected = [{ min: 0, max: 3 }, { min: 0, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: 5, max: 10 }, { min: 0, max: 3 }]; + expected = [{ min: 0, max: 10 }, { min: 0, max: 3 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -3, max: 0 }, { min: -10, max: -5 }]; + expected = [{ min: -3, max: 0 }, { min: -10, max: 0 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -10, max: -5 }, { min: -3, max: 0 }]; + expected = [{ min: -10, max: 0 }, { min: -3, max: 0 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); + + describe('both graphs on zero', () => { + it('Should shrink Y axes', () => { + yaxes = [{ min: 0, max: 3 }, { min: -10, max: 0 }]; + expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -3, max: 0 }, { min: 0, max: 10 }]; + expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); + + describe('mixed placement of graphs relative to zero', () => { + it('Should shrink Y axes', () => { + yaxes = [{ min: -10, max: 5 }, { min: 1, max: 3 }]; + expected = [{ min: -10, max: 5 }, { min: -6, max: 3 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: 1, max: 3 }, { min: -10, max: 5 }]; + expected = [{ min: -6, max: 3 }, { min: -10, max: 5 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -10, max: 5 }, { min: -3, max: -1 }]; + expected = [{ min: -10, max: 5 }, { min: -3, max: 1.5 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -3, max: -1 }, { min: -10, max: 5 }]; + expected = [{ min: -3, max: 1.5 }, { min: -10, max: 5 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); +}); From 7cddc543068c954124201f1906d0dc576a891499 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Wed, 7 Mar 2018 12:12:39 +0500 Subject: [PATCH 06/78] Add bs-tooltip to Y-Align element. --- public/app/plugins/panel/graph/axes_editor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index 7bf6756a7df..2c08755c17a 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -33,7 +33,7 @@
- +
From 916539fad9ebd9cb5a278796a2c54e2310efd755 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Wed, 7 Mar 2018 14:21:10 +0500 Subject: [PATCH 07/78] Append test to check not zero level. --- .../plugins/panel/graph/specs/align_y.jest.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/public/app/plugins/panel/graph/specs/align_y.jest.ts b/public/app/plugins/panel/graph/specs/align_y.jest.ts index 046d02ee787..ff540fd223f 100644 --- a/public/app/plugins/panel/graph/specs/align_y.jest.ts +++ b/public/app/plugins/panel/graph/specs/align_y.jest.ts @@ -158,8 +158,41 @@ describe('Graph Y axes aligner', function() { alignYLevel(yaxes, alignY); expect(yaxes).toMatchObject(expected); }); + }); + + describe('on level not zero', () => { + it('Should shrink Y axis', () => { + alignY = 1; + yaxes = [{ min: 5, max: 10 }, { min: 2, max: 4 }]; + expected = [{ min: 4, max: 10 }, { min: 2, max: 4 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); it('Should shrink Y axes', () => { + alignY = 2; + yaxes = [{ min: -3, max: 1 }, { min: 5, max: 10 }]; + expected = [{ min: -3, max: 7 }, { min: -6, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + alignY = -1; + yaxes = [{ min: -5, max: 5 }, { min: -2, max: 3 }]; + expected = [{ min: -5, max: 15 }, { min: -2, max: 3 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + alignY = -2; + yaxes = [{ min: -2, max: 3 }, { min: 5, max: 10 }]; + expected = [{ min: -2, max: 3 }, { min: -2, max: 10 }]; + alignYLevel(yaxes, alignY); expect(yaxes).toMatchObject(expected); }); From 3c9f31a0bb502d8c2f6ad85a85cb9b39d88c54e4 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 7 Mar 2018 13:10:44 +0100 Subject: [PATCH 08/78] added media breakpoint to legend-right --- public/sass/components/_panel_graph.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 716778096d6..c00af05140a 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -4,7 +4,9 @@ height: 100%; &--legend-right { - flex-direction: row; + @include media-breakpoint-up(sm) { + flex-direction: row; + } .graph-legend { flex: 0 1 10px; From 8152b9d6fe750a3f662130973141a6a71a752694 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Mon, 12 Mar 2018 10:54:03 +0500 Subject: [PATCH 09/78] Refactoring --- public/app/plugins/panel/graph/align_yaxes.ts | 191 ++++++++++-------- 1 file changed, 102 insertions(+), 89 deletions(-) diff --git a/public/app/plugins/panel/graph/align_yaxes.ts b/public/app/plugins/panel/graph/align_yaxes.ts index 74fc9a063c7..4884cd12441 100644 --- a/public/app/plugins/panel/graph/align_yaxes.ts +++ b/public/app/plugins/panel/graph/align_yaxes.ts @@ -6,118 +6,131 @@ import _ from 'lodash'; * @param align Y level */ export function alignYLevel(yaxis, alignLevel) { - var minLeft = yaxis[0].min; - var maxLeft = yaxis[0].max; - var minRight = yaxis[1].min; - var maxRight = yaxis[1].max; + moveLevelToZero(yaxis, alignLevel); - if (alignLevel !== 0) { - minLeft -= alignLevel; - maxLeft -= alignLevel; - minRight -= alignLevel; - maxRight -= alignLevel; - } - - // wide Y min and max using increased wideFactor - var deltaLeft = maxLeft - minLeft; - var deltaRight = maxRight - minRight; - var wideFactor = 0.25; - if (deltaLeft === 0) { - minLeft -= wideFactor; - maxLeft += wideFactor; - } - if (deltaRight === 0) { - minRight -= wideFactor; - maxRight += wideFactor; - } + expandStuckValues(yaxis); // one of graphs on zero - var zero = minLeft === 0 || minRight === 0 || maxLeft === 0 || maxRight === 0; + var zero = yaxis[0].min === 0 || yaxis[1].min === 0 || yaxis[0].max === 0 || yaxis[1].max === 0; - // on the one hand with respect to zero - var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); + var oneSide = checkOneSide(yaxis); if (zero && oneSide) { - minLeft = maxLeft > 0 ? 0 : minLeft; - maxLeft = maxLeft > 0 ? maxLeft : 0; - minRight = maxRight > 0 ? 0 : minRight; - maxRight = maxRight > 0 ? maxRight : 0; + yaxis[0].min = yaxis[0].max > 0 ? 0 : yaxis[0].min; + yaxis[0].max = yaxis[0].max > 0 ? yaxis[0].max : 0; + yaxis[1].min = yaxis[1].max > 0 ? 0 : yaxis[1].min; + yaxis[1].max = yaxis[1].max > 0 ? yaxis[1].max : 0; } else { // on the opposite sides with respect to zero - if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { - if (minLeft >= 0) { - minLeft = -maxLeft; - maxRight = -minRight; + if ((yaxis[0].min >= 0 && yaxis[1].max <= 0) || (yaxis[0].max <= 0 && yaxis[1].min >= 0)) { + if (yaxis[0].min >= 0) { + yaxis[0].min = -yaxis[0].max; + yaxis[1].max = -yaxis[1].min; } else { - maxLeft = -minLeft; - minRight = -maxRight; + yaxis[0].max = -yaxis[0].min; + yaxis[1].min = -yaxis[1].max; } } else { - // both across zero - var twoCross = minLeft <= 0 && maxLeft >= 0 && minRight <= 0 && maxRight >= 0; - - var rateLeft, rateRight, rate; - if (twoCross) { - rateLeft = minRight ? minLeft / minRight : 0; - rateRight = maxRight ? maxLeft / maxRight : 0; - } else { - if (oneSide) { - var absLeftMin = Math.abs(minLeft); - var absLeftMax = Math.abs(maxLeft); - var absRightMin = Math.abs(minRight); - var absRightMax = Math.abs(maxRight); - var upLeft = _.max([absLeftMin, absLeftMax]); - var downLeft = _.min([absLeftMin, absLeftMax]); - var upRight = _.max([absRightMin, absRightMax]); - var downRight = _.min([absRightMin, absRightMax]); - - rateLeft = downLeft ? upLeft / downLeft : upLeft; - rateRight = downRight ? upRight / downRight : upRight; - } else { - if (minLeft > 0 || minRight > 0) { - rateLeft = maxLeft / maxRight; - rateRight = 0; - } else { - rateLeft = 0; - rateRight = minLeft / minRight; - } - } - } - rate = rateLeft > rateRight ? rateLeft : rateRight; + var rate = getRate(yaxis); if (oneSide) { - if (minLeft > 0) { - minLeft = maxLeft / rate; - minRight = maxRight / rate; + if (yaxis[0].min > 0) { + yaxis[0].min = yaxis[0].max / rate; + yaxis[1].min = yaxis[1].max / rate; } else { - maxLeft = minLeft / rate; - maxRight = minRight / rate; + yaxis[0].max = yaxis[0].min / rate; + yaxis[1].max = yaxis[1].min / rate; } } else { - if (twoCross) { - minLeft = minRight ? minRight * rate : minLeft; - minRight = minLeft ? minLeft / rate : minRight; - maxLeft = maxRight ? maxRight * rate : maxLeft; - maxRight = maxLeft ? maxLeft / rate : maxRight; + if (checkTwoCross(yaxis)) { + yaxis[0].min = yaxis[1].min ? yaxis[1].min * rate : yaxis[0].min; + yaxis[1].min = yaxis[0].min ? yaxis[0].min / rate : yaxis[1].min; + yaxis[0].max = yaxis[1].max ? yaxis[1].max * rate : yaxis[0].max; + yaxis[1].max = yaxis[0].max ? yaxis[0].max / rate : yaxis[1].max; } else { - minLeft = minLeft > 0 ? minRight * rate : minLeft; - minRight = minRight > 0 ? minLeft / rate : minRight; - maxLeft = maxLeft < 0 ? maxRight * rate : maxLeft; - maxRight = maxRight < 0 ? maxLeft / rate : maxRight; + yaxis[0].min = yaxis[0].min > 0 ? yaxis[1].min * rate : yaxis[0].min; + yaxis[1].min = yaxis[1].min > 0 ? yaxis[0].min / rate : yaxis[1].min; + yaxis[0].max = yaxis[0].max < 0 ? yaxis[1].max * rate : yaxis[0].max; + yaxis[1].max = yaxis[1].max < 0 ? yaxis[0].max / rate : yaxis[1].max; } } } } + restoreLevelFromZero(yaxis, alignLevel); +} + +function expandStuckValues(yaxis) { + // wide Y min and max using increased wideFactor + var wideFactor = 0.25; + if (yaxis[0].max === yaxis[0].min) { + yaxis[0].min -= wideFactor; + yaxis[0].max += wideFactor; + } + if (yaxis[1].max === yaxis[1].min) { + yaxis[1].min -= wideFactor; + yaxis[1].max += wideFactor; + } +} + +function moveLevelToZero(yaxis, alignLevel) { if (alignLevel !== 0) { - minLeft += alignLevel; - maxLeft += alignLevel; - minRight += alignLevel; - maxRight += alignLevel; + yaxis[0].min -= alignLevel; + yaxis[0].max -= alignLevel; + yaxis[1].min -= alignLevel; + yaxis[1].max -= alignLevel; + } +} + +function restoreLevelFromZero(yaxis, alignLevel) { + if (alignLevel !== 0) { + yaxis[0].min += alignLevel; + yaxis[0].max += alignLevel; + yaxis[1].min += alignLevel; + yaxis[1].max += alignLevel; + } +} + +function checkOneSide(yaxis) { + // on the one hand with respect to zero + return (yaxis[0].min >= 0 && yaxis[1].min >= 0) || (yaxis[0].max <= 0 && yaxis[1].max <= 0); +} + +function checkTwoCross(yaxis) { + // both across zero + return yaxis[0].min <= 0 && yaxis[0].max >= 0 && yaxis[1].min <= 0 && yaxis[1].max >= 0; +} + +function getRate(yaxis) { + var rateLeft, rateRight, rate; + if (checkTwoCross(yaxis)) { + rateLeft = yaxis[1].min ? yaxis[0].min / yaxis[1].min : 0; + rateRight = yaxis[1].max ? yaxis[0].max / yaxis[1].max : 0; + } else { + if (checkOneSide(yaxis)) { + var absLeftMin = Math.abs(yaxis[0].min); + var absLeftMax = Math.abs(yaxis[0].max); + var absRightMin = Math.abs(yaxis[1].min); + var absRightMax = Math.abs(yaxis[1].max); + var upLeft = _.max([absLeftMin, absLeftMax]); + var downLeft = _.min([absLeftMin, absLeftMax]); + var upRight = _.max([absRightMin, absRightMax]); + var downRight = _.min([absRightMin, absRightMax]); + + rateLeft = downLeft ? upLeft / downLeft : upLeft; + rateRight = downRight ? upRight / downRight : upRight; + } else { + if (yaxis[0].min > 0 || yaxis[1].min > 0) { + rateLeft = yaxis[0].max / yaxis[1].max; + rateRight = 0; + } else { + rateLeft = 0; + rateRight = yaxis[0].min / yaxis[1].min; + } + } } - yaxis[0].min = minLeft; - yaxis[0].max = maxLeft; - yaxis[1].min = minRight; - yaxis[1].max = maxRight; + rate = rateLeft > rateRight ? rateLeft : rateRight; + + return rate; } From 11ae926388ff80e3caefdc6812a7e651ad3b7ed5 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Mon, 12 Mar 2018 23:11:11 +0500 Subject: [PATCH 10/78] Rename test file according module name. --- .../panel/graph/specs/{align_y.jest.ts => align_yaxes.jest.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename public/app/plugins/panel/graph/specs/{align_y.jest.ts => align_yaxes.jest.ts} (100%) diff --git a/public/app/plugins/panel/graph/specs/align_y.jest.ts b/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts similarity index 100% rename from public/app/plugins/panel/graph/specs/align_y.jest.ts rename to public/app/plugins/panel/graph/specs/align_yaxes.jest.ts From 8c82e5701c41f828f7d7770fb8177b258afd231f Mon Sep 17 00:00:00 2001 From: ilgizar Date: Mon, 12 Mar 2018 23:12:45 +0500 Subject: [PATCH 11/78] Replaced array values to variables yLeft and yRight for easy reading code. --- public/app/plugins/panel/graph/align_yaxes.ts | 134 +++++++++--------- 1 file changed, 70 insertions(+), 64 deletions(-) diff --git a/public/app/plugins/panel/graph/align_yaxes.ts b/public/app/plugins/panel/graph/align_yaxes.ts index 4884cd12441..b60d75e7b66 100644 --- a/public/app/plugins/panel/graph/align_yaxes.ts +++ b/public/app/plugins/panel/graph/align_yaxes.ts @@ -6,112 +6,118 @@ import _ from 'lodash'; * @param align Y level */ export function alignYLevel(yaxis, alignLevel) { - moveLevelToZero(yaxis, alignLevel); + var [yLeft, yRight] = yaxis; + moveLevelToZero(yLeft, yRight, alignLevel); - expandStuckValues(yaxis); + expandStuckValues(yLeft, yRight); // one of graphs on zero - var zero = yaxis[0].min === 0 || yaxis[1].min === 0 || yaxis[0].max === 0 || yaxis[1].max === 0; + var zero = yLeft.min === 0 || yRight.min === 0 || yLeft.max === 0 || yRight.max === 0; - var oneSide = checkOneSide(yaxis); + var oneSide = checkOneSide(yLeft, yRight); if (zero && oneSide) { - yaxis[0].min = yaxis[0].max > 0 ? 0 : yaxis[0].min; - yaxis[0].max = yaxis[0].max > 0 ? yaxis[0].max : 0; - yaxis[1].min = yaxis[1].max > 0 ? 0 : yaxis[1].min; - yaxis[1].max = yaxis[1].max > 0 ? yaxis[1].max : 0; + yLeft.min = yLeft.max > 0 ? 0 : yLeft.min; + yLeft.max = yLeft.max > 0 ? yLeft.max : 0; + yRight.min = yRight.max > 0 ? 0 : yRight.min; + yRight.max = yRight.max > 0 ? yRight.max : 0; } else { - // on the opposite sides with respect to zero - if ((yaxis[0].min >= 0 && yaxis[1].max <= 0) || (yaxis[0].max <= 0 && yaxis[1].min >= 0)) { - if (yaxis[0].min >= 0) { - yaxis[0].min = -yaxis[0].max; - yaxis[1].max = -yaxis[1].min; + if (checkOppositeSides(yLeft, yRight)) { + if (yLeft.min >= 0) { + yLeft.min = -yLeft.max; + yRight.max = -yRight.min; } else { - yaxis[0].max = -yaxis[0].min; - yaxis[1].min = -yaxis[1].max; + yLeft.max = -yLeft.min; + yRight.min = -yRight.max; } } else { - var rate = getRate(yaxis); + var rate = getRate(yLeft, yRight); if (oneSide) { - if (yaxis[0].min > 0) { - yaxis[0].min = yaxis[0].max / rate; - yaxis[1].min = yaxis[1].max / rate; + // all graphs above the Y level + if (yLeft.min > 0) { + yLeft.min = yLeft.max / rate; + yRight.min = yRight.max / rate; } else { - yaxis[0].max = yaxis[0].min / rate; - yaxis[1].max = yaxis[1].min / rate; + yLeft.max = yLeft.min / rate; + yRight.max = yRight.min / rate; } } else { - if (checkTwoCross(yaxis)) { - yaxis[0].min = yaxis[1].min ? yaxis[1].min * rate : yaxis[0].min; - yaxis[1].min = yaxis[0].min ? yaxis[0].min / rate : yaxis[1].min; - yaxis[0].max = yaxis[1].max ? yaxis[1].max * rate : yaxis[0].max; - yaxis[1].max = yaxis[0].max ? yaxis[0].max / rate : yaxis[1].max; + if (checkTwoCross(yLeft, yRight)) { + yLeft.min = yRight.min ? yRight.min * rate : yLeft.min; + yRight.min = yLeft.min ? yLeft.min / rate : yRight.min; + yLeft.max = yRight.max ? yRight.max * rate : yLeft.max; + yRight.max = yLeft.max ? yLeft.max / rate : yRight.max; } else { - yaxis[0].min = yaxis[0].min > 0 ? yaxis[1].min * rate : yaxis[0].min; - yaxis[1].min = yaxis[1].min > 0 ? yaxis[0].min / rate : yaxis[1].min; - yaxis[0].max = yaxis[0].max < 0 ? yaxis[1].max * rate : yaxis[0].max; - yaxis[1].max = yaxis[1].max < 0 ? yaxis[0].max / rate : yaxis[1].max; + yLeft.min = yLeft.min > 0 ? yRight.min * rate : yLeft.min; + yRight.min = yRight.min > 0 ? yLeft.min / rate : yRight.min; + yLeft.max = yLeft.max < 0 ? yRight.max * rate : yLeft.max; + yRight.max = yRight.max < 0 ? yLeft.max / rate : yRight.max; } } } } - restoreLevelFromZero(yaxis, alignLevel); + restoreLevelFromZero(yLeft, yRight, alignLevel); } -function expandStuckValues(yaxis) { +function expandStuckValues(yLeft, yRight) { // wide Y min and max using increased wideFactor var wideFactor = 0.25; - if (yaxis[0].max === yaxis[0].min) { - yaxis[0].min -= wideFactor; - yaxis[0].max += wideFactor; + if (yLeft.max === yLeft.min) { + yLeft.min -= wideFactor; + yLeft.max += wideFactor; } - if (yaxis[1].max === yaxis[1].min) { - yaxis[1].min -= wideFactor; - yaxis[1].max += wideFactor; + if (yRight.max === yRight.min) { + yRight.min -= wideFactor; + yRight.max += wideFactor; } } -function moveLevelToZero(yaxis, alignLevel) { +function moveLevelToZero(yLeft, yRight, alignLevel) { if (alignLevel !== 0) { - yaxis[0].min -= alignLevel; - yaxis[0].max -= alignLevel; - yaxis[1].min -= alignLevel; - yaxis[1].max -= alignLevel; + yLeft.min -= alignLevel; + yLeft.max -= alignLevel; + yRight.min -= alignLevel; + yRight.max -= alignLevel; } } -function restoreLevelFromZero(yaxis, alignLevel) { +function restoreLevelFromZero(yLeft, yRight, alignLevel) { if (alignLevel !== 0) { - yaxis[0].min += alignLevel; - yaxis[0].max += alignLevel; - yaxis[1].min += alignLevel; - yaxis[1].max += alignLevel; + yLeft.min += alignLevel; + yLeft.max += alignLevel; + yRight.min += alignLevel; + yRight.max += alignLevel; } } -function checkOneSide(yaxis) { +function checkOneSide(yLeft, yRight) { // on the one hand with respect to zero - return (yaxis[0].min >= 0 && yaxis[1].min >= 0) || (yaxis[0].max <= 0 && yaxis[1].max <= 0); + return (yLeft.min >= 0 && yRight.min >= 0) || (yLeft.max <= 0 && yRight.max <= 0); } -function checkTwoCross(yaxis) { +function checkTwoCross(yLeft, yRight) { // both across zero - return yaxis[0].min <= 0 && yaxis[0].max >= 0 && yaxis[1].min <= 0 && yaxis[1].max >= 0; + return yLeft.min <= 0 && yLeft.max >= 0 && yRight.min <= 0 && yRight.max >= 0; } -function getRate(yaxis) { +function checkOppositeSides(yLeft, yRight) { + // on the opposite sides with respect to zero + return (yLeft.min >= 0 && yRight.max <= 0) || (yLeft.max <= 0 && yRight.min >= 0); +} + +function getRate(yLeft, yRight) { var rateLeft, rateRight, rate; - if (checkTwoCross(yaxis)) { - rateLeft = yaxis[1].min ? yaxis[0].min / yaxis[1].min : 0; - rateRight = yaxis[1].max ? yaxis[0].max / yaxis[1].max : 0; + if (checkTwoCross(yLeft, yRight)) { + rateLeft = yRight.min ? yLeft.min / yRight.min : 0; + rateRight = yRight.max ? yLeft.max / yRight.max : 0; } else { - if (checkOneSide(yaxis)) { - var absLeftMin = Math.abs(yaxis[0].min); - var absLeftMax = Math.abs(yaxis[0].max); - var absRightMin = Math.abs(yaxis[1].min); - var absRightMax = Math.abs(yaxis[1].max); + if (checkOneSide(yLeft, yRight)) { + var absLeftMin = Math.abs(yLeft.min); + var absLeftMax = Math.abs(yLeft.max); + var absRightMin = Math.abs(yRight.min); + var absRightMax = Math.abs(yRight.max); var upLeft = _.max([absLeftMin, absLeftMax]); var downLeft = _.min([absLeftMin, absLeftMax]); var upRight = _.max([absRightMin, absRightMax]); @@ -120,12 +126,12 @@ function getRate(yaxis) { rateLeft = downLeft ? upLeft / downLeft : upLeft; rateRight = downRight ? upRight / downRight : upRight; } else { - if (yaxis[0].min > 0 || yaxis[1].min > 0) { - rateLeft = yaxis[0].max / yaxis[1].max; + if (yLeft.min > 0 || yRight.min > 0) { + rateLeft = yLeft.max / yRight.max; rateRight = 0; } else { rateLeft = 0; - rateRight = yaxis[0].min / yaxis[1].min; + rateRight = yLeft.min / yRight.min; } } } From 7dd66450adc6dea4ac499c1027937160997f64b4 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Mon, 12 Mar 2018 23:43:13 +0500 Subject: [PATCH 12/78] Corrected work for graphs created before this feature. --- public/app/plugins/panel/graph/graph.ts | 2 +- public/vendor/flot/jquery.flot.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 3f7d1bee33c..231b1ecaf42 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -158,7 +158,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { function processRangeHook(plot) { var yaxis = plot.getYAxes(); - if (yaxis.length > 1 && panel.yaxes[1].align !== null) { + if (yaxis.length > 1 && 'align' in panel.yaxes[1] && panel.yaxes[1].align !== null) { alignYLevel(yaxis, parseFloat(panel.yaxes[1].align)); } } diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index 401198b712d..8ee09e25c41 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -1633,7 +1633,7 @@ Licensed under the MIT license. measureTickLabels(axis); }); - if (snaped) { + if (snaped && hooks.processRange.length > 0) { executeHooks(hooks.processRange, []); snaped = false; } else { From 9d7ab78d9f0c38c143f8cdf1fda0644897493e12 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Wed, 14 Mar 2018 23:39:05 +0500 Subject: [PATCH 13/78] Resolved conflict --- public/app/plugins/panel/graph/graph.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 231b1ecaf42..bbfe63a87b2 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -17,7 +17,7 @@ import { appEvents, coreModule, updateLegendValues } from 'app/core/core'; import GraphTooltip from './graph_tooltip'; import { ThresholdManager } from './threshold_manager'; import { EventManager } from 'app/features/annotations/all'; -import { convertValuesToHistogram, getSeriesValues } from './histogram'; +import { convertToHistogramData } from './histogram'; import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; From 56695965182e55d6478b525ad7fe14c44476d03a Mon Sep 17 00:00:00 2001 From: Mitja Zivkovic Date: Thu, 15 Mar 2018 10:50:57 +0100 Subject: [PATCH 14/78] add regex search of username and password in urls, which are replaced by strings.Replace --- pkg/api/admin.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/api/admin.go b/pkg/api/admin.go index 286f23356ea..52d271ce69b 100644 --- a/pkg/api/admin.go +++ b/pkg/api/admin.go @@ -1,6 +1,7 @@ package api import ( + "regexp" "strings" "github.com/grafana/grafana/pkg/bus" @@ -21,6 +22,14 @@ func AdminGetSettings(c *m.ReqContext) { if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config")) { value = "************" } + if strings.Contains(keyName, "url") { + var rgx = regexp.MustCompile(`.*:\/\/([^:]*):([^@]*)@.*?$`) + var subs = rgx.FindAllSubmatch([]byte(value), -1) + if subs != nil && len(subs[0]) == 3 { + value = strings.Replace(value, string(subs[0][1]), "******", 1) + value = strings.Replace(value, string(subs[0][2]), "******", 1) + } + } jsonSec[keyName] = value } From 1094dc32bc75bb3dfb4266a21ec53e3aa3b6366b Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 15 Mar 2018 17:22:11 +0100 Subject: [PATCH 15/78] made a keyboard shortcut to duplicate panel --- public/app/core/services/keybindingSrv.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 1658d74be0a..0d468b6980f 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -178,6 +178,14 @@ export class KeybindingSrv { } }); + // duplicate panel + this.bind('p d', () => { + if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { + let panelIndex = dashboard.getPanelInfoById(dashboard.meta.focusPanelId).index; + dashboard.duplicatePanel(dashboard.panels[panelIndex]); + } + }); + // share panel this.bind('p s', () => { if (dashboard.meta.focusPanelId) { From 1c20126f8710d9dd13b3d14c86428d88731bc454 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 15 Mar 2018 09:51:29 +0100 Subject: [PATCH 16/78] database: update xorm to v0.6.4 and xorm core to v0.5.7 --- Gopkg.lock | 8 +- Gopkg.toml | 6 +- pkg/services/sqlstore/migrator/migrator.go | 2 +- vendor/github.com/go-xorm/core/column.go | 5 +- vendor/github.com/go-xorm/core/dialect.go | 3 + vendor/github.com/go-xorm/core/rows.go | 14 +- vendor/github.com/go-xorm/core/table.go | 1 + vendor/github.com/go-xorm/core/type.go | 5 +- .../xorm/{lru_cacher.go => cache_lru.go} | 34 +- ...{memory_store.go => cache_memory_store.go} | 0 vendor/github.com/go-xorm/xorm/context.go | 26 + vendor/github.com/go-xorm/xorm/convert.go | 105 +++- .../github.com/go-xorm/xorm/dialect_mssql.go | 31 +- .../github.com/go-xorm/xorm/dialect_mysql.go | 14 +- .../github.com/go-xorm/xorm/dialect_oracle.go | 7 + .../go-xorm/xorm/dialect_postgres.go | 73 +-- .../go-xorm/xorm/dialect_sqlite3.go | 21 +- vendor/github.com/go-xorm/xorm/doc.go | 13 +- vendor/github.com/go-xorm/xorm/engine.go | 494 ++++++++++-------- vendor/github.com/go-xorm/xorm/engine_cond.go | 230 ++++++++ .../github.com/go-xorm/xorm/engine_group.go | 194 +++++++ .../go-xorm/xorm/engine_group_policy.go | 116 ++++ .../github.com/go-xorm/xorm/engine_maxlife.go | 22 + vendor/github.com/go-xorm/xorm/error.go | 2 + vendor/github.com/go-xorm/xorm/helpers.go | 257 ++------- .../github.com/go-xorm/xorm/helpler_time.go | 21 + vendor/github.com/go-xorm/xorm/interface.go | 103 ++++ vendor/github.com/go-xorm/xorm/processors.go | 40 +- vendor/github.com/go-xorm/xorm/rows.go | 78 ++- vendor/github.com/go-xorm/xorm/session.go | 373 ++++++------- .../github.com/go-xorm/xorm/session_cols.go | 24 +- .../github.com/go-xorm/xorm/session_cond.go | 18 +- .../go-xorm/xorm/session_convert.go | 210 ++++---- .../github.com/go-xorm/xorm/session_delete.go | 78 +-- .../github.com/go-xorm/xorm/session_exist.go | 77 +++ .../github.com/go-xorm/xorm/session_find.go | 189 +++---- vendor/github.com/go-xorm/xorm/session_get.go | 129 ++--- .../github.com/go-xorm/xorm/session_insert.go | 181 ++++--- .../go-xorm/xorm/session_iterate.go | 54 ++ .../github.com/go-xorm/xorm/session_query.go | 252 +++++++++ vendor/github.com/go-xorm/xorm/session_raw.go | 247 +++++---- .../github.com/go-xorm/xorm/session_schema.go | 251 ++++----- .../github.com/go-xorm/xorm/session_stats.go | 98 ++++ vendor/github.com/go-xorm/xorm/session_sum.go | 137 ----- vendor/github.com/go-xorm/xorm/session_tx.go | 24 +- .../github.com/go-xorm/xorm/session_update.go | 200 ++++--- vendor/github.com/go-xorm/xorm/statement.go | 400 +++++--------- vendor/github.com/go-xorm/xorm/tag.go | 9 + vendor/github.com/go-xorm/xorm/xorm.go | 13 +- 49 files changed, 2995 insertions(+), 1894 deletions(-) rename vendor/github.com/go-xorm/xorm/{lru_cacher.go => cache_lru.go} (90%) rename vendor/github.com/go-xorm/xorm/{memory_store.go => cache_memory_store.go} (100%) create mode 100644 vendor/github.com/go-xorm/xorm/context.go create mode 100644 vendor/github.com/go-xorm/xorm/engine_cond.go create mode 100644 vendor/github.com/go-xorm/xorm/engine_group.go create mode 100644 vendor/github.com/go-xorm/xorm/engine_group_policy.go create mode 100644 vendor/github.com/go-xorm/xorm/engine_maxlife.go create mode 100644 vendor/github.com/go-xorm/xorm/helpler_time.go create mode 100644 vendor/github.com/go-xorm/xorm/interface.go create mode 100644 vendor/github.com/go-xorm/xorm/session_exist.go create mode 100644 vendor/github.com/go-xorm/xorm/session_query.go create mode 100644 vendor/github.com/go-xorm/xorm/session_stats.go delete mode 100644 vendor/github.com/go-xorm/xorm/session_sum.go diff --git a/Gopkg.lock b/Gopkg.lock index 8d82b29d622..20bee94cf8f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -171,12 +171,14 @@ [[projects]] name = "github.com/go-xorm/core" packages = ["."] - revision = "e8409d73255791843585964791443dbad877058c" + revision = "da1adaf7a28ca792961721a34e6e04945200c890" + version = "v0.5.7" [[projects]] name = "github.com/go-xorm/xorm" packages = ["."] - revision = "6687a2b4e824f4d87f2d65060ec5cb0d896dff1e" + revision = "1933dd69e294c0a26c0266637067f24dbb25770c" + version = "v0.6.4" [[projects]] branch = "master" @@ -631,6 +633,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "4de68f1342ba98a637ec8ca7496aeeae2021bf9e4c7c80db7924e14709151a62" + inputs-digest = "112ccff73f668c8c4dbe3d41c37ebee65fd7d839f5a4fa0665c593cae0095dad" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 22c56d29c71..a0d797f0db1 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -85,13 +85,11 @@ ignored = [ [[constraint]] name = "github.com/go-xorm/core" - revision = "e8409d73255791843585964791443dbad877058c" - #version = "0.5.7" //keeping this since we would rather depend on version then commit + version = "0.5.7" [[constraint]] name = "github.com/go-xorm/xorm" - revision = "6687a2b4e824f4d87f2d65060ec5cb0d896dff1e" - #version = "0.6.4" //keeping this since we would rather depend on version then commit + version = "0.6.4" [[constraint]] name = "github.com/gorilla/websocket" diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index a8bd36ac8a3..0fde3f27c01 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -125,7 +125,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error { condition := m.GetCondition() if condition != nil { sql, args := condition.Sql(mg.dialect) - results, err := sess.Query(sql, args...) + results, err := sess.SQL(sql).Query(args...) if err != nil || len(results) == 0 { mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id()) return sess.Rollback() diff --git a/vendor/github.com/go-xorm/core/column.go b/vendor/github.com/go-xorm/core/column.go index c59d01021fa..d9362e98578 100644 --- a/vendor/github.com/go-xorm/core/column.go +++ b/vendor/github.com/go-xorm/core/column.go @@ -13,12 +13,13 @@ const ( ONLYFROMDB ) -// database column +// Column defines database column type Column struct { Name string TableName string FieldName string SQLType SQLType + IsJSON bool Length int Length2 int Nullable bool @@ -37,6 +38,7 @@ type Column struct { SetOptions map[string]int DisableTimeZone bool TimeZone *time.Location // column specified time zone + Comment string } func NewColumn(name, fieldName string, sqlType SQLType, len1, len2 int, nullable bool) *Column { @@ -60,6 +62,7 @@ func NewColumn(name, fieldName string, sqlType SQLType, len1, len2 int, nullable IsVersion: false, DefaultIsEmpty: false, EnumOptions: make(map[string]int), + Comment: "", } } diff --git a/vendor/github.com/go-xorm/core/dialect.go b/vendor/github.com/go-xorm/core/dialect.go index 74478301e45..6f2e81d017b 100644 --- a/vendor/github.com/go-xorm/core/dialect.go +++ b/vendor/github.com/go-xorm/core/dialect.go @@ -244,6 +244,9 @@ func (b *Base) CreateTableSql(table *Table, tableName, storeEngine, charset stri sql += col.StringNoPk(b.dialect) } sql = strings.TrimSpace(sql) + if b.DriverName() == MYSQL && len(col.Comment) > 0 { + sql += " COMMENT '" + col.Comment + "'" + } sql += ", " } diff --git a/vendor/github.com/go-xorm/core/rows.go b/vendor/github.com/go-xorm/core/rows.go index c4dec23e0de..1da63837692 100644 --- a/vendor/github.com/go-xorm/core/rows.go +++ b/vendor/github.com/go-xorm/core/rows.go @@ -196,7 +196,7 @@ func (rs *Rows) ScanMap(dest interface{}) error { newDest := make([]interface{}, len(cols)) vvv := vv.Elem() - for i, _ := range cols { + for i := range cols { newDest[i] = ReflectNew(vvv.Type().Elem()).Interface() //v := reflect.New(vvv.Type().Elem()) //newDest[i] = v.Interface() @@ -247,6 +247,18 @@ type Row struct { err error // deferred error for easy chaining } +// ErrorRow return an error row +func ErrorRow(err error) *Row { + return &Row{ + err: err, + } +} + +// NewRow from rows +func NewRow(rows *Rows, err error) *Row { + return &Row{rows, err} +} + func (row *Row) Columns() ([]string, error) { if row.err != nil { return nil, row.err diff --git a/vendor/github.com/go-xorm/core/table.go b/vendor/github.com/go-xorm/core/table.go index e6f6a7518b5..88199bedd61 100644 --- a/vendor/github.com/go-xorm/core/table.go +++ b/vendor/github.com/go-xorm/core/table.go @@ -22,6 +22,7 @@ type Table struct { Cacher Cacher StoreEngine string Charset string + Comment string } func (table *Table) Columns() []*Column { diff --git a/vendor/github.com/go-xorm/core/type.go b/vendor/github.com/go-xorm/core/type.go index 86048225176..8010a2220fc 100644 --- a/vendor/github.com/go-xorm/core/type.go +++ b/vendor/github.com/go-xorm/core/type.go @@ -100,7 +100,8 @@ var ( LongBlob = "LONGBLOB" Bytea = "BYTEA" - Bool = "BOOL" + Bool = "BOOL" + Boolean = "BOOLEAN" Serial = "SERIAL" BigSerial = "BIGSERIAL" @@ -163,7 +164,7 @@ var ( uintTypes = sort.StringSlice{"*uint", "*uint16", "*uint32", "*uint8"} ) -// !nashtsai! treat following var as interal const values, these are used for reflect.TypeOf comparision +// !nashtsai! treat following var as interal const values, these are used for reflect.TypeOf comparison var ( c_EMPTY_STRING string c_BOOL_DEFAULT bool diff --git a/vendor/github.com/go-xorm/xorm/lru_cacher.go b/vendor/github.com/go-xorm/xorm/cache_lru.go similarity index 90% rename from vendor/github.com/go-xorm/xorm/lru_cacher.go rename to vendor/github.com/go-xorm/xorm/cache_lru.go index 4a74504351f..c9672cebe4d 100644 --- a/vendor/github.com/go-xorm/xorm/lru_cacher.go +++ b/vendor/github.com/go-xorm/xorm/cache_lru.go @@ -15,13 +15,12 @@ import ( // LRUCacher implments cache object facilities type LRUCacher struct { - idList *list.List - sqlList *list.List - idIndex map[string]map[string]*list.Element - sqlIndex map[string]map[string]*list.Element - store core.CacheStore - mutex sync.Mutex - // maxSize int + idList *list.List + sqlList *list.List + idIndex map[string]map[string]*list.Element + sqlIndex map[string]map[string]*list.Element + store core.CacheStore + mutex sync.Mutex MaxElementSize int Expired time.Duration GcInterval time.Duration @@ -54,8 +53,6 @@ func (m *LRUCacher) RunGC() { // GC check ids lit and sql list to remove all element expired func (m *LRUCacher) GC() { - //fmt.Println("begin gc ...") - //defer fmt.Println("end gc ...") m.mutex.Lock() defer m.mutex.Unlock() var removedNum int @@ -64,12 +61,10 @@ func (m *LRUCacher) GC() { time.Now().Sub(e.Value.(*idNode).lastVisit) > m.Expired { removedNum++ next := e.Next() - //fmt.Println("removing ...", e.Value) node := e.Value.(*idNode) m.delBean(node.tbName, node.id) e = next } else { - //fmt.Printf("removing %d cache nodes ..., left %d\n", removedNum, m.idList.Len()) break } } @@ -80,12 +75,10 @@ func (m *LRUCacher) GC() { time.Now().Sub(e.Value.(*sqlNode).lastVisit) > m.Expired { removedNum++ next := e.Next() - //fmt.Println("removing ...", e.Value) node := e.Value.(*sqlNode) m.delIds(node.tbName, node.sql) e = next } else { - //fmt.Printf("removing %d cache nodes ..., left %d\n", removedNum, m.sqlList.Len()) break } } @@ -116,7 +109,6 @@ func (m *LRUCacher) GetIds(tableName, sql string) interface{} { } m.delIds(tableName, sql) - return nil } @@ -134,7 +126,6 @@ func (m *LRUCacher) GetBean(tableName string, id string) interface{} { // if expired, remove the node and return nil if time.Now().Sub(lastTime) > m.Expired { m.delBean(tableName, id) - //m.clearIds(tableName) return nil } m.idList.MoveToBack(el) @@ -148,7 +139,6 @@ func (m *LRUCacher) GetBean(tableName string, id string) interface{} { // store bean is not exist, then remove memory's index m.delBean(tableName, id) - //m.clearIds(tableName) return nil } @@ -166,8 +156,8 @@ func (m *LRUCacher) clearIds(tableName string) { // ClearIds clears all sql-ids mapping on table tableName from cache func (m *LRUCacher) ClearIds(tableName string) { m.mutex.Lock() - defer m.mutex.Unlock() m.clearIds(tableName) + m.mutex.Unlock() } func (m *LRUCacher) clearBeans(tableName string) { @@ -184,14 +174,13 @@ func (m *LRUCacher) clearBeans(tableName string) { // ClearBeans clears all beans in some table func (m *LRUCacher) ClearBeans(tableName string) { m.mutex.Lock() - defer m.mutex.Unlock() m.clearBeans(tableName) + m.mutex.Unlock() } // PutIds pus ids into table func (m *LRUCacher) PutIds(tableName, sql string, ids interface{}) { m.mutex.Lock() - defer m.mutex.Unlock() if _, ok := m.sqlIndex[tableName]; !ok { m.sqlIndex[tableName] = make(map[string]*list.Element) } @@ -207,12 +196,12 @@ func (m *LRUCacher) PutIds(tableName, sql string, ids interface{}) { node := e.Value.(*sqlNode) m.delIds(node.tbName, node.sql) } + m.mutex.Unlock() } // PutBean puts beans into table func (m *LRUCacher) PutBean(tableName string, id string, obj interface{}) { m.mutex.Lock() - defer m.mutex.Unlock() var el *list.Element var ok bool @@ -229,6 +218,7 @@ func (m *LRUCacher) PutBean(tableName string, id string, obj interface{}) { node := e.Value.(*idNode) m.delBean(node.tbName, node.id) } + m.mutex.Unlock() } func (m *LRUCacher) delIds(tableName, sql string) { @@ -244,8 +234,8 @@ func (m *LRUCacher) delIds(tableName, sql string) { // DelIds deletes ids func (m *LRUCacher) DelIds(tableName, sql string) { m.mutex.Lock() - defer m.mutex.Unlock() m.delIds(tableName, sql) + m.mutex.Unlock() } func (m *LRUCacher) delBean(tableName string, id string) { @@ -261,8 +251,8 @@ func (m *LRUCacher) delBean(tableName string, id string) { // DelBean deletes beans in some table func (m *LRUCacher) DelBean(tableName string, id string) { m.mutex.Lock() - defer m.mutex.Unlock() m.delBean(tableName, id) + m.mutex.Unlock() } type idNode struct { diff --git a/vendor/github.com/go-xorm/xorm/memory_store.go b/vendor/github.com/go-xorm/xorm/cache_memory_store.go similarity index 100% rename from vendor/github.com/go-xorm/xorm/memory_store.go rename to vendor/github.com/go-xorm/xorm/cache_memory_store.go diff --git a/vendor/github.com/go-xorm/xorm/context.go b/vendor/github.com/go-xorm/xorm/context.go new file mode 100644 index 00000000000..074ba35a80a --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/context.go @@ -0,0 +1,26 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.8 + +package xorm + +import "context" + +// PingContext tests if database is alive +func (engine *Engine) PingContext(ctx context.Context) error { + session := engine.NewSession() + defer session.Close() + return session.PingContext(ctx) +} + +// PingContext test if database is ok +func (session *Session) PingContext(ctx context.Context) error { + if session.isAutoClose { + defer session.Close() + } + + session.engine.logger.Infof("PING DATABASE %v", session.engine.DriverName()) + return session.DB().PingContext(ctx) +} diff --git a/vendor/github.com/go-xorm/xorm/convert.go b/vendor/github.com/go-xorm/xorm/convert.go index 87f0d3f1ec5..2316ca0b4dc 100644 --- a/vendor/github.com/go-xorm/xorm/convert.go +++ b/vendor/github.com/go-xorm/xorm/convert.go @@ -209,10 +209,10 @@ func convertAssign(dest, src interface{}) error { if src == nil { dv.Set(reflect.Zero(dv.Type())) return nil - } else { - dv.Set(reflect.New(dv.Type().Elem())) - return convertAssign(dv.Interface(), src) } + + dv.Set(reflect.New(dv.Type().Elem())) + return convertAssign(dv.Interface(), src) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: s := asString(src) i64, err := strconv.ParseInt(s, 10, dv.Type().Bits()) @@ -247,3 +247,102 @@ func convertAssign(dest, src interface{}) error { return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, dest) } + +func asKind(vv reflect.Value, tp reflect.Type) (interface{}, error) { + switch tp.Kind() { + case reflect.Int64: + return vv.Int(), nil + case reflect.Int: + return int(vv.Int()), nil + case reflect.Int32: + return int32(vv.Int()), nil + case reflect.Int16: + return int16(vv.Int()), nil + case reflect.Int8: + return int8(vv.Int()), nil + case reflect.Uint64: + return vv.Uint(), nil + case reflect.Uint: + return uint(vv.Uint()), nil + case reflect.Uint32: + return uint32(vv.Uint()), nil + case reflect.Uint16: + return uint16(vv.Uint()), nil + case reflect.Uint8: + return uint8(vv.Uint()), nil + case reflect.String: + return vv.String(), nil + case reflect.Slice: + if tp.Elem().Kind() == reflect.Uint8 { + v, err := strconv.ParseInt(string(vv.Interface().([]byte)), 10, 64) + if err != nil { + return nil, err + } + return v, nil + } + + } + return nil, fmt.Errorf("unsupported primary key type: %v, %v", tp, vv) +} + +func convertFloat(v interface{}) (float64, error) { + switch v.(type) { + case float32: + return float64(v.(float32)), nil + case float64: + return v.(float64), nil + case string: + i, err := strconv.ParseFloat(v.(string), 64) + if err != nil { + return 0, err + } + return i, nil + case []byte: + i, err := strconv.ParseFloat(string(v.([]byte)), 64) + if err != nil { + return 0, err + } + return i, nil + } + return 0, fmt.Errorf("unsupported type: %v", v) +} + +func convertInt(v interface{}) (int64, error) { + switch v.(type) { + case int: + return int64(v.(int)), nil + case int8: + return int64(v.(int8)), nil + case int16: + return int64(v.(int16)), nil + case int32: + return int64(v.(int32)), nil + case int64: + return v.(int64), nil + case []byte: + i, err := strconv.ParseInt(string(v.([]byte)), 10, 64) + if err != nil { + return 0, err + } + return i, nil + case string: + i, err := strconv.ParseInt(v.(string), 10, 64) + if err != nil { + return 0, err + } + return i, nil + } + return 0, fmt.Errorf("unsupported type: %v", v) +} + +func asBool(bs []byte) (bool, error) { + if len(bs) == 0 { + return false, nil + } + if bs[0] == 0x00 { + return false, nil + } else if bs[0] == 0x01 { + return true, nil + } + return strconv.ParseBool(string(bs)) +} diff --git a/vendor/github.com/go-xorm/xorm/dialect_mssql.go b/vendor/github.com/go-xorm/xorm/dialect_mssql.go index 70fcaf6ea08..6d2291dc1da 100644 --- a/vendor/github.com/go-xorm/xorm/dialect_mssql.go +++ b/vendor/github.com/go-xorm/xorm/dialect_mssql.go @@ -215,10 +215,10 @@ func (db *mssql) SqlType(c *core.Column) string { var res string switch t := c.SQLType.Name; t { case core.Bool: - res = core.TinyInt - if c.Default == "true" { + res = core.Bit + if strings.EqualFold(c.Default, "true") { c.Default = "1" - } else if c.Default == "false" { + } else { c.Default = "0" } case core.Serial: @@ -250,6 +250,9 @@ func (db *mssql) SqlType(c *core.Column) string { case core.Uuid: res = core.Varchar c.Length = 40 + case core.TinyInt: + res = core.TinyInt + c.Length = 0 default: res = t } @@ -335,9 +338,15 @@ func (db *mssql) TableCheckSql(tableName string) (string, []interface{}) { func (db *mssql) GetColumns(tableName string) ([]string, map[string]*core.Column, error) { args := []interface{}{} s := `select a.name as name, b.name as ctype,a.max_length,a.precision,a.scale,a.is_nullable as nullable, - replace(replace(isnull(c.text,''),'(',''),')','') as vdefault - from sys.columns a left join sys.types b on a.user_type_id=b.user_type_id - left join sys.syscomments c on a.default_object_id=c.id + replace(replace(isnull(c.text,''),'(',''),')','') as vdefault, + ISNULL(i.is_primary_key, 0) + from sys.columns a + left join sys.types b on a.user_type_id=b.user_type_id + left join sys.syscomments c on a.default_object_id=c.id + LEFT OUTER JOIN + sys.index_columns ic ON ic.object_id = a.object_id AND ic.column_id = a.column_id + LEFT OUTER JOIN + sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id where a.object_id=object_id('` + tableName + `')` db.LogSQL(s, args) @@ -352,8 +361,8 @@ func (db *mssql) GetColumns(tableName string) ([]string, map[string]*core.Column for rows.Next() { var name, ctype, vdefault string var maxLen, precision, scale int - var nullable bool - err = rows.Scan(&name, &ctype, &maxLen, &precision, &scale, &nullable, &vdefault) + var nullable, isPK bool + err = rows.Scan(&name, &ctype, &maxLen, &precision, &scale, &nullable, &vdefault, &isPK) if err != nil { return nil, nil, err } @@ -363,6 +372,7 @@ func (db *mssql) GetColumns(tableName string) ([]string, map[string]*core.Column col.Name = strings.Trim(name, "` ") col.Nullable = nullable col.Default = vdefault + col.IsPrimaryKey = isPK ct := strings.ToUpper(ctype) if ct == "DECIMAL" { col.Length = precision @@ -468,9 +478,10 @@ WHERE IXS.TYPE_DESC='NONCLUSTERED' and OBJECT_NAME(IXS.OBJECT_ID) =? } colName = strings.Trim(colName, "` ") - + var isRegular bool if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { indexName = indexName[5+len(tableName):] + isRegular = true } var index *core.Index @@ -479,6 +490,7 @@ WHERE IXS.TYPE_DESC='NONCLUSTERED' and OBJECT_NAME(IXS.OBJECT_ID) =? index = new(core.Index) index.Type = indexType index.Name = indexName + index.IsRegular = isRegular indexes[indexName] = index } index.AddColumn(colName) @@ -534,7 +546,6 @@ type odbcDriver struct { func (p *odbcDriver) Parse(driverName, dataSourceName string) (*core.Uri, error) { kv := strings.Split(dataSourceName, ";") var dbName string - for _, c := range kv { vv := strings.Split(strings.TrimSpace(c), "=") if len(vv) == 2 { diff --git a/vendor/github.com/go-xorm/xorm/dialect_mysql.go b/vendor/github.com/go-xorm/xorm/dialect_mysql.go index 55cfdd7640b..99100b23251 100644 --- a/vendor/github.com/go-xorm/xorm/dialect_mysql.go +++ b/vendor/github.com/go-xorm/xorm/dialect_mysql.go @@ -299,7 +299,7 @@ func (db *mysql) TableCheckSql(tableName string) (string, []interface{}) { func (db *mysql) GetColumns(tableName string) ([]string, map[string]*core.Column, error) { args := []interface{}{db.DbName, tableName} s := "SELECT `COLUMN_NAME`, `IS_NULLABLE`, `COLUMN_DEFAULT`, `COLUMN_TYPE`," + - " `COLUMN_KEY`, `EXTRA` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?" + " `COLUMN_KEY`, `EXTRA`,`COLUMN_COMMENT` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?" db.LogSQL(s, args) rows, err := db.DB().Query(s, args...) @@ -314,13 +314,14 @@ func (db *mysql) GetColumns(tableName string) ([]string, map[string]*core.Column col := new(core.Column) col.Indexes = make(map[string]int) - var columnName, isNullable, colType, colKey, extra string + var columnName, isNullable, colType, colKey, extra, comment string var colDefault *string - err = rows.Scan(&columnName, &isNullable, &colDefault, &colType, &colKey, &extra) + err = rows.Scan(&columnName, &isNullable, &colDefault, &colType, &colKey, &extra, &comment) if err != nil { return nil, nil, err } col.Name = strings.Trim(columnName, "` ") + col.Comment = comment if "YES" == isNullable { col.Nullable = true } @@ -407,7 +408,7 @@ func (db *mysql) GetColumns(tableName string) ([]string, map[string]*core.Column func (db *mysql) GetTables() ([]*core.Table, error) { args := []interface{}{db.DbName} - s := "SELECT `TABLE_NAME`, `ENGINE`, `TABLE_ROWS`, `AUTO_INCREMENT` from " + + s := "SELECT `TABLE_NAME`, `ENGINE`, `TABLE_ROWS`, `AUTO_INCREMENT`, `TABLE_COMMENT` from " + "`INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? AND (`ENGINE`='MyISAM' OR `ENGINE` = 'InnoDB' OR `ENGINE` = 'TokuDB')" db.LogSQL(s, args) @@ -420,14 +421,15 @@ func (db *mysql) GetTables() ([]*core.Table, error) { tables := make([]*core.Table, 0) for rows.Next() { table := core.NewEmptyTable() - var name, engine, tableRows string + var name, engine, tableRows, comment string var autoIncr *string - err = rows.Scan(&name, &engine, &tableRows, &autoIncr) + err = rows.Scan(&name, &engine, &tableRows, &autoIncr, &comment) if err != nil { return nil, err } table.Name = name + table.Comment = comment table.StoreEngine = engine tables = append(tables, table) } diff --git a/vendor/github.com/go-xorm/xorm/dialect_oracle.go b/vendor/github.com/go-xorm/xorm/dialect_oracle.go index 8c43aa4cecb..ac0081b38f7 100644 --- a/vendor/github.com/go-xorm/xorm/dialect_oracle.go +++ b/vendor/github.com/go-xorm/xorm/dialect_oracle.go @@ -824,6 +824,12 @@ func (db *oracle) GetIndexes(tableName string) (map[string]*core.Index, error) { indexName = strings.Trim(indexName, `" `) + var isRegular bool + if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { + indexName = indexName[5+len(tableName):] + isRegular = true + } + if uniqueness == "UNIQUE" { indexType = core.UniqueType } else { @@ -836,6 +842,7 @@ func (db *oracle) GetIndexes(tableName string) (map[string]*core.Index, error) { index = new(core.Index) index.Type = indexType index.Name = indexName + index.IsRegular = isRegular indexes[indexName] = index } index.AddColumn(colName) diff --git a/vendor/github.com/go-xorm/xorm/dialect_postgres.go b/vendor/github.com/go-xorm/xorm/dialect_postgres.go index 05fc1235ef4..83e9a1015c4 100644 --- a/vendor/github.com/go-xorm/xorm/dialect_postgres.go +++ b/vendor/github.com/go-xorm/xorm/dialect_postgres.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "net/url" - "sort" "strconv" "strings" @@ -781,6 +780,9 @@ func (db *postgres) SqlType(c *core.Column) string { case core.TinyInt: res = core.SmallInt return res + case core.Bit: + res = core.Boolean + return res case core.MediumInt, core.Int, core.Integer: if c.IsAutoIncrement { return core.Serial @@ -1078,9 +1080,10 @@ func (db *postgres) GetIndexes(tableName string) (map[string]*core.Index, error) } cs := strings.Split(indexdef, "(") colNames = strings.Split(cs[1][0:len(cs[1])-1], ",") - + var isRegular bool if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { newIdxName := indexName[5+len(tableName):] + isRegular = true if newIdxName != "" { indexName = newIdxName } @@ -1090,6 +1093,7 @@ func (db *postgres) GetIndexes(tableName string) (map[string]*core.Index, error) for _, colName := range colNames { index.Cols = append(index.Cols, strings.Trim(colName, `" `)) } + index.IsRegular = isRegular indexes[index.Name] = index } return indexes, nil @@ -1112,10 +1116,6 @@ func (vs values) Get(k string) (v string) { return vs[k] } -func errorf(s string, args ...interface{}) { - panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...))) -} - func parseURL(connstr string) (string, error) { u, err := url.Parse(connstr) if err != nil { @@ -1126,46 +1126,18 @@ func parseURL(connstr string) (string, error) { return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) } - var kvs []string escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`) - accrue := func(k, v string) { - if v != "" { - kvs = append(kvs, k+"="+escaper.Replace(v)) - } - } - - if u.User != nil { - v := u.User.Username() - accrue("user", v) - - v, _ = u.User.Password() - accrue("password", v) - } - - i := strings.Index(u.Host, ":") - if i < 0 { - accrue("host", u.Host) - } else { - accrue("host", u.Host[:i]) - accrue("port", u.Host[i+1:]) - } if u.Path != "" { - accrue("dbname", u.Path[1:]) + return escaper.Replace(u.Path[1:]), nil } - q := u.Query() - for k := range q { - accrue(k, q.Get(k)) - } - - sort.Strings(kvs) // Makes testing easier (not a performance concern) - return strings.Join(kvs, " "), nil + return "", nil } -func parseOpts(name string, o values) { +func parseOpts(name string, o values) error { if len(name) == 0 { - return + return fmt.Errorf("invalid options: %s", name) } name = strings.TrimSpace(name) @@ -1174,31 +1146,36 @@ func parseOpts(name string, o values) { for _, p := range ps { kv := strings.Split(p, "=") if len(kv) < 2 { - errorf("invalid option: %q", p) + return fmt.Errorf("invalid option: %q", p) } o.Set(kv[0], kv[1]) } + + return nil } func (p *pqDriver) Parse(driverName, dataSourceName string) (*core.Uri, error) { db := &core.Uri{DbType: core.POSTGRES} - o := make(values) var err error + if strings.HasPrefix(dataSourceName, "postgresql://") || strings.HasPrefix(dataSourceName, "postgres://") { - dataSourceName, err = parseURL(dataSourceName) + db.DbName, err = parseURL(dataSourceName) + if err != nil { + return nil, err + } + } else { + o := make(values) + err = parseOpts(dataSourceName, o) if err != nil { return nil, err } - } - parseOpts(dataSourceName, o) - db.DbName = o.Get("dbname") + db.DbName = o.Get("dbname") + } + if db.DbName == "" { return nil, errors.New("dbname is empty") } - /*db.Schema = o.Get("schema") - if len(db.Schema) == 0 { - db.Schema = "public" - }*/ + return db, nil } diff --git a/vendor/github.com/go-xorm/xorm/dialect_sqlite3.go b/vendor/github.com/go-xorm/xorm/dialect_sqlite3.go index c190c4d900f..a55b1615e71 100644 --- a/vendor/github.com/go-xorm/xorm/dialect_sqlite3.go +++ b/vendor/github.com/go-xorm/xorm/dialect_sqlite3.go @@ -14,10 +14,6 @@ import ( "github.com/go-xorm/core" ) -// func init() { -// RegisterDialect("sqlite3", &sqlite3{}) -// } - var ( sqlite3ReservedWords = map[string]bool{ "ABORT": true, @@ -310,11 +306,25 @@ func (db *sqlite3) GetColumns(tableName string) ([]string, map[string]*core.Colu for _, colStr := range colCreates { reg = regexp.MustCompile(`,\s`) colStr = reg.ReplaceAllString(colStr, ",") + if strings.HasPrefix(strings.TrimSpace(colStr), "PRIMARY KEY") { + parts := strings.Split(strings.TrimSpace(colStr), "(") + if len(parts) == 2 { + pkCols := strings.Split(strings.TrimRight(strings.TrimSpace(parts[1]), ")"), ",") + for _, pk := range pkCols { + if col, ok := cols[strings.Trim(strings.TrimSpace(pk), "`")]; ok { + col.IsPrimaryKey = true + } + } + } + continue + } + fields := strings.Fields(strings.TrimSpace(colStr)) col := new(core.Column) col.Indexes = make(map[string]int) col.Nullable = true col.DefaultIsEmpty = true + for idx, field := range fields { if idx == 0 { col.Name = strings.Trim(strings.Trim(field, "`[] "), `"`) @@ -405,8 +415,10 @@ func (db *sqlite3) GetIndexes(tableName string) (map[string]*core.Index, error) } indexName := strings.Trim(sql[nNStart+6:nNEnd], "` []") + var isRegular bool if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { index.Name = indexName[5+len(tableName):] + isRegular = true } else { index.Name = indexName } @@ -425,6 +437,7 @@ func (db *sqlite3) GetIndexes(tableName string) (map[string]*core.Index, error) for _, col := range colIndexes { index.Cols = append(index.Cols, strings.Trim(col, "` []")) } + index.IsRegular = isRegular indexes[index.Name] = index } diff --git a/vendor/github.com/go-xorm/xorm/doc.go b/vendor/github.com/go-xorm/xorm/doc.go index 5b36fcd80ba..a687e694768 100644 --- a/vendor/github.com/go-xorm/xorm/doc.go +++ b/vendor/github.com/go-xorm/xorm/doc.go @@ -8,7 +8,7 @@ Package xorm is a simple and powerful ORM for Go. Installation -Make sure you have installed Go 1.1+ and then: +Make sure you have installed Go 1.6+ and then: go get github.com/go-xorm/xorm @@ -51,11 +51,15 @@ There are 8 major ORM methods and many helpful methods to use to operate databas // INSERT INTO struct1 () values () // INSERT INTO struct2 () values (),(),() -2. Query one record from database +2. Query one record or one variable from database has, err := engine.Get(&user) // SELECT * FROM user LIMIT 1 + var id int64 + has, err := engine.Table("user").Where("name = ?", name).Get(&id) + // SELECT id FROM user WHERE name = ? LIMIT 1 + 3. Query multiple records from database var sliceOfStructs []Struct @@ -86,7 +90,7 @@ another is Rows 5. Update one or more records - affected, err := engine.Id(...).Update(&user) + affected, err := engine.ID(...).Update(&user) // UPDATE user SET ... 6. Delete one or more records, Delete MUST has condition @@ -99,6 +103,9 @@ another is Rows counts, err := engine.Count(&user) // SELECT count(*) AS total FROM user + counts, err := engine.SQL("select count(*) FROM user").Count() + // select count(*) FROM user + 8. Sum records sumFloat64, err := engine.Sum(&user, "id") diff --git a/vendor/github.com/go-xorm/xorm/engine.go b/vendor/github.com/go-xorm/xorm/engine.go index 134e6b147d9..444611afb16 100644 --- a/vendor/github.com/go-xorm/xorm/engine.go +++ b/vendor/github.com/go-xorm/xorm/engine.go @@ -19,6 +19,7 @@ import ( "sync" "time" + "github.com/go-xorm/builder" "github.com/go-xorm/core" ) @@ -40,12 +41,29 @@ type Engine struct { showExecTime bool logger core.ILogger - TZLocation *time.Location + TZLocation *time.Location // The timezone of the application DatabaseTZ *time.Location // The timezone of the database disableGlobalCache bool tagHandlers map[string]tagHandler + + engineGroup *EngineGroup +} + +// BufferSize sets buffer size for iterate +func (engine *Engine) BufferSize(size int) *Session { + session := engine.NewSession() + session.isAutoClose = true + return session.BufferSize(size) +} + +// CondDeleted returns the conditions whether a record is soft deleted. +func (engine *Engine) CondDeleted(colName string) builder.Cond { + if engine.dialect.DBType() == core.MSSQL { + return builder.IsNull{colName} + } + return builder.IsNull{colName}.Or(builder.Eq{colName: zeroTime1}) } // ShowSQL show SQL statement or not on logger if log level is great than INFO @@ -78,6 +96,11 @@ func (engine *Engine) SetLogger(logger core.ILogger) { engine.dialect.SetLogger(logger) } +// SetLogLevel sets the logger level +func (engine *Engine) SetLogLevel(level core.LogLevel) { + engine.logger.SetLevel(level) +} + // SetDisableGlobalCache disable global cache or not func (engine *Engine) SetDisableGlobalCache(disable bool) { if engine.disableGlobalCache != disable { @@ -143,7 +166,6 @@ func (engine *Engine) Quote(value string) string { // QuoteTo quotes string and writes into the buffer func (engine *Engine) QuoteTo(buf *bytes.Buffer, value string) { - if buf == nil { return } @@ -169,7 +191,7 @@ func (engine *Engine) quote(sql string) string { return engine.dialect.QuoteStr() + sql + engine.dialect.QuoteStr() } -// SqlType will be depracated, please use SQLType instead +// SqlType will be deprecated, please use SQLType instead // // Deprecated: use SQLType instead func (engine *Engine) SqlType(c *core.Column) string { @@ -201,26 +223,36 @@ func (engine *Engine) SetDefaultCacher(cacher core.Cacher) { engine.Cacher = cacher } +// GetDefaultCacher returns the default cacher +func (engine *Engine) GetDefaultCacher() core.Cacher { + return engine.Cacher +} + // NoCache If you has set default cacher, and you want temporilly stop use cache, // you can use NoCache() func (engine *Engine) NoCache() *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.NoCache() } // NoCascade If you do not want to auto cascade load object func (engine *Engine) NoCascade() *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.NoCascade() } // MapCacher Set a table use a special cacher -func (engine *Engine) MapCacher(bean interface{}, cacher core.Cacher) { +func (engine *Engine) MapCacher(bean interface{}, cacher core.Cacher) error { v := rValue(bean) - tb := engine.autoMapType(v) + tb, err := engine.autoMapType(v) + if err != nil { + return err + } + tb.Cacher = cacher + return nil } // NewDB provides an interface to operate database directly @@ -240,7 +272,7 @@ func (engine *Engine) Dialect() core.Dialect { // NewSession New a session func (engine *Engine) NewSession() *Session { - session := &Session{Engine: engine} + session := &Session{engine: engine} session.Init() return session } @@ -254,7 +286,6 @@ func (engine *Engine) Close() error { func (engine *Engine) Ping() error { session := engine.NewSession() defer session.Close() - engine.logger.Infof("PING DATABASE %v", engine.DriverName()) return session.Ping() } @@ -262,43 +293,13 @@ func (engine *Engine) Ping() error { func (engine *Engine) logSQL(sqlStr string, sqlArgs ...interface{}) { if engine.showSQL && !engine.showExecTime { if len(sqlArgs) > 0 { - engine.logger.Infof("[SQL] %v %v", sqlStr, sqlArgs) + engine.logger.Infof("[SQL] %v %#v", sqlStr, sqlArgs) } else { engine.logger.Infof("[SQL] %v", sqlStr) } } } -func (engine *Engine) logSQLQueryTime(sqlStr string, args []interface{}, executionBlock func() (*core.Stmt, *core.Rows, error)) (*core.Stmt, *core.Rows, error) { - if engine.showSQL && engine.showExecTime { - b4ExecTime := time.Now() - stmt, res, err := executionBlock() - execDuration := time.Since(b4ExecTime) - if len(args) > 0 { - engine.logger.Infof("[SQL] %s %v - took: %v", sqlStr, args, execDuration) - } else { - engine.logger.Infof("[SQL] %s - took: %v", sqlStr, execDuration) - } - return stmt, res, err - } - return executionBlock() -} - -func (engine *Engine) logSQLExecutionTime(sqlStr string, args []interface{}, executionBlock func() (sql.Result, error)) (sql.Result, error) { - if engine.showSQL && engine.showExecTime { - b4ExecTime := time.Now() - res, err := executionBlock() - execDuration := time.Since(b4ExecTime) - if len(args) > 0 { - engine.logger.Infof("[sql] %s [args] %v - took: %v", sqlStr, args, execDuration) - } else { - engine.logger.Infof("[sql] %s - took: %v", sqlStr, execDuration) - } - return res, err - } - return executionBlock() -} - // Sql provides raw sql input parameter. When you have a complex SQL statement // and cannot use Where, Id, In and etc. Methods to describe, you can use SQL. // @@ -315,7 +316,7 @@ func (engine *Engine) Sql(querystring string, args ...interface{}) *Session { // This code will execute "select * from user" and set the records to users func (engine *Engine) SQL(query interface{}, args ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.SQL(query, args...) } @@ -324,14 +325,14 @@ func (engine *Engine) SQL(query interface{}, args ...interface{}) *Session { // invoked. Call NoAutoTime if you dont' want to fill automatically. func (engine *Engine) NoAutoTime() *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.NoAutoTime() } // NoAutoCondition disable auto generate Where condition from bean or not func (engine *Engine) NoAutoCondition(no ...bool) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.NoAutoCondition(no...) } @@ -565,56 +566,56 @@ func (engine *Engine) tbName(v reflect.Value) string { // Cascade use cascade or not func (engine *Engine) Cascade(trueOrFalse ...bool) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Cascade(trueOrFalse...) } // Where method provide a condition query func (engine *Engine) Where(query interface{}, args ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Where(query, args...) } -// Id will be depracated, please use ID instead +// Id will be deprecated, please use ID instead func (engine *Engine) Id(id interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Id(id) } // ID method provoide a condition as (id) = ? func (engine *Engine) ID(id interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.ID(id) } // Before apply before Processor, affected bean is passed to closure arg func (engine *Engine) Before(closures func(interface{})) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Before(closures) } // After apply after insert Processor, affected bean is passed to closure arg func (engine *Engine) After(closures func(interface{})) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.After(closures) } // Charset set charset when create table, only support mysql now func (engine *Engine) Charset(charset string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Charset(charset) } // StoreEngine set store engine when create table, only support mysql now func (engine *Engine) StoreEngine(storeEngine string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.StoreEngine(storeEngine) } @@ -623,35 +624,35 @@ func (engine *Engine) StoreEngine(storeEngine string) *Session { // but distinct will not provide id func (engine *Engine) Distinct(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Distinct(columns...) } // Select customerize your select columns or contents func (engine *Engine) Select(str string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Select(str) } // Cols only use the parameters as select or update columns func (engine *Engine) Cols(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Cols(columns...) } // AllCols indicates that all columns should be use func (engine *Engine) AllCols() *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.AllCols() } // MustCols specify some columns must use even if they are empty func (engine *Engine) MustCols(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.MustCols(columns...) } @@ -662,77 +663,84 @@ func (engine *Engine) MustCols(columns ...string) *Session { // it will use parameters's columns func (engine *Engine) UseBool(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.UseBool(columns...) } // Omit only not use the parameters as select or update columns func (engine *Engine) Omit(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Omit(columns...) } // Nullable set null when column is zero-value and nullable for update func (engine *Engine) Nullable(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Nullable(columns...) } // In will generate "column IN (?, ?)" func (engine *Engine) In(column string, args ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.In(column, args...) } +// NotIn will generate "column NOT IN (?, ?)" +func (engine *Engine) NotIn(column string, args ...interface{}) *Session { + session := engine.NewSession() + session.isAutoClose = true + return session.NotIn(column, args...) +} + // Incr provides a update string like "column = column + ?" func (engine *Engine) Incr(column string, arg ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Incr(column, arg...) } // Decr provides a update string like "column = column - ?" func (engine *Engine) Decr(column string, arg ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Decr(column, arg...) } // SetExpr provides a update string like "column = {expression}" func (engine *Engine) SetExpr(column string, expression string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.SetExpr(column, expression) } // Table temporarily change the Get, Find, Update's table func (engine *Engine) Table(tableNameOrBean interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Table(tableNameOrBean) } // Alias set the table alias func (engine *Engine) Alias(alias string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Alias(alias) } // Limit will generate "LIMIT start, limit" func (engine *Engine) Limit(limit int, start ...int) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Limit(limit, start...) } // Desc will generate "ORDER BY column1 DESC, column2 DESC" func (engine *Engine) Desc(colNames ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Desc(colNames...) } @@ -744,39 +752,53 @@ func (engine *Engine) Desc(colNames ...string) *Session { // func (engine *Engine) Asc(colNames ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Asc(colNames...) } // OrderBy will generate "ORDER BY order" func (engine *Engine) OrderBy(order string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.OrderBy(order) } +// Prepare enables prepare statement +func (engine *Engine) Prepare() *Session { + session := engine.NewSession() + session.isAutoClose = true + return session.Prepare() +} + // Join the join_operator should be one of INNER, LEFT OUTER, CROSS etc - this will be prepended to JOIN func (engine *Engine) Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Join(joinOperator, tablename, condition, args...) } // GroupBy generate group by statement func (engine *Engine) GroupBy(keys string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.GroupBy(keys) } // Having generate having statement func (engine *Engine) Having(conditions string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Having(conditions) } -func (engine *Engine) autoMapType(v reflect.Value) *core.Table { +// UnMapType removes the datbase mapper of a type +func (engine *Engine) UnMapType(t reflect.Type) { + engine.mutex.Lock() + defer engine.mutex.Unlock() + delete(engine.Tables, t) +} + +func (engine *Engine) autoMapType(v reflect.Value) (*core.Table, error) { t := v.Type() engine.mutex.Lock() defer engine.mutex.Unlock() @@ -785,24 +807,23 @@ func (engine *Engine) autoMapType(v reflect.Value) *core.Table { var err error table, err = engine.mapType(v) if err != nil { - engine.logger.Error(err) - } else { - engine.Tables[t] = table - if engine.Cacher != nil { - if v.CanAddr() { - engine.GobRegister(v.Addr().Interface()) - } else { - engine.GobRegister(v.Interface()) - } + return nil, err + } + + engine.Tables[t] = table + if engine.Cacher != nil { + if v.CanAddr() { + engine.GobRegister(v.Addr().Interface()) + } else { + engine.GobRegister(v.Interface()) } } } - return table + return table, nil } // GobRegister register one struct to gob for cache use func (engine *Engine) GobRegister(v interface{}) *Engine { - //fmt.Printf("Type: %[1]T => Data: %[1]#v\n", v) gob.Register(v) return engine } @@ -813,10 +834,19 @@ type Table struct { Name string } +// IsValid if table is valid +func (t *Table) IsValid() bool { + return t.Table != nil && len(t.Name) > 0 +} + // TableInfo get table info according to bean's content func (engine *Engine) TableInfo(bean interface{}) *Table { v := rValue(bean) - return &Table{engine.autoMapType(v), engine.tbName(v)} + tb, err := engine.autoMapType(v) + if err != nil { + engine.logger.Error(err) + } + return &Table{tb, engine.tbName(v)} } func addIndex(indexName string, table *core.Table, col *core.Column, indexType int) { @@ -911,6 +941,7 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) { k := strings.ToUpper(key) ctx.tagName = k + ctx.params = []string{} pStart := strings.Index(k, "(") if pStart == 0 { @@ -918,18 +949,18 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) { } if pStart > -1 { if !strings.HasSuffix(k, ")") { - return nil, errors.New("cannot match ) charactor") + return nil, fmt.Errorf("field %s tag %s cannot match ) charactor", col.FieldName, key) } ctx.tagName = k[:pStart] - ctx.params = strings.Split(k[pStart+1:len(k)-1], ",") + ctx.params = strings.Split(key[pStart+1:len(k)-1], ",") } if j > 0 { ctx.preTag = strings.ToUpper(tags[j-1]) } if j < len(tags)-1 { - ctx.nextTag = strings.ToUpper(tags[j+1]) + ctx.nextTag = tags[j+1] } else { ctx.nextTag = "" } @@ -993,6 +1024,10 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) { col = core.NewColumn(engine.ColumnMapper.Obj2Table(t.Field(i).Name), t.Field(i).Name, sqlType, sqlType.DefaultLength, sqlType.DefaultLength2, true) + + if fieldType.Kind() == reflect.Int64 && (strings.ToUpper(col.FieldName) == "ID" || strings.HasSuffix(strings.ToUpper(col.FieldName), ".ID")) { + idFieldColName = col.Name + } } if col.IsAutoIncrement { col.Nullable = false @@ -1000,9 +1035,6 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) { table.AddColumn(col) - if fieldType.Kind() == reflect.Int64 && (strings.ToUpper(col.FieldName) == "ID" || strings.HasSuffix(strings.ToUpper(col.FieldName), ".ID")) { - idFieldColName = col.Name - } } // end for if idFieldColName != "" && len(table.PrimaryKeys) == 0 { @@ -1066,21 +1098,54 @@ func (engine *Engine) IdOfV(rv reflect.Value) core.PK { // IDOfV get id from one value of struct func (engine *Engine) IDOfV(rv reflect.Value) core.PK { + pk, err := engine.idOfV(rv) + if err != nil { + engine.logger.Error(err) + return nil + } + return pk +} + +func (engine *Engine) idOfV(rv reflect.Value) (core.PK, error) { v := reflect.Indirect(rv) - table := engine.autoMapType(v) + table, err := engine.autoMapType(v) + if err != nil { + return nil, err + } + pk := make([]interface{}, len(table.PrimaryKeys)) for i, col := range table.PKColumns() { + var err error pkField := v.FieldByName(col.FieldName) switch pkField.Kind() { case reflect.String: - pk[i] = pkField.String() + pk[i], err = engine.idTypeAssertion(col, pkField.String()) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - pk[i] = pkField.Int() + pk[i], err = engine.idTypeAssertion(col, strconv.FormatInt(pkField.Int(), 10)) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - pk[i] = pkField.Uint() + // id of uint will be converted to int64 + pk[i], err = engine.idTypeAssertion(col, strconv.FormatUint(pkField.Uint(), 10)) + } + + if err != nil { + return nil, err } } - return core.PK(pk) + return core.PK(pk), nil +} + +func (engine *Engine) idTypeAssertion(col *core.Column, sid string) (interface{}, error) { + if col.SQLType.IsNumeric() { + n, err := strconv.ParseInt(sid, 10, 64) + if err != nil { + return nil, err + } + return n, nil + } else if col.SQLType.IsText() { + return sid, nil + } else { + return nil, errors.New("not supported") + } } // CreateIndexes create indexes @@ -1101,13 +1166,6 @@ func (engine *Engine) getCacher2(table *core.Table) core.Cacher { return table.Cacher } -func (engine *Engine) getCacher(v reflect.Value) core.Cacher { - if table := engine.autoMapType(v); table != nil { - return table.Cacher - } - return engine.Cacher -} - // ClearCacheBean if enabled cache, clear the cache bean func (engine *Engine) ClearCacheBean(bean interface{}, id string) error { v := rValue(bean) @@ -1116,7 +1174,10 @@ func (engine *Engine) ClearCacheBean(bean interface{}, id string) error { return errors.New("error params") } tableName := engine.tbName(v) - table := engine.autoMapType(v) + table, err := engine.autoMapType(v) + if err != nil { + return err + } cacher := table.Cacher if cacher == nil { cacher = engine.Cacher @@ -1137,7 +1198,11 @@ func (engine *Engine) ClearCache(beans ...interface{}) error { return errors.New("error params") } tableName := engine.tbName(v) - table := engine.autoMapType(v) + table, err := engine.autoMapType(v) + if err != nil { + return err + } + cacher := table.Cacher if cacher == nil { cacher = engine.Cacher @@ -1154,19 +1219,23 @@ func (engine *Engine) ClearCache(beans ...interface{}) error { // table, column, index, unique. but will not delete or change anything. // If you change some field, you should change the database manually. func (engine *Engine) Sync(beans ...interface{}) error { + session := engine.NewSession() + defer session.Close() + for _, bean := range beans { v := rValue(bean) tableName := engine.tbName(v) - table := engine.autoMapType(v) + table, err := engine.autoMapType(v) + if err != nil { + return err + } - s := engine.NewSession() - defer s.Close() - isExist, err := s.Table(bean).isTableExist(tableName) + isExist, err := session.Table(bean).isTableExist(tableName) if err != nil { return err } if !isExist { - err = engine.CreateTables(bean) + err = session.createTable(bean) if err != nil { return err } @@ -1177,11 +1246,11 @@ func (engine *Engine) Sync(beans ...interface{}) error { }*/ var isEmpty bool if isEmpty { - err = engine.DropTables(bean) + err = session.dropTable(bean) if err != nil { return err } - err = engine.CreateTables(bean) + err = session.createTable(bean) if err != nil { return err } @@ -1192,9 +1261,9 @@ func (engine *Engine) Sync(beans ...interface{}) error { return err } if !isExist { - session := engine.NewSession() - session.Statement.setRefValue(v) - defer session.Close() + if err := session.statement.setRefValue(v); err != nil { + return err + } err = session.addColumn(col.Name) if err != nil { return err @@ -1203,19 +1272,19 @@ func (engine *Engine) Sync(beans ...interface{}) error { } for name, index := range table.Indexes { - session := engine.NewSession() - session.Statement.setRefValue(v) - defer session.Close() + if err := session.statement.setRefValue(v); err != nil { + return err + } if index.Type == core.UniqueType { - //isExist, err := session.isIndexExist(table.Name, name, true) isExist, err := session.isIndexExist2(tableName, index.Cols, true) if err != nil { return err } if !isExist { - session := engine.NewSession() - session.Statement.setRefValue(v) - defer session.Close() + if err := session.statement.setRefValue(v); err != nil { + return err + } + err = session.addUnique(tableName, name) if err != nil { return err @@ -1227,9 +1296,10 @@ func (engine *Engine) Sync(beans ...interface{}) error { return err } if !isExist { - session := engine.NewSession() - session.Statement.setRefValue(v) - defer session.Close() + if err := session.statement.setRefValue(v); err != nil { + return err + } + err = session.addIndex(tableName, name) if err != nil { return err @@ -1251,35 +1321,6 @@ func (engine *Engine) Sync2(beans ...interface{}) error { return s.Sync2(beans...) } -func (engine *Engine) unMap(beans ...interface{}) (e error) { - engine.mutex.Lock() - defer engine.mutex.Unlock() - for _, bean := range beans { - t := rType(bean) - if _, ok := engine.Tables[t]; ok { - delete(engine.Tables, t) - } - } - return -} - -// Drop all mapped table -func (engine *Engine) dropAll() error { - session := engine.NewSession() - defer session.Close() - - err := session.Begin() - if err != nil { - return err - } - err = session.dropAll() - if err != nil { - session.Rollback() - return err - } - return session.Commit() -} - // CreateTables create tabls according bean func (engine *Engine) CreateTables(beans ...interface{}) error { session := engine.NewSession() @@ -1291,7 +1332,7 @@ func (engine *Engine) CreateTables(beans ...interface{}) error { } for _, bean := range beans { - err = session.CreateTable(bean) + err = session.createTable(bean) if err != nil { session.Rollback() return err @@ -1311,7 +1352,7 @@ func (engine *Engine) DropTables(beans ...interface{}) error { } for _, bean := range beans { - err = session.DropTable(bean) + err = session.dropTable(bean) if err != nil { session.Rollback() return err @@ -1320,10 +1361,11 @@ func (engine *Engine) DropTables(beans ...interface{}) error { return session.Commit() } -func (engine *Engine) createAll() error { +// DropIndexes drop indexes of a table +func (engine *Engine) DropIndexes(bean interface{}) error { session := engine.NewSession() defer session.Close() - return session.createAll() + return session.DropIndexes(bean) } // Exec raw sql @@ -1334,10 +1376,24 @@ func (engine *Engine) Exec(sql string, args ...interface{}) (sql.Result, error) } // Query a raw sql and return records as []map[string][]byte -func (engine *Engine) Query(sql string, paramStr ...interface{}) (resultsSlice []map[string][]byte, err error) { +func (engine *Engine) Query(sqlorArgs ...interface{}) (resultsSlice []map[string][]byte, err error) { session := engine.NewSession() defer session.Close() - return session.Query(sql, paramStr...) + return session.Query(sqlorArgs...) +} + +// QueryString runs a raw sql and return records as []map[string]string +func (engine *Engine) QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) { + session := engine.NewSession() + defer session.Close() + return session.QueryString(sqlorArgs...) +} + +// QueryInterface runs a raw sql and return records as []map[string]interface{} +func (engine *Engine) QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) { + session := engine.NewSession() + defer session.Close() + return session.QueryInterface(sqlorArgs...) } // Insert one or more records @@ -1381,6 +1437,13 @@ func (engine *Engine) Get(bean interface{}) (bool, error) { return session.Get(bean) } +// Exist returns true if the record exist otherwise return false +func (engine *Engine) Exist(bean ...interface{}) (bool, error) { + session := engine.NewSession() + defer session.Close() + return session.Exist(bean...) +} + // Find retrieve records from table, condiBeans's non-empty fields // are conditions. beans could be []Struct, []*Struct, map[int64]Struct // map[int64]*Struct @@ -1406,10 +1469,10 @@ func (engine *Engine) Rows(bean interface{}) (*Rows, error) { } // Count counts the records. bean's non-empty fields are conditions. -func (engine *Engine) Count(bean interface{}) (int64, error) { +func (engine *Engine) Count(bean ...interface{}) (int64, error) { session := engine.NewSession() defer session.Close() - return session.Count(bean) + return session.Count(bean...) } // Sum sum the records by some column. bean's non-empty fields are conditions. @@ -1419,6 +1482,13 @@ func (engine *Engine) Sum(bean interface{}, colName string) (float64, error) { return session.Sum(bean, colName) } +// SumInt sum the records by some column. bean's non-empty fields are conditions. +func (engine *Engine) SumInt(bean interface{}, colName string) (int64, error) { + session := engine.NewSession() + defer session.Close() + return session.SumInt(bean, colName) +} + // Sums sum the records by some columns. bean's non-empty fields are conditions. func (engine *Engine) Sums(bean interface{}, colNames ...string) ([]float64, error) { session := engine.NewSession() @@ -1474,7 +1544,6 @@ func (engine *Engine) Import(r io.Reader) ([]sql.Result, error) { results = append(results, result) if err != nil { return nil, err - //lastError = err } } } @@ -1482,49 +1551,32 @@ func (engine *Engine) Import(r io.Reader) ([]sql.Result, error) { return results, lastError } -// TZTime change one time to xorm time location -func (engine *Engine) TZTime(t time.Time) time.Time { - if !t.IsZero() { // if time is not initialized it's not suitable for Time.In() - return t.In(engine.TZLocation) +// nowTime return current time +func (engine *Engine) nowTime(col *core.Column) (interface{}, time.Time) { + t := time.Now() + var tz = engine.DatabaseTZ + if !col.DisableTimeZone && col.TimeZone != nil { + tz = col.TimeZone } - return t -} - -// NowTime return current time -func (engine *Engine) NowTime(sqlTypeName string) interface{} { - t := time.Now() - return engine.FormatTime(sqlTypeName, t) -} - -// NowTime2 return current time -func (engine *Engine) NowTime2(sqlTypeName string) (interface{}, time.Time) { - t := time.Now() - return engine.FormatTime(sqlTypeName, t), t -} - -// FormatTime format time -func (engine *Engine) FormatTime(sqlTypeName string, t time.Time) (v interface{}) { - return engine.formatTime(engine.TZLocation, sqlTypeName, t) + return engine.formatTime(col.SQLType.Name, t.In(tz)), t.In(engine.TZLocation) } func (engine *Engine) formatColTime(col *core.Column, t time.Time) (v interface{}) { - if col.DisableTimeZone { - return engine.formatTime(nil, col.SQLType.Name, t) - } else if col.TimeZone != nil { - return engine.formatTime(col.TimeZone, col.SQLType.Name, t) + if t.IsZero() { + if col.Nullable { + return nil + } + return "" } - return engine.formatTime(engine.TZLocation, col.SQLType.Name, t) + + if col.TimeZone != nil { + return engine.formatTime(col.SQLType.Name, t.In(col.TimeZone)) + } + return engine.formatTime(col.SQLType.Name, t.In(engine.DatabaseTZ)) } -func (engine *Engine) formatTime(tz *time.Location, sqlTypeName string, t time.Time) (v interface{}) { - if engine.dialect.DBType() == core.ORACLE { - return t - } - if tz != nil { - t = t.In(tz) - } else { - t = engine.TZTime(t) - } +// formatTime format time as column type +func (engine *Engine) formatTime(sqlTypeName string, t time.Time) (v interface{}) { switch sqlTypeName { case core.Time: s := t.Format("2006-01-02 15:04:05") //time.RFC3339 @@ -1532,18 +1584,10 @@ func (engine *Engine) formatTime(tz *time.Location, sqlTypeName string, t time.T case core.Date: v = t.Format("2006-01-02") case core.DateTime, core.TimeStamp: - if engine.dialect.DBType() == "ql" { - v = t - } else if engine.dialect.DBType() == "sqlite3" { - v = t.UTC().Format("2006-01-02 15:04:05") - } else { - v = t.Format("2006-01-02 15:04:05") - } + v = t.Format("2006-01-02 15:04:05") case core.TimeStampz: if engine.dialect.DBType() == core.MSSQL { v = t.Format("2006-01-02T15:04:05.9999999Z07:00") - } else if engine.DriverName() == "mssql" { - v = t } else { v = t.Format(time.RFC3339Nano) } @@ -1555,9 +1599,39 @@ func (engine *Engine) formatTime(tz *time.Location, sqlTypeName string, t time.T return } +// GetColumnMapper returns the column name mapper +func (engine *Engine) GetColumnMapper() core.IMapper { + return engine.ColumnMapper +} + +// GetTableMapper returns the table name mapper +func (engine *Engine) GetTableMapper() core.IMapper { + return engine.TableMapper +} + +// GetTZLocation returns time zone of the application +func (engine *Engine) GetTZLocation() *time.Location { + return engine.TZLocation +} + +// SetTZLocation sets time zone of the application +func (engine *Engine) SetTZLocation(tz *time.Location) { + engine.TZLocation = tz +} + +// GetTZDatabase returns time zone of the database +func (engine *Engine) GetTZDatabase() *time.Location { + return engine.DatabaseTZ +} + +// SetTZDatabase sets time zone of the database +func (engine *Engine) SetTZDatabase(tz *time.Location) { + engine.DatabaseTZ = tz +} + // Unscoped always disable struct tag "deleted" func (engine *Engine) Unscoped() *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Unscoped() } diff --git a/vendor/github.com/go-xorm/xorm/engine_cond.go b/vendor/github.com/go-xorm/xorm/engine_cond.go new file mode 100644 index 00000000000..6c8e3879cee --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/engine_cond.go @@ -0,0 +1,230 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/go-xorm/builder" + "github.com/go-xorm/core" +) + +func (engine *Engine) buildConds(table *core.Table, bean interface{}, + includeVersion bool, includeUpdated bool, includeNil bool, + includeAutoIncr bool, allUseBool bool, useAllCols bool, unscoped bool, + mustColumnMap map[string]bool, tableName, aliasName string, addedTableName bool) (builder.Cond, error) { + var conds []builder.Cond + for _, col := range table.Columns() { + if !includeVersion && col.IsVersion { + continue + } + if !includeUpdated && col.IsUpdated { + continue + } + if !includeAutoIncr && col.IsAutoIncrement { + continue + } + + if engine.dialect.DBType() == core.MSSQL && (col.SQLType.Name == core.Text || col.SQLType.IsBlob() || col.SQLType.Name == core.TimeStampz) { + continue + } + if col.SQLType.IsJson() { + continue + } + + var colName string + if addedTableName { + var nm = tableName + if len(aliasName) > 0 { + nm = aliasName + } + colName = engine.Quote(nm) + "." + engine.Quote(col.Name) + } else { + colName = engine.Quote(col.Name) + } + + fieldValuePtr, err := col.ValueOf(bean) + if err != nil { + engine.logger.Error(err) + continue + } + + if col.IsDeleted && !unscoped { // tag "deleted" is enabled + conds = append(conds, engine.CondDeleted(colName)) + } + + fieldValue := *fieldValuePtr + if fieldValue.Interface() == nil { + continue + } + + fieldType := reflect.TypeOf(fieldValue.Interface()) + requiredField := useAllCols + + if b, ok := getFlagForColumn(mustColumnMap, col); ok { + if b { + requiredField = true + } else { + continue + } + } + + if fieldType.Kind() == reflect.Ptr { + if fieldValue.IsNil() { + if includeNil { + conds = append(conds, builder.Eq{colName: nil}) + } + continue + } else if !fieldValue.IsValid() { + continue + } else { + // dereference ptr type to instance type + fieldValue = fieldValue.Elem() + fieldType = reflect.TypeOf(fieldValue.Interface()) + requiredField = true + } + } + + var val interface{} + switch fieldType.Kind() { + case reflect.Bool: + if allUseBool || requiredField { + val = fieldValue.Interface() + } else { + // if a bool in a struct, it will not be as a condition because it default is false, + // please use Where() instead + continue + } + case reflect.String: + if !requiredField && fieldValue.String() == "" { + continue + } + // for MyString, should convert to string or panic + if fieldType.String() != reflect.String.String() { + val = fieldValue.String() + } else { + val = fieldValue.Interface() + } + case reflect.Int8, reflect.Int16, reflect.Int, reflect.Int32, reflect.Int64: + if !requiredField && fieldValue.Int() == 0 { + continue + } + val = fieldValue.Interface() + case reflect.Float32, reflect.Float64: + if !requiredField && fieldValue.Float() == 0.0 { + continue + } + val = fieldValue.Interface() + case reflect.Uint8, reflect.Uint16, reflect.Uint, reflect.Uint32, reflect.Uint64: + if !requiredField && fieldValue.Uint() == 0 { + continue + } + t := int64(fieldValue.Uint()) + val = reflect.ValueOf(&t).Interface() + case reflect.Struct: + if fieldType.ConvertibleTo(core.TimeType) { + t := fieldValue.Convert(core.TimeType).Interface().(time.Time) + if !requiredField && (t.IsZero() || !fieldValue.IsValid()) { + continue + } + val = engine.formatColTime(col, t) + } else if _, ok := reflect.New(fieldType).Interface().(core.Conversion); ok { + continue + } else if valNul, ok := fieldValue.Interface().(driver.Valuer); ok { + val, _ = valNul.Value() + if val == nil { + continue + } + } else { + if col.SQLType.IsJson() { + if col.SQLType.IsText() { + bytes, err := json.Marshal(fieldValue.Interface()) + if err != nil { + engine.logger.Error(err) + continue + } + val = string(bytes) + } else if col.SQLType.IsBlob() { + var bytes []byte + var err error + bytes, err = json.Marshal(fieldValue.Interface()) + if err != nil { + engine.logger.Error(err) + continue + } + val = bytes + } + } else { + engine.autoMapType(fieldValue) + if table, ok := engine.Tables[fieldValue.Type()]; ok { + if len(table.PrimaryKeys) == 1 { + pkField := reflect.Indirect(fieldValue).FieldByName(table.PKColumns()[0].FieldName) + // fix non-int pk issues + //if pkField.Int() != 0 { + if pkField.IsValid() && !isZero(pkField.Interface()) { + val = pkField.Interface() + } else { + continue + } + } else { + //TODO: how to handler? + return nil, fmt.Errorf("not supported %v as %v", fieldValue.Interface(), table.PrimaryKeys) + } + } else { + val = fieldValue.Interface() + } + } + } + case reflect.Array: + continue + case reflect.Slice, reflect.Map: + if fieldValue == reflect.Zero(fieldType) { + continue + } + if fieldValue.IsNil() || !fieldValue.IsValid() || fieldValue.Len() == 0 { + continue + } + + if col.SQLType.IsText() { + bytes, err := json.Marshal(fieldValue.Interface()) + if err != nil { + engine.logger.Error(err) + continue + } + val = string(bytes) + } else if col.SQLType.IsBlob() { + var bytes []byte + var err error + if (fieldType.Kind() == reflect.Array || fieldType.Kind() == reflect.Slice) && + fieldType.Elem().Kind() == reflect.Uint8 { + if fieldValue.Len() > 0 { + val = fieldValue.Bytes() + } else { + continue + } + } else { + bytes, err = json.Marshal(fieldValue.Interface()) + if err != nil { + engine.logger.Error(err) + continue + } + val = bytes + } + } else { + continue + } + default: + val = fieldValue.Interface() + } + + conds = append(conds, builder.Eq{colName: val}) + } + + return builder.And(conds...), nil +} diff --git a/vendor/github.com/go-xorm/xorm/engine_group.go b/vendor/github.com/go-xorm/xorm/engine_group.go new file mode 100644 index 00000000000..1de425f372c --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/engine_group.go @@ -0,0 +1,194 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "github.com/go-xorm/core" +) + +// EngineGroup defines an engine group +type EngineGroup struct { + *Engine + slaves []*Engine + policy GroupPolicy +} + +// NewEngineGroup creates a new engine group +func NewEngineGroup(args1 interface{}, args2 interface{}, policies ...GroupPolicy) (*EngineGroup, error) { + var eg EngineGroup + if len(policies) > 0 { + eg.policy = policies[0] + } else { + eg.policy = RoundRobinPolicy() + } + + driverName, ok1 := args1.(string) + conns, ok2 := args2.([]string) + if ok1 && ok2 { + engines := make([]*Engine, len(conns)) + for i, conn := range conns { + engine, err := NewEngine(driverName, conn) + if err != nil { + return nil, err + } + engine.engineGroup = &eg + engines[i] = engine + } + + eg.Engine = engines[0] + eg.slaves = engines[1:] + return &eg, nil + } + + master, ok3 := args1.(*Engine) + slaves, ok4 := args2.([]*Engine) + if ok3 && ok4 { + master.engineGroup = &eg + for i := 0; i < len(slaves); i++ { + slaves[i].engineGroup = &eg + } + eg.Engine = master + eg.slaves = slaves + return &eg, nil + } + return nil, ErrParamsType +} + +// Close the engine +func (eg *EngineGroup) Close() error { + err := eg.Engine.Close() + if err != nil { + return err + } + + for i := 0; i < len(eg.slaves); i++ { + err := eg.slaves[i].Close() + if err != nil { + return err + } + } + return nil +} + +// Master returns the master engine +func (eg *EngineGroup) Master() *Engine { + return eg.Engine +} + +// Ping tests if database is alive +func (eg *EngineGroup) Ping() error { + if err := eg.Engine.Ping(); err != nil { + return err + } + + for _, slave := range eg.slaves { + if err := slave.Ping(); err != nil { + return err + } + } + return nil +} + +// SetColumnMapper set the column name mapping rule +func (eg *EngineGroup) SetColumnMapper(mapper core.IMapper) { + eg.Engine.ColumnMapper = mapper + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].ColumnMapper = mapper + } +} + +// SetDefaultCacher set the default cacher +func (eg *EngineGroup) SetDefaultCacher(cacher core.Cacher) { + eg.Engine.SetDefaultCacher(cacher) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetDefaultCacher(cacher) + } +} + +// SetLogger set the new logger +func (eg *EngineGroup) SetLogger(logger core.ILogger) { + eg.Engine.SetLogger(logger) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetLogger(logger) + } +} + +// SetLogLevel sets the logger level +func (eg *EngineGroup) SetLogLevel(level core.LogLevel) { + eg.Engine.SetLogLevel(level) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetLogLevel(level) + } +} + +// SetMapper set the name mapping rules +func (eg *EngineGroup) SetMapper(mapper core.IMapper) { + eg.Engine.SetMapper(mapper) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetMapper(mapper) + } +} + +// SetMaxIdleConns set the max idle connections on pool, default is 2 +func (eg *EngineGroup) SetMaxIdleConns(conns int) { + eg.Engine.db.SetMaxIdleConns(conns) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].db.SetMaxIdleConns(conns) + } +} + +// SetMaxOpenConns is only available for go 1.2+ +func (eg *EngineGroup) SetMaxOpenConns(conns int) { + eg.Engine.db.SetMaxOpenConns(conns) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].db.SetMaxOpenConns(conns) + } +} + +// SetPolicy set the group policy +func (eg *EngineGroup) SetPolicy(policy GroupPolicy) *EngineGroup { + eg.policy = policy + return eg +} + +// SetTableMapper set the table name mapping rule +func (eg *EngineGroup) SetTableMapper(mapper core.IMapper) { + eg.Engine.TableMapper = mapper + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].TableMapper = mapper + } +} + +// ShowExecTime show SQL statement and execute time or not on logger if log level is great than INFO +func (eg *EngineGroup) ShowExecTime(show ...bool) { + eg.Engine.ShowExecTime(show...) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].ShowExecTime(show...) + } +} + +// ShowSQL show SQL statement or not on logger if log level is great than INFO +func (eg *EngineGroup) ShowSQL(show ...bool) { + eg.Engine.ShowSQL(show...) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].ShowSQL(show...) + } +} + +// Slave returns one of the physical databases which is a slave according the policy +func (eg *EngineGroup) Slave() *Engine { + switch len(eg.slaves) { + case 0: + return eg.Engine + case 1: + return eg.slaves[0] + } + return eg.policy.Slave(eg) +} + +// Slaves returns all the slaves +func (eg *EngineGroup) Slaves() []*Engine { + return eg.slaves +} diff --git a/vendor/github.com/go-xorm/xorm/engine_group_policy.go b/vendor/github.com/go-xorm/xorm/engine_group_policy.go new file mode 100644 index 00000000000..5b56e8995fd --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/engine_group_policy.go @@ -0,0 +1,116 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "math/rand" + "sync" + "time" +) + +// GroupPolicy is be used by chosing the current slave from slaves +type GroupPolicy interface { + Slave(*EngineGroup) *Engine +} + +// GroupPolicyHandler should be used when a function is a GroupPolicy +type GroupPolicyHandler func(*EngineGroup) *Engine + +// Slave implements the chosen of slaves +func (h GroupPolicyHandler) Slave(eg *EngineGroup) *Engine { + return h(eg) +} + +// RandomPolicy implmentes randomly chose the slave of slaves +func RandomPolicy() GroupPolicyHandler { + var r = rand.New(rand.NewSource(time.Now().UnixNano())) + return func(g *EngineGroup) *Engine { + return g.Slaves()[r.Intn(len(g.Slaves()))] + } +} + +// WeightRandomPolicy implmentes randomly chose the slave of slaves +func WeightRandomPolicy(weights []int) GroupPolicyHandler { + var rands = make([]int, 0, len(weights)) + for i := 0; i < len(weights); i++ { + for n := 0; n < weights[i]; n++ { + rands = append(rands, i) + } + } + var r = rand.New(rand.NewSource(time.Now().UnixNano())) + + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + idx := rands[r.Intn(len(rands))] + if idx >= len(slaves) { + idx = len(slaves) - 1 + } + return slaves[idx] + } +} + +func RoundRobinPolicy() GroupPolicyHandler { + var pos = -1 + var lock sync.Mutex + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + + lock.Lock() + defer lock.Unlock() + pos++ + if pos >= len(slaves) { + pos = 0 + } + + return slaves[pos] + } +} + +func WeightRoundRobinPolicy(weights []int) GroupPolicyHandler { + var rands = make([]int, 0, len(weights)) + for i := 0; i < len(weights); i++ { + for n := 0; n < weights[i]; n++ { + rands = append(rands, i) + } + } + var pos = -1 + var lock sync.Mutex + + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + lock.Lock() + defer lock.Unlock() + pos++ + if pos >= len(rands) { + pos = 0 + } + + idx := rands[pos] + if idx >= len(slaves) { + idx = len(slaves) - 1 + } + return slaves[idx] + } +} + +// LeastConnPolicy implements GroupPolicy, every time will get the least connections slave +func LeastConnPolicy() GroupPolicyHandler { + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + connections := 0 + idx := 0 + for i := 0; i < len(slaves); i++ { + openConnections := slaves[i].DB().Stats().OpenConnections + if i == 0 { + connections = openConnections + idx = i + } else if openConnections <= connections { + connections = openConnections + idx = i + } + } + return slaves[idx] + } +} diff --git a/vendor/github.com/go-xorm/xorm/engine_maxlife.go b/vendor/github.com/go-xorm/xorm/engine_maxlife.go new file mode 100644 index 00000000000..22666c5f44c --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/engine_maxlife.go @@ -0,0 +1,22 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.6 + +package xorm + +import "time" + +// SetConnMaxLifetime sets the maximum amount of time a connection may be reused. +func (engine *Engine) SetConnMaxLifetime(d time.Duration) { + engine.db.SetConnMaxLifetime(d) +} + +// SetConnMaxLifetime sets the maximum amount of time a connection may be reused. +func (eg *EngineGroup) SetConnMaxLifetime(d time.Duration) { + eg.Engine.SetConnMaxLifetime(d) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetConnMaxLifetime(d) + } +} diff --git a/vendor/github.com/go-xorm/xorm/error.go b/vendor/github.com/go-xorm/xorm/error.go index 2a334f47c23..cfeefc31e8e 100644 --- a/vendor/github.com/go-xorm/xorm/error.go +++ b/vendor/github.com/go-xorm/xorm/error.go @@ -23,4 +23,6 @@ var ( ErrNeedDeletedCond = errors.New("Delete need at least one condition") // ErrNotImplemented not implemented ErrNotImplemented = errors.New("Not implemented") + // ErrConditionType condition type unsupported + ErrConditionType = errors.New("Unsupported conditon type") ) diff --git a/vendor/github.com/go-xorm/xorm/helpers.go b/vendor/github.com/go-xorm/xorm/helpers.go index 398ec679fe0..f39ed472560 100644 --- a/vendor/github.com/go-xorm/xorm/helpers.go +++ b/vendor/github.com/go-xorm/xorm/helpers.go @@ -196,25 +196,43 @@ func isArrayValueZero(v reflect.Value) bool { func int64ToIntValue(id int64, tp reflect.Type) reflect.Value { var v interface{} - switch tp.Kind() { - case reflect.Int16: - v = int16(id) - case reflect.Int32: - v = int32(id) - case reflect.Int: - v = int(id) - case reflect.Int64: - v = id - case reflect.Uint16: - v = uint16(id) - case reflect.Uint32: - v = uint32(id) - case reflect.Uint64: - v = uint64(id) - case reflect.Uint: - v = uint(id) + kind := tp.Kind() + + if kind == reflect.Ptr { + kind = tp.Elem().Kind() } - return reflect.ValueOf(v).Convert(tp) + + switch kind { + case reflect.Int16: + temp := int16(id) + v = &temp + case reflect.Int32: + temp := int32(id) + v = &temp + case reflect.Int: + temp := int(id) + v = &temp + case reflect.Int64: + temp := id + v = &temp + case reflect.Uint16: + temp := uint16(id) + v = &temp + case reflect.Uint32: + temp := uint32(id) + v = &temp + case reflect.Uint64: + temp := uint64(id) + v = &temp + case reflect.Uint: + temp := uint(id) + v = &temp + } + + if tp.Kind() == reflect.Ptr { + return reflect.ValueOf(v).Convert(tp) + } + return reflect.ValueOf(v).Elem().Convert(tp) } func int64ToInt(id int64, tp reflect.Type) interface{} { @@ -302,180 +320,6 @@ func sliceEq(left, right []string) bool { return true } -func reflect2value(rawValue *reflect.Value) (str string, err error) { - aa := reflect.TypeOf((*rawValue).Interface()) - vv := reflect.ValueOf((*rawValue).Interface()) - switch aa.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - str = strconv.FormatInt(vv.Int(), 10) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - str = strconv.FormatUint(vv.Uint(), 10) - case reflect.Float32, reflect.Float64: - str = strconv.FormatFloat(vv.Float(), 'f', -1, 64) - case reflect.String: - str = vv.String() - case reflect.Array, reflect.Slice: - switch aa.Elem().Kind() { - case reflect.Uint8: - data := rawValue.Interface().([]byte) - str = string(data) - default: - err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) - } - // time type - case reflect.Struct: - if aa.ConvertibleTo(core.TimeType) { - str = vv.Convert(core.TimeType).Interface().(time.Time).Format(time.RFC3339Nano) - } else { - err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) - } - case reflect.Bool: - str = strconv.FormatBool(vv.Bool()) - case reflect.Complex128, reflect.Complex64: - str = fmt.Sprintf("%v", vv.Complex()) - /* TODO: unsupported types below - case reflect.Map: - case reflect.Ptr: - case reflect.Uintptr: - case reflect.UnsafePointer: - case reflect.Chan, reflect.Func, reflect.Interface: - */ - default: - err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) - } - return -} - -func value2Bytes(rawValue *reflect.Value) (data []byte, err error) { - var str string - str, err = reflect2value(rawValue) - if err != nil { - return - } - data = []byte(str) - return -} - -func value2String(rawValue *reflect.Value) (data string, err error) { - data, err = reflect2value(rawValue) - if err != nil { - return - } - return -} - -func rows2Strings(rows *core.Rows) (resultsSlice []map[string]string, err error) { - fields, err := rows.Columns() - if err != nil { - return nil, err - } - for rows.Next() { - result, err := row2mapStr(rows, fields) - if err != nil { - return nil, err - } - resultsSlice = append(resultsSlice, result) - } - - return resultsSlice, nil -} - -func rows2maps(rows *core.Rows) (resultsSlice []map[string][]byte, err error) { - fields, err := rows.Columns() - if err != nil { - return nil, err - } - for rows.Next() { - result, err := row2map(rows, fields) - if err != nil { - return nil, err - } - resultsSlice = append(resultsSlice, result) - } - - return resultsSlice, nil -} - -func row2map(rows *core.Rows, fields []string) (resultsMap map[string][]byte, err error) { - result := make(map[string][]byte) - scanResultContainers := make([]interface{}, len(fields)) - for i := 0; i < len(fields); i++ { - var scanResultContainer interface{} - scanResultContainers[i] = &scanResultContainer - } - if err := rows.Scan(scanResultContainers...); err != nil { - return nil, err - } - - for ii, key := range fields { - rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) - //if row is null then ignore - if rawValue.Interface() == nil { - //fmt.Println("ignore ...", key, rawValue) - continue - } - - if data, err := value2Bytes(&rawValue); err == nil { - result[key] = data - } else { - return nil, err // !nashtsai! REVIEW, should return err or just error log? - } - } - return result, nil -} - -func row2mapStr(rows *core.Rows, fields []string) (resultsMap map[string]string, err error) { - result := make(map[string]string) - scanResultContainers := make([]interface{}, len(fields)) - for i := 0; i < len(fields); i++ { - var scanResultContainer interface{} - scanResultContainers[i] = &scanResultContainer - } - if err := rows.Scan(scanResultContainers...); err != nil { - return nil, err - } - - for ii, key := range fields { - rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) - //if row is null then ignore - if rawValue.Interface() == nil { - //fmt.Println("ignore ...", key, rawValue) - continue - } - - if data, err := value2String(&rawValue); err == nil { - result[key] = data - } else { - return nil, err // !nashtsai! REVIEW, should return err or just error log? - } - } - return result, nil -} - -func txQuery2(tx *core.Tx, sqlStr string, params ...interface{}) (resultsSlice []map[string]string, err error) { - rows, err := tx.Query(sqlStr, params...) - if err != nil { - return nil, err - } - defer rows.Close() - - return rows2Strings(rows) -} - -func query2(db *core.DB, sqlStr string, params ...interface{}) (resultsSlice []map[string]string, err error) { - s, err := db.Prepare(sqlStr) - if err != nil { - return nil, err - } - defer s.Close() - rows, err := s.Query(params...) - if err != nil { - return nil, err - } - defer rows.Close() - return rows2Strings(rows) -} - func setColumnInt(bean interface{}, col *core.Column, t int64) { v, err := col.ValueOf(bean) if err != nil { @@ -514,7 +358,7 @@ func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, for _, col := range table.Columns() { if useCol && !col.IsVersion && !col.IsCreated && !col.IsUpdated { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); !ok { + if _, ok := getFlagForColumn(session.statement.columnMap, col); !ok { continue } } @@ -542,6 +386,10 @@ func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, if len(fieldValue.String()) == 0 { continue } + case reflect.Ptr: + if fieldValue.Pointer() == 0 { + continue + } } } @@ -549,28 +397,32 @@ func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, continue } - if session.Statement.ColumnStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); !ok { + if session.statement.ColumnStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); !ok { + continue + } else if _, ok := session.statement.incrColumns[col.Name]; ok { + continue + } else if _, ok := session.statement.decrColumns[col.Name]; ok { continue } } - if session.Statement.OmitStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); ok { + if session.statement.OmitStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); ok { continue } } // !evalphobia! set fieldValue as nil when column is nullable and zero-value - if _, ok := getFlagForColumn(session.Statement.nullableMap, col); ok { + if _, ok := getFlagForColumn(session.statement.nullableMap, col); ok { if col.Nullable && isZero(fieldValue.Interface()) { var nilValue *int fieldValue = reflect.ValueOf(nilValue) } } - if (col.IsCreated || col.IsUpdated) && session.Statement.UseAutoTime /*&& isZero(fieldValue.Interface())*/ { + if (col.IsCreated || col.IsUpdated) && session.statement.UseAutoTime /*&& isZero(fieldValue.Interface())*/ { // if time is non-empty, then set to auto time - val, t := session.Engine.NowTime2(col.SQLType.Name) + val, t := session.engine.nowTime(col) args = append(args, val) var colName = col.Name @@ -578,7 +430,7 @@ func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, col := table.GetColumn(colName) setColumnTime(bean, col, t) }) - } else if col.IsVersion && session.Statement.checkVersion { + } else if col.IsVersion && session.statement.checkVersion { args = append(args, 1) } else { arg, err := session.value2Interface(col, fieldValue) @@ -589,7 +441,7 @@ func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, } if includeQuote { - colNames = append(colNames, session.Engine.Quote(col.Name)+" = ?") + colNames = append(colNames, session.engine.Quote(col.Name)+" = ?") } else { colNames = append(colNames, col.Name) } @@ -602,7 +454,6 @@ func indexName(tableName, idxName string) string { } func getFlagForColumn(m map[string]bool, col *core.Column) (val bool, has bool) { - if len(m) == 0 { return false, false } diff --git a/vendor/github.com/go-xorm/xorm/helpler_time.go b/vendor/github.com/go-xorm/xorm/helpler_time.go new file mode 100644 index 00000000000..f4013e27e1a --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/helpler_time.go @@ -0,0 +1,21 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import "time" + +const ( + zeroTime0 = "0000-00-00 00:00:00" + zeroTime1 = "0001-01-01 00:00:00" +) + +func formatTime(t time.Time) string { + return t.Format("2006-01-02 15:04:05") +} + +func isTimeZero(t time.Time) bool { + return t.IsZero() || formatTime(t) == zeroTime0 || + formatTime(t) == zeroTime1 +} diff --git a/vendor/github.com/go-xorm/xorm/interface.go b/vendor/github.com/go-xorm/xorm/interface.go new file mode 100644 index 00000000000..9a3b6da0b2b --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/interface.go @@ -0,0 +1,103 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "database/sql" + "reflect" + "time" + + "github.com/go-xorm/core" +) + +// Interface defines the interface which Engine, EngineGroup and Session will implementate. +type Interface interface { + AllCols() *Session + Alias(alias string) *Session + Asc(colNames ...string) *Session + BufferSize(size int) *Session + Cols(columns ...string) *Session + Count(...interface{}) (int64, error) + CreateIndexes(bean interface{}) error + CreateUniques(bean interface{}) error + Decr(column string, arg ...interface{}) *Session + Desc(...string) *Session + Delete(interface{}) (int64, error) + Distinct(columns ...string) *Session + DropIndexes(bean interface{}) error + Exec(string, ...interface{}) (sql.Result, error) + Exist(bean ...interface{}) (bool, error) + Find(interface{}, ...interface{}) error + Get(interface{}) (bool, error) + GroupBy(keys string) *Session + ID(interface{}) *Session + In(string, ...interface{}) *Session + Incr(column string, arg ...interface{}) *Session + Insert(...interface{}) (int64, error) + InsertOne(interface{}) (int64, error) + IsTableEmpty(bean interface{}) (bool, error) + IsTableExist(beanOrTableName interface{}) (bool, error) + Iterate(interface{}, IterFunc) error + Limit(int, ...int) *Session + NoAutoCondition(...bool) *Session + NotIn(string, ...interface{}) *Session + Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *Session + Omit(columns ...string) *Session + OrderBy(order string) *Session + Ping() error + Query(sqlOrAgrs ...interface{}) (resultsSlice []map[string][]byte, err error) + QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) + QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) + Rows(bean interface{}) (*Rows, error) + SetExpr(string, string) *Session + SQL(interface{}, ...interface{}) *Session + Sum(bean interface{}, colName string) (float64, error) + SumInt(bean interface{}, colName string) (int64, error) + Sums(bean interface{}, colNames ...string) ([]float64, error) + SumsInt(bean interface{}, colNames ...string) ([]int64, error) + Table(tableNameOrBean interface{}) *Session + Unscoped() *Session + Update(bean interface{}, condiBeans ...interface{}) (int64, error) + UseBool(...string) *Session + Where(interface{}, ...interface{}) *Session +} + +// EngineInterface defines the interface which Engine, EngineGroup will implementate. +type EngineInterface interface { + Interface + + Before(func(interface{})) *Session + Charset(charset string) *Session + CreateTables(...interface{}) error + DBMetas() ([]*core.Table, error) + Dialect() core.Dialect + DropTables(...interface{}) error + DumpAllToFile(fp string, tp ...core.DbType) error + GetColumnMapper() core.IMapper + GetDefaultCacher() core.Cacher + GetTableMapper() core.IMapper + GetTZDatabase() *time.Location + GetTZLocation() *time.Location + NewSession() *Session + NoAutoTime() *Session + Quote(string) string + SetDefaultCacher(core.Cacher) + SetLogLevel(core.LogLevel) + SetMapper(core.IMapper) + SetTZDatabase(tz *time.Location) + SetTZLocation(tz *time.Location) + ShowSQL(show ...bool) + Sync(...interface{}) error + Sync2(...interface{}) error + StoreEngine(storeEngine string) *Session + TableInfo(bean interface{}) *Table + UnMapType(reflect.Type) +} + +var ( + _ Interface = &Session{} + _ EngineInterface = &Engine{} + _ EngineInterface = &EngineGroup{} +) diff --git a/vendor/github.com/go-xorm/xorm/processors.go b/vendor/github.com/go-xorm/xorm/processors.go index 77dd30e5785..dcd9c6ac0b4 100644 --- a/vendor/github.com/go-xorm/xorm/processors.go +++ b/vendor/github.com/go-xorm/xorm/processors.go @@ -29,13 +29,6 @@ type AfterSetProcessor interface { AfterSet(string, Cell) } -// !nashtsai! TODO enable BeforeValidateProcessor when xorm start to support validations -//// Executed before an object is validated -//type BeforeValidateProcessor interface { -// BeforeValidate() -//} -// -- - // AfterInsertProcessor executed after an object is persisted to the database type AfterInsertProcessor interface { AfterInsert() @@ -50,3 +43,36 @@ type AfterUpdateProcessor interface { type AfterDeleteProcessor interface { AfterDelete() } + +// AfterLoadProcessor executed after an ojbect has been loaded from database +type AfterLoadProcessor interface { + AfterLoad() +} + +// AfterLoadSessionProcessor executed after an ojbect has been loaded from database with session parameter +type AfterLoadSessionProcessor interface { + AfterLoad(*Session) +} + +type executedProcessorFunc func(*Session, interface{}) error + +type executedProcessor struct { + fun executedProcessorFunc + session *Session + bean interface{} +} + +func (executor *executedProcessor) execute() error { + return executor.fun(executor.session, executor.bean) +} + +func (session *Session) executeProcessors() error { + processors := session.afterProcessors + session.afterProcessors = make([]executedProcessor, 0) + for _, processor := range processors { + if err := processor.execute(); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/go-xorm/xorm/rows.go b/vendor/github.com/go-xorm/xorm/rows.go index a91d08b7791..31e29ae26f6 100644 --- a/vendor/github.com/go-xorm/xorm/rows.go +++ b/vendor/github.com/go-xorm/xorm/rows.go @@ -17,7 +17,6 @@ type Rows struct { NoTypeCheck bool session *Session - stmt *core.Stmt rows *core.Rows fields []string beanType reflect.Type @@ -29,50 +28,33 @@ func newRows(session *Session, bean interface{}) (*Rows, error) { rows.session = session rows.beanType = reflect.Indirect(reflect.ValueOf(bean)).Type() - defer rows.session.resetStatement() - var sqlStr string var args []interface{} + var err error - rows.session.Statement.setRefValue(rValue(bean)) - if len(session.Statement.TableName()) <= 0 { + if err = rows.session.statement.setRefValue(rValue(bean)); err != nil { + return nil, err + } + + if len(session.statement.TableName()) <= 0 { return nil, ErrTableNotFound } - if rows.session.Statement.RawSQL == "" { - sqlStr, args = rows.session.Statement.genGetSQL(bean) - } else { - sqlStr = rows.session.Statement.RawSQL - args = rows.session.Statement.RawParams - } - - for _, filter := range rows.session.Engine.dialect.Filters() { - sqlStr = filter.Do(sqlStr, session.Engine.dialect, rows.session.Statement.RefTable) - } - - rows.session.saveLastSQL(sqlStr, args...) - var err error - if rows.session.prepareStmt { - rows.stmt, err = rows.session.DB().Prepare(sqlStr) + if rows.session.statement.RawSQL == "" { + sqlStr, args, err = rows.session.statement.genGetSQL(bean) if err != nil { - rows.lastError = err - rows.Close() - return nil, err - } - - rows.rows, err = rows.stmt.Query(args...) - if err != nil { - rows.lastError = err - rows.Close() return nil, err } } else { - rows.rows, err = rows.session.DB().Query(sqlStr, args...) - if err != nil { - rows.lastError = err - rows.Close() - return nil, err - } + sqlStr = rows.session.statement.RawSQL + args = rows.session.statement.RawParams + } + + rows.rows, err = rows.session.queryRows(sqlStr, args...) + if err != nil { + rows.lastError = err + rows.Close() + return nil, err } rows.fields, err = rows.rows.Columns() @@ -113,15 +95,26 @@ func (rows *Rows) Scan(bean interface{}) error { } dataStruct := rValue(bean) - rows.session.Statement.setRefValue(dataStruct) - _, err := rows.session.row2Bean(rows.rows, rows.fields, len(rows.fields), bean, &dataStruct, rows.session.Statement.RefTable) + if err := rows.session.statement.setRefValue(dataStruct); err != nil { + return err + } - return err + scanResults, err := rows.session.row2Slice(rows.rows, rows.fields, bean) + if err != nil { + return err + } + + _, err = rows.session.slice2Bean(scanResults, rows.fields, bean, &dataStruct, rows.session.statement.RefTable) + if err != nil { + return err + } + + return rows.session.executeProcessors() } // Close session if session.IsAutoClose is true, and claimed any opened resources func (rows *Rows) Close() error { - if rows.session.IsAutoClose { + if rows.session.isAutoClose { defer rows.session.Close() } @@ -129,17 +122,10 @@ func (rows *Rows) Close() error { if rows.rows != nil { rows.lastError = rows.rows.Close() if rows.lastError != nil { - defer rows.stmt.Close() return rows.lastError } } - if rows.stmt != nil { - rows.lastError = rows.stmt.Close() - } } else { - if rows.stmt != nil { - defer rows.stmt.Close() - } if rows.rows != nil { defer rows.rows.Close() } diff --git a/vendor/github.com/go-xorm/xorm/session.go b/vendor/github.com/go-xorm/xorm/session.go index 0e4cfcf1574..5c6cb5f9def 100644 --- a/vendor/github.com/go-xorm/xorm/session.go +++ b/vendor/github.com/go-xorm/xorm/session.go @@ -11,7 +11,6 @@ import ( "fmt" "hash/crc32" "reflect" - "strconv" "strings" "time" @@ -22,17 +21,16 @@ import ( // kind of database operations. type Session struct { db *core.DB - Engine *Engine - Tx *core.Tx - Statement Statement - IsAutoCommit bool - IsCommitedOrRollbacked bool - TransType string - IsAutoClose bool + engine *Engine + tx *core.Tx + statement Statement + isAutoCommit bool + isCommitedOrRollbacked bool + isAutoClose bool // Automatically reset the statement after operations that execute a SQL // query such as Count(), Find(), Get(), ... - AutoResetStatement bool + autoResetStatement bool // !nashtsai! storing these beans due to yet committed tx afterInsertBeans map[interface{}]*[]func(interface{}) @@ -43,14 +41,17 @@ type Session struct { beforeClosures []func(interface{}) afterClosures []func(interface{}) + afterProcessors []executedProcessor + prepareStmt bool stmtCache map[uint32]*core.Stmt //key: hash.Hash32 of (queryStr, len(queryStr)) - cascadeDeep int // !evalphobia! stored the last executed query on this session //beforeSQLExec func(string, ...interface{}) lastSQL string lastSQLArgs []interface{} + + err error } // Clone copy all the session's content and return a new session @@ -61,12 +62,12 @@ func (session *Session) Clone() *Session { // Init reset the session as the init status. func (session *Session) Init() { - session.Statement.Init() - session.Statement.Engine = session.Engine - session.IsAutoCommit = true - session.IsCommitedOrRollbacked = false - session.IsAutoClose = false - session.AutoResetStatement = true + session.statement.Init() + session.statement.Engine = session.engine + session.isAutoCommit = true + session.isCommitedOrRollbacked = false + session.isAutoClose = false + session.autoResetStatement = true session.prepareStmt = false // !nashtsai! is lazy init better? @@ -75,6 +76,9 @@ func (session *Session) Init() { session.afterDeleteBeans = make(map[interface{}]*[]func(interface{}), 0) session.beforeClosures = make([]func(interface{}), 0) session.afterClosures = make([]func(interface{}), 0) + session.stmtCache = make(map[uint32]*core.Stmt) + + session.afterProcessors = make([]executedProcessor, 0) session.lastSQL = "" session.lastSQLArgs = []interface{}{} @@ -89,19 +93,23 @@ func (session *Session) Close() { if session.db != nil { // When Close be called, if session is a transaction and do not call // Commit or Rollback, then call Rollback. - if session.Tx != nil && !session.IsCommitedOrRollbacked { + if session.tx != nil && !session.isCommitedOrRollbacked { session.Rollback() } - session.Tx = nil + session.tx = nil session.stmtCache = nil - session.Init() session.db = nil } } +// IsClosed returns if session is closed +func (session *Session) IsClosed() bool { + return session.db == nil +} + func (session *Session) resetStatement() { - if session.AutoResetStatement { - session.Statement.Init() + if session.autoResetStatement { + session.statement.Init() } } @@ -129,75 +137,75 @@ func (session *Session) After(closures func(interface{})) *Session { // Table can input a string or pointer to struct for special a table to operate. func (session *Session) Table(tableNameOrBean interface{}) *Session { - session.Statement.Table(tableNameOrBean) + session.statement.Table(tableNameOrBean) return session } // Alias set the table alias func (session *Session) Alias(alias string) *Session { - session.Statement.Alias(alias) + session.statement.Alias(alias) return session } // NoCascade indicate that no cascade load child object func (session *Session) NoCascade() *Session { - session.Statement.UseCascade = false + session.statement.UseCascade = false return session } // ForUpdate Set Read/Write locking for UPDATE func (session *Session) ForUpdate() *Session { - session.Statement.IsForUpdate = true + session.statement.IsForUpdate = true return session } // NoAutoCondition disable generate SQL condition from beans func (session *Session) NoAutoCondition(no ...bool) *Session { - session.Statement.NoAutoCondition(no...) + session.statement.NoAutoCondition(no...) return session } // Limit provide limit and offset query condition func (session *Session) Limit(limit int, start ...int) *Session { - session.Statement.Limit(limit, start...) + session.statement.Limit(limit, start...) return session } // OrderBy provide order by query condition, the input parameter is the content // after order by on a sql statement. func (session *Session) OrderBy(order string) *Session { - session.Statement.OrderBy(order) + session.statement.OrderBy(order) return session } // Desc provide desc order by query condition, the input parameters are columns. func (session *Session) Desc(colNames ...string) *Session { - session.Statement.Desc(colNames...) + session.statement.Desc(colNames...) return session } // Asc provide asc order by query condition, the input parameters are columns. func (session *Session) Asc(colNames ...string) *Session { - session.Statement.Asc(colNames...) + session.statement.Asc(colNames...) return session } // StoreEngine is only avialble mysql dialect currently func (session *Session) StoreEngine(storeEngine string) *Session { - session.Statement.StoreEngine = storeEngine + session.statement.StoreEngine = storeEngine return session } // Charset is only avialble mysql dialect currently func (session *Session) Charset(charset string) *Session { - session.Statement.Charset = charset + session.statement.Charset = charset return session } // Cascade indicates if loading sub Struct func (session *Session) Cascade(trueOrFalse ...bool) *Session { if len(trueOrFalse) >= 1 { - session.Statement.UseCascade = trueOrFalse[0] + session.statement.UseCascade = trueOrFalse[0] } return session } @@ -205,32 +213,32 @@ func (session *Session) Cascade(trueOrFalse ...bool) *Session { // NoCache ask this session do not retrieve data from cache system and // get data from database directly. func (session *Session) NoCache() *Session { - session.Statement.UseCache = false + session.statement.UseCache = false return session } // Join join_operator should be one of INNER, LEFT OUTER, CROSS etc - this will be prepended to JOIN func (session *Session) Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *Session { - session.Statement.Join(joinOperator, tablename, condition, args...) + session.statement.Join(joinOperator, tablename, condition, args...) return session } // GroupBy Generate Group By statement func (session *Session) GroupBy(keys string) *Session { - session.Statement.GroupBy(keys) + session.statement.GroupBy(keys) return session } // Having Generate Having statement func (session *Session) Having(conditions string) *Session { - session.Statement.Having(conditions) + session.statement.Having(conditions) return session } // DB db return the wrapper of sql.DB func (session *Session) DB() *core.DB { if session.db == nil { - session.db = session.Engine.db + session.db = session.engine.db session.stmtCache = make(map[uint32]*core.Stmt, 0) } return session.db @@ -243,25 +251,25 @@ func cleanupProcessorsClosures(slices *[]func(interface{})) { } func (session *Session) canCache() bool { - if session.Statement.RefTable == nil || - session.Statement.JoinStr != "" || - session.Statement.RawSQL != "" || - !session.Statement.UseCache || - session.Statement.IsForUpdate || - session.Tx != nil || - len(session.Statement.selectStr) > 0 { + if session.statement.RefTable == nil || + session.statement.JoinStr != "" || + session.statement.RawSQL != "" || + !session.statement.UseCache || + session.statement.IsForUpdate || + session.tx != nil || + len(session.statement.selectStr) > 0 { return false } return true } -func (session *Session) doPrepare(sqlStr string) (stmt *core.Stmt, err error) { +func (session *Session) doPrepare(db *core.DB, sqlStr string) (stmt *core.Stmt, err error) { crc := crc32.ChecksumIEEE([]byte(sqlStr)) // TODO try hash(sqlStr+len(sqlStr)) var has bool stmt, has = session.stmtCache[crc] if !has { - stmt, err = session.DB().Prepare(sqlStr) + stmt, err = db.Prepare(sqlStr) if err != nil { return nil, err } @@ -273,18 +281,18 @@ func (session *Session) doPrepare(sqlStr string) (stmt *core.Stmt, err error) { func (session *Session) getField(dataStruct *reflect.Value, key string, table *core.Table, idx int) *reflect.Value { var col *core.Column if col = table.GetColumnIdx(key, idx); col == nil { - //session.Engine.logger.Warnf("table %v has no column %v. %v", table.Name, key, table.ColumnsSeq()) + //session.engine.logger.Warnf("table %v has no column %v. %v", table.Name, key, table.ColumnsSeq()) return nil } fieldValue, err := col.ValueOfV(dataStruct) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return nil } if !fieldValue.IsValid() || !fieldValue.CanSet() { - session.Engine.logger.Warnf("table %v's column %v is not valid or cannot set", table.Name, key) + session.engine.logger.Warnf("table %v's column %v is not valid or cannot set", table.Name, key) return nil } return fieldValue @@ -293,28 +301,40 @@ func (session *Session) getField(dataStruct *reflect.Value, key string, table *c // Cell cell is a result of one column field type Cell *interface{} -func (session *Session) rows2Beans(rows *core.Rows, fields []string, fieldsCount int, +func (session *Session) rows2Beans(rows *core.Rows, fields []string, table *core.Table, newElemFunc func([]string) reflect.Value, sliceValueSetFunc func(*reflect.Value, core.PK) error) error { for rows.Next() { var newValue = newElemFunc(fields) bean := newValue.Interface() - dataStruct := rValue(bean) - pk, err := session.row2Bean(rows, fields, fieldsCount, bean, &dataStruct, table) - if err != nil { - return err - } + dataStruct := newValue.Elem() - err = sliceValueSetFunc(&newValue, pk) + // handle beforeClosures + scanResults, err := session.row2Slice(rows, fields, bean) if err != nil { return err } + pk, err := session.slice2Bean(scanResults, fields, bean, &dataStruct, table) + if err != nil { + return err + } + session.afterProcessors = append(session.afterProcessors, executedProcessor{ + fun: func(*Session, interface{}) error { + return sliceValueSetFunc(&newValue, pk) + }, + session: session, + bean: bean, + }) } return nil } -func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount int, bean interface{}, dataStruct *reflect.Value, table *core.Table) (core.PK, error) { - scanResults := make([]interface{}, fieldsCount) +func (session *Session) row2Slice(rows *core.Rows, fields []string, bean interface{}) ([]interface{}, error) { + for _, closure := range session.beforeClosures { + closure(bean) + } + + scanResults := make([]interface{}, len(fields)) for i := 0; i < len(fields); i++ { var cell interface{} scanResults[i] = &cell @@ -328,7 +348,10 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i b.BeforeSet(key, Cell(scanResults[ii].(*interface{}))) } } + return scanResults, nil +} +func (session *Session) slice2Bean(scanResults []interface{}, fields []string, bean interface{}, dataStruct *reflect.Value, table *core.Table) (core.PK, error) { defer func() { if b, hasAfterSet := bean.(AfterSetProcessor); hasAfterSet { for ii, key := range fields { @@ -337,6 +360,40 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i } }() + // handle afterClosures + for _, closure := range session.afterClosures { + session.afterProcessors = append(session.afterProcessors, executedProcessor{ + fun: func(sess *Session, bean interface{}) error { + closure(bean) + return nil + }, + session: session, + bean: bean, + }) + } + + if a, has := bean.(AfterLoadProcessor); has { + session.afterProcessors = append(session.afterProcessors, executedProcessor{ + fun: func(sess *Session, bean interface{}) error { + a.AfterLoad() + return nil + }, + session: session, + bean: bean, + }) + } + + if a, has := bean.(AfterLoadSessionProcessor); has { + session.afterProcessors = append(session.afterProcessors, executedProcessor{ + fun: func(sess *Session, bean interface{}) error { + a.AfterLoad(sess) + return nil + }, + session: session, + bean: bean, + }) + } + var tempMap = make(map[string]int) var pk core.PK for ii, key := range fields { @@ -361,9 +418,11 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if fieldValue.CanAddr() { if structConvert, ok := fieldValue.Addr().Interface().(core.Conversion); ok { if data, err := value2Bytes(&rawValue); err == nil { - structConvert.FromDB(data) + if err := structConvert.FromDB(data); err != nil { + return nil, err + } } else { - session.Engine.logger.Error(err) + return nil, err } continue } @@ -376,7 +435,7 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i } fieldValue.Interface().(core.Conversion).FromDB(data) } else { - session.Engine.logger.Error(err) + return nil, err } continue } @@ -403,17 +462,19 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i hasAssigned = true if len(bs) > 0 { + if fieldType.Kind() == reflect.String { + fieldValue.SetString(string(bs)) + continue + } if fieldValue.CanAddr() { err := json.Unmarshal(bs, fieldValue.Addr().Interface()) if err != nil { - session.Engine.logger.Error(key, err) return nil, err } } else { x := reflect.New(fieldType) err := json.Unmarshal(bs, x.Interface()) if err != nil { - session.Engine.logger.Error(key, err) return nil, err } fieldValue.Set(x.Elem()) @@ -438,14 +499,12 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if fieldValue.CanAddr() { err := json.Unmarshal(bs, fieldValue.Addr().Interface()) if err != nil { - session.Engine.logger.Error(err) return nil, err } } else { x := reflect.New(fieldType) err := json.Unmarshal(bs, x.Interface()) if err != nil { - session.Engine.logger.Error(err) return nil, err } fieldValue.Set(x.Elem()) @@ -462,14 +521,19 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i x := reflect.New(fieldType) err := json.Unmarshal(vv.Bytes(), x.Interface()) if err != nil { - session.Engine.logger.Error(err) return nil, err } fieldValue.Set(x.Elem()) } else { - for i := 0; i < fieldValue.Len(); i++ { - if i < vv.Len() { - fieldValue.Index(i).Set(vv.Index(i)) + if fieldValue.Len() > 0 { + for i := 0; i < fieldValue.Len(); i++ { + if i < vv.Len() { + fieldValue.Index(i).Set(vv.Index(i)) + } + } + } else { + for i := 0; i < vv.Len(); i++ { + fieldValue.Set(reflect.Append(*fieldValue, vv.Index(i))) } } } @@ -509,57 +573,38 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i } case reflect.Struct: if fieldType.ConvertibleTo(core.TimeType) { + dbTZ := session.engine.DatabaseTZ + if col.TimeZone != nil { + dbTZ = col.TimeZone + } + if rawValueType == core.TimeType { hasAssigned = true t := vv.Convert(core.TimeType).Interface().(time.Time) z, _ := t.Zone() - dbTZ := session.Engine.DatabaseTZ - if dbTZ == nil { - if session.Engine.dialect.DBType() == core.SQLITE { - dbTZ = time.UTC - } else { - dbTZ = time.Local - } - } - // set new location if database don't save timezone or give an incorrect timezone if len(z) == 0 || t.Year() == 0 || t.Location().String() != dbTZ.String() { // !nashtsai! HACK tmp work around for lib/pq doesn't properly time with location - session.Engine.logger.Debugf("empty zone key[%v] : %v | zone: %v | location: %+v\n", key, t, z, *t.Location()) + session.engine.logger.Debugf("empty zone key[%v] : %v | zone: %v | location: %+v\n", key, t, z, *t.Location()) t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), dbTZ) } - // !nashtsai! convert to engine location - if col.TimeZone == nil { - t = t.In(session.Engine.TZLocation) - } else { - t = t.In(col.TimeZone) - } + t = t.In(session.engine.TZLocation) fieldValue.Set(reflect.ValueOf(t).Convert(fieldType)) - - // t = fieldValue.Interface().(time.Time) - // z, _ = t.Zone() - // session.Engine.LogDebug("fieldValue key[%v]: %v | zone: %v | location: %+v\n", key, t, z, *t.Location()) } else if rawValueType == core.IntType || rawValueType == core.Int64Type || rawValueType == core.Int32Type { hasAssigned = true - var tz *time.Location - if col.TimeZone == nil { - tz = session.Engine.TZLocation - } else { - tz = col.TimeZone - } - t := time.Unix(vv.Int(), 0).In(tz) - //vv = reflect.ValueOf(t) + + t := time.Unix(vv.Int(), 0).In(session.engine.TZLocation) fieldValue.Set(reflect.ValueOf(t).Convert(fieldType)) } else { if d, ok := vv.Interface().([]uint8); ok { hasAssigned = true t, err := session.byte2Time(col, d) if err != nil { - session.Engine.logger.Error("byte2Time error:", err.Error()) + session.engine.logger.Error("byte2Time error:", err.Error()) hasAssigned = false } else { fieldValue.Set(reflect.ValueOf(t).Convert(fieldType)) @@ -568,20 +613,20 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i hasAssigned = true t, err := session.str2Time(col, d) if err != nil { - session.Engine.logger.Error("byte2Time error:", err.Error()) + session.engine.logger.Error("byte2Time error:", err.Error()) hasAssigned = false } else { fieldValue.Set(reflect.ValueOf(t).Convert(fieldType)) } } else { - panic(fmt.Sprintf("rawValueType is %v, value is %v", rawValueType, vv.Interface())) + return nil, fmt.Errorf("rawValueType is %v, value is %v", rawValueType, vv.Interface()) } } } else if nulVal, ok := fieldValue.Addr().Interface().(sql.Scanner); ok { // !! 增加支持sql.Scanner接口的结构,如sql.NullString hasAssigned = true if err := nulVal.Scan(vv.Interface()); err != nil { - session.Engine.logger.Error("sql.Sanner error:", err.Error()) + session.engine.logger.Error("sql.Sanner error:", err.Error()) hasAssigned = false } } else if col.SQLType.IsJson() { @@ -591,7 +636,6 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if len([]byte(vv.String())) > 0 { err := json.Unmarshal([]byte(vv.String()), x.Interface()) if err != nil { - session.Engine.logger.Error(err) return nil, err } fieldValue.Set(x.Elem()) @@ -602,76 +646,45 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if len(vv.Bytes()) > 0 { err := json.Unmarshal(vv.Bytes(), x.Interface()) if err != nil { - session.Engine.logger.Error(err) return nil, err } fieldValue.Set(x.Elem()) } } - } else if session.Statement.UseCascade { - table := session.Engine.autoMapType(*fieldValue) - if table != nil { - hasAssigned = true - if len(table.PrimaryKeys) != 1 { - panic("unsupported non or composited primary key cascade") - } - var pk = make(core.PK, len(table.PrimaryKeys)) + } else if session.statement.UseCascade { + table, err := session.engine.autoMapType(*fieldValue) + if err != nil { + return nil, err + } - switch rawValueType.Kind() { - case reflect.Int64: - pk[0] = vv.Int() - case reflect.Int: - pk[0] = int(vv.Int()) - case reflect.Int32: - pk[0] = int32(vv.Int()) - case reflect.Int16: - pk[0] = int16(vv.Int()) - case reflect.Int8: - pk[0] = int8(vv.Int()) - case reflect.Uint64: - pk[0] = vv.Uint() - case reflect.Uint: - pk[0] = uint(vv.Uint()) - case reflect.Uint32: - pk[0] = uint32(vv.Uint()) - case reflect.Uint16: - pk[0] = uint16(vv.Uint()) - case reflect.Uint8: - pk[0] = uint8(vv.Uint()) - case reflect.String: - pk[0] = vv.String() - case reflect.Slice: - pk[0], _ = strconv.ParseInt(string(rawValue.Interface().([]byte)), 10, 64) - default: - panic(fmt.Sprintf("unsupported primary key type: %v, %v", rawValueType, fieldValue)) - } + hasAssigned = true + if len(table.PrimaryKeys) != 1 { + return nil, errors.New("unsupported non or composited primary key cascade") + } + var pk = make(core.PK, len(table.PrimaryKeys)) + pk[0], err = asKind(vv, rawValueType) + if err != nil { + return nil, err + } - if !isPKZero(pk) { - // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch - // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne - // property to be fetched lazily - structInter := reflect.New(fieldValue.Type()) - newsession := session.Engine.NewSession() - defer newsession.Close() - has, err := newsession.Id(pk).NoCascade().Get(structInter.Interface()) - if err != nil { - return nil, err - } - if has { - //v := structInter.Elem().Interface() - //fieldValue.Set(reflect.ValueOf(v)) - fieldValue.Set(structInter.Elem()) - } else { - return nil, errors.New("cascade obj is not exist") - } + if !isPKZero(pk) { + // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch + // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne + // property to be fetched lazily + structInter := reflect.New(fieldValue.Type()) + has, err := session.ID(pk).NoCascade().get(structInter.Interface()) + if err != nil { + return nil, err + } + if has { + fieldValue.Set(structInter.Elem()) + } else { + return nil, errors.New("cascade obj is not exist") } - } else { - session.Engine.logger.Error("unsupported struct type in Scan: ", fieldValue.Type().String()) } } case reflect.Ptr: // !nashtsai! TODO merge duplicated codes above - //typeStr := fieldType.String() switch fieldType { // following types case matching ptr's native type, therefore assign ptr directly case core.PtrStringType: @@ -769,10 +782,9 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if len([]byte(vv.String())) > 0 { err := json.Unmarshal([]byte(vv.String()), &x) if err != nil { - session.Engine.logger.Error(err) - } else { - fieldValue.Set(reflect.ValueOf(&x)) + return nil, err } + fieldValue.Set(reflect.ValueOf(&x)) } hasAssigned = true case core.Complex128Type: @@ -780,24 +792,23 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if len([]byte(vv.String())) > 0 { err := json.Unmarshal([]byte(vv.String()), &x) if err != nil { - session.Engine.logger.Error(err) - } else { - fieldValue.Set(reflect.ValueOf(&x)) + return nil, err } + fieldValue.Set(reflect.ValueOf(&x)) } hasAssigned = true } // switch fieldType - // default: - // session.Engine.LogError("unsupported type in Scan: ", reflect.TypeOf(v).String()) } // switch fieldType.Kind() // !nashtsai! for value can't be assigned directly fallback to convert to []byte then back to value if !hasAssigned { data, err := value2Bytes(&rawValue) - if err == nil { - session.bytes2Value(col, fieldValue, data) - } else { - session.Engine.logger.Error(err.Error()) + if err != nil { + return nil, err + } + + if err = session.bytes2Value(col, fieldValue, data); err != nil { + return nil, err } } } @@ -805,19 +816,11 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i return pk, nil } -func (session *Session) queryPreprocess(sqlStr *string, paramStr ...interface{}) { - for _, filter := range session.Engine.dialect.Filters() { - *sqlStr = filter.Do(*sqlStr, session.Engine.dialect, session.Statement.RefTable) - } - - session.saveLastSQL(*sqlStr, paramStr...) -} - // saveLastSQL stores executed query information func (session *Session) saveLastSQL(sql string, args ...interface{}) { session.lastSQL = sql session.lastSQLArgs = args - session.Engine.logSQL(sql, args...) + session.engine.logSQL(sql, args...) } // LastSQL returns last query information @@ -827,8 +830,8 @@ func (session *Session) LastSQL() (string, []interface{}) { // tbName get some table's table name func (session *Session) tbNameNoSchema(table *core.Table) string { - if len(session.Statement.AltTableName) > 0 { - return session.Statement.AltTableName + if len(session.statement.AltTableName) > 0 { + return session.statement.AltTableName } return table.Name @@ -836,6 +839,6 @@ func (session *Session) tbNameNoSchema(table *core.Table) string { // Unscoped always disable struct tag "deleted" func (session *Session) Unscoped() *Session { - session.Statement.Unscoped() + session.statement.Unscoped() return session } diff --git a/vendor/github.com/go-xorm/xorm/session_cols.go b/vendor/github.com/go-xorm/xorm/session_cols.go index 91185defc8c..9972cb0ae4b 100644 --- a/vendor/github.com/go-xorm/xorm/session_cols.go +++ b/vendor/github.com/go-xorm/xorm/session_cols.go @@ -6,43 +6,43 @@ package xorm // Incr provides a query string like "count = count + 1" func (session *Session) Incr(column string, arg ...interface{}) *Session { - session.Statement.Incr(column, arg...) + session.statement.Incr(column, arg...) return session } // Decr provides a query string like "count = count - 1" func (session *Session) Decr(column string, arg ...interface{}) *Session { - session.Statement.Decr(column, arg...) + session.statement.Decr(column, arg...) return session } // SetExpr provides a query string like "column = {expression}" func (session *Session) SetExpr(column string, expression string) *Session { - session.Statement.SetExpr(column, expression) + session.statement.SetExpr(column, expression) return session } // Select provides some columns to special func (session *Session) Select(str string) *Session { - session.Statement.Select(str) + session.statement.Select(str) return session } // Cols provides some columns to special func (session *Session) Cols(columns ...string) *Session { - session.Statement.Cols(columns...) + session.statement.Cols(columns...) return session } // AllCols ask all columns func (session *Session) AllCols() *Session { - session.Statement.AllCols() + session.statement.AllCols() return session } // MustCols specify some columns must use even if they are empty func (session *Session) MustCols(columns ...string) *Session { - session.Statement.MustCols(columns...) + session.statement.MustCols(columns...) return session } @@ -52,7 +52,7 @@ func (session *Session) MustCols(columns ...string) *Session { // If no parameters, it will use all the bool field of struct, or // it will use parameters's columns func (session *Session) UseBool(columns ...string) *Session { - session.Statement.UseBool(columns...) + session.statement.UseBool(columns...) return session } @@ -60,25 +60,25 @@ func (session *Session) UseBool(columns ...string) *Session { // distinct will not be cached because cache system need id, // but distinct will not provide id func (session *Session) Distinct(columns ...string) *Session { - session.Statement.Distinct(columns...) + session.statement.Distinct(columns...) return session } // Omit Only not use the parameters as select or update columns func (session *Session) Omit(columns ...string) *Session { - session.Statement.Omit(columns...) + session.statement.Omit(columns...) return session } // Nullable Set null when column is zero-value and nullable for update func (session *Session) Nullable(columns ...string) *Session { - session.Statement.Nullable(columns...) + session.statement.Nullable(columns...) return session } // NoAutoTime means do not automatically give created field and updated field // the current time on the current session temporarily func (session *Session) NoAutoTime() *Session { - session.Statement.UseAutoTime = false + session.statement.UseAutoTime = false return session } diff --git a/vendor/github.com/go-xorm/xorm/session_cond.go b/vendor/github.com/go-xorm/xorm/session_cond.go index 948a90bc1fc..e1d528f2dbd 100644 --- a/vendor/github.com/go-xorm/xorm/session_cond.go +++ b/vendor/github.com/go-xorm/xorm/session_cond.go @@ -17,25 +17,25 @@ func (session *Session) Sql(query string, args ...interface{}) *Session { // SQL provides raw sql input parameter. When you have a complex SQL statement // and cannot use Where, Id, In and etc. Methods to describe, you can use SQL. func (session *Session) SQL(query interface{}, args ...interface{}) *Session { - session.Statement.SQL(query, args...) + session.statement.SQL(query, args...) return session } // Where provides custom query condition. func (session *Session) Where(query interface{}, args ...interface{}) *Session { - session.Statement.Where(query, args...) + session.statement.Where(query, args...) return session } // And provides custom query condition. func (session *Session) And(query interface{}, args ...interface{}) *Session { - session.Statement.And(query, args...) + session.statement.And(query, args...) return session } // Or provides custom query condition. func (session *Session) Or(query interface{}, args ...interface{}) *Session { - session.Statement.Or(query, args...) + session.statement.Or(query, args...) return session } @@ -48,23 +48,23 @@ func (session *Session) Id(id interface{}) *Session { // ID provides converting id as a query condition func (session *Session) ID(id interface{}) *Session { - session.Statement.ID(id) + session.statement.ID(id) return session } // In provides a query string like "id in (1, 2, 3)" func (session *Session) In(column string, args ...interface{}) *Session { - session.Statement.In(column, args...) + session.statement.In(column, args...) return session } // NotIn provides a query string like "id in (1, 2, 3)" func (session *Session) NotIn(column string, args ...interface{}) *Session { - session.Statement.NotIn(column, args...) + session.statement.NotIn(column, args...) return session } -// Conds returns session query conditions +// Conds returns session query conditions except auto bean conditions func (session *Session) Conds() builder.Cond { - return session.Statement.cond + return session.statement.cond } diff --git a/vendor/github.com/go-xorm/xorm/session_convert.go b/vendor/github.com/go-xorm/xorm/session_convert.go index 36ab465f5ee..1f9d8aa1bd0 100644 --- a/vendor/github.com/go-xorm/xorm/session_convert.go +++ b/vendor/github.com/go-xorm/xorm/session_convert.go @@ -23,41 +23,38 @@ func (session *Session) str2Time(col *core.Column, data string) (outTime time.Ti var x time.Time var err error - if sdata == "0000-00-00 00:00:00" || - sdata == "0001-01-01 00:00:00" { + var parseLoc = session.engine.DatabaseTZ + if col.TimeZone != nil { + parseLoc = col.TimeZone + } + + if sdata == zeroTime0 || sdata == zeroTime1 { } else if !strings.ContainsAny(sdata, "- :") { // !nashtsai! has only found that mymysql driver is using this for time type column // time stamp sd, err := strconv.ParseInt(sdata, 10, 64) if err == nil { x = time.Unix(sd, 0) - // !nashtsai! HACK mymysql driver is causing Local location being change to CHAT and cause wrong time conversion - if col.TimeZone == nil { - x = x.In(session.Engine.TZLocation) - } else { - x = x.In(col.TimeZone) - } - session.Engine.logger.Debugf("time(0) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + //session.engine.logger.Debugf("time(0) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else { - session.Engine.logger.Debugf("time(0) err key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + //session.engine.logger.Debugf("time(0) err key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } } else if len(sdata) > 19 && strings.Contains(sdata, "-") { - x, err = time.ParseInLocation(time.RFC3339Nano, sdata, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(1) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation(time.RFC3339Nano, sdata, parseLoc) + session.engine.logger.Debugf("time(1) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) if err != nil { - x, err = time.ParseInLocation("2006-01-02 15:04:05.999999999", sdata, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(2) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation("2006-01-02 15:04:05.999999999", sdata, parseLoc) + //session.engine.logger.Debugf("time(2) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } if err != nil { - x, err = time.ParseInLocation("2006-01-02 15:04:05.9999999 Z07:00", sdata, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(3) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation("2006-01-02 15:04:05.9999999 Z07:00", sdata, parseLoc) + //session.engine.logger.Debugf("time(3) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } - } else if len(sdata) == 19 && strings.Contains(sdata, "-") { - x, err = time.ParseInLocation("2006-01-02 15:04:05", sdata, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(4) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation("2006-01-02 15:04:05", sdata, parseLoc) + //session.engine.logger.Debugf("time(4) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else if len(sdata) == 10 && sdata[4] == '-' && sdata[7] == '-' { - x, err = time.ParseInLocation("2006-01-02", sdata, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(5) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation("2006-01-02", sdata, parseLoc) + //session.engine.logger.Debugf("time(5) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else if col.SQLType.Name == core.Time { if strings.Contains(sdata, " ") { ssd := strings.Split(sdata, " ") @@ -65,13 +62,13 @@ func (session *Session) str2Time(col *core.Column, data string) (outTime time.Ti } sdata = strings.TrimSpace(sdata) - if session.Engine.dialect.DBType() == core.MYSQL && len(sdata) > 8 { + if session.engine.dialect.DBType() == core.MYSQL && len(sdata) > 8 { sdata = sdata[len(sdata)-8:] } st := fmt.Sprintf("2006-01-02 %v", sdata) - x, err = time.ParseInLocation("2006-01-02 15:04:05", st, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(6) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation("2006-01-02 15:04:05", st, parseLoc) + //session.engine.logger.Debugf("time(6) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else { outErr = fmt.Errorf("unsupported time format %v", sdata) return @@ -80,7 +77,7 @@ func (session *Session) str2Time(col *core.Column, data string) (outTime time.Ti outErr = fmt.Errorf("unsupported time format %v: %v", sdata, err) return } - outTime = x + outTime = x.In(session.engine.TZLocation) return } @@ -108,7 +105,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, if len(data) > 0 { err := json.Unmarshal(data, x.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return err } fieldValue.Set(x.Elem()) @@ -122,7 +119,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, if len(data) > 0 { err := json.Unmarshal(data, x.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return err } fieldValue.Set(x.Elem()) @@ -135,7 +132,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, if len(data) > 0 { err := json.Unmarshal(data, x.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return err } fieldValue.Set(x.Elem()) @@ -147,8 +144,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, case reflect.String: fieldValue.SetString(string(data)) case reflect.Bool: - d := string(data) - v, err := strconv.ParseBool(d) + v, err := asBool(data) if err != nil { return fmt.Errorf("arg %v as bool: %s", key, err.Error()) } @@ -159,7 +155,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - session.Engine.dialect.DBType() == core.MYSQL { // !nashtsai! TODO dialect needs to provide conversion interface API + session.engine.dialect.DBType() == core.MYSQL { // !nashtsai! TODO dialect needs to provide conversion interface API if len(data) == 1 { x = int64(data[0]) } else { @@ -207,41 +203,39 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, } v = x fieldValue.Set(reflect.ValueOf(v).Convert(fieldType)) - } else if session.Statement.UseCascade { - table := session.Engine.autoMapType(*fieldValue) - if table != nil { - // TODO: current only support 1 primary key - if len(table.PrimaryKeys) > 1 { - panic("unsupported composited primary key cascade") - } - var pk = make(core.PK, len(table.PrimaryKeys)) - rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) - var err error - pk[0], err = str2PK(string(data), rawValueType) + } else if session.statement.UseCascade { + table, err := session.engine.autoMapType(*fieldValue) + if err != nil { + return err + } + + // TODO: current only support 1 primary key + if len(table.PrimaryKeys) > 1 { + return errors.New("unsupported composited primary key cascade") + } + + var pk = make(core.PK, len(table.PrimaryKeys)) + rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) + pk[0], err = str2PK(string(data), rawValueType) + if err != nil { + return err + } + + if !isPKZero(pk) { + // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch + // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne + // property to be fetched lazily + structInter := reflect.New(fieldValue.Type()) + has, err := session.ID(pk).NoCascade().get(structInter.Interface()) if err != nil { return err } - - if !isPKZero(pk) { - // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch - // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne - // property to be fetched lazily - structInter := reflect.New(fieldValue.Type()) - newsession := session.Engine.NewSession() - defer newsession.Close() - has, err := newsession.Id(pk).NoCascade().Get(structInter.Interface()) - if err != nil { - return err - } - if has { - v = structInter.Elem().Interface() - fieldValue.Set(reflect.ValueOf(v)) - } else { - return errors.New("cascade obj is not exist") - } + if has { + v = structInter.Elem().Interface() + fieldValue.Set(reflect.ValueOf(v)) + } else { + return errors.New("cascade obj is not exist") } - } else { - return fmt.Errorf("unsupported struct type in Scan: %s", fieldValue.Type().String()) } } } @@ -267,7 +261,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, if len(data) > 0 { err := json.Unmarshal(data, &x) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return err } fieldValue.Set(reflect.ValueOf(&x).Convert(fieldType)) @@ -278,7 +272,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, if len(data) > 0 { err := json.Unmarshal(data, &x) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return err } fieldValue.Set(reflect.ValueOf(&x).Convert(fieldType)) @@ -350,7 +344,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - strings.Contains(session.Engine.DriverName(), "mysql") { + strings.Contains(session.engine.DriverName(), "mysql") { if len(data) == 1 { x = int64(data[0]) } else { @@ -375,7 +369,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - strings.Contains(session.Engine.DriverName(), "mysql") { + strings.Contains(session.engine.DriverName(), "mysql") { if len(data) == 1 { x = int(data[0]) } else { @@ -403,7 +397,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - session.Engine.dialect.DBType() == core.MYSQL { + session.engine.dialect.DBType() == core.MYSQL { if len(data) == 1 { x = int32(data[0]) } else { @@ -431,7 +425,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - strings.Contains(session.Engine.DriverName(), "mysql") { + strings.Contains(session.engine.DriverName(), "mysql") { if len(data) == 1 { x = int8(data[0]) } else { @@ -459,7 +453,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - strings.Contains(session.Engine.DriverName(), "mysql") { + strings.Contains(session.engine.DriverName(), "mysql") { if len(data) == 1 { x = int16(data[0]) } else { @@ -491,37 +485,37 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, v = x fieldValue.Set(reflect.ValueOf(&x)) default: - if session.Statement.UseCascade { + if session.statement.UseCascade { structInter := reflect.New(fieldType.Elem()) - table := session.Engine.autoMapType(structInter.Elem()) - if table != nil { - if len(table.PrimaryKeys) > 1 { - panic("unsupported composited primary key cascade") - } - var pk = make(core.PK, len(table.PrimaryKeys)) - var err error - rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) - pk[0], err = str2PK(string(data), rawValueType) + table, err := session.engine.autoMapType(structInter.Elem()) + if err != nil { + return err + } + + if len(table.PrimaryKeys) > 1 { + return errors.New("unsupported composited primary key cascade") + } + + var pk = make(core.PK, len(table.PrimaryKeys)) + rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) + pk[0], err = str2PK(string(data), rawValueType) + if err != nil { + return err + } + + if !isPKZero(pk) { + // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch + // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne + // property to be fetched lazily + has, err := session.ID(pk).NoCascade().get(structInter.Interface()) if err != nil { return err } - - if !isPKZero(pk) { - // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch - // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne - // property to be fetched lazily - newsession := session.Engine.NewSession() - defer newsession.Close() - has, err := newsession.Id(pk).NoCascade().Get(structInter.Interface()) - if err != nil { - return err - } - if has { - v = structInter.Interface() - fieldValue.Set(reflect.ValueOf(v)) - } else { - return errors.New("cascade obj is not exist") - } + if has { + v = structInter.Interface() + fieldValue.Set(reflect.ValueOf(v)) + } else { + return errors.New("cascade obj is not exist") } } } else { @@ -570,7 +564,7 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val if fieldValue.IsNil() { return nil, nil } else if !fieldValue.IsValid() { - session.Engine.logger.Warn("the field[", col.FieldName, "] is invalid") + session.engine.logger.Warn("the field[", col.FieldName, "] is invalid") return nil, nil } else { // !nashtsai! deference pointer type to instance type @@ -588,12 +582,7 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val case reflect.Struct: if fieldType.ConvertibleTo(core.TimeType) { t := fieldValue.Convert(core.TimeType).Interface().(time.Time) - if session.Engine.dialect.DBType() == core.MSSQL { - if t.IsZero() { - return nil, nil - } - } - tf := session.Engine.FormatTime(col.SQLType.Name, t) + tf := session.engine.formatColTime(col, t) return tf, nil } @@ -603,7 +592,10 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val return v.Value() } - fieldTable := session.Engine.autoMapType(fieldValue) + fieldTable, err := session.engine.autoMapType(fieldValue) + if err != nil { + return nil, err + } if len(fieldTable.PrimaryKeys) == 1 { pkField := reflect.Indirect(fieldValue).FieldByName(fieldTable.PKColumns()[0].FieldName) return pkField.Interface(), nil @@ -614,14 +606,14 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val if col.SQLType.IsText() { bytes, err := json.Marshal(fieldValue.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return 0, err } return string(bytes), nil } else if col.SQLType.IsBlob() { bytes, err := json.Marshal(fieldValue.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return 0, err } return bytes, nil @@ -630,7 +622,7 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val case reflect.Complex64, reflect.Complex128: bytes, err := json.Marshal(fieldValue.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return 0, err } return string(bytes), nil @@ -642,7 +634,7 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val if col.SQLType.IsText() { bytes, err := json.Marshal(fieldValue.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return 0, err } return string(bytes), nil @@ -655,7 +647,7 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val } else { bytes, err = json.Marshal(fieldValue.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return 0, err } } diff --git a/vendor/github.com/go-xorm/xorm/session_delete.go b/vendor/github.com/go-xorm/xorm/session_delete.go index 1c458fe1ea4..688b122ca6d 100644 --- a/vendor/github.com/go-xorm/xorm/session_delete.go +++ b/vendor/github.com/go-xorm/xorm/session_delete.go @@ -12,26 +12,26 @@ import ( "github.com/go-xorm/core" ) -func (session *Session) cacheDelete(sqlStr string, args ...interface{}) error { - if session.Statement.RefTable == nil || - session.Tx != nil { +func (session *Session) cacheDelete(table *core.Table, tableName, sqlStr string, args ...interface{}) error { + if table == nil || + session.tx != nil { return ErrCacheFailed } - for _, filter := range session.Engine.dialect.Filters() { - sqlStr = filter.Do(sqlStr, session.Engine.dialect, session.Statement.RefTable) + for _, filter := range session.engine.dialect.Filters() { + sqlStr = filter.Do(sqlStr, session.engine.dialect, table) } - newsql := session.Statement.convertIDSQL(sqlStr) + newsql := session.statement.convertIDSQL(sqlStr) if newsql == "" { return ErrCacheFailed } - cacher := session.Engine.getCacher2(session.Statement.RefTable) - tableName := session.Statement.TableName() + cacher := session.engine.getCacher2(table) + pkColumns := table.PKColumns() ids, err := core.GetCacheSql(cacher, tableName, newsql, args) if err != nil { - resultsSlice, err := session.query(newsql, args...) + resultsSlice, err := session.queryBytes(newsql, args...) if err != nil { return err } @@ -40,7 +40,7 @@ func (session *Session) cacheDelete(sqlStr string, args ...interface{}) error { for _, data := range resultsSlice { var id int64 var pk core.PK = make([]interface{}, 0) - for _, col := range session.Statement.RefTable.PKColumns() { + for _, col := range pkColumns { if v, ok := data[col.Name]; !ok { return errors.New("no id") } else if col.SQLType.IsText() { @@ -58,33 +58,30 @@ func (session *Session) cacheDelete(sqlStr string, args ...interface{}) error { ids = append(ids, pk) } } - } /*else { - session.Engine.LogDebug("delete cache sql %v", newsql) - cacher.DelIds(tableName, genSqlKey(newsql, args)) - }*/ + } for _, id := range ids { - session.Engine.logger.Debug("[cacheDelete] delete cache obj", tableName, id) + session.engine.logger.Debug("[cacheDelete] delete cache obj:", tableName, id) sid, err := id.ToString() if err != nil { return err } cacher.DelBean(tableName, sid) } - session.Engine.logger.Debug("[cacheDelete] clear cache sql", tableName) + session.engine.logger.Debug("[cacheDelete] clear cache table:", tableName) cacher.ClearIds(tableName) return nil } // Delete records, bean's non-empty fields are conditions func (session *Session) Delete(bean interface{}) (int64, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } - session.Statement.setRefValue(rValue(bean)) - var table = session.Statement.RefTable + if err := session.statement.setRefValue(rValue(bean)); err != nil { + return 0, err + } // handle before delete processors for _, closure := range session.beforeClosures { @@ -96,13 +93,17 @@ func (session *Session) Delete(bean interface{}) (int64, error) { processor.BeforeDelete() } - // -- - condSQL, condArgs, _ := session.Statement.genConds(bean) - if len(condSQL) == 0 && session.Statement.LimitN == 0 { + condSQL, condArgs, err := session.statement.genConds(bean) + if err != nil { + return 0, err + } + if len(condSQL) == 0 && session.statement.LimitN == 0 { return 0, ErrNeedDeletedCond } - var tableName = session.Engine.Quote(session.Statement.TableName()) + var tableNameNoQuote = session.statement.TableName() + var tableName = session.engine.Quote(tableNameNoQuote) + var table = session.statement.RefTable var deleteSQL string if len(condSQL) > 0 { deleteSQL = fmt.Sprintf("DELETE FROM %v WHERE %v", tableName, condSQL) @@ -111,15 +112,15 @@ func (session *Session) Delete(bean interface{}) (int64, error) { } var orderSQL string - if len(session.Statement.OrderStr) > 0 { - orderSQL += fmt.Sprintf(" ORDER BY %s", session.Statement.OrderStr) + if len(session.statement.OrderStr) > 0 { + orderSQL += fmt.Sprintf(" ORDER BY %s", session.statement.OrderStr) } - if session.Statement.LimitN > 0 { - orderSQL += fmt.Sprintf(" LIMIT %d", session.Statement.LimitN) + if session.statement.LimitN > 0 { + orderSQL += fmt.Sprintf(" LIMIT %d", session.statement.LimitN) } if len(orderSQL) > 0 { - switch session.Engine.dialect.DBType() { + switch session.engine.dialect.DBType() { case core.POSTGRES: inSQL := fmt.Sprintf("ctid IN (SELECT ctid FROM %s%s)", tableName, orderSQL) if len(condSQL) > 0 { @@ -144,7 +145,7 @@ func (session *Session) Delete(bean interface{}) (int64, error) { var realSQL string argsForCache := make([]interface{}, 0, len(condArgs)*2) - if session.Statement.unscoped || table.DeletedColumn() == nil { // tag "deleted" is disabled + if session.statement.unscoped || table.DeletedColumn() == nil { // tag "deleted" is disabled realSQL = deleteSQL copy(argsForCache, condArgs) argsForCache = append(condArgs, argsForCache...) @@ -155,12 +156,12 @@ func (session *Session) Delete(bean interface{}) (int64, error) { deletedColumn := table.DeletedColumn() realSQL = fmt.Sprintf("UPDATE %v SET %v = ? WHERE %v", - session.Engine.Quote(session.Statement.TableName()), - session.Engine.Quote(deletedColumn.Name), + session.engine.Quote(session.statement.TableName()), + session.engine.Quote(deletedColumn.Name), condSQL) if len(orderSQL) > 0 { - switch session.Engine.dialect.DBType() { + switch session.engine.dialect.DBType() { case core.POSTGRES: inSQL := fmt.Sprintf("ctid IN (SELECT ctid FROM %s%s)", tableName, orderSQL) if len(condSQL) > 0 { @@ -183,12 +184,12 @@ func (session *Session) Delete(bean interface{}) (int64, error) { } } - // !oinume! Insert NowTime to the head of session.Statement.Params + // !oinume! Insert nowTime to the head of session.statement.Params condArgs = append(condArgs, "") paramsLen := len(condArgs) copy(condArgs[1:paramsLen], condArgs[0:paramsLen-1]) - val, t := session.Engine.NowTime2(deletedColumn.SQLType.Name) + val, t := session.engine.nowTime(deletedColumn) condArgs[0] = val var colName = deletedColumn.Name @@ -198,17 +199,18 @@ func (session *Session) Delete(bean interface{}) (int64, error) { }) } - if cacher := session.Engine.getCacher2(session.Statement.RefTable); cacher != nil && session.Statement.UseCache { - session.cacheDelete(deleteSQL, argsForCache...) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + session.cacheDelete(table, tableNameNoQuote, deleteSQL, argsForCache...) } + session.statement.RefTable = table res, err := session.exec(realSQL, condArgs...) if err != nil { return 0, err } // handle after delete processors - if session.IsAutoCommit { + if session.isAutoCommit { for _, closure := range session.afterClosures { closure(bean) } diff --git a/vendor/github.com/go-xorm/xorm/session_exist.go b/vendor/github.com/go-xorm/xorm/session_exist.go new file mode 100644 index 00000000000..049c1ddff14 --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/session_exist.go @@ -0,0 +1,77 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "errors" + "fmt" + "reflect" + + "github.com/go-xorm/builder" +) + +// Exist returns true if the record exist otherwise return false +func (session *Session) Exist(bean ...interface{}) (bool, error) { + if session.isAutoClose { + defer session.Close() + } + + var sqlStr string + var args []interface{} + var err error + + if session.statement.RawSQL == "" { + if len(bean) == 0 { + tableName := session.statement.TableName() + if len(tableName) <= 0 { + return false, ErrTableNotFound + } + + if session.statement.cond.IsValid() { + condSQL, condArgs, err := builder.ToSQL(session.statement.cond) + if err != nil { + return false, err + } + + sqlStr = fmt.Sprintf("SELECT * FROM %s WHERE %s LIMIT 1", tableName, condSQL) + args = condArgs + } else { + sqlStr = fmt.Sprintf("SELECT * FROM %s LIMIT 1", tableName) + args = []interface{}{} + } + } else { + beanValue := reflect.ValueOf(bean[0]) + if beanValue.Kind() != reflect.Ptr { + return false, errors.New("needs a pointer") + } + + if beanValue.Elem().Kind() == reflect.Struct { + if err := session.statement.setRefValue(beanValue.Elem()); err != nil { + return false, err + } + } + + if len(session.statement.TableName()) <= 0 { + return false, ErrTableNotFound + } + session.statement.Limit(1) + sqlStr, args, err = session.statement.genGetSQL(bean[0]) + if err != nil { + return false, err + } + } + } else { + sqlStr = session.statement.RawSQL + args = session.statement.RawParams + } + + rows, err := session.queryRows(sqlStr, args...) + if err != nil { + return false, err + } + defer rows.Close() + + return rows.Next(), nil +} diff --git a/vendor/github.com/go-xorm/xorm/session_find.go b/vendor/github.com/go-xorm/xorm/session_find.go index 748e319f793..f95dcfef2cb 100644 --- a/vendor/github.com/go-xorm/xorm/session_find.go +++ b/vendor/github.com/go-xorm/xorm/session_find.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "reflect" - "strconv" "strings" "github.com/go-xorm/builder" @@ -24,11 +23,13 @@ const ( // are conditions. beans could be []Struct, []*Struct, map[int64]Struct // map[int64]*Struct func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) error { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } + return session.find(rowsSlicePtr, condiBean...) +} +func (session *Session) find(rowsSlicePtr interface{}, condiBean ...interface{}) error { sliceValue := reflect.Indirect(reflect.ValueOf(rowsSlicePtr)) if sliceValue.Kind() != reflect.Slice && sliceValue.Kind() != reflect.Map { return errors.New("needs a pointer to a slice or a map") @@ -37,77 +38,79 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) sliceElementType := sliceValue.Type().Elem() var tp = tpStruct - if session.Statement.RefTable == nil { + if session.statement.RefTable == nil { if sliceElementType.Kind() == reflect.Ptr { if sliceElementType.Elem().Kind() == reflect.Struct { pv := reflect.New(sliceElementType.Elem()) - session.Statement.setRefValue(pv.Elem()) + if err := session.statement.setRefValue(pv.Elem()); err != nil { + return err + } } else { tp = tpNonStruct } } else if sliceElementType.Kind() == reflect.Struct { pv := reflect.New(sliceElementType) - session.Statement.setRefValue(pv.Elem()) + if err := session.statement.setRefValue(pv.Elem()); err != nil { + return err + } } else { tp = tpNonStruct } } - var table = session.Statement.RefTable + var table = session.statement.RefTable - var addedTableName = (len(session.Statement.JoinStr) > 0) + var addedTableName = (len(session.statement.JoinStr) > 0) var autoCond builder.Cond if tp == tpStruct { - if !session.Statement.noAutoCondition && len(condiBean) > 0 { + if !session.statement.noAutoCondition && len(condiBean) > 0 { var err error - autoCond, err = session.Statement.buildConds(table, condiBean[0], true, true, false, true, addedTableName) + autoCond, err = session.statement.buildConds(table, condiBean[0], true, true, false, true, addedTableName) if err != nil { - panic(err) + return err } } else { // !oinume! Add " IS NULL" to WHERE whatever condiBean is given. // See https://github.com/go-xorm/xorm/issues/179 - if col := table.DeletedColumn(); col != nil && !session.Statement.unscoped { // tag "deleted" is enabled - var colName = session.Engine.Quote(col.Name) + if col := table.DeletedColumn(); col != nil && !session.statement.unscoped { // tag "deleted" is enabled + var colName = session.engine.Quote(col.Name) if addedTableName { - var nm = session.Statement.TableName() - if len(session.Statement.TableAlias) > 0 { - nm = session.Statement.TableAlias + var nm = session.statement.TableName() + if len(session.statement.TableAlias) > 0 { + nm = session.statement.TableAlias } - colName = session.Engine.Quote(nm) + "." + colName - } - if session.Engine.dialect.DBType() == core.MSSQL { - autoCond = builder.IsNull{colName} - } else { - autoCond = builder.IsNull{colName}.Or(builder.Eq{colName: "0001-01-01 00:00:00"}) + colName = session.engine.Quote(nm) + "." + colName } + + autoCond = session.engine.CondDeleted(colName) } } } var sqlStr string var args []interface{} - if session.Statement.RawSQL == "" { - if len(session.Statement.TableName()) <= 0 { + var err error + if session.statement.RawSQL == "" { + if len(session.statement.TableName()) <= 0 { return ErrTableNotFound } - var columnStr = session.Statement.ColumnStr - if len(session.Statement.selectStr) > 0 { - columnStr = session.Statement.selectStr + var columnStr = session.statement.ColumnStr + if len(session.statement.selectStr) > 0 { + columnStr = session.statement.selectStr } else { - if session.Statement.JoinStr == "" { + if session.statement.JoinStr == "" { if columnStr == "" { - if session.Statement.GroupByStr != "" { - columnStr = session.Statement.Engine.Quote(strings.Replace(session.Statement.GroupByStr, ",", session.Engine.Quote(","), -1)) + if session.statement.GroupByStr != "" { + columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) } else { - columnStr = session.Statement.genColumnStr() + columnStr = session.statement.genColumnStr() } } } else { if columnStr == "" { - if session.Statement.GroupByStr != "" { - columnStr = session.Statement.Engine.Quote(strings.Replace(session.Statement.GroupByStr, ",", session.Engine.Quote(","), -1)) + if session.statement.GroupByStr != "" { + columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) } else { columnStr = "*" } @@ -118,31 +121,37 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) } } - condSQL, condArgs, _ := builder.ToSQL(session.Statement.cond.And(autoCond)) + session.statement.cond = session.statement.cond.And(autoCond) + condSQL, condArgs, err := builder.ToSQL(session.statement.cond) + if err != nil { + return err + } - args = append(session.Statement.joinArgs, condArgs...) - sqlStr = session.Statement.genSelectSQL(columnStr, condSQL) + args = append(session.statement.joinArgs, condArgs...) + sqlStr, err = session.statement.genSelectSQL(columnStr, condSQL) + if err != nil { + return err + } // for mssql and use limit qs := strings.Count(sqlStr, "?") if len(args)*2 == qs { args = append(args, args...) } } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams + sqlStr = session.statement.RawSQL + args = session.statement.RawParams } - var err error if session.canCache() { - if cacher := session.Engine.getCacher2(table); cacher != nil && - !session.Statement.IsDistinct && - !session.Statement.unscoped { + if cacher := session.engine.getCacher2(table); cacher != nil && + !session.statement.IsDistinct && + !session.statement.unscoped { err = session.cacheFind(sliceElementType, sqlStr, rowsSlicePtr, args...) if err != ErrCacheFailed { return err } err = nil // !nashtsai! reset err to nil for ErrCacheFailed - session.Engine.logger.Warn("Cache Find Failed") + session.engine.logger.Warn("Cache Find Failed") } } @@ -150,21 +159,13 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) } func (session *Session) noCacheFind(table *core.Table, containerValue reflect.Value, sqlStr string, args ...interface{}) error { - var rawRows *core.Rows - var err error - - session.queryPreprocess(&sqlStr, args...) - if session.IsAutoCommit { - _, rawRows, err = session.innerQuery(sqlStr, args...) - } else { - rawRows, err = session.Tx.Query(sqlStr, args...) - } + rows, err := session.queryRows(sqlStr, args...) if err != nil { return err } - defer rawRows.Close() + defer rows.Close() - fields, err := rawRows.Columns() + fields, err := rows.Columns() if err != nil { return err } @@ -234,20 +235,29 @@ func (session *Session) noCacheFind(table *core.Table, containerValue reflect.Va if elemType.Kind() == reflect.Struct { var newValue = newElemFunc(fields) dataStruct := rValue(newValue.Interface()) - return session.rows2Beans(rawRows, fields, len(fields), session.Engine.autoMapType(dataStruct), newElemFunc, containerValueSetFunc) + tb, err := session.engine.autoMapType(dataStruct) + if err != nil { + return err + } + err = session.rows2Beans(rows, fields, tb, newElemFunc, containerValueSetFunc) + rows.Close() + if err != nil { + return err + } + return session.executeProcessors() } - for rawRows.Next() { + for rows.Next() { var newValue = newElemFunc(fields) bean := newValue.Interface() switch elemType.Kind() { case reflect.Slice: - err = rawRows.ScanSlice(bean) + err = rows.ScanSlice(bean) case reflect.Map: - err = rawRows.ScanMap(bean) + err = rows.ScanMap(bean) default: - err = rawRows.Scan(bean) + err = rows.Scan(bean) } if err != nil { @@ -278,22 +288,21 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in return ErrCacheFailed } - for _, filter := range session.Engine.dialect.Filters() { - sqlStr = filter.Do(sqlStr, session.Engine.dialect, session.Statement.RefTable) + for _, filter := range session.engine.dialect.Filters() { + sqlStr = filter.Do(sqlStr, session.engine.dialect, session.statement.RefTable) } - newsql := session.Statement.convertIDSQL(sqlStr) + newsql := session.statement.convertIDSQL(sqlStr) if newsql == "" { return ErrCacheFailed } - tableName := session.Statement.TableName() - - table := session.Statement.RefTable - cacher := session.Engine.getCacher2(table) + tableName := session.statement.TableName() + table := session.statement.RefTable + cacher := session.engine.getCacher2(table) ids, err := core.GetCacheSql(cacher, tableName, newsql, args) if err != nil { - rows, err := session.DB().Query(newsql, args...) + rows, err := session.queryRows(newsql, args...) if err != nil { return err } @@ -304,7 +313,7 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in for rows.Next() { i++ if i > 500 { - session.Engine.logger.Debug("[cacheFind] ids length > 500, no cache") + session.engine.logger.Debug("[cacheFind] ids length > 500, no cache") return ErrCacheFailed } var res = make([]string, len(table.PrimaryKeys)) @@ -312,32 +321,24 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in if err != nil { return err } - var pk core.PK = make([]interface{}, len(table.PrimaryKeys)) for i, col := range table.PKColumns() { - if col.SQLType.IsNumeric() { - n, err := strconv.ParseInt(res[i], 10, 64) - if err != nil { - return err - } - pk[i] = n - } else if col.SQLType.IsText() { - pk[i] = res[i] - } else { - return errors.New("not supported") + pk[i], err = session.engine.idTypeAssertion(col, res[i]) + if err != nil { + return err } } ids = append(ids, pk) } - session.Engine.logger.Debug("[cacheFind] cache sql:", ids, tableName, newsql, args) + session.engine.logger.Debug("[cacheFind] cache sql:", ids, tableName, sqlStr, newsql, args) err = core.PutCacheSql(cacher, ids, tableName, newsql, args) if err != nil { return err } } else { - session.Engine.logger.Debug("[cacheFind] cache hit sql:", newsql, args) + session.engine.logger.Debug("[cacheFind] cache hit sql:", tableName, sqlStr, newsql, args) } sliceValue := reflect.Indirect(reflect.ValueOf(rowsSlicePtr)) @@ -352,20 +353,20 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in return err } bean := cacher.GetBean(tableName, sid) - if bean == nil { + if bean == nil || reflect.ValueOf(bean).Elem().Type() != t { ides = append(ides, id) ididxes[sid] = idx } else { - session.Engine.logger.Debug("[cacheFind] cache hit bean:", tableName, id, bean) + session.engine.logger.Debug("[cacheFind] cache hit bean:", tableName, id, bean) - pk := session.Engine.IdOf(bean) + pk := session.engine.IdOf(bean) xid, err := pk.ToString() if err != nil { return err } if sid != xid { - session.Engine.logger.Error("[cacheFind] error cache", xid, sid, bean) + session.engine.logger.Error("[cacheFind] error cache", xid, sid, bean) return ErrCacheFailed } temps[idx] = bean @@ -373,9 +374,6 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in } if len(ides) > 0 { - newSession := session.Engine.NewSession() - defer newSession.Close() - slices := reflect.New(reflect.SliceOf(t)) beans := slices.Interface() @@ -385,18 +383,18 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in ff = append(ff, ie[0]) } - newSession.In("`"+table.PrimaryKeys[0]+"`", ff...) + session.In("`"+table.PrimaryKeys[0]+"`", ff...) } else { for _, ie := range ides { cond := builder.NewCond() for i, name := range table.PrimaryKeys { cond = cond.And(builder.Eq{"`" + name + "`": ie[i]}) } - newSession.Or(cond) + session.Or(cond) } } - err = newSession.NoCache().Find(beans) + err = session.NoCache().Table(tableName).find(beans) if err != nil { return err } @@ -407,7 +405,10 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in if rv.Kind() != reflect.Ptr { rv = rv.Addr() } - id := session.Engine.IdOfV(rv) + id, err := session.engine.idOfV(rv) + if err != nil { + return err + } sid, err := id.ToString() if err != nil { return err @@ -415,7 +416,7 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in bean := rv.Interface() temps[ididxes[sid]] = bean - session.Engine.logger.Debug("[cacheFind] cache bean:", tableName, id, bean, temps) + session.engine.logger.Debug("[cacheFind] cache bean:", tableName, id, bean, temps) cacher.PutBean(tableName, sid, bean) } } @@ -423,7 +424,7 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in for j := 0; j < len(temps); j++ { bean := temps[j] if bean == nil { - session.Engine.logger.Warn("[cacheFind] cache no hit:", tableName, ids[j], temps) + session.engine.logger.Warn("[cacheFind] cache no hit:", tableName, ids[j], temps) // return errors.New("cache error") // !nashtsai! no need to return error, but continue instead continue } diff --git a/vendor/github.com/go-xorm/xorm/session_get.go b/vendor/github.com/go-xorm/xorm/session_get.go index 0c78ed94835..8faf53c02c7 100644 --- a/vendor/github.com/go-xorm/xorm/session_get.go +++ b/vendor/github.com/go-xorm/xorm/session_get.go @@ -15,42 +15,49 @@ import ( // Get retrieve one record from database, bean's non-empty fields // will be as conditions func (session *Session) Get(bean interface{}) (bool, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } + return session.get(bean) +} +func (session *Session) get(bean interface{}) (bool, error) { beanValue := reflect.ValueOf(bean) if beanValue.Kind() != reflect.Ptr { - return false, errors.New("needs a pointer to a struct") - } - - // FIXME: remove this after support non-struct Get - if beanValue.Elem().Kind() != reflect.Struct { - return false, errors.New("needs a pointer to a struct") + return false, errors.New("needs a pointer to a value") + } else if beanValue.Elem().Kind() == reflect.Ptr { + return false, errors.New("a pointer to a pointer is not allowed") } if beanValue.Elem().Kind() == reflect.Struct { - session.Statement.setRefValue(beanValue.Elem()) + if err := session.statement.setRefValue(beanValue.Elem()); err != nil { + return false, err + } } var sqlStr string var args []interface{} + var err error - if session.Statement.RawSQL == "" { - if len(session.Statement.TableName()) <= 0 { + if session.statement.RawSQL == "" { + if len(session.statement.TableName()) <= 0 { return false, ErrTableNotFound } - session.Statement.Limit(1) - sqlStr, args = session.Statement.genGetSQL(bean) + session.statement.Limit(1) + sqlStr, args, err = session.statement.genGetSQL(bean) + if err != nil { + return false, err + } } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams + sqlStr = session.statement.RawSQL + args = session.statement.RawParams } - if session.canCache() { - if cacher := session.Engine.getCacher2(session.Statement.RefTable); cacher != nil && - !session.Statement.unscoped { + table := session.statement.RefTable + + if session.canCache() && beanValue.Elem().Kind() == reflect.Struct { + if cacher := session.engine.getCacher2(table); cacher != nil && + !session.statement.unscoped { has, err := session.cacheGet(bean, sqlStr, args...) if err != ErrCacheFailed { return has, err @@ -58,47 +65,51 @@ func (session *Session) Get(bean interface{}) (bool, error) { } } - return session.nocacheGet(beanValue.Elem().Kind(), bean, sqlStr, args...) + return session.nocacheGet(beanValue.Elem().Kind(), table, bean, sqlStr, args...) } -func (session *Session) nocacheGet(beanKind reflect.Kind, bean interface{}, sqlStr string, args ...interface{}) (bool, error) { - var rawRows *core.Rows - var err error - session.queryPreprocess(&sqlStr, args...) - if session.IsAutoCommit { - _, rawRows, err = session.innerQuery(sqlStr, args...) - } else { - rawRows, err = session.Tx.Query(sqlStr, args...) - } +func (session *Session) nocacheGet(beanKind reflect.Kind, table *core.Table, bean interface{}, sqlStr string, args ...interface{}) (bool, error) { + rows, err := session.queryRows(sqlStr, args...) if err != nil { return false, err } + defer rows.Close() - defer rawRows.Close() + if !rows.Next() { + return false, nil + } - if rawRows.Next() { - fields, err := rawRows.Columns() + switch beanKind { + case reflect.Struct: + fields, err := rows.Columns() if err != nil { - // WARN: Alougth rawRows return true, but get fields failed + // WARN: Alougth rows return true, but get fields failed return true, err } - switch beanKind { - case reflect.Struct: - dataStruct := rValue(bean) - session.Statement.setRefValue(dataStruct) - _, err = session.row2Bean(rawRows, fields, len(fields), bean, &dataStruct, session.Statement.RefTable) - case reflect.Slice: - err = rawRows.ScanSlice(bean) - case reflect.Map: - err = rawRows.ScanMap(bean) - default: - err = rawRows.Scan(bean) + scanResults, err := session.row2Slice(rows, fields, bean) + if err != nil { + return false, err + } + // close it before covert data + rows.Close() + + dataStruct := rValue(bean) + _, err = session.slice2Bean(scanResults, fields, bean, &dataStruct, table) + if err != nil { + return true, err } - return true, err + return true, session.executeProcessors() + case reflect.Slice: + err = rows.ScanSlice(bean) + case reflect.Map: + err = rows.ScanMap(bean) + default: + err = rows.Scan(bean) } - return false, nil + + return true, err } func (session *Session) cacheGet(bean interface{}, sqlStr string, args ...interface{}) (has bool, err error) { @@ -107,22 +118,22 @@ func (session *Session) cacheGet(bean interface{}, sqlStr string, args ...interf return false, ErrCacheFailed } - for _, filter := range session.Engine.dialect.Filters() { - sqlStr = filter.Do(sqlStr, session.Engine.dialect, session.Statement.RefTable) + for _, filter := range session.engine.dialect.Filters() { + sqlStr = filter.Do(sqlStr, session.engine.dialect, session.statement.RefTable) } - newsql := session.Statement.convertIDSQL(sqlStr) + newsql := session.statement.convertIDSQL(sqlStr) if newsql == "" { return false, ErrCacheFailed } - cacher := session.Engine.getCacher2(session.Statement.RefTable) - tableName := session.Statement.TableName() - session.Engine.logger.Debug("[cacheGet] find sql:", newsql, args) + cacher := session.engine.getCacher2(session.statement.RefTable) + tableName := session.statement.TableName() + session.engine.logger.Debug("[cacheGet] find sql:", newsql, args) + table := session.statement.RefTable ids, err := core.GetCacheSql(cacher, tableName, newsql, args) - table := session.Statement.RefTable if err != nil { var res = make([]string, len(table.PrimaryKeys)) - rows, err := session.DB().Query(newsql, args...) + rows, err := session.NoCache().queryRows(newsql, args...) if err != nil { return false, err } @@ -153,19 +164,19 @@ func (session *Session) cacheGet(bean interface{}, sqlStr string, args ...interf } ids = []core.PK{pk} - session.Engine.logger.Debug("[cacheGet] cache ids:", newsql, ids) + session.engine.logger.Debug("[cacheGet] cache ids:", newsql, ids) err = core.PutCacheSql(cacher, ids, tableName, newsql, args) if err != nil { return false, err } } else { - session.Engine.logger.Debug("[cacheGet] cache hit sql:", newsql) + session.engine.logger.Debug("[cacheGet] cache hit sql:", newsql, ids) } if len(ids) > 0 { structValue := reflect.Indirect(reflect.ValueOf(bean)) id := ids[0] - session.Engine.logger.Debug("[cacheGet] get bean:", tableName, id) + session.engine.logger.Debug("[cacheGet] get bean:", tableName, id) sid, err := id.ToString() if err != nil { return false, err @@ -173,15 +184,15 @@ func (session *Session) cacheGet(bean interface{}, sqlStr string, args ...interf cacheBean := cacher.GetBean(tableName, sid) if cacheBean == nil { cacheBean = bean - has, err = session.nocacheGet(reflect.Struct, cacheBean, sqlStr, args...) + has, err = session.nocacheGet(reflect.Struct, table, cacheBean, sqlStr, args...) if err != nil || !has { return has, err } - session.Engine.logger.Debug("[cacheGet] cache bean:", tableName, id, cacheBean) + session.engine.logger.Debug("[cacheGet] cache bean:", tableName, id, cacheBean) cacher.PutBean(tableName, sid, cacheBean) } else { - session.Engine.logger.Debug("[cacheGet] cache hit bean:", tableName, id, cacheBean) + session.engine.logger.Debug("[cacheGet] cache hit bean:", tableName, id, cacheBean) has = true } structValue.Set(reflect.Indirect(reflect.ValueOf(cacheBean))) diff --git a/vendor/github.com/go-xorm/xorm/session_insert.go b/vendor/github.com/go-xorm/xorm/session_insert.go index 5b607b1fecb..129ee23098a 100644 --- a/vendor/github.com/go-xorm/xorm/session_insert.go +++ b/vendor/github.com/go-xorm/xorm/session_insert.go @@ -19,17 +19,16 @@ func (session *Session) Insert(beans ...interface{}) (int64, error) { var affected int64 var err error - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } - defer session.resetStatement() for _, bean := range beans { sliceValue := reflect.Indirect(reflect.ValueOf(bean)) if sliceValue.Kind() == reflect.Slice { size := sliceValue.Len() if size > 0 { - if session.Engine.SupportInsertMany() { + if session.engine.SupportInsertMany() { cnt, err := session.innerInsertMulti(bean) if err != nil { return affected, err @@ -67,13 +66,15 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error return 0, errors.New("could not insert a empty slice") } - session.Statement.setRefValue(sliceValue.Index(0)) + if err := session.statement.setRefValue(reflect.ValueOf(sliceValue.Index(0).Interface())); err != nil { + return 0, err + } - if len(session.Statement.TableName()) <= 0 { + if len(session.statement.TableName()) <= 0 { return 0, ErrTableNotFound } - table := session.Statement.RefTable + table := session.statement.RefTable size := sliceValue.Len() var colNames []string @@ -114,18 +115,18 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error if col.IsDeleted { continue } - if session.Statement.ColumnStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); !ok { + if session.statement.ColumnStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); !ok { continue } } - if session.Statement.OmitStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); ok { + if session.statement.OmitStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); ok { continue } } - if (col.IsCreated || col.IsUpdated) && session.Statement.UseAutoTime { - val, t := session.Engine.NowTime2(col.SQLType.Name) + if (col.IsCreated || col.IsUpdated) && session.statement.UseAutoTime { + val, t := session.engine.nowTime(col) args = append(args, val) var colName = col.Name @@ -133,7 +134,7 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error col := table.GetColumn(colName) setColumnTime(bean, col, t) }) - } else if col.IsVersion && session.Statement.checkVersion { + } else if col.IsVersion && session.statement.checkVersion { args = append(args, 1) var colName = col.Name session.afterClosures = append(session.afterClosures, func(bean interface{}) { @@ -169,18 +170,18 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error if col.IsDeleted { continue } - if session.Statement.ColumnStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); !ok { + if session.statement.ColumnStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); !ok { continue } } - if session.Statement.OmitStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); ok { + if session.statement.OmitStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); ok { continue } } - if (col.IsCreated || col.IsUpdated) && session.Statement.UseAutoTime { - val, t := session.Engine.NowTime2(col.SQLType.Name) + if (col.IsCreated || col.IsUpdated) && session.statement.UseAutoTime { + val, t := session.engine.nowTime(col) args = append(args, val) var colName = col.Name @@ -188,7 +189,7 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error col := table.GetColumn(colName) setColumnTime(bean, col, t) }) - } else if col.IsVersion && session.Statement.checkVersion { + } else if col.IsVersion && session.statement.checkVersion { args = append(args, 1) var colName = col.Name session.afterClosures = append(session.afterClosures, func(bean interface{}) { @@ -212,25 +213,26 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error var sql = "INSERT INTO %s (%v%v%v) VALUES (%v)" var statement string - if session.Engine.dialect.DBType() == core.ORACLE { + var tableName = session.statement.TableName() + if session.engine.dialect.DBType() == core.ORACLE { sql = "INSERT ALL INTO %s (%v%v%v) VALUES (%v) SELECT 1 FROM DUAL" temp := fmt.Sprintf(") INTO %s (%v%v%v) VALUES (", - session.Engine.Quote(session.Statement.TableName()), - session.Engine.QuoteStr(), - strings.Join(colNames, session.Engine.QuoteStr() + ", " + session.Engine.QuoteStr()), - session.Engine.QuoteStr()) + session.engine.Quote(tableName), + session.engine.QuoteStr(), + strings.Join(colNames, session.engine.QuoteStr()+", "+session.engine.QuoteStr()), + session.engine.QuoteStr()) statement = fmt.Sprintf(sql, - session.Engine.Quote(session.Statement.TableName()), - session.Engine.QuoteStr(), - strings.Join(colNames, session.Engine.QuoteStr() + ", " + session.Engine.QuoteStr()), - session.Engine.QuoteStr(), + session.engine.Quote(tableName), + session.engine.QuoteStr(), + strings.Join(colNames, session.engine.QuoteStr()+", "+session.engine.QuoteStr()), + session.engine.QuoteStr(), strings.Join(colMultiPlaces, temp)) } else { statement = fmt.Sprintf(sql, - session.Engine.Quote(session.Statement.TableName()), - session.Engine.QuoteStr(), - strings.Join(colNames, session.Engine.QuoteStr() + ", " + session.Engine.QuoteStr()), - session.Engine.QuoteStr(), + session.engine.Quote(tableName), + session.engine.QuoteStr(), + strings.Join(colNames, session.engine.QuoteStr()+", "+session.engine.QuoteStr()), + session.engine.QuoteStr(), strings.Join(colMultiPlaces, "),(")) } res, err := session.exec(statement, args...) @@ -238,8 +240,8 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error return 0, err } - if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache { - session.cacheInsert(session.Statement.TableName()) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + session.cacheInsert(table, tableName) } lenAfterClosures := len(session.afterClosures) @@ -247,7 +249,7 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error elemValue := reflect.Indirect(sliceValue.Index(i)).Addr().Interface() // handle AfterInsertProcessor - if session.IsAutoCommit { + if session.isAutoCommit { // !nashtsai! does user expect it's same slice to passed closure when using Before()/After() when insert multi?? for _, closure := range session.afterClosures { closure(elemValue) @@ -278,8 +280,7 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error // InsertMulti insert multiple records func (session *Session) InsertMulti(rowsSlicePtr interface{}) (int64, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } @@ -297,12 +298,14 @@ func (session *Session) InsertMulti(rowsSlicePtr interface{}) (int64, error) { } func (session *Session) innerInsert(bean interface{}) (int64, error) { - session.Statement.setRefValue(rValue(bean)) - if len(session.Statement.TableName()) <= 0 { + if err := session.statement.setRefValue(rValue(bean)); err != nil { + return 0, err + } + if len(session.statement.TableName()) <= 0 { return 0, ErrTableNotFound } - table := session.Statement.RefTable + table := session.statement.RefTable // handle BeforeInsertProcessor for _, closure := range session.beforeClosures { @@ -314,19 +317,19 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { processor.BeforeInsert() } // -- - colNames, args, err := genCols(session.Statement.RefTable, session, bean, false, false) + colNames, args, err := genCols(session.statement.RefTable, session, bean, false, false) if err != nil { return 0, err } // insert expr columns, override if exists - exprColumns := session.Statement.getExpr() + exprColumns := session.statement.getExpr() exprColVals := make([]string, 0, len(exprColumns)) for _, v := range exprColumns { // remove the expr columns for i, colName := range colNames { if colName == v.colName { - colNames = append(colNames[:i], colNames[i + 1:]...) - args = append(args[:i], args[i + 1:]...) + colNames = append(colNames[:i], colNames[i+1:]...) + args = append(args[:i], args[i+1:]...) } } @@ -335,22 +338,34 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { exprColVals = append(exprColVals, v.expr) } - colPlaces := strings.Repeat("?, ", len(colNames) - len(exprColumns)) + colPlaces := strings.Repeat("?, ", len(colNames)-len(exprColumns)) if len(exprColVals) > 0 { colPlaces = colPlaces + strings.Join(exprColVals, ", ") } else { - colPlaces = colPlaces[0 : len(colPlaces) - 2] + if len(colPlaces) > 0 { + colPlaces = colPlaces[0 : len(colPlaces)-2] + } } - sqlStr := fmt.Sprintf("INSERT INTO %s (%v%v%v) VALUES (%v)", - session.Engine.Quote(session.Statement.TableName()), - session.Engine.QuoteStr(), - strings.Join(colNames, session.Engine.Quote(", ")), - session.Engine.QuoteStr(), - colPlaces) + var sqlStr string + var tableName = session.statement.TableName() + if len(colPlaces) > 0 { + sqlStr = fmt.Sprintf("INSERT INTO %s (%v%v%v) VALUES (%v)", + session.engine.Quote(tableName), + session.engine.QuoteStr(), + strings.Join(colNames, session.engine.Quote(", ")), + session.engine.QuoteStr(), + colPlaces) + } else { + if session.engine.dialect.DBType() == core.MYSQL { + sqlStr = fmt.Sprintf("INSERT INTO %s VALUES ()", session.engine.Quote(tableName)) + } else { + sqlStr = fmt.Sprintf("INSERT INTO %s DEFAULT VALUES", session.engine.Quote(tableName)) + } + } handleAfterInsertProcessorFunc := func(bean interface{}) { - if session.IsAutoCommit { + if session.isAutoCommit { for _, closure := range session.afterClosures { closure(bean) } @@ -379,23 +394,22 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { // for postgres, many of them didn't implement lastInsertId, so we should // implemented it ourself. - if session.Engine.dialect.DBType() == core.ORACLE && len(table.AutoIncrement) > 0 { - //assert table.AutoIncrement != "" - res, err := session.query("select seq_atable.currval from dual", args...) + if session.engine.dialect.DBType() == core.ORACLE && len(table.AutoIncrement) > 0 { + res, err := session.queryBytes("select seq_atable.currval from dual", args...) if err != nil { return 0, err } - handleAfterInsertProcessorFunc(bean) + defer handleAfterInsertProcessorFunc(bean) - if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache { - session.cacheInsert(session.Statement.TableName()) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + session.cacheInsert(table, tableName) } - if table.Version != "" && session.Statement.checkVersion { + if table.Version != "" && session.statement.checkVersion { verValue, err := table.VersionColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } else if verValue.IsValid() && verValue.CanSet() { verValue.SetInt(1) } @@ -413,7 +427,7 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { aiValue, err := table.AutoIncrColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } if aiValue == nil || !aiValue.IsValid() || !aiValue.CanSet() { @@ -423,24 +437,24 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { aiValue.Set(int64ToIntValue(id, aiValue.Type())) return 1, nil - } else if session.Engine.dialect.DBType() == core.POSTGRES && len(table.AutoIncrement) > 0 { + } else if session.engine.dialect.DBType() == core.POSTGRES && len(table.AutoIncrement) > 0 { //assert table.AutoIncrement != "" - sqlStr = sqlStr + " RETURNING " + session.Engine.Quote(table.AutoIncrement) - res, err := session.query(sqlStr, args...) + sqlStr = sqlStr + " RETURNING " + session.engine.Quote(table.AutoIncrement) + res, err := session.queryBytes(sqlStr, args...) if err != nil { return 0, err } - handleAfterInsertProcessorFunc(bean) + defer handleAfterInsertProcessorFunc(bean) - if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache { - session.cacheInsert(session.Statement.TableName()) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + session.cacheInsert(table, tableName) } - if table.Version != "" && session.Statement.checkVersion { + if table.Version != "" && session.statement.checkVersion { verValue, err := table.VersionColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } else if verValue.IsValid() && verValue.CanSet() { verValue.SetInt(1) } @@ -458,7 +472,7 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { aiValue, err := table.AutoIncrColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } if aiValue == nil || !aiValue.IsValid() || !aiValue.CanSet() { @@ -476,14 +490,14 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { defer handleAfterInsertProcessorFunc(bean) - if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache { - session.cacheInsert(session.Statement.TableName()) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + session.cacheInsert(table, tableName) } - if table.Version != "" && session.Statement.checkVersion { + if table.Version != "" && session.statement.checkVersion { verValue, err := table.VersionColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } else if verValue.IsValid() && verValue.CanSet() { verValue.SetInt(1) } @@ -501,7 +515,7 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { aiValue, err := table.AutoIncrColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } if aiValue == nil || !aiValue.IsValid() || !aiValue.CanSet() { @@ -518,24 +532,21 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { // The in parameter bean must a struct or a point to struct. The return // parameter is inserted and error func (session *Session) InsertOne(bean interface{}) (int64, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } return session.innerInsert(bean) } -func (session *Session) cacheInsert(tables ...string) error { - if session.Statement.RefTable == nil { +func (session *Session) cacheInsert(table *core.Table, tables ...string) error { + if table == nil { return ErrCacheFailed } - table := session.Statement.RefTable - cacher := session.Engine.getCacher2(table) - + cacher := session.engine.getCacher2(table) for _, t := range tables { - session.Engine.logger.Debug("[cache] clear sql:", t) + session.engine.logger.Debug("[cache] clear sql:", t) cacher.ClearIds(t) } diff --git a/vendor/github.com/go-xorm/xorm/session_iterate.go b/vendor/github.com/go-xorm/xorm/session_iterate.go index 7c148095902..071fce49921 100644 --- a/vendor/github.com/go-xorm/xorm/session_iterate.go +++ b/vendor/github.com/go-xorm/xorm/session_iterate.go @@ -19,6 +19,14 @@ func (session *Session) Rows(bean interface{}) (*Rows, error) { // are conditions. beans could be []Struct, []*Struct, map[int64]Struct // map[int64]*Struct func (session *Session) Iterate(bean interface{}, fun IterFunc) error { + if session.isAutoClose { + defer session.Close() + } + + if session.statement.bufferSize > 0 { + return session.bufferIterate(bean, fun) + } + rows, err := session.Rows(bean) if err != nil { return err @@ -40,3 +48,49 @@ func (session *Session) Iterate(bean interface{}, fun IterFunc) error { } return err } + +// BufferSize sets the buffersize for iterate +func (session *Session) BufferSize(size int) *Session { + session.statement.bufferSize = size + return session +} + +func (session *Session) bufferIterate(bean interface{}, fun IterFunc) error { + if session.isAutoClose { + defer session.Close() + } + + var bufferSize = session.statement.bufferSize + var limit = session.statement.LimitN + if limit > 0 && bufferSize > limit { + bufferSize = limit + } + var start = session.statement.Start + v := rValue(bean) + sliceType := reflect.SliceOf(v.Type()) + var idx = 0 + for { + slice := reflect.New(sliceType) + if err := session.Limit(bufferSize, start).find(slice.Interface(), bean); err != nil { + return err + } + + for i := 0; i < slice.Elem().Len(); i++ { + if err := fun(idx, slice.Elem().Index(i).Addr().Interface()); err != nil { + return err + } + idx++ + } + + start = start + slice.Elem().Len() + if limit > 0 && idx+bufferSize > limit { + bufferSize = limit - idx + } + + if bufferSize <= 0 || slice.Elem().Len() < bufferSize || idx == limit { + break + } + } + + return nil +} diff --git a/vendor/github.com/go-xorm/xorm/session_query.go b/vendor/github.com/go-xorm/xorm/session_query.go new file mode 100644 index 00000000000..5b4e0dc45d0 --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/session_query.go @@ -0,0 +1,252 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "time" + + "github.com/go-xorm/builder" + "github.com/go-xorm/core" +) + +func (session *Session) genQuerySQL(sqlorArgs ...interface{}) (string, []interface{}, error) { + if len(sqlorArgs) > 0 { + return sqlorArgs[0].(string), sqlorArgs[1:], nil + } + + if session.statement.RawSQL != "" { + return session.statement.RawSQL, session.statement.RawParams, nil + } + + if len(session.statement.TableName()) <= 0 { + return "", nil, ErrTableNotFound + } + + var columnStr = session.statement.ColumnStr + if len(session.statement.selectStr) > 0 { + columnStr = session.statement.selectStr + } else { + if session.statement.JoinStr == "" { + if columnStr == "" { + if session.statement.GroupByStr != "" { + columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) + } else { + columnStr = session.statement.genColumnStr() + } + } + } else { + if columnStr == "" { + if session.statement.GroupByStr != "" { + columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) + } else { + columnStr = "*" + } + } + } + if columnStr == "" { + columnStr = "*" + } + } + + condSQL, condArgs, err := builder.ToSQL(session.statement.cond) + if err != nil { + return "", nil, err + } + + args := append(session.statement.joinArgs, condArgs...) + sqlStr, err := session.statement.genSelectSQL(columnStr, condSQL) + if err != nil { + return "", nil, err + } + // for mssql and use limit + qs := strings.Count(sqlStr, "?") + if len(args)*2 == qs { + args = append(args, args...) + } + + return sqlStr, args, nil +} + +// Query runs a raw sql and return records as []map[string][]byte +func (session *Session) Query(sqlorArgs ...interface{}) ([]map[string][]byte, error) { + if session.isAutoClose { + defer session.Close() + } + + sqlStr, args, err := session.genQuerySQL(sqlorArgs...) + if err != nil { + return nil, err + } + + return session.queryBytes(sqlStr, args...) +} + +func value2String(rawValue *reflect.Value) (str string, err error) { + aa := reflect.TypeOf((*rawValue).Interface()) + vv := reflect.ValueOf((*rawValue).Interface()) + switch aa.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + str = strconv.FormatInt(vv.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + str = strconv.FormatUint(vv.Uint(), 10) + case reflect.Float32, reflect.Float64: + str = strconv.FormatFloat(vv.Float(), 'f', -1, 64) + case reflect.String: + str = vv.String() + case reflect.Array, reflect.Slice: + switch aa.Elem().Kind() { + case reflect.Uint8: + data := rawValue.Interface().([]byte) + str = string(data) + if str == "\x00" { + str = "0" + } + default: + err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) + } + // time type + case reflect.Struct: + if aa.ConvertibleTo(core.TimeType) { + str = vv.Convert(core.TimeType).Interface().(time.Time).Format(time.RFC3339Nano) + } else { + err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) + } + case reflect.Bool: + str = strconv.FormatBool(vv.Bool()) + case reflect.Complex128, reflect.Complex64: + str = fmt.Sprintf("%v", vv.Complex()) + /* TODO: unsupported types below + case reflect.Map: + case reflect.Ptr: + case reflect.Uintptr: + case reflect.UnsafePointer: + case reflect.Chan, reflect.Func, reflect.Interface: + */ + default: + err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) + } + return +} + +func row2mapStr(rows *core.Rows, fields []string) (resultsMap map[string]string, err error) { + result := make(map[string]string) + scanResultContainers := make([]interface{}, len(fields)) + for i := 0; i < len(fields); i++ { + var scanResultContainer interface{} + scanResultContainers[i] = &scanResultContainer + } + if err := rows.Scan(scanResultContainers...); err != nil { + return nil, err + } + + for ii, key := range fields { + rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) + // if row is null then as empty string + if rawValue.Interface() == nil { + result[key] = "" + continue + } + + if data, err := value2String(&rawValue); err == nil { + result[key] = data + } else { + return nil, err + } + } + return result, nil +} + +func rows2Strings(rows *core.Rows) (resultsSlice []map[string]string, err error) { + fields, err := rows.Columns() + if err != nil { + return nil, err + } + for rows.Next() { + result, err := row2mapStr(rows, fields) + if err != nil { + return nil, err + } + resultsSlice = append(resultsSlice, result) + } + + return resultsSlice, nil +} + +// QueryString runs a raw sql and return records as []map[string]string +func (session *Session) QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) { + if session.isAutoClose { + defer session.Close() + } + + sqlStr, args, err := session.genQuerySQL(sqlorArgs...) + if err != nil { + return nil, err + } + + rows, err := session.queryRows(sqlStr, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + return rows2Strings(rows) +} + +func row2mapInterface(rows *core.Rows, fields []string) (resultsMap map[string]interface{}, err error) { + resultsMap = make(map[string]interface{}, len(fields)) + scanResultContainers := make([]interface{}, len(fields)) + for i := 0; i < len(fields); i++ { + var scanResultContainer interface{} + scanResultContainers[i] = &scanResultContainer + } + if err := rows.Scan(scanResultContainers...); err != nil { + return nil, err + } + + for ii, key := range fields { + resultsMap[key] = reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])).Interface() + } + return +} + +func rows2Interfaces(rows *core.Rows) (resultsSlice []map[string]interface{}, err error) { + fields, err := rows.Columns() + if err != nil { + return nil, err + } + for rows.Next() { + result, err := row2mapInterface(rows, fields) + if err != nil { + return nil, err + } + resultsSlice = append(resultsSlice, result) + } + + return resultsSlice, nil +} + +// QueryInterface runs a raw sql and return records as []map[string]interface{} +func (session *Session) QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) { + if session.isAutoClose { + defer session.Close() + } + + sqlStr, args, err := session.genQuerySQL(sqlorArgs...) + if err != nil { + return nil, err + } + + rows, err := session.queryRows(sqlStr, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + return rows2Interfaces(rows) +} diff --git a/vendor/github.com/go-xorm/xorm/session_raw.go b/vendor/github.com/go-xorm/xorm/session_raw.go index 9351d5cf94b..69bf9b3c6bf 100644 --- a/vendor/github.com/go-xorm/xorm/session_raw.go +++ b/vendor/github.com/go-xorm/xorm/session_raw.go @@ -6,21 +6,140 @@ package xorm import ( "database/sql" + "reflect" + "time" "github.com/go-xorm/core" ) -func (session *Session) query(sqlStr string, paramStr ...interface{}) (resultsSlice []map[string][]byte, err error) { - session.queryPreprocess(&sqlStr, paramStr...) - - if session.IsAutoCommit { - return session.innerQuery2(sqlStr, paramStr...) +func (session *Session) queryPreprocess(sqlStr *string, paramStr ...interface{}) { + for _, filter := range session.engine.dialect.Filters() { + *sqlStr = filter.Do(*sqlStr, session.engine.dialect, session.statement.RefTable) } - return session.txQuery(session.Tx, sqlStr, paramStr...) + + session.lastSQL = *sqlStr + session.lastSQLArgs = paramStr } -func (session *Session) txQuery(tx *core.Tx, sqlStr string, params ...interface{}) (resultsSlice []map[string][]byte, err error) { - rows, err := tx.Query(sqlStr, params...) +func (session *Session) queryRows(sqlStr string, args ...interface{}) (*core.Rows, error) { + defer session.resetStatement() + + session.queryPreprocess(&sqlStr, args...) + + if session.engine.showSQL { + if session.engine.showExecTime { + b4ExecTime := time.Now() + defer func() { + execDuration := time.Since(b4ExecTime) + if len(args) > 0 { + session.engine.logger.Infof("[SQL] %s %#v - took: %v", sqlStr, args, execDuration) + } else { + session.engine.logger.Infof("[SQL] %s - took: %v", sqlStr, execDuration) + } + }() + } else { + if len(args) > 0 { + session.engine.logger.Infof("[SQL] %v %#v", sqlStr, args) + } else { + session.engine.logger.Infof("[SQL] %v", sqlStr) + } + } + } + + if session.isAutoCommit { + var db *core.DB + if session.engine.engineGroup != nil { + db = session.engine.engineGroup.Slave().DB() + } else { + db = session.DB() + } + + if session.prepareStmt { + // don't clear stmt since session will cache them + stmt, err := session.doPrepare(db, sqlStr) + if err != nil { + return nil, err + } + + rows, err := stmt.Query(args...) + if err != nil { + return nil, err + } + return rows, nil + } + + rows, err := db.Query(sqlStr, args...) + if err != nil { + return nil, err + } + return rows, nil + } + + rows, err := session.tx.Query(sqlStr, args...) + if err != nil { + return nil, err + } + return rows, nil +} + +func (session *Session) queryRow(sqlStr string, args ...interface{}) *core.Row { + return core.NewRow(session.queryRows(sqlStr, args...)) +} + +func value2Bytes(rawValue *reflect.Value) ([]byte, error) { + str, err := value2String(rawValue) + if err != nil { + return nil, err + } + return []byte(str), nil +} + +func row2map(rows *core.Rows, fields []string) (resultsMap map[string][]byte, err error) { + result := make(map[string][]byte) + scanResultContainers := make([]interface{}, len(fields)) + for i := 0; i < len(fields); i++ { + var scanResultContainer interface{} + scanResultContainers[i] = &scanResultContainer + } + if err := rows.Scan(scanResultContainers...); err != nil { + return nil, err + } + + for ii, key := range fields { + rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) + //if row is null then ignore + if rawValue.Interface() == nil { + result[key] = []byte{} + continue + } + + if data, err := value2Bytes(&rawValue); err == nil { + result[key] = data + } else { + return nil, err // !nashtsai! REVIEW, should return err or just error log? + } + } + return result, nil +} + +func rows2maps(rows *core.Rows) (resultsSlice []map[string][]byte, err error) { + fields, err := rows.Columns() + if err != nil { + return nil, err + } + for rows.Next() { + result, err := row2map(rows, fields) + if err != nil { + return nil, err + } + resultsSlice = append(resultsSlice, result) + } + + return resultsSlice, nil +} + +func (session *Session) queryBytes(sqlStr string, args ...interface{}) ([]map[string][]byte, error) { + rows, err := session.queryRows(sqlStr, args...) if err != nil { return nil, err } @@ -29,73 +148,37 @@ func (session *Session) txQuery(tx *core.Tx, sqlStr string, params ...interface{ return rows2maps(rows) } -func (session *Session) innerQuery(sqlStr string, params ...interface{}) (*core.Stmt, *core.Rows, error) { - var callback func() (*core.Stmt, *core.Rows, error) - if session.prepareStmt { - callback = func() (*core.Stmt, *core.Rows, error) { - stmt, err := session.doPrepare(sqlStr) - if err != nil { - return nil, nil, err - } - rows, err := stmt.Query(params...) - if err != nil { - return nil, nil, err - } - return stmt, rows, nil - } - } else { - callback = func() (*core.Stmt, *core.Rows, error) { - rows, err := session.DB().Query(sqlStr, params...) - if err != nil { - return nil, nil, err - } - return nil, rows, err - } - } - stmt, rows, err := session.Engine.logSQLQueryTime(sqlStr, params, callback) - if err != nil { - return nil, nil, err - } - return stmt, rows, nil -} - -func (session *Session) innerQuery2(sqlStr string, params ...interface{}) ([]map[string][]byte, error) { - _, rows, err := session.innerQuery(sqlStr, params...) - if rows != nil { - defer rows.Close() - } - if err != nil { - return nil, err - } - return rows2maps(rows) -} - -// Query a raw sql and return records as []map[string][]byte -func (session *Session) Query(sqlStr string, paramStr ...interface{}) (resultsSlice []map[string][]byte, err error) { +func (session *Session) exec(sqlStr string, args ...interface{}) (sql.Result, error) { defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() + + session.queryPreprocess(&sqlStr, args...) + + if session.engine.showSQL { + if session.engine.showExecTime { + b4ExecTime := time.Now() + defer func() { + execDuration := time.Since(b4ExecTime) + if len(args) > 0 { + session.engine.logger.Infof("[SQL] %s %#v - took: %v", sqlStr, args, execDuration) + } else { + session.engine.logger.Infof("[SQL] %s - took: %v", sqlStr, execDuration) + } + }() + } else { + if len(args) > 0 { + session.engine.logger.Infof("[SQL] %v %#v", sqlStr, args) + } else { + session.engine.logger.Infof("[SQL] %v", sqlStr) + } + } } - return session.query(sqlStr, paramStr...) -} - -// ============================= -// for string -// ============================= -func (session *Session) query2(sqlStr string, paramStr ...interface{}) (resultsSlice []map[string]string, err error) { - session.queryPreprocess(&sqlStr, paramStr...) - - if session.IsAutoCommit { - return query2(session.DB(), sqlStr, paramStr...) + if !session.isAutoCommit { + return session.tx.Exec(sqlStr, args...) } - return txQuery2(session.Tx, sqlStr, paramStr...) -} -// Execute sql -func (session *Session) innerExec(sqlStr string, args ...interface{}) (sql.Result, error) { if session.prepareStmt { - stmt, err := session.doPrepare(sqlStr) + stmt, err := session.doPrepare(session.DB(), sqlStr) if err != nil { return nil, err } @@ -110,33 +193,9 @@ func (session *Session) innerExec(sqlStr string, args ...interface{}) (sql.Resul return session.DB().Exec(sqlStr, args...) } -func (session *Session) exec(sqlStr string, args ...interface{}) (sql.Result, error) { - for _, filter := range session.Engine.dialect.Filters() { - // TODO: for table name, it's no need to RefTable - sqlStr = filter.Do(sqlStr, session.Engine.dialect, session.Statement.RefTable) - } - - session.saveLastSQL(sqlStr, args...) - - return session.Engine.logSQLExecutionTime(sqlStr, args, func() (sql.Result, error) { - if session.IsAutoCommit { - // FIXME: oci8 can not auto commit (github.com/mattn/go-oci8) - if session.Engine.dialect.DBType() == core.ORACLE { - session.Begin() - r, err := session.Tx.Exec(sqlStr, args...) - session.Commit() - return r, err - } - return session.innerExec(sqlStr, args...) - } - return session.Tx.Exec(sqlStr, args...) - }) -} - // Exec raw sql func (session *Session) Exec(sqlStr string, args ...interface{}) (sql.Result, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } diff --git a/vendor/github.com/go-xorm/xorm/session_schema.go b/vendor/github.com/go-xorm/xorm/session_schema.go index 21fa2996149..a2708b736c0 100644 --- a/vendor/github.com/go-xorm/xorm/session_schema.go +++ b/vendor/github.com/go-xorm/xorm/session_schema.go @@ -16,38 +16,50 @@ import ( // Ping test if database is ok func (session *Session) Ping() error { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } + session.engine.logger.Infof("PING DATABASE %v", session.engine.DriverName()) return session.DB().Ping() } // CreateTable create a table according a bean func (session *Session) CreateTable(bean interface{}) error { - v := rValue(bean) - session.Statement.setRefValue(v) - - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } - return session.createOneTable() + return session.createTable(bean) +} + +func (session *Session) createTable(bean interface{}) error { + v := rValue(bean) + if err := session.statement.setRefValue(v); err != nil { + return err + } + + sqlStr := session.statement.genCreateTableSQL() + _, err := session.exec(sqlStr) + return err } // CreateIndexes create indexes func (session *Session) CreateIndexes(bean interface{}) error { - v := rValue(bean) - session.Statement.setRefValue(v) - - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } - sqls := session.Statement.genIndexSQL() + return session.createIndexes(bean) +} + +func (session *Session) createIndexes(bean interface{}) error { + v := rValue(bean) + if err := session.statement.setRefValue(v); err != nil { + return err + } + + sqls := session.statement.genIndexSQL() for _, sqlStr := range sqls { _, err := session.exec(sqlStr) if err != nil { @@ -59,15 +71,19 @@ func (session *Session) CreateIndexes(bean interface{}) error { // CreateUniques create uniques func (session *Session) CreateUniques(bean interface{}) error { - v := rValue(bean) - session.Statement.setRefValue(v) - - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } + return session.createUniques(bean) +} - sqls := session.Statement.genUniqueSQL() +func (session *Session) createUniques(bean interface{}) error { + v := rValue(bean) + if err := session.statement.setRefValue(v); err != nil { + return err + } + + sqls := session.statement.genUniqueSQL() for _, sqlStr := range sqls { _, err := session.exec(sqlStr) if err != nil { @@ -77,41 +93,22 @@ func (session *Session) CreateUniques(bean interface{}) error { return nil } -func (session *Session) createOneTable() error { - sqlStr := session.Statement.genCreateTableSQL() - _, err := session.exec(sqlStr) - return err -} - -// to be deleted -func (session *Session) createAll() error { - if session.IsAutoClose { - defer session.Close() - } - - for _, table := range session.Engine.Tables { - session.Statement.RefTable = table - session.Statement.tableName = table.Name - err := session.createOneTable() - session.resetStatement() - if err != nil { - return err - } - } - return nil -} - // DropIndexes drop indexes func (session *Session) DropIndexes(bean interface{}) error { - v := rValue(bean) - session.Statement.setRefValue(v) - - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } - sqls := session.Statement.genDelIndexSQL() + return session.dropIndexes(bean) +} + +func (session *Session) dropIndexes(bean interface{}) error { + v := rValue(bean) + if err := session.statement.setRefValue(v); err != nil { + return err + } + + sqls := session.statement.genDelIndexSQL() for _, sqlStr := range sqls { _, err := session.exec(sqlStr) if err != nil { @@ -123,15 +120,23 @@ func (session *Session) DropIndexes(bean interface{}) error { // DropTable drop table will drop table if exist, if drop failed, it will return error func (session *Session) DropTable(beanOrTableName interface{}) error { - tableName, err := session.Engine.tableName(beanOrTableName) + if session.isAutoClose { + defer session.Close() + } + + return session.dropTable(beanOrTableName) +} + +func (session *Session) dropTable(beanOrTableName interface{}) error { + tableName, err := session.engine.tableName(beanOrTableName) if err != nil { return err } var needDrop = true - if !session.Engine.dialect.SupportDropIfExists() { - sqlStr, args := session.Engine.dialect.TableCheckSql(tableName) - results, err := session.query(sqlStr, args...) + if !session.engine.dialect.SupportDropIfExists() { + sqlStr, args := session.engine.dialect.TableCheckSql(tableName) + results, err := session.queryBytes(sqlStr, args...) if err != nil { return err } @@ -139,7 +144,7 @@ func (session *Session) DropTable(beanOrTableName interface{}) error { } if needDrop { - sqlStr := session.Engine.Dialect().DropTableSql(tableName) + sqlStr := session.engine.Dialect().DropTableSql(tableName) _, err = session.exec(sqlStr) return err } @@ -148,7 +153,11 @@ func (session *Session) DropTable(beanOrTableName interface{}) error { // IsTableExist if a table is exist func (session *Session) IsTableExist(beanOrTableName interface{}) (bool, error) { - tableName, err := session.Engine.tableName(beanOrTableName) + if session.isAutoClose { + defer session.Close() + } + + tableName, err := session.engine.tableName(beanOrTableName) if err != nil { return false, err } @@ -157,12 +166,8 @@ func (session *Session) IsTableExist(beanOrTableName interface{}) (bool, error) } func (session *Session) isTableExist(tableName string) (bool, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - sqlStr, args := session.Engine.dialect.TableCheckSql(tableName) - results, err := session.query(sqlStr, args...) + sqlStr, args := session.engine.dialect.TableCheckSql(tableName) + results, err := session.queryBytes(sqlStr, args...) return len(results) > 0, err } @@ -172,6 +177,9 @@ func (session *Session) IsTableEmpty(bean interface{}) (bool, error) { t := v.Type() if t.Kind() == reflect.String { + if session.isAutoClose { + defer session.Close() + } return session.isTableEmpty(bean.(string)) } else if t.Kind() == reflect.Struct { rows, err := session.Count(bean) @@ -181,15 +189,9 @@ func (session *Session) IsTableEmpty(bean interface{}) (bool, error) { } func (session *Session) isTableEmpty(tableName string) (bool, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - var total int64 - sqlStr := fmt.Sprintf("select count(*) from %s", session.Engine.Quote(tableName)) - err := session.DB().QueryRow(sqlStr).Scan(&total) - session.saveLastSQL(sqlStr) + sqlStr := fmt.Sprintf("select count(*) from %s", session.engine.Quote(tableName)) + err := session.queryRow(sqlStr).Scan(&total) if err != nil { if err == sql.ErrNoRows { err = nil @@ -200,30 +202,9 @@ func (session *Session) isTableEmpty(tableName string) (bool, error) { return total == 0, nil } -func (session *Session) isIndexExist(tableName, idxName string, unique bool) (bool, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - var idx string - if unique { - idx = uniqueName(tableName, idxName) - } else { - idx = indexName(tableName, idxName) - } - sqlStr, args := session.Engine.dialect.IndexCheckSql(tableName, idx) - results, err := session.query(sqlStr, args...) - return len(results) > 0, err -} - // find if index is exist according cols func (session *Session) isIndexExist2(tableName string, cols []string, unique bool) (bool, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - indexes, err := session.Engine.dialect.GetIndexes(tableName) + indexes, err := session.engine.dialect.GetIndexes(tableName) if err != nil { return false, err } @@ -240,62 +221,34 @@ func (session *Session) isIndexExist2(tableName string, cols []string, unique bo } func (session *Session) addColumn(colName string) error { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - col := session.Statement.RefTable.GetColumn(colName) - sql, args := session.Statement.genAddColumnStr(col) + col := session.statement.RefTable.GetColumn(colName) + sql, args := session.statement.genAddColumnStr(col) _, err := session.exec(sql, args...) return err } func (session *Session) addIndex(tableName, idxName string) error { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - index := session.Statement.RefTable.Indexes[idxName] - sqlStr := session.Engine.dialect.CreateIndexSql(tableName, index) - + index := session.statement.RefTable.Indexes[idxName] + sqlStr := session.engine.dialect.CreateIndexSql(tableName, index) _, err := session.exec(sqlStr) return err } func (session *Session) addUnique(tableName, uqeName string) error { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - index := session.Statement.RefTable.Indexes[uqeName] - sqlStr := session.Engine.dialect.CreateIndexSql(tableName, index) + index := session.statement.RefTable.Indexes[uqeName] + sqlStr := session.engine.dialect.CreateIndexSql(tableName, index) _, err := session.exec(sqlStr) return err } -// To be deleted -func (session *Session) dropAll() error { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - for _, table := range session.Engine.Tables { - session.Statement.Init() - session.Statement.RefTable = table - sqlStr := session.Engine.Dialect().DropTableSql(session.Statement.TableName()) - _, err := session.exec(sqlStr) - if err != nil { - return err - } - } - return nil -} - // Sync2 synchronize structs to database tables func (session *Session) Sync2(beans ...interface{}) error { - engine := session.Engine + engine := session.engine + + if session.isAutoClose { + session.isAutoClose = false + defer session.Close() + } tables, err := engine.DBMetas() if err != nil { @@ -322,17 +275,17 @@ func (session *Session) Sync2(beans ...interface{}) error { } if oriTable == nil { - err = session.StoreEngine(session.Statement.StoreEngine).CreateTable(bean) + err = session.StoreEngine(session.statement.StoreEngine).createTable(bean) if err != nil { return err } - err = session.CreateUniques(bean) + err = session.createUniques(bean) if err != nil { return err } - err = session.CreateIndexes(bean) + err = session.createIndexes(bean) if err != nil { return err } @@ -357,7 +310,7 @@ func (session *Session) Sync2(beans ...interface{}) error { engine.dialect.DBType() == core.POSTGRES { engine.logger.Infof("Table %s column %s change type from %s to %s\n", tbName, col.Name, curType, expectedType) - _, err = engine.Exec(engine.dialect.ModifyColumnSql(table.Name, col)) + _, err = session.exec(engine.dialect.ModifyColumnSql(table.Name, col)) } else { engine.logger.Warnf("Table %s column %s db type is %s, struct type is %s\n", tbName, col.Name, curType, expectedType) @@ -367,7 +320,7 @@ func (session *Session) Sync2(beans ...interface{}) error { if oriCol.Length < col.Length { engine.logger.Infof("Table %s column %s change type from varchar(%d) to varchar(%d)\n", tbName, col.Name, oriCol.Length, col.Length) - _, err = engine.Exec(engine.dialect.ModifyColumnSql(table.Name, col)) + _, err = session.exec(engine.dialect.ModifyColumnSql(table.Name, col)) } } } else { @@ -381,7 +334,7 @@ func (session *Session) Sync2(beans ...interface{}) error { if oriCol.Length < col.Length { engine.logger.Infof("Table %s column %s change type from varchar(%d) to varchar(%d)\n", tbName, col.Name, oriCol.Length, col.Length) - _, err = engine.Exec(engine.dialect.ModifyColumnSql(table.Name, col)) + _, err = session.exec(engine.dialect.ModifyColumnSql(table.Name, col)) } } } @@ -394,10 +347,8 @@ func (session *Session) Sync2(beans ...interface{}) error { tbName, col.Name, oriCol.Nullable, col.Nullable) } } else { - session := engine.NewSession() - session.Statement.RefTable = table - session.Statement.tableName = tbName - defer session.Close() + session.statement.RefTable = table + session.statement.tableName = tbName err = session.addColumn(col.Name) } if err != nil { @@ -421,7 +372,7 @@ func (session *Session) Sync2(beans ...interface{}) error { if oriIndex != nil { if oriIndex.Type != index.Type { sql := engine.dialect.DropIndexSql(tbName, oriIndex) - _, err = engine.Exec(sql) + _, err = session.exec(sql) if err != nil { return err } @@ -437,7 +388,7 @@ func (session *Session) Sync2(beans ...interface{}) error { for name2, index2 := range oriTable.Indexes { if _, ok := foundIndexNames[name2]; !ok { sql := engine.dialect.DropIndexSql(tbName, index2) - _, err = engine.Exec(sql) + _, err = session.exec(sql) if err != nil { return err } @@ -446,16 +397,12 @@ func (session *Session) Sync2(beans ...interface{}) error { for name, index := range addedNames { if index.Type == core.UniqueType { - session := engine.NewSession() - session.Statement.RefTable = table - session.Statement.tableName = tbName - defer session.Close() + session.statement.RefTable = table + session.statement.tableName = tbName err = session.addUnique(tbName, name) } else if index.Type == core.IndexType { - session := engine.NewSession() - session.Statement.RefTable = table - session.Statement.tableName = tbName - defer session.Close() + session.statement.RefTable = table + session.statement.tableName = tbName err = session.addIndex(tbName, name) } if err != nil { diff --git a/vendor/github.com/go-xorm/xorm/session_stats.go b/vendor/github.com/go-xorm/xorm/session_stats.go new file mode 100644 index 00000000000..c2cac830697 --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/session_stats.go @@ -0,0 +1,98 @@ +// Copyright 2016 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "database/sql" + "errors" + "reflect" +) + +// Count counts the records. bean's non-empty fields +// are conditions. +func (session *Session) Count(bean ...interface{}) (int64, error) { + if session.isAutoClose { + defer session.Close() + } + + var sqlStr string + var args []interface{} + var err error + if session.statement.RawSQL == "" { + sqlStr, args, err = session.statement.genCountSQL(bean...) + if err != nil { + return 0, err + } + } else { + sqlStr = session.statement.RawSQL + args = session.statement.RawParams + } + + var total int64 + err = session.queryRow(sqlStr, args...).Scan(&total) + if err == sql.ErrNoRows || err == nil { + return total, nil + } + + return 0, err +} + +// sum call sum some column. bean's non-empty fields are conditions. +func (session *Session) sum(res interface{}, bean interface{}, columnNames ...string) error { + if session.isAutoClose { + defer session.Close() + } + + v := reflect.ValueOf(res) + if v.Kind() != reflect.Ptr { + return errors.New("need a pointer to a variable") + } + + var isSlice = v.Elem().Kind() == reflect.Slice + var sqlStr string + var args []interface{} + var err error + if len(session.statement.RawSQL) == 0 { + sqlStr, args, err = session.statement.genSumSQL(bean, columnNames...) + if err != nil { + return err + } + } else { + sqlStr = session.statement.RawSQL + args = session.statement.RawParams + } + + if isSlice { + err = session.queryRow(sqlStr, args...).ScanSlice(res) + } else { + err = session.queryRow(sqlStr, args...).Scan(res) + } + if err == sql.ErrNoRows || err == nil { + return nil + } + return err +} + +// Sum call sum some column. bean's non-empty fields are conditions. +func (session *Session) Sum(bean interface{}, columnName string) (res float64, err error) { + return res, session.sum(&res, bean, columnName) +} + +// SumInt call sum some column. bean's non-empty fields are conditions. +func (session *Session) SumInt(bean interface{}, columnName string) (res int64, err error) { + return res, session.sum(&res, bean, columnName) +} + +// Sums call sum some columns. bean's non-empty fields are conditions. +func (session *Session) Sums(bean interface{}, columnNames ...string) ([]float64, error) { + var res = make([]float64, len(columnNames), len(columnNames)) + return res, session.sum(&res, bean, columnNames...) +} + +// SumsInt sum specify columns and return as []int64 instead of []float64 +func (session *Session) SumsInt(bean interface{}, columnNames ...string) ([]int64, error) { + var res = make([]int64, len(columnNames), len(columnNames)) + return res, session.sum(&res, bean, columnNames...) +} diff --git a/vendor/github.com/go-xorm/xorm/session_sum.go b/vendor/github.com/go-xorm/xorm/session_sum.go deleted file mode 100644 index e1409c7ff43..00000000000 --- a/vendor/github.com/go-xorm/xorm/session_sum.go +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2016 The Xorm Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xorm - -import "database/sql" - -// Count counts the records. bean's non-empty fields -// are conditions. -func (session *Session) Count(bean interface{}) (int64, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - var sqlStr string - var args []interface{} - if session.Statement.RawSQL == "" { - sqlStr, args = session.Statement.genCountSQL(bean) - } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams - } - - session.queryPreprocess(&sqlStr, args...) - - var err error - var total int64 - if session.IsAutoCommit { - err = session.DB().QueryRow(sqlStr, args...).Scan(&total) - } else { - err = session.Tx.QueryRow(sqlStr, args...).Scan(&total) - } - - if err == sql.ErrNoRows || err == nil { - return total, nil - } - - return 0, err -} - -// Sum call sum some column. bean's non-empty fields are conditions. -func (session *Session) Sum(bean interface{}, columnName string) (float64, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - var sqlStr string - var args []interface{} - if len(session.Statement.RawSQL) == 0 { - sqlStr, args = session.Statement.genSumSQL(bean, columnName) - } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams - } - - session.queryPreprocess(&sqlStr, args...) - - var err error - var res float64 - if session.IsAutoCommit { - err = session.DB().QueryRow(sqlStr, args...).Scan(&res) - } else { - err = session.Tx.QueryRow(sqlStr, args...).Scan(&res) - } - - if err == sql.ErrNoRows || err == nil { - return res, nil - } - return 0, err -} - -// Sums call sum some columns. bean's non-empty fields are conditions. -func (session *Session) Sums(bean interface{}, columnNames ...string) ([]float64, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - var sqlStr string - var args []interface{} - if len(session.Statement.RawSQL) == 0 { - sqlStr, args = session.Statement.genSumSQL(bean, columnNames...) - } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams - } - - session.queryPreprocess(&sqlStr, args...) - - var err error - var res = make([]float64, len(columnNames), len(columnNames)) - if session.IsAutoCommit { - err = session.DB().QueryRow(sqlStr, args...).ScanSlice(&res) - } else { - err = session.Tx.QueryRow(sqlStr, args...).ScanSlice(&res) - } - - if err == sql.ErrNoRows || err == nil { - return res, nil - } - return nil, err -} - -// SumsInt sum specify columns and return as []int64 instead of []float64 -func (session *Session) SumsInt(bean interface{}, columnNames ...string) ([]int64, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - var sqlStr string - var args []interface{} - if len(session.Statement.RawSQL) == 0 { - sqlStr, args = session.Statement.genSumSQL(bean, columnNames...) - } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams - } - - session.queryPreprocess(&sqlStr, args...) - - var err error - var res = make([]int64, len(columnNames), len(columnNames)) - if session.IsAutoCommit { - err = session.DB().QueryRow(sqlStr, args...).ScanSlice(&res) - } else { - err = session.Tx.QueryRow(sqlStr, args...).ScanSlice(&res) - } - - if err == sql.ErrNoRows || err == nil { - return res, nil - } - return nil, err -} diff --git a/vendor/github.com/go-xorm/xorm/session_tx.go b/vendor/github.com/go-xorm/xorm/session_tx.go index 302bc104d54..84d2f7f9dcf 100644 --- a/vendor/github.com/go-xorm/xorm/session_tx.go +++ b/vendor/github.com/go-xorm/xorm/session_tx.go @@ -6,14 +6,14 @@ package xorm // Begin a transaction func (session *Session) Begin() error { - if session.IsAutoCommit { + if session.isAutoCommit { tx, err := session.DB().Begin() if err != nil { return err } - session.IsAutoCommit = false - session.IsCommitedOrRollbacked = false - session.Tx = tx + session.isAutoCommit = false + session.isCommitedOrRollbacked = false + session.tx = tx session.saveLastSQL("BEGIN TRANSACTION") } return nil @@ -21,25 +21,23 @@ func (session *Session) Begin() error { // Rollback When using transaction, you can rollback if any error func (session *Session) Rollback() error { - if !session.IsAutoCommit && !session.IsCommitedOrRollbacked { - session.saveLastSQL(session.Engine.dialect.RollBackStr()) - session.IsCommitedOrRollbacked = true - return session.Tx.Rollback() + if !session.isAutoCommit && !session.isCommitedOrRollbacked { + session.saveLastSQL(session.engine.dialect.RollBackStr()) + session.isCommitedOrRollbacked = true + return session.tx.Rollback() } return nil } // Commit When using transaction, Commit will commit all operations. func (session *Session) Commit() error { - if !session.IsAutoCommit && !session.IsCommitedOrRollbacked { + if !session.isAutoCommit && !session.isCommitedOrRollbacked { session.saveLastSQL("COMMIT") - session.IsCommitedOrRollbacked = true + session.isCommitedOrRollbacked = true var err error - if err = session.Tx.Commit(); err == nil { + if err = session.tx.Commit(); err == nil { // handle processors after tx committed - closureCallFunc := func(closuresPtr *[]func(interface{}), bean interface{}) { - if closuresPtr != nil { for _, closure := range *closuresPtr { closure(bean) diff --git a/vendor/github.com/go-xorm/xorm/session_update.go b/vendor/github.com/go-xorm/xorm/session_update.go index 0f2d1b5cefb..f558745667f 100644 --- a/vendor/github.com/go-xorm/xorm/session_update.go +++ b/vendor/github.com/go-xorm/xorm/session_update.go @@ -15,20 +15,20 @@ import ( "github.com/go-xorm/core" ) -func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { - if session.Statement.RefTable == nil || - session.Tx != nil { +func (session *Session) cacheUpdate(table *core.Table, tableName, sqlStr string, args ...interface{}) error { + if table == nil || + session.tx != nil { return ErrCacheFailed } - oldhead, newsql := session.Statement.convertUpdateSQL(sqlStr) + oldhead, newsql := session.statement.convertUpdateSQL(sqlStr) if newsql == "" { return ErrCacheFailed } - for _, filter := range session.Engine.dialect.Filters() { - newsql = filter.Do(newsql, session.Engine.dialect, session.Statement.RefTable) + for _, filter := range session.engine.dialect.Filters() { + newsql = filter.Do(newsql, session.engine.dialect, table) } - session.Engine.logger.Debug("[cacheUpdate] new sql", oldhead, newsql) + session.engine.logger.Debug("[cacheUpdate] new sql", oldhead, newsql) var nStart int if len(args) > 0 { @@ -39,13 +39,12 @@ func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { nStart = strings.Count(oldhead, "$") } } - table := session.Statement.RefTable - cacher := session.Engine.getCacher2(table) - tableName := session.Statement.TableName() - session.Engine.logger.Debug("[cacheUpdate] get cache sql", newsql, args[nStart:]) + + cacher := session.engine.getCacher2(table) + session.engine.logger.Debug("[cacheUpdate] get cache sql", newsql, args[nStart:]) ids, err := core.GetCacheSql(cacher, tableName, newsql, args[nStart:]) if err != nil { - rows, err := session.DB().Query(newsql, args[nStart:]...) + rows, err := session.NoCache().queryRows(newsql, args[nStart:]...) if err != nil { return err } @@ -75,9 +74,9 @@ func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { ids = append(ids, pk) } - session.Engine.logger.Debug("[cacheUpdate] find updated id", ids) + session.engine.logger.Debug("[cacheUpdate] find updated id", ids) } /*else { - session.Engine.LogDebug("[xorm:cacheUpdate] del cached sql:", tableName, newsql, args) + session.engine.LogDebug("[xorm:cacheUpdate] del cached sql:", tableName, newsql, args) cacher.DelIds(tableName, genSqlKey(newsql, args)) }*/ @@ -103,36 +102,36 @@ func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { colName := sps2[len(sps2)-1] if strings.Contains(colName, "`") { colName = strings.TrimSpace(strings.Replace(colName, "`", "", -1)) - } else if strings.Contains(colName, session.Engine.QuoteStr()) { - colName = strings.TrimSpace(strings.Replace(colName, session.Engine.QuoteStr(), "", -1)) + } else if strings.Contains(colName, session.engine.QuoteStr()) { + colName = strings.TrimSpace(strings.Replace(colName, session.engine.QuoteStr(), "", -1)) } else { - session.Engine.logger.Debug("[cacheUpdate] cannot find column", tableName, colName) + session.engine.logger.Debug("[cacheUpdate] cannot find column", tableName, colName) return ErrCacheFailed } if col := table.GetColumn(colName); col != nil { fieldValue, err := col.ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } else { - session.Engine.logger.Debug("[cacheUpdate] set bean field", bean, colName, fieldValue.Interface()) - if col.IsVersion && session.Statement.checkVersion { + session.engine.logger.Debug("[cacheUpdate] set bean field", bean, colName, fieldValue.Interface()) + if col.IsVersion && session.statement.checkVersion { fieldValue.SetInt(fieldValue.Int() + 1) } else { fieldValue.Set(reflect.ValueOf(args[idx])) } } } else { - session.Engine.logger.Errorf("[cacheUpdate] ERROR: column %v is not table %v's", + session.engine.logger.Errorf("[cacheUpdate] ERROR: column %v is not table %v's", colName, table.Name) } } - session.Engine.logger.Debug("[cacheUpdate] update cache", tableName, id, bean) + session.engine.logger.Debug("[cacheUpdate] update cache", tableName, id, bean) cacher.PutBean(tableName, sid, bean) } } - session.Engine.logger.Debug("[cacheUpdate] clear cached table sql:", tableName) + session.engine.logger.Debug("[cacheUpdate] clear cached table sql:", tableName) cacher.ClearIds(tableName) return nil } @@ -144,8 +143,7 @@ func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { // You should call UseBool if you have bool to use. // 2.float32 & float64 may be not inexact as conditions func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int64, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } @@ -169,19 +167,21 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 var isMap = t.Kind() == reflect.Map var isStruct = t.Kind() == reflect.Struct if isStruct { - session.Statement.setRefValue(v) + if err := session.statement.setRefValue(v); err != nil { + return 0, err + } - if len(session.Statement.TableName()) <= 0 { + if len(session.statement.TableName()) <= 0 { return 0, ErrTableNotFound } - if session.Statement.ColumnStr == "" { - colNames, args = buildUpdates(session.Engine, session.Statement.RefTable, bean, false, false, - false, false, session.Statement.allUseBool, session.Statement.useAllCols, - session.Statement.mustColumnMap, session.Statement.nullableMap, - session.Statement.columnMap, true, session.Statement.unscoped) + if session.statement.ColumnStr == "" { + colNames, args = buildUpdates(session.engine, session.statement.RefTable, bean, false, false, + false, false, session.statement.allUseBool, session.statement.useAllCols, + session.statement.mustColumnMap, session.statement.nullableMap, + session.statement.columnMap, true, session.statement.unscoped) } else { - colNames, args, err = genCols(session.Statement.RefTable, session, bean, true, true) + colNames, args, err = genCols(session.statement.RefTable, session, bean, true, true) if err != nil { return 0, err } @@ -192,68 +192,84 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 bValue := reflect.Indirect(reflect.ValueOf(bean)) for _, v := range bValue.MapKeys() { - colNames = append(colNames, session.Engine.Quote(v.String())+" = ?") + colNames = append(colNames, session.engine.Quote(v.String())+" = ?") args = append(args, bValue.MapIndex(v).Interface()) } } else { return 0, ErrParamsType } - table := session.Statement.RefTable + table := session.statement.RefTable - if session.Statement.UseAutoTime && table != nil && table.Updated != "" { - colNames = append(colNames, session.Engine.Quote(table.Updated)+" = ?") - col := table.UpdatedColumn() - val, t := session.Engine.NowTime2(col.SQLType.Name) - args = append(args, val) + if session.statement.UseAutoTime && table != nil && table.Updated != "" { + if _, ok := session.statement.columnMap[strings.ToLower(table.Updated)]; !ok { + colNames = append(colNames, session.engine.Quote(table.Updated)+" = ?") + col := table.UpdatedColumn() + val, t := session.engine.nowTime(col) + args = append(args, val) - var colName = col.Name - if isStruct { - session.afterClosures = append(session.afterClosures, func(bean interface{}) { - col := table.GetColumn(colName) - setColumnTime(bean, col, t) - }) + var colName = col.Name + if isStruct { + session.afterClosures = append(session.afterClosures, func(bean interface{}) { + col := table.GetColumn(colName) + setColumnTime(bean, col, t) + }) + } } } //for update action to like "column = column + ?" - incColumns := session.Statement.getInc() + incColumns := session.statement.getInc() for _, v := range incColumns { - colNames = append(colNames, session.Engine.Quote(v.colName)+" = "+session.Engine.Quote(v.colName)+" + ?") + colNames = append(colNames, session.engine.Quote(v.colName)+" = "+session.engine.Quote(v.colName)+" + ?") args = append(args, v.arg) } //for update action to like "column = column - ?" - decColumns := session.Statement.getDec() + decColumns := session.statement.getDec() for _, v := range decColumns { - colNames = append(colNames, session.Engine.Quote(v.colName)+" = "+session.Engine.Quote(v.colName)+" - ?") + colNames = append(colNames, session.engine.Quote(v.colName)+" = "+session.engine.Quote(v.colName)+" - ?") args = append(args, v.arg) } //for update action to like "column = expression" - exprColumns := session.Statement.getExpr() + exprColumns := session.statement.getExpr() for _, v := range exprColumns { - colNames = append(colNames, session.Engine.Quote(v.colName)+" = "+v.expr) + colNames = append(colNames, session.engine.Quote(v.colName)+" = "+v.expr) } - session.Statement.processIDParam() + if err = session.statement.processIDParam(); err != nil { + return 0, err + } var autoCond builder.Cond - if !session.Statement.noAutoCondition && len(condiBean) > 0 { - var err error - autoCond, err = session.Statement.buildConds(session.Statement.RefTable, condiBean[0], true, true, false, true, false) - if err != nil { - return 0, err + if !session.statement.noAutoCondition && len(condiBean) > 0 { + if c, ok := condiBean[0].(map[string]interface{}); ok { + autoCond = builder.Eq(c) + } else { + ct := reflect.TypeOf(condiBean[0]) + k := ct.Kind() + if k == reflect.Ptr { + k = ct.Elem().Kind() + } + if k == reflect.Struct { + var err error + autoCond, err = session.statement.buildConds(session.statement.RefTable, condiBean[0], true, true, false, true, false) + if err != nil { + return 0, err + } + } else { + return 0, ErrConditionType + } } } - st := session.Statement - defer session.resetStatement() + st := &session.statement var sqlStr string var condArgs []interface{} var condSQL string - cond := session.Statement.cond.And(autoCond) + cond := session.statement.cond.And(autoCond) - var doIncVer = (table != nil && table.Version != "" && session.Statement.checkVersion) + var doIncVer = (table != nil && table.Version != "" && session.statement.checkVersion) var verValue *reflect.Value if doIncVer { verValue, err = table.VersionColumn().ValueOf(bean) @@ -261,11 +277,15 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 return 0, err } - cond = cond.And(builder.Eq{session.Engine.Quote(table.Version): verValue.Interface()}) - colNames = append(colNames, session.Engine.Quote(table.Version)+" = "+session.Engine.Quote(table.Version)+" + 1") + cond = cond.And(builder.Eq{session.engine.Quote(table.Version): verValue.Interface()}) + colNames = append(colNames, session.engine.Quote(table.Version)+" = "+session.engine.Quote(table.Version)+" + 1") + } + + condSQL, condArgs, err = builder.ToSQL(cond) + if err != nil { + return 0, err } - condSQL, condArgs, _ = builder.ToSQL(cond) if len(condSQL) > 0 { condSQL = "WHERE " + condSQL } @@ -274,6 +294,7 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 condSQL = condSQL + fmt.Sprintf(" ORDER BY %v", st.OrderStr) } + var tableName = session.statement.TableName() // TODO: Oracle support needed var top string if st.LimitN > 0 { @@ -282,27 +303,53 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 } else if st.Engine.dialect.DBType() == core.SQLITE { tempCondSQL := condSQL + fmt.Sprintf(" LIMIT %d", st.LimitN) cond = cond.And(builder.Expr(fmt.Sprintf("rowid IN (SELECT rowid FROM %v %v)", - session.Engine.Quote(session.Statement.TableName()), tempCondSQL), condArgs...)) - condSQL, condArgs, _ = builder.ToSQL(cond) + session.engine.Quote(tableName), tempCondSQL), condArgs...)) + condSQL, condArgs, err = builder.ToSQL(cond) + if err != nil { + return 0, err + } if len(condSQL) > 0 { condSQL = "WHERE " + condSQL } } else if st.Engine.dialect.DBType() == core.POSTGRES { tempCondSQL := condSQL + fmt.Sprintf(" LIMIT %d", st.LimitN) cond = cond.And(builder.Expr(fmt.Sprintf("CTID IN (SELECT CTID FROM %v %v)", - session.Engine.Quote(session.Statement.TableName()), tempCondSQL), condArgs...)) - condSQL, condArgs, _ = builder.ToSQL(cond) + session.engine.Quote(tableName), tempCondSQL), condArgs...)) + condSQL, condArgs, err = builder.ToSQL(cond) + if err != nil { + return 0, err + } + if len(condSQL) > 0 { condSQL = "WHERE " + condSQL } } else if st.Engine.dialect.DBType() == core.MSSQL { - top = fmt.Sprintf("top (%d) ", st.LimitN) + if st.OrderStr != "" && st.Engine.dialect.DBType() == core.MSSQL && + table != nil && len(table.PrimaryKeys) == 1 { + cond = builder.Expr(fmt.Sprintf("%s IN (SELECT TOP (%d) %s FROM %v%v)", + table.PrimaryKeys[0], st.LimitN, table.PrimaryKeys[0], + session.engine.Quote(tableName), condSQL), condArgs...) + + condSQL, condArgs, err = builder.ToSQL(cond) + if err != nil { + return 0, err + } + if len(condSQL) > 0 { + condSQL = "WHERE " + condSQL + } + } else { + top = fmt.Sprintf("TOP (%d) ", st.LimitN) + } } } + if len(colNames) <= 0 { + return 0, errors.New("No content found to be updated") + } + sqlStr = fmt.Sprintf("UPDATE %v%v SET %v %v", top, - session.Engine.Quote(session.Statement.TableName()), + session.engine.Quote(tableName), strings.Join(colNames, ", "), condSQL) @@ -316,19 +363,20 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 } if table != nil { - if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache { - cacher.ClearIds(session.Statement.TableName()) - cacher.ClearBeans(session.Statement.TableName()) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + //session.cacheUpdate(table, tableName, sqlStr, args...) + cacher.ClearIds(tableName) + cacher.ClearBeans(tableName) } } // handle after update processors - if session.IsAutoCommit { + if session.isAutoCommit { for _, closure := range session.afterClosures { closure(bean) } if processor, ok := interface{}(bean).(AfterUpdateProcessor); ok { - session.Engine.logger.Debug("[event]", session.Statement.TableName(), " has after update processor") + session.engine.logger.Debug("[event]", tableName, " has after update processor") processor.AfterUpdate() } } else { diff --git a/vendor/github.com/go-xorm/xorm/statement.go b/vendor/github.com/go-xorm/xorm/statement.go index 82101ff20e9..6400425b20e 100644 --- a/vendor/github.com/go-xorm/xorm/statement.go +++ b/vendor/github.com/go-xorm/xorm/statement.go @@ -73,6 +73,7 @@ type Statement struct { decrColumns map[string]decrParam exprColumns map[string]exprParam cond builder.Cond + bufferSize int } // Init reset all the statement's fields @@ -111,6 +112,7 @@ func (statement *Statement) Init() { statement.decrColumns = make(map[string]decrParam) statement.exprColumns = make(map[string]exprParam) statement.cond = builder.NewCond() + statement.bufferSize = 0 } // NoAutoCondition if you do not want convert bean's field as query condition, then use this function @@ -158,6 +160,9 @@ func (statement *Statement) And(query interface{}, args ...interface{}) *Stateme case string: cond := builder.Expr(query.(string), args...) statement.cond = statement.cond.And(cond) + case map[string]interface{}: + cond := builder.Eq(query.(map[string]interface{})) + statement.cond = statement.cond.And(cond) case builder.Cond: cond := query.(builder.Cond) statement.cond = statement.cond.And(cond) @@ -179,6 +184,9 @@ func (statement *Statement) Or(query interface{}, args ...interface{}) *Statemen case string: cond := builder.Expr(query.(string), args...) statement.cond = statement.cond.Or(cond) + case map[string]interface{}: + cond := builder.Eq(query.(map[string]interface{})) + statement.cond = statement.cond.Or(cond) case builder.Cond: cond := query.(builder.Cond) statement.cond = statement.cond.Or(cond) @@ -207,9 +215,14 @@ func (statement *Statement) NotIn(column string, args ...interface{}) *Statement return statement } -func (statement *Statement) setRefValue(v reflect.Value) { - statement.RefTable = statement.Engine.autoMapType(reflect.Indirect(v)) +func (statement *Statement) setRefValue(v reflect.Value) error { + var err error + statement.RefTable, err = statement.Engine.autoMapType(reflect.Indirect(v)) + if err != nil { + return err + } statement.tableName = statement.Engine.tbName(v) + return nil } // Table tempororily set table name, the parameter could be a string or a pointer of struct @@ -219,7 +232,12 @@ func (statement *Statement) Table(tableNameOrBean interface{}) *Statement { if t.Kind() == reflect.String { statement.AltTableName = tableNameOrBean.(string) } else if t.Kind() == reflect.Struct { - statement.RefTable = statement.Engine.autoMapType(v) + var err error + statement.RefTable, err = statement.Engine.autoMapType(v) + if err != nil { + statement.Engine.logger.Error(err) + return statement + } statement.AltTableName = statement.Engine.tbName(v) } return statement @@ -262,6 +280,9 @@ func buildUpdates(engine *Engine, table *core.Table, bean interface{}, fieldValue := *fieldValuePtr fieldType := reflect.TypeOf(fieldValue.Interface()) + if fieldType == nil { + continue + } requiredField := useAllCols includeNil := useAllCols @@ -366,7 +387,7 @@ func buildUpdates(engine *Engine, table *core.Table, bean interface{}, if !requiredField && (t.IsZero() || !fieldValue.IsValid()) { continue } - val = engine.FormatTime(col.SQLType.Name, t) + val = engine.formatColTime(col, t) } else if nulType, ok := fieldValue.Interface().(driver.Valuer); ok { val, _ = nulType.Value() } else { @@ -480,224 +501,6 @@ func (statement *Statement) colName(col *core.Column, tableName string) string { return statement.Engine.Quote(col.Name) } -func buildConds(engine *Engine, table *core.Table, bean interface{}, - includeVersion bool, includeUpdated bool, includeNil bool, - includeAutoIncr bool, allUseBool bool, useAllCols bool, unscoped bool, - mustColumnMap map[string]bool, tableName, aliasName string, addedTableName bool) (builder.Cond, error) { - var conds []builder.Cond - for _, col := range table.Columns() { - if !includeVersion && col.IsVersion { - continue - } - if !includeUpdated && col.IsUpdated { - continue - } - if !includeAutoIncr && col.IsAutoIncrement { - continue - } - - if engine.dialect.DBType() == core.MSSQL && (col.SQLType.Name == core.Text || col.SQLType.IsBlob() || col.SQLType.Name == core.TimeStampz) { - continue - } - if col.SQLType.IsJson() { - continue - } - - var colName string - if addedTableName { - var nm = tableName - if len(aliasName) > 0 { - nm = aliasName - } - colName = engine.Quote(nm) + "." + engine.Quote(col.Name) - } else { - colName = engine.Quote(col.Name) - } - - fieldValuePtr, err := col.ValueOf(bean) - if err != nil { - engine.logger.Error(err) - continue - } - - if col.IsDeleted && !unscoped { // tag "deleted" is enabled - if engine.dialect.DBType() == core.MSSQL { - conds = append(conds, builder.IsNull{colName}) - } else { - conds = append(conds, builder.IsNull{colName}.Or(builder.Eq{colName: "0001-01-01 00:00:00"})) - } - } - - fieldValue := *fieldValuePtr - if fieldValue.Interface() == nil { - continue - } - - fieldType := reflect.TypeOf(fieldValue.Interface()) - requiredField := useAllCols - - if b, ok := getFlagForColumn(mustColumnMap, col); ok { - if b { - requiredField = true - } else { - continue - } - } - - if fieldType.Kind() == reflect.Ptr { - if fieldValue.IsNil() { - if includeNil { - conds = append(conds, builder.Eq{colName: nil}) - } - continue - } else if !fieldValue.IsValid() { - continue - } else { - // dereference ptr type to instance type - fieldValue = fieldValue.Elem() - fieldType = reflect.TypeOf(fieldValue.Interface()) - requiredField = true - } - } - - var val interface{} - switch fieldType.Kind() { - case reflect.Bool: - if allUseBool || requiredField { - val = fieldValue.Interface() - } else { - // if a bool in a struct, it will not be as a condition because it default is false, - // please use Where() instead - continue - } - case reflect.String: - if !requiredField && fieldValue.String() == "" { - continue - } - // for MyString, should convert to string or panic - if fieldType.String() != reflect.String.String() { - val = fieldValue.String() - } else { - val = fieldValue.Interface() - } - case reflect.Int8, reflect.Int16, reflect.Int, reflect.Int32, reflect.Int64: - if !requiredField && fieldValue.Int() == 0 { - continue - } - val = fieldValue.Interface() - case reflect.Float32, reflect.Float64: - if !requiredField && fieldValue.Float() == 0.0 { - continue - } - val = fieldValue.Interface() - case reflect.Uint8, reflect.Uint16, reflect.Uint, reflect.Uint32, reflect.Uint64: - if !requiredField && fieldValue.Uint() == 0 { - continue - } - t := int64(fieldValue.Uint()) - val = reflect.ValueOf(&t).Interface() - case reflect.Struct: - if fieldType.ConvertibleTo(core.TimeType) { - t := fieldValue.Convert(core.TimeType).Interface().(time.Time) - if !requiredField && (t.IsZero() || !fieldValue.IsValid()) { - continue - } - val = engine.FormatTime(col.SQLType.Name, t) - } else if _, ok := reflect.New(fieldType).Interface().(core.Conversion); ok { - continue - } else if valNul, ok := fieldValue.Interface().(driver.Valuer); ok { - val, _ = valNul.Value() - if val == nil { - continue - } - } else { - if col.SQLType.IsJson() { - if col.SQLType.IsText() { - bytes, err := json.Marshal(fieldValue.Interface()) - if err != nil { - engine.logger.Error(err) - continue - } - val = string(bytes) - } else if col.SQLType.IsBlob() { - var bytes []byte - var err error - bytes, err = json.Marshal(fieldValue.Interface()) - if err != nil { - engine.logger.Error(err) - continue - } - val = bytes - } - } else { - engine.autoMapType(fieldValue) - if table, ok := engine.Tables[fieldValue.Type()]; ok { - if len(table.PrimaryKeys) == 1 { - pkField := reflect.Indirect(fieldValue).FieldByName(table.PKColumns()[0].FieldName) - // fix non-int pk issues - //if pkField.Int() != 0 { - if pkField.IsValid() && !isZero(pkField.Interface()) { - val = pkField.Interface() - } else { - continue - } - } else { - //TODO: how to handler? - panic(fmt.Sprintln("not supported", fieldValue.Interface(), "as", table.PrimaryKeys)) - } - } else { - val = fieldValue.Interface() - } - } - } - case reflect.Array: - continue - case reflect.Slice, reflect.Map: - if fieldValue == reflect.Zero(fieldType) { - continue - } - if fieldValue.IsNil() || !fieldValue.IsValid() || fieldValue.Len() == 0 { - continue - } - - if col.SQLType.IsText() { - bytes, err := json.Marshal(fieldValue.Interface()) - if err != nil { - engine.logger.Error(err) - continue - } - val = string(bytes) - } else if col.SQLType.IsBlob() { - var bytes []byte - var err error - if (fieldType.Kind() == reflect.Array || fieldType.Kind() == reflect.Slice) && - fieldType.Elem().Kind() == reflect.Uint8 { - if fieldValue.Len() > 0 { - val = fieldValue.Bytes() - } else { - continue - } - } else { - bytes, err = json.Marshal(fieldValue.Interface()) - if err != nil { - engine.logger.Error(err) - continue - } - val = bytes - } - } else { - continue - } - default: - val = fieldValue.Interface() - } - - conds = append(conds, builder.Eq{colName: val}) - } - - return builder.And(conds...), nil -} - // TableName return current tableName func (statement *Statement) TableName() string { if statement.AltTableName != "" { @@ -800,6 +603,22 @@ func (statement *Statement) col2NewColsWithQuote(columns ...string) []string { return newColumns } +func (statement *Statement) colmap2NewColsWithQuote() []string { + newColumns := make([]string, 0, len(statement.columnMap)) + for col := range statement.columnMap { + fields := strings.Split(strings.TrimSpace(col), ".") + if len(fields) == 1 { + newColumns = append(newColumns, statement.Engine.quote(fields[0])) + } else if len(fields) == 2 { + newColumns = append(newColumns, statement.Engine.quote(fields[0])+"."+ + statement.Engine.quote(fields[1])) + } else { + panic(errors.New("unwanted colnames")) + } + } + return newColumns +} + // Distinct generates "DISTINCT col1, col2 " statement func (statement *Statement) Distinct(columns ...string) *Statement { statement.IsDistinct = true @@ -826,7 +645,7 @@ func (statement *Statement) Cols(columns ...string) *Statement { statement.columnMap[strings.ToLower(nc)] = true } - newColumns := statement.col2NewColsWithQuote(columns...) + newColumns := statement.colmap2NewColsWithQuote() statement.ColumnStr = strings.Join(newColumns, ", ") statement.ColumnStr = strings.Replace(statement.ColumnStr, statement.Engine.quote("*"), "*", -1) return statement @@ -1088,33 +907,50 @@ func (statement *Statement) genDelIndexSQL() []string { func (statement *Statement) genAddColumnStr(col *core.Column) (string, []interface{}) { quote := statement.Engine.Quote - sql := fmt.Sprintf("ALTER TABLE %v ADD %v;", quote(statement.TableName()), + sql := fmt.Sprintf("ALTER TABLE %v ADD %v", quote(statement.TableName()), col.String(statement.Engine.dialect)) + if statement.Engine.dialect.DBType() == core.MYSQL && len(col.Comment) > 0 { + sql += " COMMENT '" + col.Comment + "'" + } + sql += ";" return sql, []interface{}{} } func (statement *Statement) buildConds(table *core.Table, bean interface{}, includeVersion bool, includeUpdated bool, includeNil bool, includeAutoIncr bool, addedTableName bool) (builder.Cond, error) { - return buildConds(statement.Engine, table, bean, includeVersion, includeUpdated, includeNil, includeAutoIncr, statement.allUseBool, statement.useAllCols, + return statement.Engine.buildConds(table, bean, includeVersion, includeUpdated, includeNil, includeAutoIncr, statement.allUseBool, statement.useAllCols, statement.unscoped, statement.mustColumnMap, statement.TableName(), statement.TableAlias, addedTableName) } -func (statement *Statement) genConds(bean interface{}) (string, []interface{}, error) { +func (statement *Statement) mergeConds(bean interface{}) error { if !statement.noAutoCondition { var addedTableName = (len(statement.JoinStr) > 0) autoCond, err := statement.buildConds(statement.RefTable, bean, true, true, false, true, addedTableName) if err != nil { - return "", nil, err + return err } statement.cond = statement.cond.And(autoCond) } - statement.processIDParam() + if err := statement.processIDParam(); err != nil { + return err + } + return nil +} + +func (statement *Statement) genConds(bean interface{}) (string, []interface{}, error) { + if err := statement.mergeConds(bean); err != nil { + return "", nil, err + } return builder.ToSQL(statement.cond) } -func (statement *Statement) genGetSQL(bean interface{}) (string, []interface{}) { - statement.setRefValue(rValue(bean)) +func (statement *Statement) genGetSQL(bean interface{}) (string, []interface{}, error) { + v := rValue(bean) + isStruct := v.Kind() == reflect.Struct + if isStruct { + statement.setRefValue(v) + } var columnStr = statement.ColumnStr if len(statement.selectStr) > 0 { @@ -1133,22 +969,46 @@ func (statement *Statement) genGetSQL(bean interface{}) (string, []interface{}) if len(columnStr) == 0 { if len(statement.GroupByStr) > 0 { columnStr = statement.Engine.Quote(strings.Replace(statement.GroupByStr, ",", statement.Engine.Quote(","), -1)) - } else { - columnStr = "*" } } } } - condSQL, condArgs, _ := statement.genConds(bean) + if len(columnStr) == 0 { + columnStr = "*" + } - return statement.genSelectSQL(columnStr, condSQL), append(statement.joinArgs, condArgs...) + if isStruct { + if err := statement.mergeConds(bean); err != nil { + return "", nil, err + } + } + condSQL, condArgs, err := builder.ToSQL(statement.cond) + if err != nil { + return "", nil, err + } + + sqlStr, err := statement.genSelectSQL(columnStr, condSQL) + if err != nil { + return "", nil, err + } + + return sqlStr, append(statement.joinArgs, condArgs...), nil } -func (statement *Statement) genCountSQL(bean interface{}) (string, []interface{}) { - statement.setRefValue(rValue(bean)) - - condSQL, condArgs, _ := statement.genConds(bean) +func (statement *Statement) genCountSQL(beans ...interface{}) (string, []interface{}, error) { + var condSQL string + var condArgs []interface{} + var err error + if len(beans) > 0 { + statement.setRefValue(rValue(beans[0])) + condSQL, condArgs, err = statement.genConds(beans[0]) + } else { + condSQL, condArgs, err = builder.ToSQL(statement.cond) + } + if err != nil { + return "", nil, err + } var selectSQL = statement.selectStr if len(selectSQL) <= 0 { @@ -1158,23 +1018,40 @@ func (statement *Statement) genCountSQL(bean interface{}) (string, []interface{} selectSQL = "count(*)" } } - return statement.genSelectSQL(selectSQL, condSQL), append(statement.joinArgs, condArgs...) + sqlStr, err := statement.genSelectSQL(selectSQL, condSQL) + if err != nil { + return "", nil, err + } + + return sqlStr, append(statement.joinArgs, condArgs...), nil } -func (statement *Statement) genSumSQL(bean interface{}, columns ...string) (string, []interface{}) { +func (statement *Statement) genSumSQL(bean interface{}, columns ...string) (string, []interface{}, error) { statement.setRefValue(rValue(bean)) var sumStrs = make([]string, 0, len(columns)) for _, colName := range columns { - sumStrs = append(sumStrs, fmt.Sprintf("COALESCE(sum(%s),0)", statement.Engine.Quote(colName))) + if !strings.Contains(colName, " ") && !strings.Contains(colName, "(") { + colName = statement.Engine.Quote(colName) + } + sumStrs = append(sumStrs, fmt.Sprintf("COALESCE(sum(%s),0)", colName)) + } + sumSelect := strings.Join(sumStrs, ", ") + + condSQL, condArgs, err := statement.genConds(bean) + if err != nil { + return "", nil, err } - condSQL, condArgs, _ := statement.genConds(bean) + sqlStr, err := statement.genSelectSQL(sumSelect, condSQL) + if err != nil { + return "", nil, err + } - return statement.genSelectSQL(strings.Join(sumStrs, ", "), condSQL), append(statement.joinArgs, condArgs...) + return sqlStr, append(statement.joinArgs, condArgs...), nil } -func (statement *Statement) genSelectSQL(columnStr, condSQL string) (a string) { +func (statement *Statement) genSelectSQL(columnStr, condSQL string) (a string, err error) { var distinct string if statement.IsDistinct && !strings.HasPrefix(columnStr, "count") { distinct = "DISTINCT " @@ -1185,15 +1062,23 @@ func (statement *Statement) genSelectSQL(columnStr, condSQL string) (a string) { var top string var mssqlCondi string - statement.processIDParam() + if err := statement.processIDParam(); err != nil { + return "", err + } var buf bytes.Buffer if len(condSQL) > 0 { fmt.Fprintf(&buf, " WHERE %v", condSQL) } var whereStr = buf.String() + var fromStr = " FROM " + + if dialect.DBType() == core.MSSQL && strings.Contains(statement.TableName(), "..") { + fromStr += statement.TableName() + } else { + fromStr += quote(statement.TableName()) + } - var fromStr = " FROM " + quote(statement.TableName()) if statement.TableAlias != "" { if dialect.DBType() == core.ORACLE { fromStr += " " + quote(statement.TableAlias) @@ -1246,7 +1131,7 @@ func (statement *Statement) genSelectSQL(columnStr, condSQL string) (a string) { } // !nashtsai! REVIEW Sprintf is considered slowest mean of string concatnation, better to work with builder pattern - a = fmt.Sprintf("SELECT %v%v%v%v%v", top, distinct, columnStr, fromStr, whereStr) + a = fmt.Sprintf("SELECT %v%v%v%v%v", distinct, top, columnStr, fromStr, whereStr) if len(mssqlCondi) > 0 { if len(whereStr) > 0 { a += " AND " + mssqlCondi @@ -1282,19 +1167,23 @@ func (statement *Statement) genSelectSQL(columnStr, condSQL string) (a string) { return } -func (statement *Statement) processIDParam() { +func (statement *Statement) processIDParam() error { if statement.idParam == nil { - return + return nil + } + + if len(statement.RefTable.PrimaryKeys) != len(*statement.idParam) { + return fmt.Errorf("ID condition is error, expect %d primarykeys, there are %d", + len(statement.RefTable.PrimaryKeys), + len(*statement.idParam), + ) } for i, col := range statement.RefTable.PKColumns() { var colName = statement.colName(col, statement.TableName()) - if i < len(*(statement.idParam)) { - statement.cond = statement.cond.And(builder.Eq{colName: (*(statement.idParam))[i]}) - } else { - statement.cond = statement.cond.And(builder.Eq{colName: ""}) - } + statement.cond = statement.cond.And(builder.Eq{colName: (*(statement.idParam))[i]}) } + return nil } func (statement *Statement) joinColumns(cols []*core.Column, includeTableName bool) string { @@ -1328,7 +1217,8 @@ func (statement *Statement) convertIDSQL(sqlStr string) string { top = fmt.Sprintf("TOP %d ", statement.LimitN) } - return fmt.Sprintf("SELECT %s%s FROM %v", top, colstrs, sqls[1]) + newsql := fmt.Sprintf("SELECT %s%s FROM %v", top, colstrs, sqls[1]) + return newsql } return "" } diff --git a/vendor/github.com/go-xorm/xorm/tag.go b/vendor/github.com/go-xorm/xorm/tag.go index 4b0e3f54a57..e1c821fb540 100644 --- a/vendor/github.com/go-xorm/xorm/tag.go +++ b/vendor/github.com/go-xorm/xorm/tag.go @@ -54,6 +54,7 @@ var ( "UNIQUE": UniqueTagHandler, "CACHE": CacheTagHandler, "NOCACHE": NoCacheTagHandler, + "COMMENT": CommentTagHandler, } ) @@ -192,6 +193,14 @@ func UniqueTagHandler(ctx *tagContext) error { return nil } +// CommentTagHandler add comment to column +func CommentTagHandler(ctx *tagContext) error { + if len(ctx.params) > 0 { + ctx.col.Comment = strings.Trim(ctx.params[0], "' ") + } + return nil +} + // SQLTypeTagHandler describes SQL Type tag handler func SQLTypeTagHandler(ctx *tagContext) error { ctx.col.SQLType = core.SQLType{Name: ctx.tagName} diff --git a/vendor/github.com/go-xorm/xorm/xorm.go b/vendor/github.com/go-xorm/xorm/xorm.go index 2cfbe9ecd31..4fdadf2fade 100644 --- a/vendor/github.com/go-xorm/xorm/xorm.go +++ b/vendor/github.com/go-xorm/xorm/xorm.go @@ -17,7 +17,7 @@ import ( const ( // Version show the xorm's version - Version string = "0.6.2.0326" + Version string = "0.6.4.0910" ) func regDrvsNDialects() bool { @@ -50,10 +50,13 @@ func close(engine *Engine) { engine.Close() } +func init() { + regDrvsNDialects() +} + // NewEngine new a db manager according to the parameter. Currently support four // drivers func NewEngine(driverName string, dataSourceName string) (*Engine, error) { - regDrvsNDialects() driver := core.QueryDriver(driverName) if driver == nil { return nil, fmt.Errorf("Unsupported driver name: %v", driverName) @@ -89,6 +92,12 @@ func NewEngine(driverName string, dataSourceName string) (*Engine, error) { tagHandlers: defaultTagHandlers, } + if uri.DbType == core.SQLITE { + engine.DatabaseTZ = time.UTC + } else { + engine.DatabaseTZ = time.Local + } + logger := NewSimpleLogger(os.Stdout) logger.SetLevel(core.LOG_INFO) engine.SetLogger(logger) From 5a3ba68a9cf0cbb5f9587287028fddbd83c7a9c6 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 16 Mar 2018 00:08:25 +0100 Subject: [PATCH 17/78] database: fixes after xorm update --- pkg/services/sqlstore/alert.go | 2 +- pkg/services/sqlstore/dashboard_folder_test.go | 1 + pkg/services/sqlstore/org_test.go | 3 +++ pkg/services/sqlstore/quota.go | 13 +++++++++---- pkg/services/sqlstore/quota_test.go | 4 ++-- pkg/services/sqlstore/sqlstore.go | 8 ++++++-- 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index f449bec5849..e99367e4d6f 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -255,7 +255,7 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error { } alert.State = cmd.State - alert.StateChanges += 1 + alert.StateChanges++ alert.NewStateDate = timeNow() alert.EvalData = cmd.EvalData diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index ea8f1216706..4c92c097931 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -46,6 +46,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}, } err := SearchDashboards(query) + So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index c57d15a48d5..63b20aa6e86 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -2,6 +2,7 @@ package sqlstore import ( "testing" + "time" . "github.com/smartystreets/goconvey/convey" @@ -241,6 +242,8 @@ func TestAccountDataAccess(t *testing.T) { func testHelperUpdateDashboardAcl(dashboardId int64, items ...m.DashboardAcl) error { cmd := m.UpdateDashboardAclCommand{DashboardId: dashboardId} for _, item := range items { + item.Created = time.Now() + item.Updated = time.Now() cmd.Items = append(cmd.Items, &item) } return UpdateDashboardAcl(&cmd) diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go index 0a857efce40..3db3fc2657e 100644 --- a/pkg/services/sqlstore/quota.go +++ b/pkg/services/sqlstore/quota.go @@ -2,6 +2,7 @@ package sqlstore import ( "fmt" + "time" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" @@ -98,8 +99,9 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error { return inTransaction(func(sess *DBSession) error { //Check if quota is already defined in the DB quota := m.Quota{ - Target: cmd.Target, - OrgId: cmd.OrgId, + Target: cmd.Target, + OrgId: cmd.OrgId, + Updated: time.Now(), } has, err := sess.Get("a) if err != nil { @@ -107,6 +109,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error { } quota.Limit = cmd.Limit if has == false { + quota.Created = time.Now() //No quota in the DB for this target, so create a new one. if _, err := sess.Insert("a); err != nil { return err @@ -198,8 +201,9 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error { return inTransaction(func(sess *DBSession) error { //Check if quota is already defined in the DB quota := m.Quota{ - Target: cmd.Target, - UserId: cmd.UserId, + Target: cmd.Target, + UserId: cmd.UserId, + Updated: time.Now(), } has, err := sess.Get("a) if err != nil { @@ -207,6 +211,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error { } quota.Limit = cmd.Limit if has == false { + quota.Created = time.Now() //No quota in the DB for this target, so create a new one. if _, err := sess.Insert("a); err != nil { return err diff --git a/pkg/services/sqlstore/quota_test.go b/pkg/services/sqlstore/quota_test.go index 5ef618e166d..ed6565b1c3f 100644 --- a/pkg/services/sqlstore/quota_test.go +++ b/pkg/services/sqlstore/quota_test.go @@ -104,12 +104,12 @@ func TestQuotaCommandsAndQueries(t *testing.T) { }) }) Convey("Given saved user quota for org", func() { - userQoutaCmd := m.UpdateUserQuotaCmd{ + userQuotaCmd := m.UpdateUserQuotaCmd{ UserId: userId, Target: "org_user", Limit: 10, } - err := UpdateUserQuota(&userQoutaCmd) + err := UpdateUserQuota(&userQuotaCmd) So(err, ShouldBeNil) Convey("Should be able to get saved quota by user id and target", func() { diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 5843c5c300b..493243f5185 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" @@ -225,8 +226,8 @@ var ( func InitTestDB(t *testing.T) *xorm.Engine { selectedDb := dbSqlite - //selectedDb := dbMySql - //selectedDb := dbPostgres + // selectedDb := dbMySql + // selectedDb := dbPostgres var x *xorm.Engine var err error @@ -245,6 +246,9 @@ func InitTestDB(t *testing.T) *xorm.Engine { x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr) } + x.DatabaseTZ = time.UTC + x.TZLocation = time.UTC + // x.ShowSQL() if err != nil { From 9cdd7cb04c0114398f0d6f080c332ca968e44356 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 16 Mar 2018 00:25:15 +0100 Subject: [PATCH 18/78] database: expose SetConnMaxLifetime as config setting For MySQL, setting this to be shorter than the wait_timeout MySQL setting solves the issue with connection errors after the session has timed out for the connection to the database via xorm. --- conf/defaults.ini | 3 +++ conf/sample.ini | 3 +++ docs/sources/installation/configuration.md | 5 +++++ pkg/services/sqlstore/sqlstore.go | 26 +++++++++++++--------- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 4a2240f1924..557c5e49ee1 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -82,6 +82,9 @@ max_idle_conn = 2 # Max conn setting default is 0 (mean not set) max_open_conn = +# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) +conn_max_lifetime = 14400 + # Set to true to log the sql calls and execution times. log_queries = diff --git a/conf/sample.ini b/conf/sample.ini index 3e45ac44d61..fa30c301014 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -90,6 +90,9 @@ # Max conn setting default is 0 (mean not set) ;max_open_conn = +# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) +;conn_max_lifetime = 14400 + # Set to true to log the sql calls and execution times. log_queries = diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 66072a98f84..6169280b798 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -234,7 +234,12 @@ The maximum number of connections in the idle connection pool. ### max_open_conn The maximum number of open connections to the database. +### conn_max_lifetime + +Sets the maximum amount of time a connection may be reused. The default is 14400 (which means 14400 seconds or 4 hours). For MySQL, this setting should be shorter than the [`wait_timeout`](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_wait_timeout) variable. + ### log_queries + Set to `true` to log the sql calls and execution times.
diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 493243f5185..ae1e3fc482a 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -34,6 +34,7 @@ type DatabaseConfig struct { ServerCertName string MaxOpenConn int MaxIdleConn int + ConnMaxLifetime int } var ( @@ -158,18 +159,20 @@ func getEngine() (*xorm.Engine, error) { engine, err := xorm.NewEngine(DbCfg.Type, cnnstr) if err != nil { return nil, err - } else { - engine.SetMaxOpenConns(DbCfg.MaxOpenConn) - engine.SetMaxIdleConns(DbCfg.MaxIdleConn) - debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false) - if !debugSql { - engine.SetLogger(&xorm.DiscardLogger{}) - } else { - engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm"))) - engine.ShowSQL(true) - engine.ShowExecTime(true) - } } + + engine.SetMaxOpenConns(DbCfg.MaxOpenConn) + engine.SetMaxIdleConns(DbCfg.MaxIdleConn) + engine.SetConnMaxLifetime(time.Second * time.Duration(DbCfg.ConnMaxLifetime)) + debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false) + if !debugSql { + engine.SetLogger(&xorm.DiscardLogger{}) + } else { + engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm"))) + engine.ShowSQL(true) + engine.ShowExecTime(true) + } + return engine, nil } @@ -203,6 +206,7 @@ func LoadConfig() { } DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0) DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0) + DbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400) if DbCfg.Type == "sqlite3" { UseSQLite3 = true From 3ca1e06509eec9ea3fd3781d8ffb7264926dce4f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 15 Mar 2018 21:23:33 +0100 Subject: [PATCH 19/78] session: fork Macaron mysql session middleware This changes forks the mysql part of the Macaron session middleware. In the forked mysql file: - takes in a config setting for SetConnMaxLifetime (this solves wait_timeout problem if it is set to a shorter interval than wait_timeout) - removes the panic when an error is returned in the Exist function. - retries the exist query once - retries the GC query once --- pkg/api/common_test.go | 2 +- pkg/api/http_server.go | 2 +- pkg/middleware/middleware_test.go | 2 +- pkg/middleware/recovery_test.go | 2 +- pkg/middleware/session.go | 4 +- pkg/services/session/mysql.go | 218 ++++++++++++++++++++++++++++++ pkg/services/session/session.go | 5 +- pkg/setting/setting.go | 5 +- 8 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 pkg/services/session/mysql.go diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index e1cbd20edb3..a4a547d8bbf 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -99,7 +99,7 @@ func setupScenarioContext(url string) *scenarioContext { })) sc.m.Use(middleware.GetContextHandler()) - sc.m.Use(middleware.Sessioner(&session.Options{})) + sc.m.Use(middleware.Sessioner(&session.Options{}, 0)) return sc } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index b911780913d..c6a9286a5d8 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -175,7 +175,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron { m.Use(hs.healthHandler) m.Use(hs.metricsEndpoint) m.Use(middleware.GetContextHandler()) - m.Use(middleware.Sessioner(&setting.SessionOptions)) + m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime)) m.Use(middleware.OrgRedirect()) // needs to be after context handler diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 83efc65d4d4..c8e9e535cfa 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -338,7 +338,7 @@ func middlewareScenario(desc string, fn scenarioFunc) { sc.m.Use(GetContextHandler()) // mock out gc goroutine session.StartSessionGC = func() {} - sc.m.Use(Sessioner(&ms.Options{})) + sc.m.Use(Sessioner(&ms.Options{}, 0)) sc.m.Use(OrgRedirect()) sc.m.Use(AddDefaultResponseHeaders()) diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 32545b7caca..c63a0e81e57 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -63,7 +63,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) { sc.m.Use(GetContextHandler()) // mock out gc goroutine session.StartSessionGC = func() {} - sc.m.Use(Sessioner(&ms.Options{})) + sc.m.Use(Sessioner(&ms.Options{}, 0)) sc.m.Use(OrgRedirect()) sc.m.Use(AddDefaultResponseHeaders()) diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index 5654a42cb7d..19cfa368b49 100644 --- a/pkg/middleware/session.go +++ b/pkg/middleware/session.go @@ -8,8 +8,8 @@ import ( "github.com/grafana/grafana/pkg/services/session" ) -func Sessioner(options *ms.Options) macaron.Handler { - session.Init(options) +func Sessioner(options *ms.Options, sessionConnMaxLifetime int64) macaron.Handler { + session.Init(options, sessionConnMaxLifetime) return func(ctx *m.ReqContext) { ctx.Next() diff --git a/pkg/services/session/mysql.go b/pkg/services/session/mysql.go new file mode 100644 index 00000000000..f8c5d828cfa --- /dev/null +++ b/pkg/services/session/mysql.go @@ -0,0 +1,218 @@ +// Copyright 2013 Beego Authors +// Copyright 2014 The Macaron Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package session + +import ( + "database/sql" + "fmt" + "log" + "sync" + "time" + + _ "github.com/go-sql-driver/mysql" + + "github.com/go-macaron/session" +) + +// MysqlStore represents a mysql session store implementation. +type MysqlStore struct { + c *sql.DB + sid string + lock sync.RWMutex + data map[interface{}]interface{} +} + +// NewMysqlStore creates and returns a mysql session store. +func NewMysqlStore(c *sql.DB, sid string, kv map[interface{}]interface{}) *MysqlStore { + return &MysqlStore{ + c: c, + sid: sid, + data: kv, + } +} + +// Set sets value to given key in session. +func (s *MysqlStore) Set(key, val interface{}) error { + s.lock.Lock() + defer s.lock.Unlock() + + s.data[key] = val + return nil +} + +// Get gets value by given key in session. +func (s *MysqlStore) Get(key interface{}) interface{} { + s.lock.RLock() + defer s.lock.RUnlock() + + return s.data[key] +} + +// Delete delete a key from session. +func (s *MysqlStore) Delete(key interface{}) error { + s.lock.Lock() + defer s.lock.Unlock() + + delete(s.data, key) + return nil +} + +// ID returns current session ID. +func (s *MysqlStore) ID() string { + return s.sid +} + +// Release releases resource and save data to provider. +func (s *MysqlStore) Release() error { + data, err := session.EncodeGob(s.data) + if err != nil { + return err + } + + _, err = s.c.Exec("UPDATE session SET data=?, expiry=? WHERE `key`=?", + data, time.Now().Unix(), s.sid) + return err +} + +// Flush deletes all session data. +func (s *MysqlStore) Flush() error { + s.lock.Lock() + defer s.lock.Unlock() + + s.data = make(map[interface{}]interface{}) + return nil +} + +// MysqlProvider represents a mysql session provider implementation. +type MysqlProvider struct { + c *sql.DB + expire int64 +} + +// Init initializes mysql session provider. +// connStr: username:password@protocol(address)/dbname?param=value +func (p *MysqlProvider) Init(expire int64, connStr string) (err error) { + p.expire = expire + + p.c, err = sql.Open("mysql", connStr) + p.c.SetConnMaxLifetime(time.Second * time.Duration(sessionConnMaxLifetime)) + if err != nil { + return err + } + return p.c.Ping() +} + +// Read returns raw session store by session ID. +func (p *MysqlProvider) Read(sid string) (session.RawStore, error) { + var data []byte + err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data) + if err == sql.ErrNoRows { + _, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)", + sid, "", time.Now().Unix()) + } + if err != nil { + return nil, err + } + + var kv map[interface{}]interface{} + if len(data) == 0 { + kv = make(map[interface{}]interface{}) + } else { + kv, err = session.DecodeGob(data) + if err != nil { + return nil, err + } + } + + return NewMysqlStore(p.c, sid, kv), nil +} + +// Exist returns true if session with given ID exists. +func (p *MysqlProvider) Exist(sid string) bool { + exists, err := p.queryExists(sid) + + if err != nil { + exists, err = p.queryExists(sid) + } + + if err != nil { + log.Printf("session/mysql: error checking if session exists: %v", err) + return false + } + + return exists +} + +func (p *MysqlProvider) queryExists(sid string) (bool, error) { + var data []byte + err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data) + + if err != nil && err != sql.ErrNoRows { + return false, err + } + + return err != sql.ErrNoRows, nil +} + +// Destory deletes a session by session ID. +func (p *MysqlProvider) Destory(sid string) error { + _, err := p.c.Exec("DELETE FROM session WHERE `key`=?", sid) + return err +} + +// Regenerate regenerates a session store from old session ID to new one. +func (p *MysqlProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { + if p.Exist(sid) { + return nil, fmt.Errorf("new sid '%s' already exists", sid) + } + + if !p.Exist(oldsid) { + if _, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)", + oldsid, "", time.Now().Unix()); err != nil { + return nil, err + } + } + + if _, err = p.c.Exec("UPDATE session SET `key`=? WHERE `key`=?", sid, oldsid); err != nil { + return nil, err + } + + return p.Read(sid) +} + +// Count counts and returns number of sessions. +func (p *MysqlProvider) Count() (total int) { + if err := p.c.QueryRow("SELECT COUNT(*) AS NUM FROM session").Scan(&total); err != nil { + panic("session/mysql: error counting records: " + err.Error()) + } + return total +} + +// GC calls GC to clean expired sessions. +func (p *MysqlProvider) GC() { + var err error + if _, err = p.c.Exec("DELETE FROM session WHERE expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire); err != nil { + _, err = p.c.Exec("DELETE FROM session WHERE expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire) + } + + if err != nil { + log.Printf("session/mysql: error garbage collecting: %v", err) + } +} + +func init() { + session.Register("mysql", &MysqlProvider{}) +} diff --git a/pkg/services/session/session.go b/pkg/services/session/session.go index 2ca9296b97f..0161b2113c8 100644 --- a/pkg/services/session/session.go +++ b/pkg/services/session/session.go @@ -6,7 +6,6 @@ import ( ms "github.com/go-macaron/session" _ "github.com/go-macaron/session/memcache" - _ "github.com/go-macaron/session/mysql" _ "github.com/go-macaron/session/postgres" _ "github.com/go-macaron/session/redis" "github.com/grafana/grafana/pkg/log" @@ -25,6 +24,7 @@ var sessionOptions *ms.Options var StartSessionGC func() var GetSessionCount func() int var sessionLogger = log.New("session") +var sessionConnMaxLifetime int64 func init() { StartSessionGC = func() { @@ -37,9 +37,10 @@ func init() { } } -func Init(options *ms.Options) { +func Init(options *ms.Options, connMaxLifetime int64) { var err error sessionOptions = prepareOptions(options) + sessionConnMaxLifetime = connMaxLifetime sessionManager, err = ms.NewManager(options.Provider, *options) if err != nil { panic(err) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 6099388f668..c19043c69d0 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -131,7 +131,8 @@ var ( PluginAppsSkipVerifyTLS bool // Session settings. - SessionOptions session.Options + SessionOptions session.Options + SessionConnMaxLifetime int64 // Global setting objects. Cfg *ini.File @@ -634,6 +635,8 @@ func readSessionConfig() { if SessionOptions.CookiePath == "" { SessionOptions.CookiePath = "/" } + + SessionConnMaxLifetime = Cfg.Section("session").Key("conn_max_lifetime").MustInt64(0) } func initLogging() { From 0e5b790b54bab1e5832ade3f4d455b291d9e3d12 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 16 Mar 2018 21:03:49 +0300 Subject: [PATCH 20/78] dashboard: fix rendering link to panel in collapsed row --- .../app/features/dashboard/view_state_srv.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 148f64beab0..8cb35a8ca99 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -1,6 +1,7 @@ import angular from 'angular'; import _ from 'lodash'; import config from 'app/core/config'; +import { DashboardModel } from './dashboard_model'; // represents the transient view state // like fullscreen panel & edit @@ -8,7 +9,7 @@ export class DashboardViewState { state: any; panelScopes: any; $scope: any; - dashboard: any; + dashboard: DashboardModel; editStateChanged: any; fullscreenPanel: any; oldTimeRange: any; @@ -89,6 +90,12 @@ export class DashboardViewState { } } + if (this.state.fullscreen && this.state.panelId) { + // Trying to render panel in fullscreen when it's in the collapsed row causes an issue. + // So in this case expand collapsed row first. + this.toggleCollapsedPanelRow(this.state.panelId); + } + // if no edit state cleanup tab parm if (!this.state.edit) { delete this.state.tab; @@ -103,6 +110,19 @@ export class DashboardViewState { this.syncState(); } + toggleCollapsedPanelRow(panelId) { + for (let panel of this.dashboard.panels) { + if (panel.collapsed) { + for (let rowPanel of panel.panels) { + if (rowPanel.id === panelId) { + this.dashboard.toggleRow(panel); + return; + } + } + } + } + } + syncState() { if (this.panelScopes.length === 0) { return; From 205714759e6368dadf1bc91ea36e108638c53523 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 16 Mar 2018 21:45:31 +0300 Subject: [PATCH 21/78] fix failed tests for dashboard view state --- public/app/features/dashboard/specs/viewstate_srv_specs.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/specs/viewstate_srv_specs.ts b/public/app/features/dashboard/specs/viewstate_srv_specs.ts index 928037adc88..d34b15b9113 100644 --- a/public/app/features/dashboard/specs/viewstate_srv_specs.ts +++ b/public/app/features/dashboard/specs/viewstate_srv_specs.ts @@ -30,7 +30,10 @@ describe('when updating view state', function() { beforeEach( angularMocks.inject(function(dashboardViewStateSrv, $location, $rootScope) { $rootScope.onAppEvent = function() {}; - $rootScope.dashboard = { meta: {} }; + $rootScope.dashboard = { + meta: {}, + panels: [], + }; viewState = dashboardViewStateSrv.create($rootScope); location = $location; }) From b816f18b3d9cc01bdad02be64c6030e7644e34c6 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Mar 2018 09:23:18 +0100 Subject: [PATCH 22/78] fix: dep ensure. now without gofmt on ventor directory --- .../bradfitz/gomemcache/memcache/memcache.go | 2 +- .../github.com/denisenkom/go-mssqldb/mssql.go | 2 +- .../github.com/denisenkom/go-mssqldb/tds.go | 4 +- .../github.com/hashicorp/go-plugin/client.go | 2 +- .../hashicorp/go-plugin/rpc_client.go | 2 +- .../hashicorp/go-plugin/rpc_server.go | 2 +- .../sergi/go-diff/diffmatchpatch/diff.go | 26 ++++----- .../sergi/go-diff/diffmatchpatch/patch.go | 4 +- vendor/golang.org/x/net/http2/transport.go | 4 +- vendor/golang.org/x/text/language/gen.go | 2 +- vendor/golang.org/x/text/language/lookup.go | 56 +++++++++---------- vendor/golang.org/x/text/language/tables.go | 6 +- vendor/golang.org/x/text/unicode/cldr/cldr.go | 2 +- .../golang.org/x/text/unicode/cldr/resolve.go | 4 +- .../golang.org/x/text/unicode/cldr/slice.go | 2 +- .../x/text/unicode/norm/maketables.go | 2 +- vendor/gopkg.in/macaron.v1/context.go | 2 +- 17 files changed, 61 insertions(+), 63 deletions(-) diff --git a/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go b/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go index 8508063bc35..b98a7653467 100644 --- a/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go +++ b/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go @@ -457,7 +457,7 @@ func (c *Client) GetMulti(keys []string) (map[string]*Item, error) { } var err error - for range keyMap { + for _ = range keyMap { if ge := <-ch; ge != nil { err = ge } diff --git a/vendor/github.com/denisenkom/go-mssqldb/mssql.go b/vendor/github.com/denisenkom/go-mssqldb/mssql.go index bc773f77ccd..8a3f28f1151 100644 --- a/vendor/github.com/denisenkom/go-mssqldb/mssql.go +++ b/vendor/github.com/denisenkom/go-mssqldb/mssql.go @@ -582,7 +582,7 @@ type Rows struct { func (rc *Rows) Close() error { rc.cancel() - for range rc.tokchan { + for _ = range rc.tokchan { } rc.tokchan = nil return nil diff --git a/vendor/github.com/denisenkom/go-mssqldb/tds.go b/vendor/github.com/denisenkom/go-mssqldb/tds.go index 6519b341121..a18c406c1f8 100644 --- a/vendor/github.com/denisenkom/go-mssqldb/tds.go +++ b/vendor/github.com/denisenkom/go-mssqldb/tds.go @@ -166,7 +166,7 @@ func writePrelogin(w *tdsBuffer, fields map[uint8][]byte) error { w.BeginPacket(packPrelogin, false) offset := uint16(5*len(fields) + 1) keys := make(KeySlice, 0, len(fields)) - for k := range fields { + for k, _ := range fields { keys = append(keys, k) } sort.Sort(keys) @@ -1147,7 +1147,7 @@ func dialConnection(ctx context.Context, p connectParams) (conn net.Conn, err er } // Wait for either the *first* successful connection, or all the errors wait_loop: - for i := range ips { + for i, _ := range ips { select { case conn = <-connChan: // Got a connection to use, close any others diff --git a/vendor/github.com/hashicorp/go-plugin/client.go b/vendor/github.com/hashicorp/go-plugin/client.go index de03690703f..b912826b200 100644 --- a/vendor/github.com/hashicorp/go-plugin/client.go +++ b/vendor/github.com/hashicorp/go-plugin/client.go @@ -567,7 +567,7 @@ func (c *Client) Start() (addr net.Addr, err error) { // so they don't block since it is an io.Pipe defer func() { go func() { - for range linesCh { + for _ = range linesCh { } }() }() diff --git a/vendor/github.com/hashicorp/go-plugin/rpc_client.go b/vendor/github.com/hashicorp/go-plugin/rpc_client.go index 4d99d42c7e1..f30a4b1d387 100644 --- a/vendor/github.com/hashicorp/go-plugin/rpc_client.go +++ b/vendor/github.com/hashicorp/go-plugin/rpc_client.go @@ -75,7 +75,7 @@ func NewRPCClient(conn io.ReadWriteCloser, plugins map[string]Plugin) (*RPCClien // Connect stdout, stderr streams stdstream := make([]net.Conn, 2) - for i := range stdstream { + for i, _ := range stdstream { stdstream[i], err = mux.Open() if err != nil { mux.Close() diff --git a/vendor/github.com/hashicorp/go-plugin/rpc_server.go b/vendor/github.com/hashicorp/go-plugin/rpc_server.go index 168ef7dd944..5bb18dd5db1 100644 --- a/vendor/github.com/hashicorp/go-plugin/rpc_server.go +++ b/vendor/github.com/hashicorp/go-plugin/rpc_server.go @@ -78,7 +78,7 @@ func (s *RPCServer) ServeConn(conn io.ReadWriteCloser) { // Connect the stdstreams (in, out, err) stdstream := make([]net.Conn, 2) - for i := range stdstream { + for i, _ := range stdstream { stdstream[i], err = mux.Accept() if err != nil { mux.Close() diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go index 1857f93f226..82ad7bc8f1c 100644 --- a/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go +++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go @@ -85,7 +85,7 @@ func (dmp *DiffMatchPatch) diffMainRunes(text1, text2 []rune, checklines bool, d // Restore the prefix and suffix. if len(commonprefix) != 0 { - diffs = append([]Diff{{DiffEqual, string(commonprefix)}}, diffs...) + diffs = append([]Diff{Diff{DiffEqual, string(commonprefix)}}, diffs...) } if len(commonsuffix) != 0 { diffs = append(diffs, Diff{DiffEqual, string(commonsuffix)}) @@ -122,16 +122,16 @@ func (dmp *DiffMatchPatch) diffCompute(text1, text2 []rune, checklines bool, dea } // Shorter text is inside the longer text (speedup). return []Diff{ - {op, string(longtext[:i])}, - {DiffEqual, string(shorttext)}, - {op, string(longtext[i+len(shorttext):])}, + Diff{op, string(longtext[:i])}, + Diff{DiffEqual, string(shorttext)}, + Diff{op, string(longtext[i+len(shorttext):])}, } } else if len(shorttext) == 1 { // Single character string. // After the previous speedup, the character can't be an equality. return []Diff{ - {DiffDelete, string(text1)}, - {DiffInsert, string(text2)}, + Diff{DiffDelete, string(text1)}, + Diff{DiffInsert, string(text2)}, } // Check to see if the problem can be split in two. } else if hm := dmp.diffHalfMatch(text1, text2); hm != nil { @@ -145,7 +145,7 @@ func (dmp *DiffMatchPatch) diffCompute(text1, text2 []rune, checklines bool, dea diffsA := dmp.diffMainRunes(text1A, text2A, checklines, deadline) diffsB := dmp.diffMainRunes(text1B, text2B, checklines, deadline) // Merge the results. - return append(diffsA, append([]Diff{{DiffEqual, string(midCommon)}}, diffsB...)...) + return append(diffsA, append([]Diff{Diff{DiffEqual, string(midCommon)}}, diffsB...)...) } else if checklines && len(text1) > 100 && len(text2) > 100 { return dmp.diffLineMode(text1, text2, deadline) } @@ -330,8 +330,8 @@ func (dmp *DiffMatchPatch) diffBisect(runes1, runes2 []rune, deadline time.Time) } // Diff took too long and hit the deadline or number of diffs equals number of characters, no commonality at all. return []Diff{ - {DiffDelete, string(runes1)}, - {DiffInsert, string(runes2)}, + Diff{DiffDelete, string(runes1)}, + Diff{DiffInsert, string(runes2)}, } } @@ -673,7 +673,7 @@ func (dmp *DiffMatchPatch) DiffCleanupSemantic(diffs []Diff) []Diff { insPoint := equalities.data diffs = append( diffs[:insPoint], - append([]Diff{{DiffDelete, lastequality}}, diffs[insPoint:]...)...) + append([]Diff{Diff{DiffDelete, lastequality}}, diffs[insPoint:]...)...) // Change second copy to insert. diffs[insPoint+1].Type = DiffInsert @@ -726,7 +726,7 @@ func (dmp *DiffMatchPatch) DiffCleanupSemantic(diffs []Diff) []Diff { // Overlap found. Insert an equality and trim the surrounding edits. diffs = append( diffs[:pointer], - append([]Diff{{DiffEqual, insertion[:overlapLength1]}}, diffs[pointer:]...)...) + append([]Diff{Diff{DiffEqual, insertion[:overlapLength1]}}, diffs[pointer:]...)...) diffs[pointer-1].Text = deletion[0 : len(deletion)-overlapLength1] @@ -955,7 +955,7 @@ func (dmp *DiffMatchPatch) DiffCleanupEfficiency(diffs []Diff) []Diff { // Duplicate record. diffs = append(diffs[:insPoint], - append([]Diff{{DiffDelete, lastequality}}, diffs[insPoint:]...)...) + append([]Diff{Diff{DiffDelete, lastequality}}, diffs[insPoint:]...)...) // Change second copy to insert. diffs[insPoint+1].Type = DiffInsert @@ -1028,7 +1028,7 @@ func (dmp *DiffMatchPatch) DiffCleanupMerge(diffs []Diff) []Diff { if x > 0 && diffs[x-1].Type == DiffEqual { diffs[x-1].Text += string(textInsert[:commonlength]) } else { - diffs = append([]Diff{{DiffEqual, string(textInsert[:commonlength])}}, diffs...) + diffs = append([]Diff{Diff{DiffEqual, string(textInsert[:commonlength])}}, diffs...) pointer++ } textInsert = textInsert[commonlength:] diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go index 1708a96fbed..223c43c4268 100644 --- a/vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go +++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go @@ -93,7 +93,7 @@ func (dmp *DiffMatchPatch) PatchAddContext(patch Patch, text string) Patch { // Add the prefix. prefix := text[max(0, patch.Start2-padding):patch.Start2] if len(prefix) != 0 { - patch.diffs = append([]Diff{{DiffEqual, prefix}}, patch.diffs...) + patch.diffs = append([]Diff{Diff{DiffEqual, prefix}}, patch.diffs...) } // Add the suffix. suffix := text[patch.Start2+patch.Length1 : min(len(text), patch.Start2+patch.Length1+padding)] @@ -336,7 +336,7 @@ func (dmp *DiffMatchPatch) PatchAddPadding(patches []Patch) string { // Add some padding on start of first diff. if len(patches[0].diffs) == 0 || patches[0].diffs[0].Type != DiffEqual { // Add nullPadding equality. - patches[0].diffs = append([]Diff{{DiffEqual, nullPadding}}, patches[0].diffs...) + patches[0].diffs = append([]Diff{Diff{DiffEqual, nullPadding}}, patches[0].diffs...) patches[0].Start1 -= paddingLength // Should be 0. patches[0].Start2 -= paddingLength // Should be 0. patches[0].Length1 += paddingLength diff --git a/vendor/golang.org/x/net/http2/transport.go b/vendor/golang.org/x/net/http2/transport.go index a3fe975f049..e6b321f4bb6 100644 --- a/vendor/golang.org/x/net/http2/transport.go +++ b/vendor/golang.org/x/net/http2/transport.go @@ -321,9 +321,7 @@ func (noCachedConnError) Error() string { return "http2: no cached c // or its equivalent renamed type in net/http2's h2_bundle.go. Both types // may coexist in the same running program. func isNoCachedConnError(err error) bool { - _, ok := err.(interface { - IsHTTP2NoCachedConnError() - }) + _, ok := err.(interface{ IsHTTP2NoCachedConnError() }) return ok } diff --git a/vendor/golang.org/x/text/language/gen.go b/vendor/golang.org/x/text/language/gen.go index fea288d4621..302f1940aaf 100644 --- a/vendor/golang.org/x/text/language/gen.go +++ b/vendor/golang.org/x/text/language/gen.go @@ -1050,7 +1050,7 @@ func (b *builder) writeRegion() { m49Index := [9]int16{} fromM49 := []uint16{} m49 := []int{} - for k := range fromM49map { + for k, _ := range fromM49map { m49 = append(m49, int(k)) } sort.Ints(m49) diff --git a/vendor/golang.org/x/text/language/lookup.go b/vendor/golang.org/x/text/language/lookup.go index 96d16dac9d3..1d80ac37082 100644 --- a/vendor/golang.org/x/text/language/lookup.go +++ b/vendor/golang.org/x/text/language/lookup.go @@ -344,39 +344,39 @@ var ( // grandfatheredMap holds a mapping from legacy and grandfathered tags to // their base language or index to more elaborate tag. grandfatheredMap = map[[maxLen]byte]int16{ - {'a', 'r', 't', '-', 'l', 'o', 'j', 'b', 'a', 'n'}: _jbo, // art-lojban - {'i', '-', 'a', 'm', 'i'}: _ami, // i-ami - {'i', '-', 'b', 'n', 'n'}: _bnn, // i-bnn - {'i', '-', 'h', 'a', 'k'}: _hak, // i-hak - {'i', '-', 'k', 'l', 'i', 'n', 'g', 'o', 'n'}: _tlh, // i-klingon - {'i', '-', 'l', 'u', 'x'}: _lb, // i-lux - {'i', '-', 'n', 'a', 'v', 'a', 'j', 'o'}: _nv, // i-navajo - {'i', '-', 'p', 'w', 'n'}: _pwn, // i-pwn - {'i', '-', 't', 'a', 'o'}: _tao, // i-tao - {'i', '-', 't', 'a', 'y'}: _tay, // i-tay - {'i', '-', 't', 's', 'u'}: _tsu, // i-tsu - {'n', 'o', '-', 'b', 'o', 'k'}: _nb, // no-bok - {'n', 'o', '-', 'n', 'y', 'n'}: _nn, // no-nyn - {'s', 'g', 'n', '-', 'b', 'e', '-', 'f', 'r'}: _sfb, // sgn-BE-FR - {'s', 'g', 'n', '-', 'b', 'e', '-', 'n', 'l'}: _vgt, // sgn-BE-NL - {'s', 'g', 'n', '-', 'c', 'h', '-', 'd', 'e'}: _sgg, // sgn-CH-DE - {'z', 'h', '-', 'g', 'u', 'o', 'y', 'u'}: _cmn, // zh-guoyu - {'z', 'h', '-', 'h', 'a', 'k', 'k', 'a'}: _hak, // zh-hakka - {'z', 'h', '-', 'm', 'i', 'n', '-', 'n', 'a', 'n'}: _nan, // zh-min-nan - {'z', 'h', '-', 'x', 'i', 'a', 'n', 'g'}: _hsn, // zh-xiang + [maxLen]byte{'a', 'r', 't', '-', 'l', 'o', 'j', 'b', 'a', 'n'}: _jbo, // art-lojban + [maxLen]byte{'i', '-', 'a', 'm', 'i'}: _ami, // i-ami + [maxLen]byte{'i', '-', 'b', 'n', 'n'}: _bnn, // i-bnn + [maxLen]byte{'i', '-', 'h', 'a', 'k'}: _hak, // i-hak + [maxLen]byte{'i', '-', 'k', 'l', 'i', 'n', 'g', 'o', 'n'}: _tlh, // i-klingon + [maxLen]byte{'i', '-', 'l', 'u', 'x'}: _lb, // i-lux + [maxLen]byte{'i', '-', 'n', 'a', 'v', 'a', 'j', 'o'}: _nv, // i-navajo + [maxLen]byte{'i', '-', 'p', 'w', 'n'}: _pwn, // i-pwn + [maxLen]byte{'i', '-', 't', 'a', 'o'}: _tao, // i-tao + [maxLen]byte{'i', '-', 't', 'a', 'y'}: _tay, // i-tay + [maxLen]byte{'i', '-', 't', 's', 'u'}: _tsu, // i-tsu + [maxLen]byte{'n', 'o', '-', 'b', 'o', 'k'}: _nb, // no-bok + [maxLen]byte{'n', 'o', '-', 'n', 'y', 'n'}: _nn, // no-nyn + [maxLen]byte{'s', 'g', 'n', '-', 'b', 'e', '-', 'f', 'r'}: _sfb, // sgn-BE-FR + [maxLen]byte{'s', 'g', 'n', '-', 'b', 'e', '-', 'n', 'l'}: _vgt, // sgn-BE-NL + [maxLen]byte{'s', 'g', 'n', '-', 'c', 'h', '-', 'd', 'e'}: _sgg, // sgn-CH-DE + [maxLen]byte{'z', 'h', '-', 'g', 'u', 'o', 'y', 'u'}: _cmn, // zh-guoyu + [maxLen]byte{'z', 'h', '-', 'h', 'a', 'k', 'k', 'a'}: _hak, // zh-hakka + [maxLen]byte{'z', 'h', '-', 'm', 'i', 'n', '-', 'n', 'a', 'n'}: _nan, // zh-min-nan + [maxLen]byte{'z', 'h', '-', 'x', 'i', 'a', 'n', 'g'}: _hsn, // zh-xiang // Grandfathered tags with no modern replacement will be converted as // follows: - {'c', 'e', 'l', '-', 'g', 'a', 'u', 'l', 'i', 's', 'h'}: -1, // cel-gaulish - {'e', 'n', '-', 'g', 'b', '-', 'o', 'e', 'd'}: -2, // en-GB-oed - {'i', '-', 'd', 'e', 'f', 'a', 'u', 'l', 't'}: -3, // i-default - {'i', '-', 'e', 'n', 'o', 'c', 'h', 'i', 'a', 'n'}: -4, // i-enochian - {'i', '-', 'm', 'i', 'n', 'g', 'o'}: -5, // i-mingo - {'z', 'h', '-', 'm', 'i', 'n'}: -6, // zh-min + [maxLen]byte{'c', 'e', 'l', '-', 'g', 'a', 'u', 'l', 'i', 's', 'h'}: -1, // cel-gaulish + [maxLen]byte{'e', 'n', '-', 'g', 'b', '-', 'o', 'e', 'd'}: -2, // en-GB-oed + [maxLen]byte{'i', '-', 'd', 'e', 'f', 'a', 'u', 'l', 't'}: -3, // i-default + [maxLen]byte{'i', '-', 'e', 'n', 'o', 'c', 'h', 'i', 'a', 'n'}: -4, // i-enochian + [maxLen]byte{'i', '-', 'm', 'i', 'n', 'g', 'o'}: -5, // i-mingo + [maxLen]byte{'z', 'h', '-', 'm', 'i', 'n'}: -6, // zh-min // CLDR-specific tag. - {'r', 'o', 'o', 't'}: 0, // root - {'e', 'n', '-', 'u', 's', '-', 'p', 'o', 's', 'i', 'x'}: -7, // en_US_POSIX" + [maxLen]byte{'r', 'o', 'o', 't'}: 0, // root + [maxLen]byte{'e', 'n', '-', 'u', 's', '-', 'p', 'o', 's', 'i', 'x'}: -7, // en_US_POSIX" } altTagIndex = [...]uint8{0, 17, 31, 45, 61, 74, 86, 102} diff --git a/vendor/golang.org/x/text/language/tables.go b/vendor/golang.org/x/text/language/tables.go index a28524e1d72..b738d457b5d 100644 --- a/vendor/golang.org/x/text/language/tables.go +++ b/vendor/golang.org/x/text/language/tables.go @@ -3348,9 +3348,9 @@ var regionToGroups = [358]uint8{ // Size: 18 bytes, 3 elements var paradigmLocales = [3][3]uint16{ - 0: {0x139, 0x0, 0x7b}, - 1: {0x13e, 0x0, 0x1f}, - 2: {0x3c0, 0x41, 0xee}, + 0: [3]uint16{0x139, 0x0, 0x7b}, + 1: [3]uint16{0x13e, 0x0, 0x1f}, + 2: [3]uint16{0x3c0, 0x41, 0xee}, } type mutualIntelligibility struct { diff --git a/vendor/golang.org/x/text/unicode/cldr/cldr.go b/vendor/golang.org/x/text/unicode/cldr/cldr.go index 19b8cefd706..2197f8ac268 100644 --- a/vendor/golang.org/x/text/unicode/cldr/cldr.go +++ b/vendor/golang.org/x/text/unicode/cldr/cldr.go @@ -110,7 +110,7 @@ func (cldr *CLDR) Supplemental() *SupplementalData { func (cldr *CLDR) Locales() []string { loc := []string{"root"} hasRoot := false - for l := range cldr.locale { + for l, _ := range cldr.locale { if l == "root" { hasRoot = true continue diff --git a/vendor/golang.org/x/text/unicode/cldr/resolve.go b/vendor/golang.org/x/text/unicode/cldr/resolve.go index c6919216b80..691b5903fe4 100644 --- a/vendor/golang.org/x/text/unicode/cldr/resolve.go +++ b/vendor/golang.org/x/text/unicode/cldr/resolve.go @@ -289,7 +289,7 @@ var distinguishing = map[string][]string{ "mzone": nil, "from": nil, "to": nil, - "type": { + "type": []string{ "abbreviationFallback", "default", "mapping", @@ -527,7 +527,7 @@ func (cldr *CLDR) inheritSlice(enc, v, parent reflect.Value) (res reflect.Value, } } keys := make([]string, 0, len(index)) - for k := range index { + for k, _ := range index { keys = append(keys, k) } sort.Strings(keys) diff --git a/vendor/golang.org/x/text/unicode/cldr/slice.go b/vendor/golang.org/x/text/unicode/cldr/slice.go index ea5f31a3903..388c983ff13 100644 --- a/vendor/golang.org/x/text/unicode/cldr/slice.go +++ b/vendor/golang.org/x/text/unicode/cldr/slice.go @@ -83,7 +83,7 @@ func (s Slice) Group(fn func(e Elem) string) []Slice { m[key] = append(m[key], vi) } keys := []string{} - for k := range m { + for k, _ := range m { keys = append(keys, k) } sort.Strings(keys) diff --git a/vendor/golang.org/x/text/unicode/norm/maketables.go b/vendor/golang.org/x/text/unicode/norm/maketables.go index f66778d450a..338c395ee6f 100644 --- a/vendor/golang.org/x/text/unicode/norm/maketables.go +++ b/vendor/golang.org/x/text/unicode/norm/maketables.go @@ -241,7 +241,7 @@ func compactCCC() { m[c.ccc] = 0 } cccs := []int{} - for v := range m { + for v, _ := range m { cccs = append(cccs, int(v)) } sort.Ints(cccs) diff --git a/vendor/gopkg.in/macaron.v1/context.go b/vendor/gopkg.in/macaron.v1/context.go index 0f86e0d41ec..94a8c45d7da 100644 --- a/vendor/gopkg.in/macaron.v1/context.go +++ b/vendor/gopkg.in/macaron.v1/context.go @@ -270,7 +270,7 @@ func (ctx *Context) SetParams(name, val string) { // ReplaceAllParams replace all current params with given params func (ctx *Context) ReplaceAllParams(params Params) { - ctx.params = params + ctx.params = params; } // ParamsEscape returns escapred params result. From 720711d1fe7e47dd5a4465fd2ad705f64e25e5f7 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Mar 2018 09:24:04 +0100 Subject: [PATCH 23/78] fix: only run gofmt on pkg directory omitting vendor directory --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3d24245b6d7..6dcfc16b82b 100644 --- a/package.json +++ b/package.json @@ -118,8 +118,8 @@ "prettier --write", "git add" ], - "*.go": [ - "gofmt -w -s pkg", + "*pkg/**/*.go": [ + "gofmt -w -s", "git add" ] }, From 2a50bc35a3916dd3778c221ed88563d287672ad9 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 20 Mar 2018 11:07:31 +0100 Subject: [PATCH 24/78] converted file to ts --- .../datasource/graphite/add_graphite_func.js | 155 ----------------- .../datasource/graphite/add_graphite_func.ts | 159 ++++++++++++++++++ 2 files changed, 159 insertions(+), 155 deletions(-) delete mode 100644 public/app/plugins/datasource/graphite/add_graphite_func.js create mode 100644 public/app/plugins/datasource/graphite/add_graphite_func.ts diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.js b/public/app/plugins/datasource/graphite/add_graphite_func.js deleted file mode 100644 index 1d72c2c11eb..00000000000 --- a/public/app/plugins/datasource/graphite/add_graphite_func.js +++ /dev/null @@ -1,155 +0,0 @@ -define(['angular', 'lodash', 'jquery', 'rst2html', 'tether-drop'], function(angular, _, $, rst2html, Drop) { - 'use strict'; - - angular.module('grafana.directives').directive('graphiteAddFunc', function($compile) { - var inputTemplate = - ''; - - var buttonTemplate = - '' + - ''; - - return { - link: function($scope, elem) { - var ctrl = $scope.ctrl; - - var $input = $(inputTemplate); - var $button = $(buttonTemplate); - - $input.appendTo(elem); - $button.appendTo(elem); - - ctrl.datasource.getFuncDefs().then(function(funcDefs) { - var allFunctions = _.map(funcDefs, 'name').sort(); - - $scope.functionMenu = createFunctionDropDownMenu(funcDefs); - - $input.attr('data-provide', 'typeahead'); - $input.typeahead({ - source: allFunctions, - minLength: 1, - items: 10, - updater: function(value) { - var funcDef = ctrl.datasource.getFuncDef(value); - if (!funcDef) { - // try find close match - value = value.toLowerCase(); - funcDef = _.find(allFunctions, function(funcName) { - return funcName.toLowerCase().indexOf(value) === 0; - }); - - if (!funcDef) { - return; - } - } - - $scope.$apply(function() { - ctrl.addFunction(funcDef); - }); - - $input.trigger('blur'); - return ''; - }, - }); - - $button.click(function() { - $button.hide(); - $input.show(); - $input.focus(); - }); - - $input.keyup(function() { - elem.toggleClass('open', $input.val() === ''); - }); - - $input.blur(function() { - // clicking the function dropdown menu wont - // work if you remove class at once - setTimeout(function() { - $input.val(''); - $input.hide(); - $button.show(); - elem.removeClass('open'); - }, 200); - }); - - $compile(elem.contents())($scope); - }); - - var drop; - var cleanUpDrop = function() { - if (drop) { - drop.destroy(); - drop = null; - } - }; - - $(elem) - .on('mouseenter', 'ul.dropdown-menu li', function() { - cleanUpDrop(); - - var funcDef; - try { - funcDef = ctrl.datasource.getFuncDef($('a', this).text()); - } catch (e) { - // ignore - } - - if (funcDef && funcDef.description) { - var shortDesc = funcDef.description; - if (shortDesc.length > 500) { - shortDesc = shortDesc.substring(0, 497) + '...'; - } - - var contentElement = document.createElement('div'); - contentElement.innerHTML = '

' + funcDef.name + '

' + rst2html(shortDesc); - - drop = new Drop({ - target: this, - content: contentElement, - classes: 'drop-popover', - openOn: 'always', - tetherOptions: { - attachment: 'bottom left', - targetAttachment: 'bottom right', - }, - }); - } - }) - .on('mouseout', 'ul.dropdown-menu li', function() { - cleanUpDrop(); - }); - - $scope.$on('$destroy', cleanUpDrop); - }, - }; - }); - - function createFunctionDropDownMenu(funcDefs) { - var categories = {}; - - _.forEach(funcDefs, function(funcDef) { - if (!funcDef.category) { - return; - } - if (!categories[funcDef.category]) { - categories[funcDef.category] = []; - } - categories[funcDef.category].push({ - text: funcDef.name, - click: "ctrl.addFunction('" + funcDef.name + "')", - }); - }); - - return _.sortBy( - _.map(categories, function(submenu, category) { - return { - text: category, - submenu: _.sortBy(submenu, 'text'), - }; - }), - 'text' - ); - } -}); diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.ts b/public/app/plugins/datasource/graphite/add_graphite_func.ts new file mode 100644 index 00000000000..360d606c924 --- /dev/null +++ b/public/app/plugins/datasource/graphite/add_graphite_func.ts @@ -0,0 +1,159 @@ +import angular from 'angular'; +import _ from 'lodash'; +import $ from 'jquery'; +import rst2html from 'rst2html'; +import Drop from 'tether-drop'; + +export function graphiteAddFunc($compile) { + var inputTemplate = + ''; + + var buttonTemplate = + '' + + ''; + + return { + link: function($scope, elem) { + var ctrl = $scope.ctrl; + + var $input = $(inputTemplate); + var $button = $(buttonTemplate); + + $input.appendTo(elem); + $button.appendTo(elem); + + ctrl.datasource.getFuncDefs().then(function(funcDefs) { + var allFunctions = _.map(funcDefs, 'name').sort(); + + $scope.functionMenu = createFunctionDropDownMenu(funcDefs); + + $input.attr('data-provide', 'typeahead'); + $input.typeahead({ + source: allFunctions, + minLength: 1, + items: 10, + updater: function(value) { + var funcDef = ctrl.datasource.getFuncDef(value); + if (!funcDef) { + // try find close match + value = value.toLowerCase(); + funcDef = _.find(allFunctions, function(funcName) { + return funcName.toLowerCase().indexOf(value) === 0; + }); + + if (!funcDef) { + return ''; + } + } + + $scope.$apply(function() { + ctrl.addFunction(funcDef); + }); + + $input.trigger('blur'); + return ''; + }, + }); + + $button.click(function() { + $button.hide(); + $input.show(); + $input.focus(); + }); + + $input.keyup(function() { + elem.toggleClass('open', $input.val() === ''); + }); + + $input.blur(function() { + // clicking the function dropdown menu wont + // work if you remove class at once + setTimeout(function() { + $input.val(''); + $input.hide(); + $button.show(); + elem.removeClass('open'); + }, 200); + }); + + $compile(elem.contents())($scope); + }); + + var drop; + var cleanUpDrop = function() { + if (drop) { + drop.destroy(); + drop = null; + } + }; + + $(elem) + .on('mouseenter', 'ul.dropdown-menu li', function() { + cleanUpDrop(); + + var funcDef; + try { + funcDef = ctrl.datasource.getFuncDef($('a', this).text()); + } catch (e) { + // ignore + } + + if (funcDef && funcDef.description) { + var shortDesc = funcDef.description; + if (shortDesc.length > 500) { + shortDesc = shortDesc.substring(0, 497) + '...'; + } + + var contentElement = document.createElement('div'); + contentElement.innerHTML = '

' + funcDef.name + '

' + rst2html(shortDesc); + + drop = new Drop({ + target: this, + content: contentElement, + classes: 'drop-popover', + openOn: 'always', + tetherOptions: { + attachment: 'bottom left', + targetAttachment: 'bottom right', + }, + }); + } + }) + .on('mouseout', 'ul.dropdown-menu li', function() { + cleanUpDrop(); + }); + + $scope.$on('$destroy', cleanUpDrop); + }, + }; +} + +angular.module('grafana.directives').directive('graphiteAddFunc', graphiteAddFunc); + +function createFunctionDropDownMenu(funcDefs) { + var categories = {}; + + _.forEach(funcDefs, function(funcDef) { + if (!funcDef.category) { + return; + } + if (!categories[funcDef.category]) { + categories[funcDef.category] = []; + } + categories[funcDef.category].push({ + text: funcDef.name, + click: "ctrl.addFunction('" + funcDef.name + "')", + }); + }); + + return _.sortBy( + _.map(categories, function(submenu, category) { + return { + text: category, + submenu: _.sortBy(submenu, 'text'), + }; + }), + 'text' + ); +} From 230f018c7820542162e7ae4f9e52bf56fcd7011a Mon Sep 17 00:00:00 2001 From: ilgizar Date: Tue, 20 Mar 2018 15:37:18 +0500 Subject: [PATCH 25/78] Added validation of input parameters. --- public/app/plugins/panel/graph/align_yaxes.ts | 12 ++++++++++++ .../plugins/panel/graph/specs/align_yaxes.jest.ts | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/public/app/plugins/panel/graph/align_yaxes.ts b/public/app/plugins/panel/graph/align_yaxes.ts index b60d75e7b66..71bfcd8423d 100644 --- a/public/app/plugins/panel/graph/align_yaxes.ts +++ b/public/app/plugins/panel/graph/align_yaxes.ts @@ -6,6 +6,10 @@ import _ from 'lodash'; * @param align Y level */ export function alignYLevel(yaxis, alignLevel) { + if (isNaN(alignLevel) || !checkCorrectAxis(yaxis)) { + return; + } + var [yLeft, yRight] = yaxis; moveLevelToZero(yLeft, yRight, alignLevel); @@ -92,6 +96,14 @@ function restoreLevelFromZero(yLeft, yRight, alignLevel) { } } +function checkCorrectAxis(axis) { + return axis.length === 2 && checkCorrectAxes(axis[0]) && checkCorrectAxes(axis[1]); +} + +function checkCorrectAxes(axes) { + return 'min' in axes && 'max' in axes; +} + function checkOneSide(yLeft, yRight) { // on the one hand with respect to zero return (yLeft.min >= 0 && yRight.min >= 0) || (yLeft.max <= 0 && yRight.max <= 0); diff --git a/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts b/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts index ff540fd223f..963ecfbfa1f 100644 --- a/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts +++ b/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts @@ -197,4 +197,15 @@ describe('Graph Y axes aligner', function() { expect(yaxes).toMatchObject(expected); }); }); + + describe('on level not number value', () => { + it('Should ignore without errors', () => { + alignY = 'q'; + yaxes = [{ min: 5, max: 10 }, { min: 2, max: 4 }]; + expected = [{ min: 5, max: 10 }, { min: 2, max: 4 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); }); From 1588295375574bfda62d34271707535dfed6c16d Mon Sep 17 00:00:00 2001 From: ilgizar Date: Tue, 20 Mar 2018 15:38:48 +0500 Subject: [PATCH 26/78] Changed the way this feature was activated. And changed tolltip. --- public/app/plugins/panel/graph/axes_editor.html | 11 ++++++++--- public/app/plugins/panel/graph/graph.ts | 5 +++-- public/app/plugins/panel/graph/module.ts | 3 ++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index 2c08755c17a..f17c9ce105f 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -31,9 +31,14 @@
-
- - +
+
+ +
+
+ + +
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 73b261fce3a..713d7079152 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -158,8 +158,9 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { function processRangeHook(plot) { var yaxis = plot.getYAxes(); - if (yaxis.length > 1 && 'align' in panel.yaxes[1] && panel.yaxes[1].align !== null) { - alignYLevel(yaxis, parseFloat(panel.yaxes[1].align)); + if (yaxis.length > 1 && panel.yaxes[1].alignment) { + var align = panel.yaxes[1].align || 0; + alignYLevel(yaxis, parseFloat(align)); } } diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index c198d118115..7e7b270bd61 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -46,7 +46,8 @@ class GraphCtrl extends MetricsPanelCtrl { min: null, max: null, format: 'short', - align: null, + alignment: false, + align: 0, }, ], xaxis: { From e015047ed1c234eed8d14c91b3b6eb5f7f5b7612 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Tue, 20 Mar 2018 16:09:52 +0500 Subject: [PATCH 27/78] Fixed unit test. --- public/app/plugins/panel/graph/specs/align_yaxes.jest.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts b/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts index 963ecfbfa1f..da3aff91275 100644 --- a/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts +++ b/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts @@ -200,11 +200,10 @@ describe('Graph Y axes aligner', function() { describe('on level not number value', () => { it('Should ignore without errors', () => { - alignY = 'q'; yaxes = [{ min: 5, max: 10 }, { min: 2, max: 4 }]; expected = [{ min: 5, max: 10 }, { min: 2, max: 4 }]; - alignYLevel(yaxes, alignY); + alignYLevel(yaxes, 'q'); expect(yaxes).toMatchObject(expected); }); }); From 05ac7d8fca8af0556695517abeb3d72ea1e081ee Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 20 Mar 2018 14:13:31 +0300 Subject: [PATCH 28/78] dashboard: fix phantomjs panel rendering in collapsed row --- public/app/features/dashboard/view_state_srv.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 8cb35a8ca99..576b8b6fce8 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -90,7 +90,7 @@ export class DashboardViewState { } } - if (this.state.fullscreen && this.state.panelId) { + if ((this.state.fullscreen || this.dashboard.meta.soloMode) && this.state.panelId) { // Trying to render panel in fullscreen when it's in the collapsed row causes an issue. // So in this case expand collapsed row first. this.toggleCollapsedPanelRow(this.state.panelId); From 51cbf23c4a550c4a055d60512dbab6fdc5e6ceb5 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Mar 2018 12:26:37 +0100 Subject: [PATCH 29/78] changelog: notes about #10093 and #11298 [ci skip] --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69862c3f214..5f293953892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 5.1.0 (unreleased) +* **MSSQL**: New Microsoft SQL Server data source [#10093](https://github.com/grafana/grafana/pull/10093), [#11298](https://github.com/grafana/grafana/pull/11298), thx [@linuxchips](https://github.com/linuxchips) * **Prometheus**: The heatmap panel now support Prometheus histograms [#10009](https://github.com/grafana/grafana/issues/10009) * **Postgres/MySQL**: Ability to insert 0s or nulls for missing intervals [#9487](https://github.com/grafana/grafana/issues/9487), thanks [@svenklemm](https://github.com/svenklemm) * **Graph**: Thresholds for Right Y axis [#7107](https://github.com/grafana/grafana/issues/7107), thx [@ilgizar](https://github.com/ilgizar) @@ -13,7 +14,7 @@ * **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes) * **Cloudwatch**: Support high resolution metrics [#10925](https://github.com/grafana/grafana/pull/10925), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: Add dimension filtering to CloudWatch `dimension_values()` [#10029](https://github.com/grafana/grafana/issues/10029), thx [@willyhutw](https://github.com/willyhutw) -* **Units**: Second to HH:mm:ss formatter [#11107](https://github.com/grafana/grafana/issues/11107), thx [@gladdiologist](https://github.com/gladdiologist) +* **Units**: Second to HH:mm:ss formatter [#11107](https://github.com/grafana/grafana/issues/11107), thx [@gladdiologist](https://github.com/gladdiologist) * **Singlestat**: Add color to prefix and postfix in singlestat panel [#11143](https://github.com/grafana/grafana/pull/11143), thx [@ApsOps](https://github.com/ApsOps) # 5.0.3 (2018-03-16) From 2a90370230e8515711dad7de448c2c034d571c4b Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 20 Mar 2018 12:36:02 +0100 Subject: [PATCH 30/78] converted file to ts --- .../datasource/graphite/func_editor.js | 309 ----------------- .../datasource/graphite/func_editor.ts | 318 ++++++++++++++++++ 2 files changed, 318 insertions(+), 309 deletions(-) delete mode 100644 public/app/plugins/datasource/graphite/func_editor.js create mode 100644 public/app/plugins/datasource/graphite/func_editor.ts diff --git a/public/app/plugins/datasource/graphite/func_editor.js b/public/app/plugins/datasource/graphite/func_editor.js deleted file mode 100644 index 5648aa4935b..00000000000 --- a/public/app/plugins/datasource/graphite/func_editor.js +++ /dev/null @@ -1,309 +0,0 @@ -define([ - 'angular', - 'lodash', - 'jquery', - 'rst2html', -], -function (angular, _, $, rst2html) { - 'use strict'; - - angular - .module('grafana.directives') - .directive('graphiteFuncEditor', function($compile, templateSrv, popoverSrv) { - - var funcSpanTemplate = '{{func.def.name}}('; - var paramTemplate = ''; - - var funcControlsTemplate = - '
' + - '' + - '' + - '' + - '' + - '
'; - - return { - restrict: 'A', - link: function postLink($scope, elem) { - var $funcLink = $(funcSpanTemplate); - var $funcControls = $(funcControlsTemplate); - var ctrl = $scope.ctrl; - var func = $scope.func; - var scheduledRelink = false; - var paramCountAtLink = 0; - var cancelBlur = null; - - function clickFuncParam(paramIndex) { - /*jshint validthis:true */ - - var $link = $(this); - var $comma = $link.prev('.comma'); - var $input = $link.next(); - - $input.val(func.params[paramIndex]); - - $comma.removeClass('query-part__last'); - $link.hide(); - $input.show(); - $input.focus(); - $input.select(); - - var typeahead = $input.data('typeahead'); - if (typeahead) { - $input.val(''); - typeahead.lookup(); - } - } - - function scheduledRelinkIfNeeded() { - if (paramCountAtLink === func.params.length) { - return; - } - - if (!scheduledRelink) { - scheduledRelink = true; - setTimeout(function() { - relink(); - scheduledRelink = false; - }, 200); - } - } - - function paramDef(index) { - if (index < func.def.params.length) { - return func.def.params[index]; - } - if (_.last(func.def.params).multiple) { - return _.assign({}, _.last(func.def.params), {optional: true}); - } - return {}; - } - - function switchToLink(inputElem, paramIndex) { - /*jshint validthis:true */ - var $input = $(inputElem); - - clearTimeout(cancelBlur); - cancelBlur = null; - - var $link = $input.prev(); - var $comma = $link.prev('.comma'); - var newValue = $input.val(); - - // remove optional empty params - if (newValue !== '' || paramDef(paramIndex).optional) { - func.updateParam(newValue, paramIndex); - $link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : ' '); - } - - scheduledRelinkIfNeeded(); - - $scope.$apply(function() { - ctrl.targetChanged(); - }); - - if ($link.hasClass('query-part__last') && newValue === '') { - $comma.addClass('query-part__last'); - } else { - $link.removeClass('query-part__last'); - } - - $input.hide(); - $link.show(); - } - - // this = input element - function inputBlur(paramIndex) { - /*jshint validthis:true */ - var inputElem = this; - // happens long before the click event on the typeahead options - // need to have long delay because the blur - cancelBlur = setTimeout(function() { - switchToLink(inputElem, paramIndex); - }, 200); - } - - function inputKeyPress(paramIndex, e) { - /*jshint validthis:true */ - if(e.which === 13) { - $(this).blur(); - } - } - - function inputKeyDown() { - /*jshint validthis:true */ - this.style.width = (3 + this.value.length) * 8 + 'px'; - } - - function addTypeahead($input, paramIndex) { - $input.attr('data-provide', 'typeahead'); - - var options = paramDef(paramIndex).options; - if (paramDef(paramIndex).type === 'int') { - options = _.map(options, function(val) { return val.toString(); }); - } - - $input.typeahead({ - source: options, - minLength: 0, - items: 20, - updater: function (value) { - $input.val(value); - switchToLink($input[0], paramIndex); - return value; - } - }); - - var typeahead = $input.data('typeahead'); - typeahead.lookup = function () { - this.query = this.$element.val() || ''; - return this.process(this.source); - }; - } - - function toggleFuncControls() { - var targetDiv = elem.closest('.tight-form'); - - if (elem.hasClass('show-function-controls')) { - elem.removeClass('show-function-controls'); - targetDiv.removeClass('has-open-function'); - $funcControls.hide(); - return; - } - - elem.addClass('show-function-controls'); - targetDiv.addClass('has-open-function'); - - $funcControls.show(); - } - - function addElementsAndCompile() { - $funcControls.appendTo(elem); - $funcLink.appendTo(elem); - - var defParams = _.clone(func.def.params); - var lastParam = _.last(func.def.params); - - while (func.params.length >= defParams.length && lastParam && lastParam.multiple) { - defParams.push(_.assign({}, lastParam, {optional: true})); - } - - _.each(defParams, function(param, index) { - if (param.optional && func.params.length < index) { - return false; - } - - var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); - - var last = (index >= func.params.length - 1) && param.optional && !paramValue; - if (last && param.multiple) { - paramValue = '+'; - } - - if (index > 0) { - $(', ').appendTo(elem); - } - - var $paramLink = $( - '' - + (paramValue || ' ') + ''); - var $input = $(paramTemplate); - $input.attr('placeholder', param.name); - - paramCountAtLink++; - - $paramLink.appendTo(elem); - $input.appendTo(elem); - - $input.blur(_.partial(inputBlur, index)); - $input.keyup(inputKeyDown); - $input.keypress(_.partial(inputKeyPress, index)); - $paramLink.click(_.partial(clickFuncParam, index)); - - if (param.options) { - addTypeahead($input, index); - } - }); - - $(')').appendTo(elem); - - $compile(elem.contents())($scope); - } - - function ifJustAddedFocusFirstParam() { - if ($scope.func.added) { - $scope.func.added = false; - setTimeout(function() { - elem.find('.graphite-func-param-link').first().click(); - }, 10); - } - } - - function registerFuncControlsToggle() { - $funcLink.click(toggleFuncControls); - } - - function registerFuncControlsActions() { - $funcControls.click(function(e) { - var $target = $(e.target); - if ($target.hasClass('fa-remove')) { - toggleFuncControls(); - $scope.$apply(function() { - ctrl.removeFunction($scope.func); - }); - return; - } - - if ($target.hasClass('fa-arrow-left')) { - $scope.$apply(function() { - _.move(ctrl.queryModel.functions, $scope.$index, $scope.$index - 1); - ctrl.targetChanged(); - }); - return; - } - - if ($target.hasClass('fa-arrow-right')) { - $scope.$apply(function() { - _.move(ctrl.queryModel.functions, $scope.$index, $scope.$index + 1); - ctrl.targetChanged(); - }); - return; - } - - if ($target.hasClass('fa-question-circle')) { - var funcDef = ctrl.datasource.getFuncDef(func.def.name); - if (funcDef && funcDef.description) { - popoverSrv.show({ - element: e.target, - position: 'bottom left', - classNames: 'drop-popover drop-function-def', - template: '
' - + '

' + funcDef.name + '

' + rst2html(funcDef.description) + '
', - openOn: 'click', - }); - } else { - window.open( - "http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + func.def.name,'_blank'); - } - return; - } - }); - } - - function relink() { - elem.children().remove(); - - addElementsAndCompile(); - ifJustAddedFocusFirstParam(); - registerFuncControlsToggle(); - registerFuncControlsActions(); - } - - relink(); - } - }; - - }); - -}); diff --git a/public/app/plugins/datasource/graphite/func_editor.ts b/public/app/plugins/datasource/graphite/func_editor.ts new file mode 100644 index 00000000000..1a4c6d4313a --- /dev/null +++ b/public/app/plugins/datasource/graphite/func_editor.ts @@ -0,0 +1,318 @@ +import angular from 'angular'; +import _ from 'lodash'; +import $ from 'jquery'; +import rst2html from 'rst2html'; + +export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { + var funcSpanTemplate = '{{func.def.name}}('; + var paramTemplate = ''; + + var funcControlsTemplate = + '
' + + '' + + '' + + '' + + '' + + '
'; + + return { + restrict: 'A', + link: function postLink($scope, elem) { + var $funcLink = $(funcSpanTemplate); + var $funcControls = $(funcControlsTemplate); + var ctrl = $scope.ctrl; + var func = $scope.func; + var scheduledRelink = false; + var paramCountAtLink = 0; + var cancelBlur = null; + + function clickFuncParam(paramIndex) { + /*jshint validthis:true */ + + var $link = $(this); + var $comma = $link.prev('.comma'); + var $input = $link.next(); + + $input.val(func.params[paramIndex]); + + $comma.removeClass('query-part__last'); + $link.hide(); + $input.show(); + $input.focus(); + $input.select(); + + var typeahead = $input.data('typeahead'); + if (typeahead) { + $input.val(''); + typeahead.lookup(); + } + } + + function scheduledRelinkIfNeeded() { + if (paramCountAtLink === func.params.length) { + return; + } + + if (!scheduledRelink) { + scheduledRelink = true; + setTimeout(function() { + relink(); + scheduledRelink = false; + }, 200); + } + } + + function paramDef(index) { + if (index < func.def.params.length) { + return func.def.params[index]; + } + if (_.last(func.def.params).multiple) { + return _.assign({}, _.last(func.def.params), { optional: true }); + } + return {}; + } + + function switchToLink(inputElem, paramIndex) { + /*jshint validthis:true */ + var $input = $(inputElem); + + clearTimeout(cancelBlur); + cancelBlur = null; + + var $link = $input.prev(); + var $comma = $link.prev('.comma'); + var newValue = $input.val(); + + // remove optional empty params + if (newValue !== '' || paramDef(paramIndex).optional) { + func.updateParam(newValue, paramIndex); + $link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : ' '); + } + + scheduledRelinkIfNeeded(); + + $scope.$apply(function() { + ctrl.targetChanged(); + }); + + if ($link.hasClass('query-part__last') && newValue === '') { + $comma.addClass('query-part__last'); + } else { + $link.removeClass('query-part__last'); + } + + $input.hide(); + $link.show(); + } + + // this = input element + function inputBlur(paramIndex) { + /*jshint validthis:true */ + var inputElem = this; + // happens long before the click event on the typeahead options + // need to have long delay because the blur + cancelBlur = setTimeout(function() { + switchToLink(inputElem, paramIndex); + }, 200); + } + + function inputKeyPress(paramIndex, e) { + /*jshint validthis:true */ + if (e.which === 13) { + $(this).blur(); + } + } + + function inputKeyDown() { + /*jshint validthis:true */ + this.style.width = (3 + this.value.length) * 8 + 'px'; + } + + function addTypeahead($input, paramIndex) { + $input.attr('data-provide', 'typeahead'); + + var options = paramDef(paramIndex).options; + if (paramDef(paramIndex).type === 'int') { + options = _.map(options, function(val) { + return val.toString(); + }); + } + + $input.typeahead({ + source: options, + minLength: 0, + items: 20, + updater: function(value) { + $input.val(value); + switchToLink($input[0], paramIndex); + return value; + }, + }); + + var typeahead = $input.data('typeahead'); + typeahead.lookup = function() { + this.query = this.$element.val() || ''; + return this.process(this.source); + }; + } + + function toggleFuncControls() { + var targetDiv = elem.closest('.tight-form'); + + if (elem.hasClass('show-function-controls')) { + elem.removeClass('show-function-controls'); + targetDiv.removeClass('has-open-function'); + $funcControls.hide(); + return; + } + + elem.addClass('show-function-controls'); + targetDiv.addClass('has-open-function'); + + $funcControls.show(); + } + + function addElementsAndCompile() { + $funcControls.appendTo(elem); + $funcLink.appendTo(elem); + + var defParams = _.clone(func.def.params); + var lastParam = _.last(func.def.params); + + while (func.params.length >= defParams.length && lastParam && lastParam.multiple) { + defParams.push(_.assign({}, lastParam, { optional: true })); + } + + _.each(defParams, function(param, index) { + if (param.optional && func.params.length < index) { + return false; + } + + var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); + + var last = index >= func.params.length - 1 && param.optional && !paramValue; + if (last && param.multiple) { + paramValue = '+'; + } + + if (index > 0) { + $(', ').appendTo(elem); + } + + var $paramLink = $( + '' + + (paramValue || ' ') + + '' + ); + var $input = $(paramTemplate); + $input.attr('placeholder', param.name); + + paramCountAtLink++; + + $paramLink.appendTo(elem); + $input.appendTo(elem); + + $input.blur(_.partial(inputBlur, index)); + $input.keyup(inputKeyDown); + $input.keypress(_.partial(inputKeyPress, index)); + $paramLink.click(_.partial(clickFuncParam, index)); + + if (param.options) { + addTypeahead($input, index); + } + + return true; + }); + + $(')').appendTo(elem); + + $compile(elem.contents())($scope); + } + + function ifJustAddedFocusFirstParam() { + if ($scope.func.added) { + $scope.func.added = false; + setTimeout(function() { + elem + .find('.graphite-func-param-link') + .first() + .click(); + }, 10); + } + } + + function registerFuncControlsToggle() { + $funcLink.click(toggleFuncControls); + } + + function registerFuncControlsActions() { + $funcControls.click(function(e) { + var $target = $(e.target); + if ($target.hasClass('fa-remove')) { + toggleFuncControls(); + $scope.$apply(function() { + ctrl.removeFunction($scope.func); + }); + return; + } + + if ($target.hasClass('fa-arrow-left')) { + $scope.$apply(function() { + _.move(ctrl.queryModel.functions, $scope.$index, $scope.$index - 1); + ctrl.targetChanged(); + }); + return; + } + + if ($target.hasClass('fa-arrow-right')) { + $scope.$apply(function() { + _.move(ctrl.queryModel.functions, $scope.$index, $scope.$index + 1); + ctrl.targetChanged(); + }); + return; + } + + if ($target.hasClass('fa-question-circle')) { + var funcDef = ctrl.datasource.getFuncDef(func.def.name); + if (funcDef && funcDef.description) { + popoverSrv.show({ + element: e.target, + position: 'bottom left', + classNames: 'drop-popover drop-function-def', + template: + '
' + + '

' + + funcDef.name + + '

' + + rst2html(funcDef.description) + + '
', + openOn: 'click', + }); + } else { + window.open( + 'http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions.' + func.def.name, + '_blank' + ); + } + return; + } + }); + } + + function relink() { + elem.children().remove(); + + addElementsAndCompile(); + ifJustAddedFocusFirstParam(); + registerFuncControlsToggle(); + registerFuncControlsActions(); + } + + relink(); + }, + }; +} + +angular.module('grafana.directives').directive('graphiteFuncEditor', graphiteFuncEditor); From ae4c6e4648ffb1382d737de6ffac2e4f4b06a611 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Mar 2018 12:57:26 +0100 Subject: [PATCH 31/78] mssql: fix precision for time column in table mode ref #11306 --- pkg/tsdb/mssql/mssql.go | 2 +- .../datasource/mssql/response_parser.ts | 2 +- .../mssql/specs/datasource_specs.ts | 135 +++++++++--------- 3 files changed, 68 insertions(+), 71 deletions(-) diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index af68ca0424e..6da61d63e42 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -124,7 +124,7 @@ func (e MssqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, if timeIndex != -1 { switch value := values[timeIndex].(type) { case time.Time: - values[timeIndex] = float64(value.Unix()) + values[timeIndex] = (float64(value.Unix()) * 1000) + float64(value.Nanosecond()/1e6) // in case someone is trying to map times beyond 2262 :D } } diff --git a/public/app/plugins/datasource/mssql/response_parser.ts b/public/app/plugins/datasource/mssql/response_parser.ts index b7d96d820cb..c98a9652b0e 100644 --- a/public/app/plugins/datasource/mssql/response_parser.ts +++ b/public/app/plugins/datasource/mssql/response_parser.ts @@ -128,7 +128,7 @@ export default class ResponseParser { const row = table.rows[i]; list.push({ annotation: options.annotation, - time: Math.floor(row[timeColumnIndex]) * 1000, + time: row[timeColumnIndex], text: row[textColumnIndex], tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [], }); diff --git a/public/app/plugins/datasource/mssql/specs/datasource_specs.ts b/public/app/plugins/datasource/mssql/specs/datasource_specs.ts index a144f21a258..18fcb6331be 100644 --- a/public/app/plugins/datasource/mssql/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/mssql/specs/datasource_specs.ts @@ -1,24 +1,26 @@ -import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common'; +import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; import moment from 'moment'; import helpers from 'test/specs/helpers'; -import {MssqlDatasource} from '../datasource'; -import {CustomVariable} from 'app/features/templating/custom_variable'; +import { MssqlDatasource } from '../datasource'; +import { CustomVariable } from 'app/features/templating/custom_variable'; describe('MSSQLDatasource', function() { var ctx = new helpers.ServiceTestContext(); - var instanceSettings = {name: 'mssql'}; + var instanceSettings = { name: 'mssql' }; beforeEach(angularMocks.module('grafana.core')); beforeEach(angularMocks.module('grafana.services')); beforeEach(ctx.providePhase(['backendSrv'])); - beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - ctx.$q = $q; - ctx.$httpBackend = $httpBackend; - ctx.$rootScope = $rootScope; - ctx.ds = $injector.instantiate(MssqlDatasource, {instanceSettings: instanceSettings}); - $httpBackend.when('GET', /\.html$/).respond(''); - })); + beforeEach( + angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { + ctx.$q = $q; + ctx.$httpBackend = $httpBackend; + ctx.$rootScope = $rootScope; + ctx.ds = $injector.instantiate(MssqlDatasource, { instanceSettings: instanceSettings }); + $httpBackend.when('GET', /\.html$/).respond(''); + }) + ); describe('When performing annotationQuery', function() { let results; @@ -28,12 +30,12 @@ describe('MSSQLDatasource', function() { const options = { annotation: { name: annotationName, - rawQuery: 'select time, text, tags from table;' + rawQuery: 'select time, text, tags from table;', }, range: { from: moment(1432288354), - to: moment(1432288401) - } + to: moment(1432288401), + }, }; const response = { @@ -42,23 +44,25 @@ describe('MSSQLDatasource', function() { refId: annotationName, tables: [ { - columns: [{text: 'time'}, {text: 'text'}, {text: 'tags'}], + columns: [{ text: 'time' }, { text: 'text' }, { text: 'tags' }], rows: [ - [1432288355, 'some text', 'TagA,TagB'], - [1432288390, 'some text2', ' TagB , TagC'], - [1432288400, 'some text3'] - ] - } - ] - } - } + [1521546171129, 'some text', 'TagA,TagB'], + [1521546531404, 'some text2', ' TagB , TagC'], + [1521546901702, 'some text3'], + ], + }, + ], + }, + }, }; beforeEach(function() { ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({data: response, status: 200}); + return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.annotationQuery(options).then(function(data) { results = data; }); + ctx.ds.annotationQuery(options).then(function(data) { + results = data; + }); ctx.$rootScope.$apply(); }); @@ -83,28 +87,26 @@ describe('MSSQLDatasource', function() { results: { tempvar: { meta: { - rowCount: 3 + rowCount: 3, }, refId: 'tempvar', tables: [ { - columns: [{text: 'title'}, {text: 'text'}], - rows: [ - ['aTitle', 'some text'], - ['aTitle2', 'some text2'], - ['aTitle3', 'some text3'] - ] - } - ] - } - } + columns: [{ text: 'title' }, { text: 'text' }], + rows: [['aTitle', 'some text'], ['aTitle2', 'some text2'], ['aTitle3', 'some text3']], + }, + ], + }, + }, }; beforeEach(function() { ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({data: response, status: 200}); + return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); + ctx.ds.metricFindQuery(query).then(function(data) { + results = data; + }); ctx.$rootScope.$apply(); }); @@ -122,28 +124,26 @@ describe('MSSQLDatasource', function() { results: { tempvar: { meta: { - rowCount: 3 + rowCount: 3, }, refId: 'tempvar', tables: [ { - columns: [{text: '__value'}, {text: '__text'}], - rows: [ - ['value1', 'aTitle'], - ['value2', 'aTitle2'], - ['value3', 'aTitle3'] - ] - } - ] - } - } + columns: [{ text: '__value' }, { text: '__text' }], + rows: [['value1', 'aTitle'], ['value2', 'aTitle2'], ['value3', 'aTitle3']], + }, + ], + }, + }, }; beforeEach(function() { ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({data: response, status: 200}); + return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); + ctx.ds.metricFindQuery(query).then(function(data) { + results = data; + }); ctx.$rootScope.$apply(); }); @@ -163,28 +163,26 @@ describe('MSSQLDatasource', function() { results: { tempvar: { meta: { - rowCount: 3 + rowCount: 3, }, refId: 'tempvar', tables: [ { - columns: [{text: '__text'}, {text: '__value'}], - rows: [ - ['aTitle', 'same'], - ['aTitle', 'same'], - ['aTitle', 'diff'] - ] - } - ] - } - } + columns: [{ text: '__text' }, { text: '__value' }], + rows: [['aTitle', 'same'], ['aTitle', 'same'], ['aTitle', 'diff']], + }, + ], + }, + }, }; beforeEach(function() { ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({data: response, status: 200}); + return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); + ctx.ds.metricFindQuery(query).then(function(data) { + results = data; + }); ctx.$rootScope.$apply(); }); @@ -197,7 +195,7 @@ describe('MSSQLDatasource', function() { describe('When interpolating variables', () => { beforeEach(function() { - ctx.variable = new CustomVariable({},{}); + ctx.variable = new CustomVariable({}, {}); }); describe('and value is a string', () => { @@ -214,23 +212,22 @@ describe('MSSQLDatasource', function() { describe('and value is an array of strings', () => { it('should return comma separated quoted values', () => { - expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql('\'a\',\'b\',\'c\''); + expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql("'a','b','c'"); }); }); describe('and variable allows multi-value and value is a string', () => { it('should return a quoted value', () => { ctx.variable.multi = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('\'abc\''); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); }); }); describe('and variable allows all and value is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('\'abc\''); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); }); }); - }); }); From d34cd8730eef69ea64926f7acf62e54f338d02e8 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Mar 2018 13:01:32 +0100 Subject: [PATCH 32/78] mssql: convert tests to jest --- ...datasource_specs.ts => datasource.jest.ts} | 99 +++++++++---------- 1 file changed, 47 insertions(+), 52 deletions(-) rename public/app/plugins/datasource/mssql/specs/{datasource_specs.ts => datasource.jest.ts} (67%) diff --git a/public/app/plugins/datasource/mssql/specs/datasource_specs.ts b/public/app/plugins/datasource/mssql/specs/datasource.jest.ts similarity index 67% rename from public/app/plugins/datasource/mssql/specs/datasource_specs.ts rename to public/app/plugins/datasource/mssql/specs/datasource.jest.ts index 18fcb6331be..dd2d4a60cec 100644 --- a/public/app/plugins/datasource/mssql/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/mssql/specs/datasource.jest.ts @@ -1,26 +1,21 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; import moment from 'moment'; -import helpers from 'test/specs/helpers'; import { MssqlDatasource } from '../datasource'; +import { TemplateSrvStub } from 'test/specs/helpers'; import { CustomVariable } from 'app/features/templating/custom_variable'; +import q from 'q'; describe('MSSQLDatasource', function() { - var ctx = new helpers.ServiceTestContext(); - var instanceSettings = { name: 'mssql' }; + const ctx: any = { + backendSrv: {}, + templateSrv: new TemplateSrvStub(), + }; - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach(ctx.providePhase(['backendSrv'])); + beforeEach(function() { + ctx.$q = q; + ctx.instanceSettings = { name: 'mssql' }; - beforeEach( - angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - ctx.$q = $q; - ctx.$httpBackend = $httpBackend; - ctx.$rootScope = $rootScope; - ctx.ds = $injector.instantiate(MssqlDatasource, { instanceSettings: instanceSettings }); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); + ctx.ds = new MssqlDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.$q, ctx.templateSrv); + }); describe('When performing annotationQuery', function() { let results; @@ -46,9 +41,9 @@ describe('MSSQLDatasource', function() { { columns: [{ text: 'time' }, { text: 'text' }, { text: 'tags' }], rows: [ - [1521546171129, 'some text', 'TagA,TagB'], - [1521546531404, 'some text2', ' TagB , TagC'], - [1521546901702, 'some text3'], + [1521545610656, 'some text', 'TagA,TagB'], + [1521546251185, 'some text2', ' TagB , TagC'], + [1521546501378, 'some text3'], ], }, ], @@ -56,27 +51,27 @@ describe('MSSQLDatasource', function() { }, }; - beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { + beforeEach(() => { + ctx.backendSrv.datasourceRequest = options => { return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.annotationQuery(options).then(function(data) { + + return ctx.ds.annotationQuery(options).then(data => { results = data; }); - ctx.$rootScope.$apply(); }); it('should return annotation list', function() { - expect(results.length).to.be(3); + expect(results.length).toBe(3); - expect(results[0].text).to.be('some text'); - expect(results[0].tags[0]).to.be('TagA'); - expect(results[0].tags[1]).to.be('TagB'); + expect(results[0].text).toBe('some text'); + expect(results[0].tags[0]).toBe('TagA'); + expect(results[0].tags[1]).toBe('TagB'); - expect(results[1].tags[0]).to.be('TagB'); - expect(results[1].tags[1]).to.be('TagC'); + expect(results[1].tags[0]).toBe('TagB'); + expect(results[1].tags[1]).toBe('TagC'); - expect(results[2].tags.length).to.be(0); + expect(results[2].tags.length).toBe(0); }); }); @@ -104,16 +99,16 @@ describe('MSSQLDatasource', function() { ctx.backendSrv.datasourceRequest = function(options) { return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { + + return ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return list of all column values', function() { - expect(results.length).to.be(6); - expect(results[0].text).to.be('aTitle'); - expect(results[5].text).to.be('some text3'); + expect(results.length).toBe(6); + expect(results[0].text).toBe('aTitle'); + expect(results[5].text).toBe('some text3'); }); }); @@ -141,18 +136,18 @@ describe('MSSQLDatasource', function() { ctx.backendSrv.datasourceRequest = function(options) { return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { + + return ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return list of as text, value', function() { - expect(results.length).to.be(3); - expect(results[0].text).to.be('aTitle'); - expect(results[0].value).to.be('value1'); - expect(results[2].text).to.be('aTitle3'); - expect(results[2].value).to.be('value3'); + expect(results.length).toBe(3); + expect(results[0].text).toBe('aTitle'); + expect(results[0].value).toBe('value1'); + expect(results[2].text).toBe('aTitle3'); + expect(results[2].value).toBe('value3'); }); }); @@ -180,16 +175,16 @@ describe('MSSQLDatasource', function() { ctx.backendSrv.datasourceRequest = function(options) { return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { + + return ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return list of unique keys', function() { - expect(results.length).to.be(1); - expect(results[0].text).to.be('aTitle'); - expect(results[0].value).to.be('same'); + expect(results.length).toBe(1); + expect(results[0].text).toBe('aTitle'); + expect(results[0].value).toBe('same'); }); }); @@ -200,33 +195,33 @@ describe('MSSQLDatasource', function() { describe('and value is a string', () => { it('should return an unquoted value', () => { - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('abc'); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual('abc'); }); }); describe('and value is a number', () => { it('should return an unquoted value', () => { - expect(ctx.ds.interpolateVariable(1000, ctx.variable)).to.eql(1000); + expect(ctx.ds.interpolateVariable(1000, ctx.variable)).toEqual(1000); }); }); describe('and value is an array of strings', () => { it('should return comma separated quoted values', () => { - expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql("'a','b','c'"); + expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).toEqual("'a','b','c'"); }); }); describe('and variable allows multi-value and value is a string', () => { it('should return a quoted value', () => { ctx.variable.multi = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'"); }); }); describe('and variable allows all and value is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'"); }); }); }); From fdf7a4c435ad1809fc0ff289fb5c11126e1bb72c Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 20 Mar 2018 13:15:45 +0100 Subject: [PATCH 33/78] changelog: adds note about closing #11114 & #11086 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f293953892..304b1ba6d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ * **Units**: Second to HH:mm:ss formatter [#11107](https://github.com/grafana/grafana/issues/11107), thx [@gladdiologist](https://github.com/gladdiologist) * **Singlestat**: Add color to prefix and postfix in singlestat panel [#11143](https://github.com/grafana/grafana/pull/11143), thx [@ApsOps](https://github.com/ApsOps) +# 5.0.4 (unreleased) +* **Dashboard** Fixed bug where collapsed panels could not be directly linked to/renderer [#11114](https://github.com/grafana/grafana/issues/11114) & [#11086](https://github.com/grafana/grafana/issues/11086) + # 5.0.3 (2018-03-16) * **Mysql**: Mysql panic occurring occasionally upon Grafana dashboard access (a bigger patch than the one in 5.0.2) [#11155](https://github.com/grafana/grafana/issues/11155) From 3bdd0062912abe2e9afa1540d26e61a0a171ce1e Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 20 Mar 2018 14:26:03 +0100 Subject: [PATCH 34/78] changed var to const --- public/app/plugins/datasource/graphite/add_graphite_func.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.ts b/public/app/plugins/datasource/graphite/add_graphite_func.ts index 360d606c924..f2a596c7071 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.ts +++ b/public/app/plugins/datasource/graphite/add_graphite_func.ts @@ -5,10 +5,10 @@ import rst2html from 'rst2html'; import Drop from 'tether-drop'; export function graphiteAddFunc($compile) { - var inputTemplate = + const inputTemplate = ''; - var buttonTemplate = + const buttonTemplate = '' + ''; From 54c4b6a11ae43c08792eed529c8f6d27326d7bcf Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 20 Mar 2018 14:39:19 +0100 Subject: [PATCH 35/78] Remove unused kibana images --- public/img/kibana.png | Bin 5290 -> 0 bytes public/img/small.png | Bin 335 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 public/img/kibana.png delete mode 100644 public/img/small.png diff --git a/public/img/kibana.png b/public/img/kibana.png deleted file mode 100644 index 85220e3322682aa4154ee6f893c4aac6d65ef8a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5290 zcmX|F3p7+~*q#}CX12?)31KjMLLqX$wl9rKBF$lzS$NDtYDH zHmSvPLB!H6(!Y~_S#Ikc+U1*X>CwBm(3M??b=es9n}`VXn6&pDP)j`JeDw8y3% zlR9xtka3t1r2;;^+f|9wgQ@AP<(wi!pMd35_^m+o!vu_ zb=G6a<4R%u?GYR}HHVoR4LP~fAd%Fz-79ZZ0OvxTykIgv9$-k^n1qIft!Ru7f^hS4VaFsO}VQ8+b-#VKEclSVg2&}0`XH@<(dbe70HuQ z3m&PNY8fFSyZxYqwA$3}e}c&A&E=Fmkh1#Ms-5-&2hSv*d+v0rdAKy|P>E?5`)Hvs z{K~a{y(QJ5!QaiU{t)!3HXgFsk^c$vwdQin-ltE?!=|?mNnuPJ7#~?n&n7U!IfVB~ zGcB0x2Pq$Y9(670;qz7P^DyI-i|#snhcZ$m-{hC)rnR#xU)}T%yMn|jrgh?@){ybe z$}1-l9wa(dVN^TK-fsvuGb#>^-jRQ}yT49zuj%G{^Zk2TpT|-XosymBF%BLdQiR3V z(nJdqFwxV!7Y)JJhzbxZi?ei#M=<*!fiWn+hiI?oo(+T&jKJuV5OWY_$3jbFgtzu{ zHy#N$lo((G5vBtX%ZEP?axgOp<~z(j1<4tK*#QvK2gw^7;3;NLx$A&MM65GI`6vs- zJ+XEr#ztc(J?V2O^u7U~rYMi|nlTGEomZ=pK^Od>m8aHhN-seOizdJZR+ds`{WecL zl4=MJ_YzE>S+ggqv93LYIhe*Sr{LQZfiem7V|P(_ZD7P0tkepG!cw#M>1NuH&E zW6_wjYQzZq_Fpnz?!rr8Z7%c#hX-J7K%@ykw(j8T#oIFIiXY_KOMniNX{Vz_)_N82 z;3*4e*jF1Z`eV^}yCK-=gYc4~F{#mc`d|%5__H#>9%WPX&+GDG;Pij*q14iBJ1l+$Z+5><@}mdVsg`BxLP)SY0|3a4-G$^aPiDYSW*! zW?8Q)%ffrCBYUdfe0W_|7L~Yz2|2*k0eHm= zWk81@4*Y;L-loWh8CV63w8eLfN-P!XKf1^4;s(Gk!S(UEBUBjI^5 zJ0{|tO6P^YRckuJi$OFbKEN!u-I%@hh1B!-;a8e%AjC@BPZl%}z^cHja>{-v-4ALs z01sT&22B4~;QtLw0J{LwY66ta8$fik%nah+)q0BwG#Ja2!E8la$5tiBeR6b9cECByfmmBFlF?KPB#bEF`W*I-ls@gSmSk!_d+XC0v4qFFoyLuHfLV!lT*122VpMn(^Z3zsyC-0uX2*4qKPCjBq9Y0ah0Vo zHw2QzTS)O!Do}!?j{BnHPI@p%_iapqH5=fU)z+Crs`#Q=RyoNK6jA`mJ~|B2w6>(~ z9pG(64TPIhxBEw@Ztn|s0vn^6Z2ZY?if1qWS}RDJgT-Hd&ZPu8+O4cnw-|yU>VSza zdbcItdCP;HG5Qa7o+#mO(R)&Ay?A6>;uqQNlfK9Amu`2s;qw~^y(!T;7F3d0W(<9#H~r^J8~1Cs){Y)*xki+Wq%c>TL~&RUw$i*Mc#N3aUyP9k_Bm5GZb%&t-f*y1h@H ztD^~sl_K4zbSQ~E-u9Ht{W@>koSTjYh}RvNLL&j8MMt$T=>2mWDIp()E36k@B+oo) zPL><=)OQ3jr9rpZOIsa(#a9m_Q86FdGe2)dW$0>zhJSNo;d;YWPbXYzZ|)f>+}sgY zVN|&ckukmL_MnMcL1h{<4kWfV?W$iDDbmoxiB(B@B^+l~o`fPI15mO4aqnQ9HLs|0=v3|P z=-uY1s4&9-u>?;mpZqv%2TAMZSw2_~HU_K=6rs=z!hG~PFXzG9KL_!;emzgdj9YR? z#_n=G3Bsn1n#@_8w2GJ9dL7W@GlH=|#GjhV6)3X005sKm@#Pd*-vz1@_M}S?E`aVA zu2h9D3p;4^=$$)+C-RgA`f~*+vtORuLTuQ5=54Bb(AXXt{le-K9p8R@H{PHs z!BT{XH&DQpJ1qJoWu4oW@`V=m`v?03Sya@}0CCqb3)!P=QyZC;rQRP9_x;E*%5`iD z;0UgzL}u)zoH2xgE9;lYRGILGp;+VVnC5)}Aj+W<) zyCJYR4WT1D`bd|H5M@nCxo-9%Zjh;;r9z}1B8OUHca~yl8ZdLSp0{ZI?;_aP%liIN zqUn8*$a$g52^b0YKqLF4g0YJuS}eSEqj@mh{a(^%|K*2QZ$z&nG~9Exz_==}il4Y_ ze8iS2T`wPzp1@am85WZ0;fF90@KQ3HsBC(Q%O&w zq@)^wA2JKc6|;rs+KJunlm-)F5`63W<4^OZdMpzCSWBG)ye{O!SKkmaV_gkZHHWz2 z&>Mlk3~cmhNKyd|)7&zbd?9URupz%f(Fs54?gDK#|MlRw2_LunGq$mdV|sux7Yq+q zRF~kKfJyhiO{FqHP18EdgOI7#%I3L6VOLr<@l5u*2HdRFqRyr9>i^F+AqOtzrGLpY5q_G0E&0q{i&c{mQ=Cuc9!yd-w z@uAF3)6b6Tw8K9%ejDM%aoT-EIlLi}#pfxnjFSK?ID7BO zk_pflI=XU`P3R~&*JI}eI{$73W~Is4ZVp5R_t-A%%+AO{zQp@R*>@>Y+L^}PG}g5P zMo)0UFNomXa6WCYa$faURyvIe?Miyr=KQ+9Mr;SyZn9enly25ay(wEni~t{ywjVf)NVb(6kQjdnEjS7nE zN8LQ=^9Mc%0nv#av&s+ExVY7`J)(nMM650OV}!RHC;Vj!iSs2-T(^Z3o(3 zr$WI1p#1Hgs?kWVbUU?+Td9P*h}K+#?67rb zK=XZ;>SToeNt)bJhMfjirvrbeojk!jhW>^8dw46DMZW0tM~7ZKQt{rh!FAwoo`$L* ztVO3?SV~jMvuKc=JnBJ8(YiV4JL+Pav$w@YyJy|WQ2wawf>8ojx1F4fXq`TPNR`8A zj$&}uQ}gA}pd$`MtNVwf!2{oPyCdMeUSHy#$wVH`Ypv~=8|oV1*(El1<(LfPjk8AG zZjqiaiJmo8i8KZ0 ztwnQusZMmiX$O^%<>K?mru*~M1ANGtmN(>1WdyUG)h{fyw9%DM-g>>oM0Rc@sk8&f zvmKPO%FDj&=%{UikV=Xabmb@Lq>>PwOyUvbLeDEYbe^v~$$VcTgKJkg(;cV&kkhpF zdmXgPP>hR=HPbE8`3E7%b6A}(LTcvKJiM@H=%h9-Yr&y=B+ZQ&vcDFeC^EeB1;tFu zSc$cisxCC;9#H()kLNO&}6B)mrk-~10wluW(bH`{;_-odiz@Zj^Fp*eyMsBjkJ^o#MUqV zxReA99NJ=(aiPLK=Y<{%Y$}%iiaq)f=1&W4;ibf2joE#0+O)Klt?g2xvwC@sGK}>P z?wixP|5oGTOD!t)fvi>OspWY5)QDy+$CIHD!?rvHwZFGbOw!Wz5x6ebWx^-b%9R79 z&gEVRjNSa*G3XO`MPq%M30NU^%)2=TJw4{@1-V#BUyX@ zM)P3Qbv*7SgY=O+g$CX^l>M`k$JR!BM9-rjJK=BH9D+#Skf*K>KA}g6<(2kpL{@0% z7A(dIH!M0P1tLd(L6Q4fw zqpe4x#41Ytm`GEyZV}mQw0>r`uO|&iC_Nn1q`{pn!v0Fjr)qO5Hgq$!J!tM4HoYEU zJ4+iL@Yp%=wF`We?xG-q)GtR}bCYhSo(HXDN6l`?{2t}XoEmqV#dksfroq+i6+x#B zJZLR;gpjCGgzP~(LY~;5Yi`lajQ60~hdF_S&J5X8Q4{9ASOHdkZ{>qcgkIG zUqV-I2B|OCr1n(K!r$Q25#j>MHAiq%WV_V-@N*WfCdBO6+4rW(1%i0}JDS`h7wiY@ zTyTBP!tRp8l(jMD8Fy{&R1sm`Ius%ZO*7toP)ce%&TwD;Q{g@BrF7P1 zVzGUR+U(g1h6NC!`FKa%=nJs449;TVKElu4{zhXx$j|8m%ui{xYI*Lsw=b9wlWyEn;6wV!Ic~*EOEZSHQKsZ9SwMdpZ)q^th-@GA-K#(-7zI z91Xo`p*BP%{2TgPWF|i!K9;dX(zrJ8FhAAY&r>|DK!JKYx32Mp zE$}`hWtyuK=eT+=bRa$@lao>`y|a0WPrmvw?0T+X{se?M7TD4~qg8{kvuStTi}QKJ;_RscDo8|d2V^5nGp@pz zuOiseGB5+Di^pF-XV_iu01}9YwoCHi&0}AYNHfm?3_k*#HrpvU2q`>%;UB?~WVojp zia7ZVHX3sT1qEVYvSZ30`q5QyJ{T3nE@uHc7KbME&eStN|{P}Xjw*cE)6~b$*DF!75{w+Q46ck(=FO& z_LAx3F?OP000mO1^@s60003NNklo?`jrj{lDapKK63x=d6r|UQ7O|RXYyAjC#g2j?wK)mJEPK&8P`X~-dp66YBuoOto z1LAwAmTb;VMUL#6O*x7{@w0GCc3M7pwadx?Sz3^at+L^ojF!{7I zXIS$uKIj5l0*u`G*iu!)mO`bvjk!}mhP>Ee<&PtyW`iv`|DZeb(nCS)rNM*U3=Ny} zZEz*uh7GwcueO`xl>fgfl= Date: Tue, 20 Mar 2018 14:38:56 +0100 Subject: [PATCH 36/78] mssql: adds test for time should be ms in table mode --- pkg/tsdb/mssql/mssql_test.go | 291 +++++++++++++++++++---------------- 1 file changed, 159 insertions(+), 132 deletions(-) diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 88b35b1aa2c..7ac135ec2f5 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -41,40 +41,40 @@ func TestMSSQL(t *testing.T) { Convey("Given a table with different native data types", func() { sql := ` - IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL - DROP TABLE dbo.[mssql_types] + IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL + DROP TABLE dbo.[mssql_types] - CREATE TABLE [mssql_types] ( - c_bit bit, + CREATE TABLE [mssql_types] ( + c_bit bit, - c_tinyint tinyint, - c_smallint smallint, - c_int int, - c_bigint bigint, + c_tinyint tinyint, + c_smallint smallint, + c_int int, + c_bigint bigint, - c_money money, - c_smallmoney smallmoney, - c_numeric numeric(10,5), - c_real real, - c_decimal decimal(10,2), - c_float float, + c_money money, + c_smallmoney smallmoney, + c_numeric numeric(10,5), + c_real real, + c_decimal decimal(10,2), + c_float float, - c_char char(10), - c_varchar varchar(10), - c_text text, + c_char char(10), + c_varchar varchar(10), + c_text text, - c_nchar nchar(12), - c_nvarchar nvarchar(12), - c_ntext ntext, + c_nchar nchar(12), + c_nvarchar nvarchar(12), + c_ntext ntext, - c_datetime datetime, - c_datetime2 datetime2, - c_smalldatetime smalldatetime, - c_date date, - c_time time, - c_datetimeoffset datetimeoffset - ) - ` + c_datetime datetime, + c_datetime2 datetime2, + c_smalldatetime smalldatetime, + c_date date, + c_time time, + c_datetimeoffset datetimeoffset + ) + ` _, err := sess.Exec(sql) So(err, ShouldBeNil) @@ -87,14 +87,14 @@ func TestMSSQL(t *testing.T) { d2 := dt2.Format(dt2Format) sql = fmt.Sprintf(` - INSERT INTO [mssql_types] - SELECT - 1, 5, 20020, 980300, 1420070400, '$20000.15', '£2.15', 12345.12, - 1.11, 2.22, 3.33, - 'char10', 'varchar10', 'text', - N'☺nchar12☺', N'☺nvarchar12☺', N'☺text☺', - CAST('%s' AS DATETIME), CAST('%s' AS DATETIME2), CAST('%s' AS SMALLDATETIME), CAST('%s' AS DATE), CAST('%s' AS TIME), SWITCHOFFSET(CAST('%s' AS DATETIMEOFFSET), '-07:00') - `, d, d2, d, d, d, d2) + INSERT INTO [mssql_types] + SELECT + 1, 5, 20020, 980300, 1420070400, '$20000.15', '£2.15', 12345.12, + 1.11, 2.22, 3.33, + 'char10', 'varchar10', 'text', + N'☺nchar12☺', N'☺nvarchar12☺', N'☺text☺', + CAST('%s' AS DATETIME), CAST('%s' AS DATETIME2), CAST('%s' AS SMALLDATETIME), CAST('%s' AS DATE), CAST('%s' AS TIME), SWITCHOFFSET(CAST('%s' AS DATETIMEOFFSET), '-07:00') + `, d, d2, d, d, d, d2) _, err = sess.Exec(sql) So(err, ShouldBeNil) @@ -151,14 +151,14 @@ func TestMSSQL(t *testing.T) { Convey("Given a table with metrics that lacks data for some series ", func() { sql := ` - IF OBJECT_ID('dbo.[metric]', 'U') IS NOT NULL - DROP TABLE dbo.[metric] + IF OBJECT_ID('dbo.[metric]', 'U') IS NOT NULL + DROP TABLE dbo.[metric] - CREATE TABLE [metric] ( - time datetime, - value int - ) - ` + CREATE TABLE [metric] ( + time datetime, + value int + ) + ` _, err := sess.Exec(sql) So(err, ShouldBeNil) @@ -189,9 +189,9 @@ func TestMSSQL(t *testing.T) { dtFormat := "2006-01-02 15:04:05.999999999" for _, s := range series { sql = fmt.Sprintf(` - INSERT INTO metric (time, value) - VALUES(CAST('%s' AS DATETIME), %d) - `, s.Time.Format(dtFormat), s.Value) + INSERT INTO metric (time, value) + VALUES(CAST('%s' AS DATETIME), %d) + `, s.Time.Format(dtFormat), s.Value) _, err = sess.Exec(sql) So(err, ShouldBeNil) @@ -306,16 +306,16 @@ func TestMSSQL(t *testing.T) { Convey("Given a table with metrics having multiple values and measurements", func() { sql := ` - IF OBJECT_ID('dbo.[metric_values]', 'U') IS NOT NULL - DROP TABLE dbo.[metric_values] + IF OBJECT_ID('dbo.[metric_values]', 'U') IS NOT NULL + DROP TABLE dbo.[metric_values] - CREATE TABLE [metric_values] ( - time datetime, - measurement nvarchar(100), - valueOne int, - valueTwo int, - ) - ` + CREATE TABLE [metric_values] ( + time datetime, + measurement nvarchar(100), + valueOne int, + valueTwo int, + ) + ` _, err := sess.Exec(sql) So(err, ShouldBeNil) @@ -351,9 +351,9 @@ func TestMSSQL(t *testing.T) { dtFormat := "2006-01-02 15:04:05" for _, s := range series { sql = fmt.Sprintf(` - INSERT metric_values (time, measurement, valueOne, valueTwo) - VALUES(CAST('%s' AS DATETIME), '%s', %d, %d) - `, s.Time.Format(dtFormat), s.Measurement, s.ValueOne, s.ValueTwo) + INSERT metric_values (time, measurement, valueOne, valueTwo) + VALUES(CAST('%s' AS DATETIME), '%s', %d, %d) + `, s.Time.Format(dtFormat), s.Measurement, s.ValueOne, s.ValueTwo) _, err = sess.Exec(sql) So(err, ShouldBeNil) @@ -407,45 +407,45 @@ func TestMSSQL(t *testing.T) { Convey("Given a stored procedure that takes @from and @to in epoch time", func() { sql := ` - IF object_id('sp_test_epoch') IS NOT NULL - DROP PROCEDURE sp_test_epoch - ` + IF object_id('sp_test_epoch') IS NOT NULL + DROP PROCEDURE sp_test_epoch + ` _, err := sess.Exec(sql) So(err, ShouldBeNil) sql = ` - CREATE PROCEDURE sp_test_epoch( - @from int, - @to int - ) AS - BEGIN - SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, - measurement + ' - value one' as metric, - avg(valueOne) as value - FROM - metric_values - WHERE - time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') - GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), - measurement - UNION ALL - SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, - measurement + ' - value two' as metric, - avg(valueTwo) as value - FROM - metric_values - WHERE - time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') - GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), - measurement - ORDER BY 1 - END - ` + CREATE PROCEDURE sp_test_epoch( + @from int, + @to int + ) AS + BEGIN + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + measurement + ' - value one' as metric, + avg(valueOne) as value + FROM + metric_values + WHERE + time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + measurement + UNION ALL + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + measurement + ' - value two' as metric, + avg(valueTwo) as value + FROM + metric_values + WHERE + time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + measurement + ORDER BY 1 + END + ` _, err = sess.Exec(sql) So(err, ShouldBeNil) @@ -456,10 +456,10 @@ func TestMSSQL(t *testing.T) { { Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `DECLARE - @from int = $__unixEpochFrom(), - @to int = $__unixEpochTo() + @from int = $__unixEpochFrom(), + @to int = $__unixEpochTo() - EXEC dbo.sp_test_epoch @from, @to`, + EXEC dbo.sp_test_epoch @from, @to`, "format": "time_series", }), RefId: "A", @@ -486,45 +486,45 @@ func TestMSSQL(t *testing.T) { Convey("Given a stored procedure that takes @from and @to in datetime", func() { sql := ` - IF object_id('sp_test_datetime') IS NOT NULL - DROP PROCEDURE sp_test_datetime - ` + IF object_id('sp_test_datetime') IS NOT NULL + DROP PROCEDURE sp_test_datetime + ` _, err := sess.Exec(sql) So(err, ShouldBeNil) sql = ` - CREATE PROCEDURE sp_test_datetime( - @from datetime, - @to datetime - ) AS - BEGIN - SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, - measurement + ' - value one' as metric, - avg(valueOne) as value - FROM - metric_values - WHERE - time >= @from AND time <= @to - GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), - measurement - UNION ALL - SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, - measurement + ' - value two' as metric, - avg(valueTwo) as value - FROM - metric_values - WHERE - time >= @from AND time <= @to - GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), - measurement - ORDER BY 1 - END - ` + CREATE PROCEDURE sp_test_datetime( + @from datetime, + @to datetime + ) AS + BEGIN + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + measurement + ' - value one' as metric, + avg(valueOne) as value + FROM + metric_values + WHERE + time >= @from AND time <= @to + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + measurement + UNION ALL + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + measurement + ' - value two' as metric, + avg(valueTwo) as value + FROM + metric_values + WHERE + time >= @from AND time <= @to + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + measurement + ORDER BY 1 + END + ` _, err = sess.Exec(sql) So(err, ShouldBeNil) @@ -535,10 +535,10 @@ func TestMSSQL(t *testing.T) { { Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `DECLARE - @from int = $__unixEpochFrom(), - @to int = $__unixEpochTo() + @from int = $__unixEpochFrom(), + @to int = $__unixEpochTo() - EXEC dbo.sp_test_epoch @from, @to`, + EXEC dbo.sp_test_epoch @from, @to`, "format": "time_series", }), RefId: "A", @@ -654,6 +654,33 @@ func TestMSSQL(t *testing.T) { So(err, ShouldBeNil) So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) }) + + Convey("When doing an annotation query with a time column in datetime format", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT DATEADD(s, time_sec, {d '1970-01-01'}) AS time, description as [text], tags FROM [event] WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC", + "format": "table", + }), + RefId: "Tickets", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Tickets"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(float64), ShouldBeGreaterThan, 1000000000000) + }) }) }) } From 47215098a3f3b3016ba22296c2b65520c84423bc Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 20 Mar 2018 14:43:09 +0100 Subject: [PATCH 37/78] changed var to const, changed to string interpolation --- .../datasource/graphite/func_editor.ts | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/public/app/plugins/datasource/graphite/func_editor.ts b/public/app/plugins/datasource/graphite/func_editor.ts index 1a4c6d4313a..86135aef343 100644 --- a/public/app/plugins/datasource/graphite/func_editor.ts +++ b/public/app/plugins/datasource/graphite/func_editor.ts @@ -4,16 +4,17 @@ import $ from 'jquery'; import rst2html from 'rst2html'; export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { - var funcSpanTemplate = '{{func.def.name}}('; - var paramTemplate = ''; + const funcSpanTemplate = '{{func.def.name}}('; + const paramTemplate = + ''; - var funcControlsTemplate = - '
' + - '' + - '' + - '' + - '' + - '
'; + const funcControlsTemplate = ` +
+ + + + +
`; return { restrict: 'A', @@ -281,13 +282,11 @@ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { element: e.target, position: 'bottom left', classNames: 'drop-popover drop-function-def', - template: - '
' + - '

' + - funcDef.name + - '

' + - rst2html(funcDef.description) + - '
', + template: ` +
+

${funcDef.name}

+ ${rst2html(funcDef.description)} +
`, openOn: 'click', }); } else { From f9acb4157b515c9dbb9e7f2b77b362dcf491e1fa Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 20 Mar 2018 15:26:41 +0100 Subject: [PATCH 38/78] Expose option to disable snippets --- public/app/core/components/code_editor/code_editor.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 8cbd888bb1b..886ae2a6407 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -21,6 +21,8 @@ * data-tab-size - Tab size, default is 2. * data-behaviours-enabled - Specifies whether to use behaviors or not. "Behaviors" in this case is the auto-pairing of * special characters, like quotation marks, parenthesis, or brackets. + * data-snippets-enabled - Specifies whether to use snippets or not. "Snippets" are small pieces of code that can be + * inserted via the completion box. * * Keybindings: * Ctrl-Enter (Command-Enter): run onChange() function @@ -49,6 +51,7 @@ const DEFAULT_MODE = 'text'; const DEFAULT_MAX_LINES = 10; const DEFAULT_TAB_SIZE = 2; const DEFAULT_BEHAVIOURS = true; +const DEFAULT_SNIPPETS = true; let editorTemplate = `
`; @@ -59,6 +62,7 @@ function link(scope, elem, attrs) { let showGutter = attrs.showGutter !== undefined; let tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; let behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; + let snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS; // Initialize editor let aceElem = elem.get(0); @@ -143,7 +147,7 @@ function link(scope, elem, attrs) { codeEditor.setOptions({ enableBasicAutocompletion: true, enableLiveAutocompletion: true, - enableSnippets: true, + enableSnippets: snippetsEnabled, }); if (scope.getCompleter()) { From 0e159dada1be72b121c93cb2d682b890f44968e9 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Tue, 20 Mar 2018 22:44:48 +0500 Subject: [PATCH 39/78] Allocated to a separate alignment block. Replaced the attribute of the second axis by the attribute of the axes. --- .../app/plugins/panel/graph/axes_editor.html | 20 +++++++++---------- public/app/plugins/panel/graph/graph.ts | 4 ++-- public/app/plugins/panel/graph/module.ts | 6 ++++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index f17c9ce105f..9020bbe4446 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -31,16 +31,6 @@
-
-
- -
-
- - -
-
-
@@ -77,6 +67,16 @@
+
+
+
Y-Axes
+ +
+ + +
+
+
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 713d7079152..dc801a1b33f 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -158,8 +158,8 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { function processRangeHook(plot) { var yaxis = plot.getYAxes(); - if (yaxis.length > 1 && panel.yaxes[1].alignment) { - var align = panel.yaxes[1].align || 0; + if (yaxis.length > 1 && panel.yaxis.alignment) { + var align = panel.yaxis.align || 0; alignYLevel(yaxis, parseFloat(align)); } } diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 7e7b270bd61..2fe1ecc8684 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -46,8 +46,6 @@ class GraphCtrl extends MetricsPanelCtrl { min: null, max: null, format: 'short', - alignment: false, - align: 0, }, ], xaxis: { @@ -57,6 +55,10 @@ class GraphCtrl extends MetricsPanelCtrl { values: [], buckets: null, }, + yaxis: { + alignment: false, + align: 0, + }, // show/hide lines lines: true, // fill factor From 70630d742ef564307bc52d1aa2a4ada6f259744c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Mar 2018 16:40:40 +0100 Subject: [PATCH 40/78] snapshots: removes errors for empty values in ViewStore Occurs when opening a snapshot. --- public/app/stores/ViewStore/ViewStore.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/public/app/stores/ViewStore/ViewStore.ts b/public/app/stores/ViewStore/ViewStore.ts index d00bf3c429f..ba966a194d8 100644 --- a/public/app/stores/ViewStore/ViewStore.ts +++ b/public/app/stores/ViewStore/ViewStore.ts @@ -26,7 +26,9 @@ export const ViewStore = types function updateQuery(query: any) { self.query.clear(); for (let key of Object.keys(query)) { - self.query.set(key, query[key]); + if (query[key]) { + self.query.set(key, query[key]); + } } } @@ -34,7 +36,9 @@ export const ViewStore = types function updateRouteParams(routeParams: any) { self.routeParams.clear(); for (let key of Object.keys(routeParams)) { - self.routeParams.set(key, routeParams[key]); + if (routeParams[key]) { + self.routeParams.set(key, routeParams[key]); + } } } From 92388f7faf80bcc68f496a8633e8d5b4748e0cf8 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Mar 2018 19:31:01 +0100 Subject: [PATCH 41/78] session: update defaults for ConnMaxLifetime to be the same as the 5.0.3 release defaults --- conf/defaults.ini | 3 +++ pkg/setting/setting.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 557c5e49ee1..11d173d955d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -128,6 +128,9 @@ cookie_secure = false session_life_time = 86400 gc_interval_time = 86400 +# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) +conn_max_lifetime = 14400 + #################################### Data proxy ########################### [dataproxy] diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index c19043c69d0..5b79e866964 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -636,7 +636,7 @@ func readSessionConfig() { SessionOptions.CookiePath = "/" } - SessionConnMaxLifetime = Cfg.Section("session").Key("conn_max_lifetime").MustInt64(0) + SessionConnMaxLifetime = Cfg.Section("session").Key("conn_max_lifetime").MustInt64(14400) } func initLogging() { From a472d38fbf97dc721137ac7347c759e8e3f8db88 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 20 Mar 2018 21:33:54 +0300 Subject: [PATCH 42/78] snapshot: fix legend rendering bug --- public/app/features/panel/metrics_panel_ctrl.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 373211611d8..6e7f326dc08 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -78,8 +78,11 @@ class MetricsPanelCtrl extends PanelCtrl { data = data.data; } - this.events.emit('data-snapshot-load', data); - return; + // Defer panel rendering till the next digest cycle. + // For some reason snapshot panels don't init at this time, so this helps to avoid rendering issues. + return this.$timeout(() => { + this.events.emit('data-snapshot-load', data); + }); } // // ignore if we have data stream From 5d23e7710ba0be0d8b501a493d65211cdf9ea366 Mon Sep 17 00:00:00 2001 From: Thibault Chataigner Date: Fri, 16 Feb 2018 17:00:13 +0100 Subject: [PATCH 43/78] Alerting: Add retry mechanism and its unitests Signed-off-by: Thibault Chataigner --- pkg/services/alerting/engine.go | 89 ++++++++++++++------ pkg/services/alerting/engine_test.go | 118 +++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 26 deletions(-) create mode 100644 pkg/services/alerting/engine_test.go diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 4448a5cb978..49a11b54c1d 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -86,17 +86,63 @@ func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error { case <-grafanaCtx.Done(): return dispatcherGroup.Wait() case job := <-e.execQueue: - dispatcherGroup.Go(func() error { return e.processJob(alertCtx, job) }) + dispatcherGroup.Go(func() error { return e.processJobWithRetry(alertCtx, job) }) } } } var ( unfinishedWorkTimeout time.Duration = time.Second * 5 - alertTimeout time.Duration = time.Second * 30 + // TODO: Make alertTimeout and alertMaxAttempts configurable in the config file. + alertTimeout time.Duration = time.Second * 30 + alertMaxAttempts int = 3 ) -func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error { +func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error { + defer func() { + if err := recover(); err != nil { + e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1)) + } + }() + + cancelChan := make(chan context.CancelFunc, alertMaxAttempts) + attemptChan := make(chan int, 1) + + // Initialize with first attemptID=1 + attemptChan <- 1 + job.Running = true + + for { + select { + case <-grafanaCtx.Done(): + // In case grafana server context is cancel, let a chance to job processing + // to finish gracefully - by waiting a timeout duration - before forcing its end. + unfinishedWorkTimer := time.NewTimer(unfinishedWorkTimeout) + select { + case <-unfinishedWorkTimer.C: + return e.endJob(grafanaCtx.Err(), cancelChan, job) + case <-attemptChan: + return e.endJob(nil, cancelChan, job) + } + case attemptID, more := <-attemptChan: + if !more { + return e.endJob(nil, cancelChan, job) + } + go e.processJob(attemptID, attemptChan, cancelChan, job) + } + } +} + +func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error { + job.Running = false + close(cancelChan) + for cancelFn := range cancelChan { + cancelFn() + } + return err +} + +func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) { defer func() { if err := recover(); err != nil { e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1)) @@ -104,14 +150,13 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error { }() alertCtx, cancelFn := context.WithTimeout(context.Background(), alertTimeout) + cancelChan <- cancelFn span := opentracing.StartSpan("alert execution") alertCtx = opentracing.ContextWithSpan(alertCtx, span) - job.Running = true evalContext := NewEvalContext(alertCtx, job.Rule) evalContext.Ctx = alertCtx - done := make(chan struct{}) go func() { defer func() { if err := recover(); err != nil { @@ -122,43 +167,35 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error { tlog.String("message", "failed to execute alert rule. panic was recovered."), ) span.Finish() - close(done) + close(attemptChan) } }() e.evalHandler.Eval(evalContext) - e.resultHandler.Handle(evalContext) span.SetTag("alertId", evalContext.Rule.Id) span.SetTag("dashboardId", evalContext.Rule.DashboardId) span.SetTag("firing", evalContext.Firing) span.SetTag("nodatapoints", evalContext.NoDataFound) + span.SetTag("attemptID", attemptID) + if evalContext.Error != nil { ext.Error.Set(span, true) span.LogFields( tlog.Error(evalContext.Error), - tlog.String("message", "alerting execution failed"), + tlog.String("message", "alerting execution attempt failed"), ) + if attemptID < alertMaxAttempts { + span.Finish() + e.log.Debug("Job Execution attempt triggered retry", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) + attemptChan <- (attemptID + 1) + return + } } + e.resultHandler.Handle(evalContext) span.Finish() - close(done) + e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) + close(attemptChan) }() - - var err error = nil - select { - case <-grafanaCtx.Done(): - select { - case <-time.After(unfinishedWorkTimeout): - cancelFn() - err = grafanaCtx.Err() - case <-done: - } - case <-done: - } - - e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing) - job.Running = false - cancelFn() - return err } diff --git a/pkg/services/alerting/engine_test.go b/pkg/services/alerting/engine_test.go new file mode 100644 index 00000000000..64f954c6dd5 --- /dev/null +++ b/pkg/services/alerting/engine_test.go @@ -0,0 +1,118 @@ +package alerting + +import ( + "context" + "errors" + "math" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +type FakeEvalHandler struct { + SuccessCallID int // 0 means never sucess + CallNb int +} + +func NewFakeEvalHandler(successCallID int) *FakeEvalHandler { + return &FakeEvalHandler{ + SuccessCallID: successCallID, + CallNb: 0, + } +} + +func (handler *FakeEvalHandler) Eval(evalContext *EvalContext) { + handler.CallNb++ + if handler.CallNb != handler.SuccessCallID { + evalContext.Error = errors.New("Fake evaluation failure") + } +} + +type FakeResultHandler struct{} + +func (handler *FakeResultHandler) Handle(evalContext *EvalContext) error { + return nil +} + +func TestEngineProcessJob(t *testing.T) { + Convey("Alerting engine job processing", t, func() { + engine := NewEngine() + engine.resultHandler = &FakeResultHandler{} + job := &Job{Running: true, Rule: &Rule{}} + + Convey("Should trigger retry if needed", func() { + + Convey("error + not last attempt -> retry", func() { + engine.evalHandler = NewFakeEvalHandler(0) + + for i := 1; i < alertMaxAttempts; i++ { + attemptChan := make(chan int, 1) + cancelChan := make(chan context.CancelFunc, alertMaxAttempts) + + engine.processJob(i, attemptChan, cancelChan, job) + nextAttemptID, more := <-attemptChan + + So(nextAttemptID, ShouldEqual, i+1) + So(more, ShouldEqual, true) + So(<-cancelChan, ShouldNotBeNil) + } + }) + + Convey("error + last attempt -> no retry", func() { + engine.evalHandler = NewFakeEvalHandler(0) + attemptChan := make(chan int, 1) + cancelChan := make(chan context.CancelFunc, alertMaxAttempts) + + engine.processJob(alertMaxAttempts, attemptChan, cancelChan, job) + nextAttemptID, more := <-attemptChan + + So(nextAttemptID, ShouldEqual, 0) + So(more, ShouldEqual, false) + So(<-cancelChan, ShouldNotBeNil) + }) + + Convey("no error -> no retry", func() { + engine.evalHandler = NewFakeEvalHandler(1) + attemptChan := make(chan int, 1) + cancelChan := make(chan context.CancelFunc, alertMaxAttempts) + + engine.processJob(1, attemptChan, cancelChan, job) + nextAttemptID, more := <-attemptChan + + So(nextAttemptID, ShouldEqual, 0) + So(more, ShouldEqual, false) + So(<-cancelChan, ShouldNotBeNil) + }) + }) + + Convey("Should trigger as many retries as needed", func() { + + Convey("never sucess -> max retries number", func() { + expectedAttempts := alertMaxAttempts + evalHandler := NewFakeEvalHandler(0) + engine.evalHandler = evalHandler + + engine.processJobWithRetry(context.TODO(), job) + So(evalHandler.CallNb, ShouldEqual, expectedAttempts) + }) + + Convey("always sucess -> never retry", func() { + expectedAttempts := 1 + evalHandler := NewFakeEvalHandler(1) + engine.evalHandler = evalHandler + + engine.processJobWithRetry(context.TODO(), job) + So(evalHandler.CallNb, ShouldEqual, expectedAttempts) + }) + + Convey("some errors before sucess -> some retries", func() { + expectedAttempts := int(math.Ceil(float64(alertMaxAttempts) / 2)) + evalHandler := NewFakeEvalHandler(expectedAttempts) + engine.evalHandler = evalHandler + + engine.processJobWithRetry(context.TODO(), job) + So(evalHandler.CallNb, ShouldEqual, expectedAttempts) + }) + }) + }) +} From 7a4475fbf3d7af7d2e495ecfbdc3245d08d3cfde Mon Sep 17 00:00:00 2001 From: Jordan Hamel Date: Tue, 20 Mar 2018 16:55:57 -0700 Subject: [PATCH 44/78] update email default year and name from 2016 grafana and raintank to 2018 Grafana Labs --- emails/templates/layouts/default.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emails/templates/layouts/default.html b/emails/templates/layouts/default.html index 07eb32874c7..ca54acdd206 100644 --- a/emails/templates/layouts/default.html +++ b/emails/templates/layouts/default.html @@ -143,7 +143,7 @@ td[class="stack-column-center"] {

Sent by Grafana v[[.BuildVersion]] -
© 2016 Grafana and raintank +
© 2018 Grafana Labs

From f41f2089a29e8f1365066155334ed90276769265 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Mar 2018 08:53:47 +0100 Subject: [PATCH 45/78] docs: details about provisioning elastic closes #11292 --- docs/sources/administration/provisioning.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 135973df52a..a8cb9fe3023 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -133,12 +133,18 @@ datasources: editable: false ``` +#### Extra info per datasource + +| Datasource | Misc | +| ---- | ---- | +| Elasticserach | Elasticsearch uses the `database` property to configure the index for a datasource | + #### Json data Since not all datasources have the same configuration settings we only have the most common ones as fields. The rest should be stored as a json blob in the `json_data` field. Here are the most common settings that the core datasources use. -| Name | Type | Datasource |Description | -| ----| ---- | ---- | --- | +| Name | Type | Datasource | Description | +| ---- | ---- | ---- | ---- | | tlsAuth | boolean | *All* | Enable TLS authentication using client cert configured in secure json data | | tlsAuthWithCACert | boolean | *All* | Enable TLS authtication using CA cert | | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | From 3cb0bc3da1216fd76b22c16e00b4b02d54b1a3a9 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Mar 2018 19:40:10 +0100 Subject: [PATCH 46/78] sql datasource: extract common logic for converting time column to epoch time in ms --- pkg/tsdb/sql_engine.go | 28 ++++++++++++++++++++++ pkg/tsdb/sql_engine_test.go | 46 +++++++++++++++++++++++++++++++++++++ pkg/tsdb/time_range.go | 10 ++++++++ 3 files changed, 84 insertions(+) create mode 100644 pkg/tsdb/sql_engine_test.go diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 7ea0682235f..16370a4ea7f 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -3,6 +3,7 @@ package tsdb import ( "context" "sync" + "time" "github.com/go-xorm/core" "github.com/go-xorm/xorm" @@ -133,3 +134,30 @@ func (e *DefaultSqlEngine) Query( return result, nil } + +// ConvertTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds +// to make native datetime types and epoch dates work in annotation and table queries. +func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) { + if timeIndex >= 0 { + switch value := values[timeIndex].(type) { + case time.Time: + values[timeIndex] = EpochPrecisionToMs(float64(value.Unix())) + case *time.Time: + if value != nil { + values[timeIndex] = EpochPrecisionToMs(float64((*value).Unix())) + } + case int64: + values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) + case *int64: + if value != nil { + values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) + } + case float64: + values[timeIndex] = EpochPrecisionToMs(value) + case *float64: + if value != nil { + values[timeIndex] = EpochPrecisionToMs(*value) + } + } + } +} diff --git a/pkg/tsdb/sql_engine_test.go b/pkg/tsdb/sql_engine_test.go new file mode 100644 index 00000000000..48aac2c4d45 --- /dev/null +++ b/pkg/tsdb/sql_engine_test.go @@ -0,0 +1,46 @@ +package tsdb + +import ( + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestSqlEngine(t *testing.T) { + Convey("SqlEngine", t, func() { + Convey("Given row values with time columns when converting them", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + fixtures := make([]interface{}, 8) + fixtures[0] = dt + fixtures[1] = dt.Unix() * 1000 + fixtures[2] = dt.Unix() + fixtures[3] = float64(dt.Unix() * 1000) + fixtures[4] = float64(dt.Unix()) + + var nilDt *time.Time + var nilInt64 *int64 + var nilFloat64 *float64 + fixtures[5] = nilDt + fixtures[6] = nilInt64 + fixtures[7] = nilFloat64 + + for i := range fixtures { + ConvertSqlTimeColumnToEpochMs(fixtures, i) + } + + Convey("Should convert sql time columns to epoch time in ms ", func() { + expected := float64(dt.Unix() * 1000) + So(fixtures[0].(float64), ShouldEqual, expected) + So(fixtures[1].(int64), ShouldEqual, expected) + So(fixtures[2].(int64), ShouldEqual, expected) + So(fixtures[3].(float64), ShouldEqual, expected) + So(fixtures[4].(float64), ShouldEqual, expected) + + So(fixtures[5], ShouldBeNil) + So(fixtures[6], ShouldBeNil) + So(fixtures[7], ShouldBeNil) + }) + }) + }) +} diff --git a/pkg/tsdb/time_range.go b/pkg/tsdb/time_range.go index fd797bf731a..fd0cb3f8e82 100644 --- a/pkg/tsdb/time_range.go +++ b/pkg/tsdb/time_range.go @@ -88,3 +88,13 @@ func (tr *TimeRange) ParseTo() (time.Time, error) { return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To) } + +// EpochPrecisionToMs converts epoch precision to millisecond, if needed. +// Only seconds to milliseconds supported right now +func EpochPrecisionToMs(value float64) float64 { + if int64(value)/1e10 == 0 { + return float64(value * 1e3) + } + + return float64(value) +} From 624dac16fa2807ca551ba528389500ff747df72d Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 21 Mar 2018 13:23:50 +0100 Subject: [PATCH 47/78] docs: add variable regex examples (#11327) --- docs/sources/reference/templating.md | 69 +++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index 3a15b4ed7d1..f9e16e26610 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -1,6 +1,6 @@ +++ title = "Variables" -keywords = ["grafana", "templating", "documentation", "guide"] +keywords = ["grafana", "templating", "documentation", "guide", "template", "variable"] type = "docs" [menu.docs] name = "Variables" @@ -80,6 +80,73 @@ Option | Description *Regex* | Regex to filter or capture specific parts of the names return by your data source query. Optional. *Sort* | Define sort order for options in dropdown. **Disabled** means that the order of options returned by your data source query will be used. +#### Using regex to filter/modify values in the Variable dropdown + +Using the Regex Query Option, you filter the list of options returned by the Variable query or modify the options returned. + +Examples of filtering on the following list of options: + +```text +backend_01 +backend_02 +backend_03 +backend_04 +``` + +##### Filter so that only the options that end with `01` or `02` are returned: + +Regex: + +```regex +/.*[01|02]/ +``` + +Result: + +```text +backend_01 +backend_02 +``` + +##### Filter and modify the options using a regex capture group to return part of the text: + +Regex: + +```regex +/.*(01|02)/ +``` + +Result: + +```text +01 +02 +``` + +#### Filter and modify - Prometheus Example + +List of options: + +```text +up{instance="demo.robustperception.io:9090",job="prometheus"} 1 1521630638000 +up{instance="demo.robustperception.io:9093",job="alertmanager"} 1 1521630638000 +up{instance="demo.robustperception.io:9100",job="node"} 1 1521630638000 +``` + +Regex: + +```regex +/.*instance="([^"]*).*/ +``` + +Result: + +```text +demo.robustperception.io:9090 +demo.robustperception.io:9093 +demo.robustperception.io:9100 +``` + ### Query expressions The query expressions are different for each data source. From fc2d1d6ca913df0c0a0a9d2f62f27aace755eace Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Mar 2018 17:08:25 +0300 Subject: [PATCH 48/78] fix dashboard version cleanup on large datasets --- pkg/services/sqlstore/dashboard_version.go | 39 ++++++++----------- .../sqlstore/dashboard_version_test.go | 2 +- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index 547f62628f3..3644af77355 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -1,7 +1,7 @@ package sqlstore import ( - "strings" + "fmt" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" @@ -69,36 +69,31 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error { func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { return inTransaction(func(sess *DBSession) error { - versions := []DashboardVersionExp{} versionsToKeep := setting.DashboardVersionsToKeep - if versionsToKeep < 1 { versionsToKeep = 1 } - err := sess.Table("dashboard_version"). - Select("dashboard_version.id, dashboard_version.version, dashboard_version.dashboard_id"). - Where(`dashboard_id IN ( - SELECT dashboard_id FROM dashboard_version - GROUP BY dashboard_id HAVING COUNT(dashboard_version.id) > ? - )`, versionsToKeep). - Desc("dashboard_version.dashboard_id", "dashboard_version.version"). - Find(&versions) + // Idea of this query is finding version IDs to delete based on formula: + // min_version_to_keep = min_version + (versions_count - versions_to_keep) + // where version stats is processed for each dashboard. This guarantees that we keep at least versions_to_keep + // versions, but in some cases (when versions are sparse) this number may be more. + versionIdsToDeleteSybqueryTemplate := `SELECT id + FROM dashboard_version, ( + SELECT dashboard_id, count(version) as count, min(version) as min + FROM dashboard_version + GROUP BY dashboard_id + ) AS vtd + WHERE dashboard_version.dashboard_id=vtd.dashboard_id + AND version < vtd.min + vtd.count - %v` + versionIdsToDeleteSubquery := fmt.Sprintf(versionIdsToDeleteSybqueryTemplate, versionsToKeep) + deleteExpiredSql := fmt.Sprintf(`DELETE FROM dashboard_version WHERE id IN (%s)`, versionIdsToDeleteSubquery) + expiredResponse, err := sess.Exec(deleteExpiredSql) if err != nil { return err } - - // Keep last versionsToKeep versions and delete other - versionIdsToDelete := getVersionIDsToDelete(versions, versionsToKeep) - if len(versionIdsToDelete) > 0 { - deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)` - expiredResponse, err := sess.Exec(deleteExpiredSql, versionIdsToDelete...) - if err != nil { - return err - } - cmd.DeletedRows, _ = expiredResponse.RowsAffected() - } + cmd.DeletedRows, _ = expiredResponse.RowsAffected() return nil }) diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go index 1b74e7847c4..151dc7c4be2 100644 --- a/pkg/services/sqlstore/dashboard_version_test.go +++ b/pkg/services/sqlstore/dashboard_version_test.go @@ -136,7 +136,7 @@ func TestDeleteExpiredVersions(t *testing.T) { err := DeleteExpiredVersions(&m.DeleteExpiredVersionsCommand{}) So(err, ShouldBeNil) - query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1} + query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWrite} GetDashboardVersions(&query) So(len(query.Result), ShouldEqual, versionsToWrite) From f976b690ca4a208c827077eabf4d2d7e856ba076 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Mar 2018 20:26:27 +0300 Subject: [PATCH 49/78] limit number of rows deleted by dashboard version cleanup --- pkg/services/sqlstore/dashboard_version.go | 53 ++++++++-------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index 3644af77355..754bcd504a7 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -2,6 +2,7 @@ package sqlstore import ( "fmt" + "strings" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" @@ -69,6 +70,8 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error { func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { return inTransaction(func(sess *DBSession) error { + const MAX_VERSIONS_TO_DELETE = 100 + versionsToKeep := setting.DashboardVersionsToKeep if versionsToKeep < 1 { versionsToKeep = 1 @@ -83,12 +86,25 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { SELECT dashboard_id, count(version) as count, min(version) as min FROM dashboard_version GROUP BY dashboard_id - ) AS vtd - WHERE dashboard_version.dashboard_id=vtd.dashboard_id + ) AS vtd + WHERE dashboard_version.dashboard_id=vtd.dashboard_id AND version < vtd.min + vtd.count - %v` versionIdsToDeleteSubquery := fmt.Sprintf(versionIdsToDeleteSybqueryTemplate, versionsToKeep) - deleteExpiredSql := fmt.Sprintf(`DELETE FROM dashboard_version WHERE id IN (%s)`, versionIdsToDeleteSubquery) + versions := []string{} + err := sess.SQL(versionIdsToDeleteSubquery).Find(&versions) + if err != nil { + return err + } + + // Don't delete more than MAX_VERSIONS_TO_DELETE version per time + limit := MAX_VERSIONS_TO_DELETE + if len(versions) < MAX_VERSIONS_TO_DELETE { + limit = len(versions) + } + versions = versions[:limit] + + deleteExpiredSql := fmt.Sprintf(`DELETE FROM dashboard_version WHERE id IN (%s)`, strings.Join(versions, `,`)) expiredResponse, err := sess.Exec(deleteExpiredSql) if err != nil { return err @@ -98,34 +114,3 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { return nil }) } - -// Short version of DashboardVersion for getting expired versions -type DashboardVersionExp struct { - Id int64 `json:"id"` - DashboardId int64 `json:"dashboardId"` - Version int `json:"version"` -} - -func getVersionIDsToDelete(versions []DashboardVersionExp, versionsToKeep int) []interface{} { - versionIds := make([]interface{}, 0) - - if len(versions) == 0 { - return versionIds - } - - currentDashboard := versions[0].DashboardId - count := 0 - for _, v := range versions { - if v.DashboardId == currentDashboard { - count++ - } else { - count = 1 - currentDashboard = v.DashboardId - } - if count > versionsToKeep { - versionIds = append(versionIds, v.Id) - } - } - - return versionIds -} From 3f85fcce2ddc0ff81f83565373d218fdef2dfb3f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Mar 2018 21:01:15 +0300 Subject: [PATCH 50/78] refactor: dashboard version cleanup --- pkg/services/sqlstore/dashboard_version.go | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index 754bcd504a7..54c858df7de 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -81,7 +81,7 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { // min_version_to_keep = min_version + (versions_count - versions_to_keep) // where version stats is processed for each dashboard. This guarantees that we keep at least versions_to_keep // versions, but in some cases (when versions are sparse) this number may be more. - versionIdsToDeleteSybqueryTemplate := `SELECT id + versionIdsToDeleteSubqueryTemplate := `SELECT id FROM dashboard_version, ( SELECT dashboard_id, count(version) as count, min(version) as min FROM dashboard_version @@ -90,26 +90,28 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { WHERE dashboard_version.dashboard_id=vtd.dashboard_id AND version < vtd.min + vtd.count - %v` - versionIdsToDeleteSubquery := fmt.Sprintf(versionIdsToDeleteSybqueryTemplate, versionsToKeep) - versions := []string{} - err := sess.SQL(versionIdsToDeleteSubquery).Find(&versions) + versionIdsToDeleteSubquery := fmt.Sprintf(versionIdsToDeleteSubqueryTemplate, versionsToKeep) + var versionIdsToDelete []interface{} + err := sess.SQL(versionIdsToDeleteSubquery).Find(&versionIdsToDelete) if err != nil { return err } // Don't delete more than MAX_VERSIONS_TO_DELETE version per time limit := MAX_VERSIONS_TO_DELETE - if len(versions) < MAX_VERSIONS_TO_DELETE { - limit = len(versions) + if len(versionIdsToDelete) < MAX_VERSIONS_TO_DELETE { + limit = len(versionIdsToDelete) } - versions = versions[:limit] + versionIdsToDelete = versionIdsToDelete[:limit] - deleteExpiredSql := fmt.Sprintf(`DELETE FROM dashboard_version WHERE id IN (%s)`, strings.Join(versions, `,`)) - expiredResponse, err := sess.Exec(deleteExpiredSql) - if err != nil { - return err + if len(versionIdsToDelete) > 0 { + deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)` + expiredResponse, err := sess.Exec(deleteExpiredSql, versionIdsToDelete...) + if err != nil { + return err + } + cmd.DeletedRows, _ = expiredResponse.RowsAffected() } - cmd.DeletedRows, _ = expiredResponse.RowsAffected() return nil }) From 2ade0881b164437fb68870fdd1ae43fa66612923 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Mar 2018 22:48:17 +0300 Subject: [PATCH 51/78] minor refactor of dashboard version cleanup --- pkg/services/sqlstore/dashboard_version.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index 54c858df7de..d91b5727545 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -1,7 +1,6 @@ package sqlstore import ( - "fmt" "strings" "github.com/grafana/grafana/pkg/bus" @@ -81,18 +80,17 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { // min_version_to_keep = min_version + (versions_count - versions_to_keep) // where version stats is processed for each dashboard. This guarantees that we keep at least versions_to_keep // versions, but in some cases (when versions are sparse) this number may be more. - versionIdsToDeleteSubqueryTemplate := `SELECT id + versionIdsToDeleteSubquery := `SELECT id FROM dashboard_version, ( SELECT dashboard_id, count(version) as count, min(version) as min FROM dashboard_version GROUP BY dashboard_id ) AS vtd WHERE dashboard_version.dashboard_id=vtd.dashboard_id - AND version < vtd.min + vtd.count - %v` + AND version < vtd.min + vtd.count - ?` - versionIdsToDeleteSubquery := fmt.Sprintf(versionIdsToDeleteSubqueryTemplate, versionsToKeep) var versionIdsToDelete []interface{} - err := sess.SQL(versionIdsToDeleteSubquery).Find(&versionIdsToDelete) + err := sess.SQL(versionIdsToDeleteSubquery, versionsToKeep).Find(&versionIdsToDelete) if err != nil { return err } From 38bdb8dfb38b0cfc42c5f28aef0dbd5f4fb0bb5a Mon Sep 17 00:00:00 2001 From: Thibault Chataigner Date: Wed, 21 Mar 2018 20:48:29 +0100 Subject: [PATCH 52/78] Alerting: move getNewState to EvalContext This fix alert state update when several evaluation attempts are needed Signed-off-by: Thibault Chataigner --- pkg/services/alerting/engine.go | 1 + pkg/services/alerting/eval_context.go | 31 ++++++++++ pkg/services/alerting/eval_context_test.go | 69 ++++++++++++++++++++- pkg/services/alerting/eval_handler.go | 34 ----------- pkg/services/alerting/eval_handler_test.go | 70 ---------------------- pkg/services/alerting/test_rule.go | 1 + 6 files changed, 101 insertions(+), 105 deletions(-) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 49a11b54c1d..a6f97333d1b 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -193,6 +193,7 @@ func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan } } + evalContext.Rule.State = evalContext.GetNewState() e.resultHandler.Handle(evalContext) span.Finish() e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index d598203d675..91d0e179a14 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -112,3 +112,34 @@ func (c *EvalContext) GetRuleUrl() (string, error) { return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil } } + +func (c *EvalContext) GetNewState() m.AlertStateType { + if c.Error != nil { + c.log.Error("Alert Rule Result Error", + "ruleId", c.Rule.Id, + "name", c.Rule.Name, + "error", c.Error, + "changing state to", c.Rule.ExecutionErrorState.ToAlertState()) + + if c.Rule.ExecutionErrorState == m.ExecutionErrorKeepState { + return c.PrevAlertState + } + return c.Rule.ExecutionErrorState.ToAlertState() + + } else if c.Firing { + return m.AlertStateAlerting + + } else if c.NoDataFound { + c.log.Info("Alert Rule returned no data", + "ruleId", c.Rule.Id, + "name", c.Rule.Name, + "changing state to", c.Rule.NoDataState.ToAlertState()) + + if c.Rule.NoDataState == m.NoDataKeepState { + return c.PrevAlertState + } + return c.Rule.NoDataState.ToAlertState() + } + + return m.AlertStateOK +} diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 019ca1ed01f..709eeee4e5e 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -2,6 +2,7 @@ package alerting import ( "context" + "fmt" "testing" "github.com/grafana/grafana/pkg/models" @@ -12,7 +13,7 @@ func TestAlertingEvalContext(t *testing.T) { Convey("Eval context", t, func() { ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}) - Convey("Should update alert state", func() { + Convey("Should update alert state when needed", func() { Convey("ok -> alerting", func() { ctx.PrevAlertState = models.AlertStateOK @@ -28,5 +29,71 @@ func TestAlertingEvalContext(t *testing.T) { So(ctx.ShouldUpdateAlertState(), ShouldBeFalse) }) }) + + Convey("Should compute and replace properly new rule state", func() { + dummieError := fmt.Errorf("dummie error") + + Convey("ok -> alerting", func() { + ctx.PrevAlertState = models.AlertStateOK + ctx.Firing = true + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) + }) + + Convey("ok -> error(alerting)", func() { + ctx.PrevAlertState = models.AlertStateOK + ctx.Error = dummieError + ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) + }) + + Convey("ok -> error(keep_last)", func() { + ctx.PrevAlertState = models.AlertStateOK + ctx.Error = dummieError + ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) + }) + + Convey("pending -> error(keep_last)", func() { + ctx.PrevAlertState = models.AlertStatePending + ctx.Error = dummieError + ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) + }) + + Convey("ok -> no_data(alerting)", func() { + ctx.PrevAlertState = models.AlertStateOK + ctx.Rule.NoDataState = models.NoDataSetAlerting + ctx.NoDataFound = true + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) + }) + + Convey("ok -> no_data(keep_last)", func() { + ctx.PrevAlertState = models.AlertStateOK + ctx.Rule.NoDataState = models.NoDataKeepState + ctx.NoDataFound = true + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) + }) + + Convey("pending -> no_data(keep_last)", func() { + ctx.PrevAlertState = models.AlertStatePending + ctx.Rule.NoDataState = models.NoDataKeepState + ctx.NoDataFound = true + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) + }) + }) }) } diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index 457e02000fa..aa24efa77cd 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/models" ) type DefaultEvalHandler struct { @@ -66,40 +65,7 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { context.Firing = firing context.NoDataFound = noDataFound context.EndTime = time.Now() - context.Rule.State = e.getNewState(context) elapsedTime := context.EndTime.Sub(context.StartTime).Nanoseconds() / int64(time.Millisecond) metrics.M_Alerting_Execution_Time.Observe(float64(elapsedTime)) } - -// This should be move into evalContext once its been refactored. (Carl Bergquist) -func (handler *DefaultEvalHandler) getNewState(evalContext *EvalContext) models.AlertStateType { - if evalContext.Error != nil { - handler.log.Error("Alert Rule Result Error", - "ruleId", evalContext.Rule.Id, - "name", evalContext.Rule.Name, - "error", evalContext.Error, - "changing state to", evalContext.Rule.ExecutionErrorState.ToAlertState()) - - if evalContext.Rule.ExecutionErrorState == models.ExecutionErrorKeepState { - return evalContext.PrevAlertState - } else { - return evalContext.Rule.ExecutionErrorState.ToAlertState() - } - } else if evalContext.Firing { - return models.AlertStateAlerting - } else if evalContext.NoDataFound { - handler.log.Info("Alert Rule returned no data", - "ruleId", evalContext.Rule.Id, - "name", evalContext.Rule.Name, - "changing state to", evalContext.Rule.NoDataState.ToAlertState()) - - if evalContext.Rule.NoDataState == models.NoDataKeepState { - return evalContext.PrevAlertState - } else { - return evalContext.Rule.NoDataState.ToAlertState() - } - } - - return models.AlertStateOK -} diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index c942e24818f..a7c1f1e67fa 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -2,10 +2,8 @@ package alerting import ( "context" - "fmt" "testing" - "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" ) @@ -203,73 +201,5 @@ func TestAlertingEvaluationHandler(t *testing.T) { handler.Eval(context) So(context.NoDataFound, ShouldBeTrue) }) - - Convey("EvalHandler can replace alert state based for errors and no_data", func() { - ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}) - dummieError := fmt.Errorf("dummie error") - Convey("Should update alert state", func() { - - Convey("ok -> alerting", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Firing = true - - So(handler.getNewState(ctx), ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> error(alerting)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> error(keep_last)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) - }) - - Convey("pending -> error(keep_last)", func() { - ctx.PrevAlertState = models.AlertStatePending - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) - }) - - Convey("ok -> no_data(alerting)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Rule.NoDataState = models.NoDataSetAlerting - ctx.NoDataFound = true - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> no_data(keep_last)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Rule.NoDataState = models.NoDataKeepState - ctx.NoDataFound = true - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) - }) - - Convey("pending -> no_data(keep_last)", func() { - ctx.PrevAlertState = models.AlertStatePending - ctx.Rule.NoDataState = models.NoDataKeepState - ctx.NoDataFound = true - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) - }) - }) - }) }) } diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go index e3aa95e0ede..88418bff14e 100644 --- a/pkg/services/alerting/test_rule.go +++ b/pkg/services/alerting/test_rule.go @@ -53,6 +53,7 @@ func testAlertRule(rule *Rule) *EvalContext { context.IsTestRun = true handler.Eval(context) + context.Rule.State = context.GetNewState() return context } From f2f709989fae4920deeb1006526cec1172db98d9 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 22 Mar 2018 09:41:05 +0100 Subject: [PATCH 53/78] fixed so legend right works like legend under on small screens --- public/app/plugins/panel/graph/legend.ts | 4 +++- public/sass/components/_panel_graph.scss | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index 0c8852bf55a..7a9c75d4f1d 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -227,6 +227,8 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } function renderLegendElement(tableHeaderElem) { + let legendWidth = elem.width(); + var seriesElements = renderSeriesLegendElements(); if (panel.legend.alignAsTable) { @@ -238,7 +240,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { elem.append(seriesElements); } - if (!panel.legend.rightSide) { + if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== 10)) { addScrollbar(); } else { destroyScrollbar(); diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index c00af05140a..48d88872074 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -6,11 +6,11 @@ &--legend-right { @include media-breakpoint-up(sm) { flex-direction: row; - } - .graph-legend { - flex: 0 1 10px; - max-height: 100%; + .graph-legend { + flex: 0 1 10px; + max-height: 100%; + } } .graph-legend-series { From 7aab6a88873f5516636b1d710736d47cc33e84d8 Mon Sep 17 00:00:00 2001 From: Julian Kornberger Date: Thu, 22 Mar 2018 12:37:35 +0100 Subject: [PATCH 54/78] Make golint happier --- pkg/api/admin_users.go | 14 ++-- pkg/api/alerting.go | 6 +- pkg/api/annotations.go | 34 ++++----- pkg/api/api.go | 38 +++++----- pkg/api/apikey.go | 6 +- pkg/api/app_routes.go | 4 +- pkg/api/dashboard.go | 39 +++++----- pkg/api/dashboard_permission.go | 16 ++-- pkg/api/dashboard_snapshot.go | 4 +- pkg/api/dashboard_test.go | 20 ++--- pkg/api/dataproxy.go | 8 +- pkg/api/datasources.go | 25 +++---- pkg/api/folder.go | 4 +- pkg/api/folder_test.go | 4 +- pkg/api/http_server.go | 2 +- pkg/api/index.go | 29 ++++---- pkg/api/login.go | 6 +- pkg/api/metrics.go | 6 +- pkg/api/org.go | 12 +-- pkg/api/org_invite.go | 31 ++++---- pkg/api/org_users.go | 18 ++--- pkg/api/playlist.go | 4 +- pkg/api/playlist_play.go | 56 +++++++------- pkg/api/plugins.go | 90 ++++++++++++----------- pkg/api/preferences.go | 10 +-- pkg/api/search.go | 16 ++-- pkg/api/team.go | 4 +- pkg/api/user.go | 36 ++++----- pkg/cmd/grafana-server/server.go | 2 +- pkg/middleware/auth.go | 6 +- pkg/middleware/dashboard_redirect.go | 2 +- pkg/middleware/dashboard_redirect_test.go | 18 ++--- pkg/middleware/recovery_test.go | 12 +-- 33 files changed, 289 insertions(+), 293 deletions(-) diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 4cf7f4db4ec..dc3d390dda9 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -47,14 +47,14 @@ func AdminCreateUser(c *m.ReqContext, form dtos.AdminCreateUserForm) { } func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordForm) { - userId := c.ParamsInt64(":id") + userID := c.ParamsInt64(":id") if len(form.Password) < 4 { c.JsonApiErr(400, "New password too short", nil) return } - userQuery := m.GetUserByIdQuery{Id: userId} + userQuery := m.GetUserByIdQuery{Id: userID} if err := bus.Dispatch(&userQuery); err != nil { c.JsonApiErr(500, "Could not read user from database", err) @@ -64,7 +64,7 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF passwordHashed := util.EncodePassword(form.Password, userQuery.Result.Salt) cmd := m.ChangeUserPasswordCommand{ - UserId: userId, + UserId: userID, NewPassword: passwordHashed, } @@ -77,10 +77,10 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF } func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) { - userId := c.ParamsInt64(":id") + userID := c.ParamsInt64(":id") cmd := m.UpdateUserPermissionsCommand{ - UserId: userId, + UserId: userID, IsGrafanaAdmin: form.IsGrafanaAdmin, } @@ -93,9 +93,9 @@ func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermis } func AdminDeleteUser(c *m.ReqContext) { - userId := c.ParamsInt64(":id") + userID := c.ParamsInt64(":id") - cmd := m.DeleteUserCommand{UserId: userId} + cmd := m.DeleteUserCommand{UserId: userID} if err := bus.Dispatch(&cmd); err != nil { c.JsonApiErr(500, "Failed to delete user", err) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index eea4ef90c05..803823f6a94 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -26,9 +26,9 @@ func ValidateOrgAlert(c *m.ReqContext) { } func GetAlertStatesForDashboard(c *m.ReqContext) Response { - dashboardId := c.QueryInt64("dashboardId") + dashboardID := c.QueryInt64("dashboardId") - if dashboardId == 0 { + if dashboardID == 0 { return ApiError(400, "Missing query parameter dashboardId", nil) } @@ -151,7 +151,7 @@ func GetAlertNotifications(c *m.ReqContext) Response { return Json(200, result) } -func GetAlertNotificationById(c *m.ReqContext) Response { +func GetAlertNotificationByID(c *m.ReqContext) Response { query := &m.GetAlertNotificationsQuery{ OrgId: c.OrgId, Id: c.ParamsInt64("notificationId"), diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index fb75e0bf129..b4e328793cc 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -52,7 +52,7 @@ func (e *CreateAnnotationError) Error() string { } func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response { - if canSave, err := canSaveByDashboardId(c, cmd.DashboardId); err != nil || !canSave { + if canSave, err := canSaveByDashboardID(c, cmd.DashboardId); err != nil || !canSave { return dashboardGuardianResponse(err) } @@ -179,18 +179,18 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd } func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response { - annotationId := c.ParamsInt64(":annotationId") + annotationID := c.ParamsInt64(":annotationId") repo := annotations.GetRepository() - if resp := canSave(c, repo, annotationId); resp != nil { + if resp := canSave(c, repo, annotationID); resp != nil { return resp } item := annotations.Item{ OrgId: c.OrgId, UserId: c.UserId, - Id: annotationId, + Id: annotationID, Epoch: cmd.Time / 1000, Text: cmd.Text, Tags: cmd.Tags, @@ -254,14 +254,14 @@ func DeleteAnnotationById(c *m.ReqContext) Response { func DeleteAnnotationRegion(c *m.ReqContext) Response { repo := annotations.GetRepository() - regionId := c.ParamsInt64(":regionId") + regionID := c.ParamsInt64(":regionId") - if resp := canSave(c, repo, regionId); resp != nil { + if resp := canSave(c, repo, regionID); resp != nil { return resp } err := repo.Delete(&annotations.DeleteParams{ - RegionId: regionId, + RegionId: regionID, }) if err != nil { @@ -271,13 +271,13 @@ func DeleteAnnotationRegion(c *m.ReqContext) Response { return ApiSuccess("Annotation region deleted") } -func canSaveByDashboardId(c *m.ReqContext, dashboardId int64) (bool, error) { - if dashboardId == 0 && !c.SignedInUser.HasRole(m.ROLE_EDITOR) { +func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) { + if dashboardID == 0 && !c.SignedInUser.HasRole(m.ROLE_EDITOR) { return false, nil } - if dashboardId > 0 { - guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser) + if dashboardID > 0 { + guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser) if canEdit, err := guardian.CanEdit(); err != nil || !canEdit { return false, err } @@ -293,25 +293,25 @@ func canSave(c *m.ReqContext, repo annotations.Repository, annotationId int64) R return ApiError(500, "Could not find annotation to update", err) } - dashboardId := items[0].DashboardId + dashboardID := items[0].DashboardId - if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave { + if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave { return dashboardGuardianResponse(err) } return nil } -func canSaveByRegionId(c *m.ReqContext, repo annotations.Repository, regionId int64) Response { - items, err := repo.Find(&annotations.ItemQuery{RegionId: regionId, OrgId: c.OrgId}) +func canSaveByRegionID(c *m.ReqContext, repo annotations.Repository, regionID int64) Response { + items, err := repo.Find(&annotations.ItemQuery{RegionId: regionID, OrgId: c.OrgId}) if err != nil || len(items) == 0 { return ApiError(500, "Could not find annotation to update", err) } - dashboardId := items[0].DashboardId + dashboardID := items[0].DashboardId - if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave { + if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave { return dashboardGuardianResponse(err) } diff --git a/pkg/api/api.go b/pkg/api/api.go index 5b3cde09fd5..22d5d773d2a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -15,7 +15,7 @@ func (hs *HttpServer) registerRoutes() { reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}) reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN) reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN) - redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardUrl() + redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardSoloUrl := middleware.RedirectFromLegacyDashboardSoloUrl() quota := middleware.Quota bind := binding.Bind @@ -110,7 +110,7 @@ func (hs *HttpServer) registerRoutes() { r.Get("/api/snapshots-delete/:key", reqEditorRole, wrap(DeleteDashboardSnapshot)) // api renew session based on remember cookie - r.Get("/api/login/ping", quota("session"), LoginApiPing) + r.Get("/api/login/ping", quota("session"), LoginAPIPing) // authed api r.Group("/api", func(apiRoute RouteRegister) { @@ -139,7 +139,7 @@ func (hs *HttpServer) registerRoutes() { apiRoute.Group("/users", func(usersRoute RouteRegister) { usersRoute.Get("/", wrap(SearchUsers)) usersRoute.Get("/search", wrap(SearchUsersWithPaging)) - usersRoute.Get("/:id", wrap(GetUserById)) + usersRoute.Get("/:id", wrap(GetUserByID)) usersRoute.Get("/:id/orgs", wrap(GetUserOrgList)) // query parameters /users/lookup?loginOrEmail=admin@example.com usersRoute.Get("/lookup", wrap(GetUserByLoginOrEmail)) @@ -149,11 +149,11 @@ func (hs *HttpServer) registerRoutes() { // team (admin permission required) apiRoute.Group("/teams", func(teamsRoute RouteRegister) { - teamsRoute.Get("/:teamId", wrap(GetTeamById)) + teamsRoute.Get("/:teamId", wrap(GetTeamByID)) teamsRoute.Get("/search", wrap(SearchTeams)) teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam)) teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam)) - teamsRoute.Delete("/:teamId", wrap(DeleteTeamById)) + teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID)) teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers)) teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember)) @@ -192,10 +192,10 @@ func (hs *HttpServer) registerRoutes() { // orgs (admin routes) apiRoute.Group("/orgs/:orgId", func(orgsRoute RouteRegister) { - orgsRoute.Get("/", wrap(GetOrgById)) + orgsRoute.Get("/", wrap(GetOrgByID)) orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrg)) orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddress)) - orgsRoute.Delete("/", wrap(DeleteOrgById)) + orgsRoute.Delete("/", wrap(DeleteOrgByID)) orgsRoute.Get("/users", wrap(GetOrgUsers)) orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser)) orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser)) @@ -211,9 +211,9 @@ func (hs *HttpServer) registerRoutes() { // auth api keys apiRoute.Group("/auth/keys", func(keysRoute RouteRegister) { - keysRoute.Get("/", wrap(GetApiKeys)) - keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey)) - keysRoute.Delete("/:id", wrap(DeleteApiKey)) + keysRoute.Get("/", wrap(GetAPIKeys)) + keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddAPIKey)) + keysRoute.Delete("/:id", wrap(DeleteAPIKey)) }, reqOrgAdmin) // Preferences @@ -226,16 +226,16 @@ func (hs *HttpServer) registerRoutes() { datasourceRoute.Get("/", wrap(GetDataSources)) datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), wrap(AddDataSource)) datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource)) - datasourceRoute.Delete("/:id", wrap(DeleteDataSourceById)) + datasourceRoute.Delete("/:id", wrap(DeleteDataSourceByID)) datasourceRoute.Delete("/name/:name", wrap(DeleteDataSourceByName)) - datasourceRoute.Get("/:id", wrap(GetDataSourceById)) + datasourceRoute.Get("/:id", wrap(GetDataSourceByID)) datasourceRoute.Get("/name/:name", wrap(GetDataSourceByName)) }, reqOrgAdmin) - apiRoute.Get("/datasources/id/:name", wrap(GetDataSourceIdByName), reqSignedIn) + apiRoute.Get("/datasources/id/:name", wrap(GetDataSourceIDByName), reqSignedIn) apiRoute.Get("/plugins", wrap(GetPluginList)) - apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById)) + apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingByID)) apiRoute.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown)) apiRoute.Group("/plugins", func(pluginRoute RouteRegister) { @@ -250,11 +250,11 @@ func (hs *HttpServer) registerRoutes() { // Folders apiRoute.Group("/folders", func(folderRoute RouteRegister) { folderRoute.Get("/", wrap(GetFolders)) - folderRoute.Get("/id/:id", wrap(GetFolderById)) + folderRoute.Get("/id/:id", wrap(GetFolderByID)) folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder)) folderRoute.Group("/:uid", func(folderUidRoute RouteRegister) { - folderUidRoute.Get("/", wrap(GetFolderByUid)) + folderUidRoute.Get("/", wrap(GetFolderByUID)) folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder)) folderUidRoute.Delete("/", wrap(DeleteFolder)) @@ -268,7 +268,7 @@ func (hs *HttpServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) { dashboardRoute.Get("/uid/:uid", wrap(GetDashboard)) - dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUid)) + dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUID)) dashboardRoute.Get("/db/:slug", wrap(GetDashboard)) dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard)) @@ -314,7 +314,7 @@ func (hs *HttpServer) registerRoutes() { // metrics apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics)) apiRoute.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios)) - apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSqlTestData)) + apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSQLTestData)) apiRoute.Get("/tsdb/testdata/random-walk", wrap(GetTestDataRandomWalk)) apiRoute.Group("/alerts", func(alertsRoute RouteRegister) { @@ -332,7 +332,7 @@ func (hs *HttpServer) registerRoutes() { alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest)) alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification)) alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification)) - alertNotifications.Get("/:notificationId", wrap(GetAlertNotificationById)) + alertNotifications.Get("/:notificationId", wrap(GetAlertNotificationByID)) alertNotifications.Delete("/:notificationId", wrap(DeleteAlertNotification)) }, reqEditorRole) diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index 24ed69ec691..7195c7453f6 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -7,7 +7,7 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func GetApiKeys(c *m.ReqContext) Response { +func GetAPIKeys(c *m.ReqContext) Response { query := m.GetApiKeysQuery{OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { @@ -26,7 +26,7 @@ func GetApiKeys(c *m.ReqContext) Response { return Json(200, result) } -func DeleteApiKey(c *m.ReqContext) Response { +func DeleteAPIKey(c *m.ReqContext) Response { id := c.ParamsInt64(":id") cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId} @@ -39,7 +39,7 @@ func DeleteApiKey(c *m.ReqContext) Response { return ApiSuccess("API key deleted") } -func AddApiKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response { +func AddAPIKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response { if !cmd.Role.IsValid() { return ApiError(400, "Invalid role specified", nil) } diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index 8d74d96396b..0b7dcd32ce3 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.go @@ -55,11 +55,11 @@ func InitAppPluginRoutes(r *macaron.Macaron) { } } -func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler { +func AppPluginRoute(route *plugins.AppPluginRoute, appID string) macaron.Handler { return func(c *m.ReqContext) { path := c.Params("*") - proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId) + proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID) proxy.Transport = pluginProxyTransport proxy.ServeHTTP(c.Resp, c.Req.Request) } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 877524ad5dd..25c89238933 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -22,12 +22,12 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func isDashboardStarredByUser(c *m.ReqContext, dashId int64) (bool, error) { +func isDashboardStarredByUser(c *m.ReqContext, dashID int64) (bool, error) { if !c.IsSignedIn { return false, nil } - query := m.IsStarredByUserQuery{UserId: c.UserId, DashboardId: dashId} + query := m.IsStarredByUserQuery{UserId: c.UserId, DashboardId: dashID} if err := bus.Dispatch(&query); err != nil { return false, err } @@ -114,24 +114,22 @@ func GetDashboard(c *m.ReqContext) Response { return Json(200, dto) } -func getUserLogin(userId int64) string { - query := m.GetUserByIdQuery{Id: userId} +func getUserLogin(userID int64) string { + query := m.GetUserByIdQuery{Id: userID} err := bus.Dispatch(&query) if err != nil { return "Anonymous" - } else { - user := query.Result - return user.Login } + return query.Result.Login } -func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dashboard, Response) { +func getDashboardHelper(orgID int64, slug string, id int64, uid string) (*m.Dashboard, Response) { var query m.GetDashboardQuery if len(uid) > 0 { - query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgId} + query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgID} } else { - query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId} + query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgID} } if err := bus.Dispatch(&query); err != nil { @@ -173,7 +171,7 @@ func DeleteDashboard(c *m.ReqContext) Response { }) } -func DeleteDashboardByUid(c *m.ReqContext) Response { +func DeleteDashboardByUID(c *m.ReqContext) Response { dash, rsp := getDashboardHelper(c.OrgId, "", 0, c.Params(":uid")) if rsp != nil { return rsp @@ -291,9 +289,8 @@ func GetHomeDashboard(c *m.ReqContext) Response { url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug) dashRedirect := dtos.DashboardRedirect{RedirectUri: url} return Json(200, &dashRedirect) - } else { - log.Warn("Failed to get slug from database, %s", err.Error()) } + log.Warn("Failed to get slug from database, %s", err.Error()) } filePath := path.Join(setting.StaticRootPath, "dashboards/home.json") @@ -339,22 +336,22 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) { // GetDashboardVersions returns all dashboard versions as JSON func GetDashboardVersions(c *m.ReqContext) Response { - dashId := c.ParamsInt64(":dashboardId") + dashID := c.ParamsInt64(":dashboardId") - guardian := guardian.New(dashId, c.OrgId, c.SignedInUser) + guardian := guardian.New(dashID, c.OrgId, c.SignedInUser) if canSave, err := guardian.CanSave(); err != nil || !canSave { return dashboardGuardianResponse(err) } query := m.GetDashboardVersionsQuery{ OrgId: c.OrgId, - DashboardId: dashId, + DashboardId: dashID, Limit: c.QueryInt("limit"), Start: c.QueryInt("start"), } if err := bus.Dispatch(&query); err != nil { - return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashId), err) + return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashID), err) } for _, version := range query.Result { @@ -378,21 +375,21 @@ func GetDashboardVersions(c *m.ReqContext) Response { // GetDashboardVersion returns the dashboard version with the given ID. func GetDashboardVersion(c *m.ReqContext) Response { - dashId := c.ParamsInt64(":dashboardId") + dashID := c.ParamsInt64(":dashboardId") - guardian := guardian.New(dashId, c.OrgId, c.SignedInUser) + guardian := guardian.New(dashID, c.OrgId, c.SignedInUser) if canSave, err := guardian.CanSave(); err != nil || !canSave { return dashboardGuardianResponse(err) } query := m.GetDashboardVersionQuery{ OrgId: c.OrgId, - DashboardId: dashId, + DashboardId: dashID, Version: c.ParamsInt(":id"), } if err := bus.Dispatch(&query); err != nil { - return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashId), err) + return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err) } creator := "Anonymous" diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index a62c27ab320..c852033829a 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -10,14 +10,14 @@ import ( ) func GetDashboardPermissionList(c *m.ReqContext) Response { - dashId := c.ParamsInt64(":dashboardId") + dashID := c.ParamsInt64(":dashboardId") - _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") + _, rsp := getDashboardHelper(c.OrgId, "", dashID, "") if rsp != nil { return rsp } - g := guardian.New(dashId, c.OrgId, c.SignedInUser) + g := guardian.New(dashID, c.OrgId, c.SignedInUser) if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { return dashboardGuardianResponse(err) @@ -38,25 +38,25 @@ func GetDashboardPermissionList(c *m.ReqContext) Response { } func UpdateDashboardPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response { - dashId := c.ParamsInt64(":dashboardId") + dashID := c.ParamsInt64(":dashboardId") - _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") + _, rsp := getDashboardHelper(c.OrgId, "", dashID, "") if rsp != nil { return rsp } - g := guardian.New(dashId, c.OrgId, c.SignedInUser) + g := guardian.New(dashID, c.OrgId, c.SignedInUser) if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { return dashboardGuardianResponse(err) } cmd := m.UpdateDashboardAclCommand{} - cmd.DashboardId = dashId + cmd.DashboardId = dashID for _, item := range apiCmd.Items { cmd.Items = append(cmd.Items, &m.DashboardAcl{ OrgId: c.OrgId, - DashboardId: dashId, + DashboardId: dashID, UserId: item.UserId, TeamId: item.TeamId, Role: item.Role, diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 4656940d2bb..b74dfe5fd34 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -106,9 +106,9 @@ func DeleteDashboardSnapshot(c *m.ReqContext) Response { return ApiError(404, "Failed to get dashboard snapshot", nil) } dashboard := query.Result.Dashboard - dashboardId := dashboard.Get("id").MustInt64() + dashboardID := dashboard.Get("id").MustInt64() - guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser) + guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser) canEdit, err := guardian.CanEdit() if err != nil { return ApiError(500, "Error while checking permissions for snapshot", err) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 6c5b4e4c102..d940c5406be 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -105,7 +105,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -165,7 +165,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 200) Convey("Should lookup dashboard by uid", func() { @@ -271,7 +271,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -329,7 +329,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -398,7 +398,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 200) Convey("Should lookup dashboard by uid", func() { @@ -468,7 +468,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -527,7 +527,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 200) Convey("Should lookup dashboard by uid", func() { @@ -594,7 +594,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -837,12 +837,12 @@ func CallDeleteDashboard(sc *scenarioContext) { sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } -func CallDeleteDashboardByUid(sc *scenarioContext) { +func CallDeleteDashboardByUID(sc *scenarioContext) { bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error { return nil }) - sc.handlerFunc = DeleteDashboardByUid + sc.handlerFunc = DeleteDashboardByUID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index c6fe8b6cd8c..2e97e5d2d6f 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -13,19 +13,19 @@ import ( const HeaderNameNoBackendCache = "X-Grafana-NoCache" -func (hs *HttpServer) getDatasourceById(id int64, orgId int64, nocache bool) (*m.DataSource, error) { +func (hs *HttpServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m.DataSource, error) { cacheKey := fmt.Sprintf("ds-%d", id) if !nocache { if cached, found := hs.cache.Get(cacheKey); found { ds := cached.(*m.DataSource) - if ds.OrgId == orgId { + if ds.OrgId == orgID { return ds, nil } } } - query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgId} + query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgID} if err := bus.Dispatch(&query); err != nil { return nil, err } @@ -39,7 +39,7 @@ func (hs *HttpServer) ProxyDataSourceRequest(c *m.ReqContext) { nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true" - ds, err := hs.getDatasourceById(c.ParamsInt64(":id"), c.OrgId, nocache) + ds, err := hs.getDatasourceByID(c.ParamsInt64(":id"), c.OrgId, nocache) if err != nil { c.JsonApiErr(500, "Unable to load datasource meta data", err) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index ed8fc5d2a66..4b5aa56d6e7 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -49,7 +49,7 @@ func GetDataSources(c *m.ReqContext) Response { return Json(200, &result) } -func GetDataSourceById(c *m.ReqContext) Response { +func GetDataSourceByID(c *m.ReqContext) Response { query := m.GetDataSourceByIdQuery{ Id: c.ParamsInt64(":id"), OrgId: c.OrgId, @@ -68,14 +68,14 @@ func GetDataSourceById(c *m.ReqContext) Response { return Json(200, &dtos) } -func DeleteDataSourceById(c *m.ReqContext) Response { +func DeleteDataSourceByID(c *m.ReqContext) Response { id := c.ParamsInt64(":id") if id <= 0 { return ApiError(400, "Missing valid datasource id", nil) } - ds, err := getRawDataSourceById(id, c.OrgId) + ds, err := getRawDataSourceByID(id, c.OrgId) if err != nil { return ApiError(400, "Failed to delete datasource", nil) } @@ -143,7 +143,7 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response { cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":id") - err := fillWithSecureJsonData(&cmd) + err := fillWithSecureJSONData(&cmd) if err != nil { return ApiError(500, "Failed to update datasource", err) } @@ -152,9 +152,8 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response { if err != nil { if err == m.ErrDataSourceUpdatingOldVersion { return ApiError(500, "Failed to update datasource. Reload new version and try again", err) - } else { - return ApiError(500, "Failed to update datasource", err) } + return ApiError(500, "Failed to update datasource", err) } ds := convertModelToDtos(cmd.Result) return Json(200, util.DynMap{ @@ -165,12 +164,12 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response { }) } -func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { +func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error { if len(cmd.SecureJsonData) == 0 { return nil } - ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId) + ds, err := getRawDataSourceByID(cmd.Id, cmd.OrgId) if err != nil { return err } @@ -179,8 +178,8 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { return m.ErrDatasourceIsReadOnly } - secureJsonData := ds.SecureJsonData.Decrypt() - for k, v := range secureJsonData { + secureJSONData := ds.SecureJsonData.Decrypt() + for k, v := range secureJSONData { if _, ok := cmd.SecureJsonData[k]; !ok { cmd.SecureJsonData[k] = v @@ -190,10 +189,10 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { return nil } -func getRawDataSourceById(id int64, orgId int64) (*m.DataSource, error) { +func getRawDataSourceByID(id int64, orgID int64) (*m.DataSource, error) { query := m.GetDataSourceByIdQuery{ Id: id, - OrgId: orgId, + OrgId: orgID, } if err := bus.Dispatch(&query); err != nil { @@ -220,7 +219,7 @@ func GetDataSourceByName(c *m.ReqContext) Response { } // Get /api/datasources/id/:name -func GetDataSourceIdByName(c *m.ReqContext) Response { +func GetDataSourceIDByName(c *m.ReqContext) Response { query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 143892fa6e8..e88f7fc2c3b 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -31,7 +31,7 @@ func GetFolders(c *m.ReqContext) Response { return Json(200, result) } -func GetFolderByUid(c *m.ReqContext) Response { +func GetFolderByUID(c *m.ReqContext) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) folder, err := s.GetFolderByUid(c.Params(":uid")) @@ -43,7 +43,7 @@ func GetFolderByUid(c *m.ReqContext) Response { return Json(200, toFolderDto(g, folder)) } -func GetFolderById(c *m.ReqContext) Response { +func GetFolderByID(c *m.ReqContext) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) folder, err := s.GetFolderById(c.ParamsInt64(":id")) if err != nil { diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 7cefdcf8544..e3c63ac0745 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -133,8 +133,8 @@ func TestFoldersApiEndpoint(t *testing.T) { }) } -func callGetFolderByUid(sc *scenarioContext) { - sc.handlerFunc = GetFolderByUid +func callGetFolderByUID(sc *scenarioContext) { + sc.handlerFunc = GetFolderByUID sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index c6a9286a5d8..772a27bd4cd 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -39,7 +39,7 @@ type HttpServer struct { httpSrv *http.Server } -func NewHttpServer() *HttpServer { +func NewHTTPServer() *HttpServer { return &HttpServer{ log: log.New("http.server"), cache: gocache.New(5*time.Minute, 10*time.Minute), diff --git a/pkg/api/index.go b/pkg/api/index.go index e50c59e082a..a1d21d1c686 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -32,13 +32,13 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { locale = parts[0] } - appUrl := setting.AppUrl - appSubUrl := setting.AppSubUrl + appURL := setting.AppUrl + appSubURL := setting.AppSubUrl // special case when doing localhost call from phantomjs if c.IsRenderCall { - appUrl = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort) - appSubUrl = "" + appURL = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort) + appSubURL = "" settings["appSubUrl"] = "" } @@ -62,8 +62,8 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { }, Settings: settings, Theme: prefs.Theme, - AppUrl: appUrl, - AppSubUrl: appSubUrl, + AppUrl: appURL, + AppSubUrl: appSubURL, GoogleAnalyticsId: setting.GoogleAnalyticsId, GoogleTagManagerId: setting.GoogleTagManagerId, BuildVersion: setting.BuildVersion, @@ -80,8 +80,8 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { data.User.Name = data.User.Login } - themeUrlParam := c.Query("theme") - if themeUrlParam == "light" { + themeURLParam := c.Query("theme") + if themeURLParam == "light" { data.User.LightTheme = true data.Theme = "light" } @@ -299,12 +299,12 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { } func Index(c *m.ReqContext) { - if data, err := setIndexViewData(c); err != nil { + data, err := setIndexViewData(c) + if err != nil { c.Handle(500, "Failed to get settings", err) return - } else { - c.HTML(200, "index", data) } + c.HTML(200, "index", data) } func NotFoundHandler(c *m.ReqContext) { @@ -313,10 +313,11 @@ func NotFoundHandler(c *m.ReqContext) { return } - if data, err := setIndexViewData(c); err != nil { + data, err := setIndexViewData(c) + if err != nil { c.Handle(500, "Failed to get settings", err) return - } else { - c.HTML(404, "index", data) } + + c.HTML(404, "index", data) } diff --git a/pkg/api/login.go b/pkg/api/login.go index 2ca2ce5a3e2..dc5ae730721 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -14,7 +14,7 @@ import ( ) const ( - VIEW_INDEX = "index" + ViewIndex = "index" ) func LoginView(c *m.ReqContext) { @@ -40,7 +40,7 @@ func LoginView(c *m.ReqContext) { } if !tryLoginUsingRememberCookie(c) { - c.HTML(200, VIEW_INDEX, viewData) + c.HTML(200, ViewIndex, viewData) return } @@ -87,7 +87,7 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool { return true } -func LoginApiPing(c *m.ReqContext) { +func LoginAPIPing(c *m.ReqContext) { if !tryLoginUsingRememberCookie(c) { c.JsonApiErr(401, "Unauthorized", nil) return diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 5d395d655a9..38bb8dc0688 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -20,12 +20,12 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response { return ApiError(400, "No queries found in query", nil) } - dsId, err := reqDto.Queries[0].Get("datasourceId").Int64() + dsID, err := reqDto.Queries[0].Get("datasourceId").Int64() if err != nil { return ApiError(400, "Query missing datasourceId", nil) } - dsQuery := m.GetDataSourceByIdQuery{Id: dsId, OrgId: c.OrgId} + dsQuery := m.GetDataSourceByIdQuery{Id: dsID, OrgId: c.OrgId} if err := bus.Dispatch(&dsQuery); err != nil { return ApiError(500, "failed to fetch data source", err) } @@ -82,7 +82,7 @@ func GenerateError(c *m.ReqContext) Response { } // GET /api/tsdb/testdata/gensql -func GenerateSqlTestData(c *m.ReqContext) Response { +func GenerateSQLTestData(c *m.ReqContext) Response { if err := bus.Dispatch(&m.InsertSqlTestDataCommand{}); err != nil { return ApiError(500, "Failed to insert test data", err) } diff --git a/pkg/api/org.go b/pkg/api/org.go index 5f20559dbbe..7735bd6a7eb 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -15,7 +15,7 @@ func GetOrgCurrent(c *m.ReqContext) Response { } // GET /api/orgs/:orgId -func GetOrgById(c *m.ReqContext) Response { +func GetOrgByID(c *m.ReqContext) Response { return getOrgHelper(c.ParamsInt64(":orgId")) } @@ -106,8 +106,8 @@ func UpdateOrg(c *m.ReqContext, form dtos.UpdateOrgForm) Response { return updateOrgHelper(form, c.ParamsInt64(":orgId")) } -func updateOrgHelper(form dtos.UpdateOrgForm, orgId int64) Response { - cmd := m.UpdateOrgCommand{Name: form.Name, OrgId: orgId} +func updateOrgHelper(form dtos.UpdateOrgForm, orgID int64) Response { + cmd := m.UpdateOrgCommand{Name: form.Name, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrOrgNameTaken { return ApiError(400, "Organization name taken", err) @@ -128,9 +128,9 @@ func UpdateOrgAddress(c *m.ReqContext, form dtos.UpdateOrgAddressForm) Response return updateOrgAddressHelper(form, c.ParamsInt64(":orgId")) } -func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Response { +func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgID int64) Response { cmd := m.UpdateOrgAddressCommand{ - OrgId: orgId, + OrgId: orgID, Address: m.Address{ Address1: form.Address1, Address2: form.Address2, @@ -149,7 +149,7 @@ func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Respons } // GET /api/orgs/:orgId -func DeleteOrgById(c *m.ReqContext) Response { +func DeleteOrgByID(c *m.ReqContext) Response { if err := bus.Dispatch(&m.DeleteOrgCommand{Id: c.ParamsInt64(":orgId")}); err != nil { if err == m.ErrOrgNotFound { return ApiError(404, "Failed to delete organization. ID not found", nil) diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 6a727dd95cc..0486287a31b 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -96,26 +96,25 @@ func inviteExistingUserToOrg(c *m.ReqContext, user *m.User, inviteDto *dtos.AddI return ApiError(412, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err) } return ApiError(500, "Error while trying to create org user", err) - } else { + } - if inviteDto.SendEmail && util.IsEmail(user.Email) { - emailCmd := m.SendEmailCommand{ - To: []string{user.Email}, - Template: "invited_to_org.html", - Data: map[string]interface{}{ - "Name": user.NameOrFallback(), - "OrgName": c.OrgName, - "InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login), - }, - } - - if err := bus.Dispatch(&emailCmd); err != nil { - return ApiError(500, "Failed to send email invited_to_org", err) - } + if inviteDto.SendEmail && util.IsEmail(user.Email) { + emailCmd := m.SendEmailCommand{ + To: []string{user.Email}, + Template: "invited_to_org.html", + Data: map[string]interface{}{ + "Name": user.NameOrFallback(), + "OrgName": c.OrgName, + "InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login), + }, } - return ApiSuccess(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName)) + if err := bus.Dispatch(&emailCmd); err != nil { + return ApiError(500, "Failed to send email invited_to_org", err) + } } + + return ApiSuccess(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName)) } func RevokeInvite(c *m.ReqContext) Response { diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 6d7c2bb94bd..cd1430d22fb 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -53,9 +53,9 @@ func GetOrgUsers(c *m.ReqContext) Response { return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0) } -func getOrgUsersHelper(orgId int64, query string, limit int) Response { +func getOrgUsersHelper(orgID int64, query string, limit int) Response { q := m.GetOrgUsersQuery{ - OrgId: orgId, + OrgId: orgID, Query: query, Limit: limit, } @@ -102,19 +102,19 @@ func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response { // DELETE /api/org/users/:userId func RemoveOrgUserForCurrentOrg(c *m.ReqContext) Response { - userId := c.ParamsInt64(":userId") - return removeOrgUserHelper(c.OrgId, userId) + userID := c.ParamsInt64(":userId") + return removeOrgUserHelper(c.OrgId, userID) } // DELETE /api/orgs/:orgId/users/:userId func RemoveOrgUser(c *m.ReqContext) Response { - userId := c.ParamsInt64(":userId") - orgId := c.ParamsInt64(":orgId") - return removeOrgUserHelper(orgId, userId) + userID := c.ParamsInt64(":userId") + orgID := c.ParamsInt64(":orgId") + return removeOrgUserHelper(orgID, userID) } -func removeOrgUserHelper(orgId int64, userId int64) Response { - cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: userId} +func removeOrgUserHelper(orgID int64, userID int64) Response { + cmd := m.RemoveOrgUserCommand{OrgId: orgID, UserId: userID} if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrLastOrgAdmin { diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 45de40ce337..0198850252d 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -127,9 +127,9 @@ func GetPlaylistItems(c *m.ReqContext) Response { } func GetPlaylistDashboards(c *m.ReqContext) Response { - playlistId := c.ParamsInt64(":id") + playlistID := c.ParamsInt64(":id") - playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistId) + playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistID) if err != nil { return ApiError(500, "Could not load dashboards", err) } diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index 1d059e06be5..69a2caef7e0 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -34,29 +34,27 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i return result, nil } -func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice { +func populateDashboardsByTag(orgID int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice { result := make(dtos.PlaylistDashboardsSlice, 0) - if len(dashboardByTag) > 0 { - for _, tag := range dashboardByTag { - searchQuery := search.Query{ - Title: "", - Tags: []string{tag}, - SignedInUser: signedInUser, - Limit: 100, - IsStarred: false, - OrgId: orgId, - } + for _, tag := range dashboardByTag { + searchQuery := search.Query{ + Title: "", + Tags: []string{tag}, + SignedInUser: signedInUser, + Limit: 100, + IsStarred: false, + OrgId: orgID, + } - if err := bus.Dispatch(&searchQuery); err == nil { - for _, item := range searchQuery.Result { - result = append(result, dtos.PlaylistDashboard{ - Id: item.Id, - Title: item.Title, - Uri: item.Uri, - Order: dashboardTagOrder[tag], - }) - } + if err := bus.Dispatch(&searchQuery); err == nil { + for _, item := range searchQuery.Result { + result = append(result, dtos.PlaylistDashboard{ + Id: item.Id, + Title: item.Title, + Uri: item.Uri, + Order: dashboardTagOrder[tag], + }) } } } @@ -64,19 +62,19 @@ func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboar return result } -func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistId int64) (dtos.PlaylistDashboardsSlice, error) { - playlistItems, _ := LoadPlaylistItems(playlistId) +func LoadPlaylistDashboards(orgID int64, signedInUser *m.SignedInUser, playlistID int64) (dtos.PlaylistDashboardsSlice, error) { + playlistItems, _ := LoadPlaylistItems(playlistID) - dashboardByIds := make([]int64, 0) + dashboardByIDs := make([]int64, 0) dashboardByTag := make([]string, 0) - dashboardIdOrder := make(map[int64]int) + dashboardIDOrder := make(map[int64]int) dashboardTagOrder := make(map[string]int) for _, i := range playlistItems { if i.Type == "dashboard_by_id" { - dashboardId, _ := strconv.ParseInt(i.Value, 10, 64) - dashboardByIds = append(dashboardByIds, dashboardId) - dashboardIdOrder[dashboardId] = i.Order + dashboardID, _ := strconv.ParseInt(i.Value, 10, 64) + dashboardByIDs = append(dashboardByIDs, dashboardID) + dashboardIDOrder[dashboardID] = i.Order } if i.Type == "dashboard_by_tag" { @@ -87,9 +85,9 @@ func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistI result := make(dtos.PlaylistDashboardsSlice, 0) - var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder) + var k, _ = populateDashboardsById(dashboardByIDs, dashboardIDOrder) result = append(result, k...) - result = append(result, populateDashboardsByTag(orgId, signedInUser, dashboardByTag, dashboardTagOrder)...) + result = append(result, populateDashboardsByTag(orgID, signedInUser, dashboardByTag, dashboardTagOrder)...) sort.Sort(result) return result, nil diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index bc38f4a7775..81b3ac10cef 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -78,48 +78,48 @@ func GetPluginList(c *m.ReqContext) Response { return Json(200, result) } -func GetPluginSettingById(c *m.ReqContext) Response { - pluginId := c.Params(":pluginId") +func GetPluginSettingByID(c *m.ReqContext) Response { + pluginID := c.Params(":pluginId") - if def, exists := plugins.Plugins[pluginId]; !exists { + def, exists := plugins.Plugins[pluginID] + if !exists { return ApiError(404, "Plugin not found, no installed plugin with that id", nil) - } else { - - dto := &dtos.PluginSetting{ - Type: def.Type, - Id: def.Id, - Name: def.Name, - Info: &def.Info, - Dependencies: &def.Dependencies, - Includes: def.Includes, - BaseUrl: def.BaseUrl, - Module: def.Module, - DefaultNavUrl: def.DefaultNavUrl, - LatestVersion: def.GrafanaNetVersion, - HasUpdate: def.GrafanaNetHasUpdate, - State: def.State, - } - - query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId} - if err := bus.Dispatch(&query); err != nil { - if err != m.ErrPluginSettingNotFound { - return ApiError(500, "Failed to get login settings", nil) - } - } else { - dto.Enabled = query.Result.Enabled - dto.Pinned = query.Result.Pinned - dto.JsonData = query.Result.JsonData - } - - return Json(200, dto) } + + dto := &dtos.PluginSetting{ + Type: def.Type, + Id: def.Id, + Name: def.Name, + Info: &def.Info, + Dependencies: &def.Dependencies, + Includes: def.Includes, + BaseUrl: def.BaseUrl, + Module: def.Module, + DefaultNavUrl: def.DefaultNavUrl, + LatestVersion: def.GrafanaNetVersion, + HasUpdate: def.GrafanaNetHasUpdate, + State: def.State, + } + + query := m.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId} + if err := bus.Dispatch(&query); err != nil { + if err != m.ErrPluginSettingNotFound { + return ApiError(500, "Failed to get login settings", nil) + } + } else { + dto.Enabled = query.Result.Enabled + dto.Pinned = query.Result.Pinned + dto.JsonData = query.Result.JsonData + } + + return Json(200, dto) } func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response { - pluginId := c.Params(":pluginId") + pluginID := c.Params(":pluginId") cmd.OrgId = c.OrgId - cmd.PluginId = pluginId + cmd.PluginId = pluginID if _, ok := plugins.Apps[cmd.PluginId]; !ok { return ApiError(404, "Plugin not installed.", nil) @@ -133,34 +133,36 @@ func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response } func GetPluginDashboards(c *m.ReqContext) Response { - pluginId := c.Params(":pluginId") + pluginID := c.Params(":pluginId") - if list, err := plugins.GetPluginDashboards(c.OrgId, pluginId); err != nil { + list, err := plugins.GetPluginDashboards(c.OrgId, pluginID) + if err != nil { if notfound, ok := err.(plugins.PluginNotFoundError); ok { return ApiError(404, notfound.Error(), nil) } return ApiError(500, "Failed to get plugin dashboards", err) - } else { - return Json(200, list) } + + return Json(200, list) } func GetPluginMarkdown(c *m.ReqContext) Response { - pluginId := c.Params(":pluginId") + pluginID := c.Params(":pluginId") name := c.Params(":name") - if content, err := plugins.GetPluginMarkdown(pluginId, name); err != nil { + content, err := plugins.GetPluginMarkdown(pluginID, name) + if err != nil { if notfound, ok := err.(plugins.PluginNotFoundError); ok { return ApiError(404, notfound.Error(), nil) } return ApiError(500, "Could not get markdown file", err) - } else { - resp := Respond(200, content) - resp.Header("Content-Type", "text/plain; charset=utf-8") - return resp } + + resp := Respond(200, content) + resp.Header("Content-Type", "text/plain; charset=utf-8") + return resp } func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Response { diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index eb0ffa14b39..2b85c3dcee1 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -24,8 +24,8 @@ func GetUserPreferences(c *m.ReqContext) Response { return getPreferencesFor(c.OrgId, c.UserId) } -func getPreferencesFor(orgId int64, userId int64) Response { - prefsQuery := m.GetPreferencesQuery{UserId: userId, OrgId: orgId} +func getPreferencesFor(orgID int64, userID int64) Response { + prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID} if err := bus.Dispatch(&prefsQuery); err != nil { return ApiError(500, "Failed to get preferences", err) @@ -45,10 +45,10 @@ func UpdateUserPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd) } -func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd) Response { +func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd) Response { saveCmd := m.SavePreferencesCommand{ - UserId: userId, - OrgId: orgId, + UserId: userID, + OrgId: orgID, Theme: dtoCmd.Theme, Timezone: dtoCmd.Timezone, HomeDashboardId: dtoCmd.HomeDashboardId, diff --git a/pkg/api/search.go b/pkg/api/search.go index c8a0a5592bb..8c2b708d5a2 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -25,19 +25,19 @@ func Search(c *m.ReqContext) { permission = m.PERMISSION_EDIT } - dbids := make([]int64, 0) + dbIDs := make([]int64, 0) for _, id := range c.QueryStrings("dashboardIds") { - dashboardId, err := strconv.ParseInt(id, 10, 64) + dashboardID, err := strconv.ParseInt(id, 10, 64) if err == nil { - dbids = append(dbids, dashboardId) + dbIDs = append(dbIDs, dashboardID) } } - folderIds := make([]int64, 0) + folderIDs := make([]int64, 0) for _, id := range c.QueryStrings("folderIds") { - folderId, err := strconv.ParseInt(id, 10, 64) + folderID, err := strconv.ParseInt(id, 10, 64) if err == nil { - folderIds = append(folderIds, folderId) + folderIDs = append(folderIDs, folderID) } } @@ -48,9 +48,9 @@ func Search(c *m.ReqContext) { Limit: limit, IsStarred: starred == "true", OrgId: c.OrgId, - DashboardIds: dbids, + DashboardIds: dbIDs, Type: dashboardType, - FolderIds: folderIds, + FolderIds: folderIDs, Permission: permission, } diff --git a/pkg/api/team.go b/pkg/api/team.go index 316adfc4e7c..13e23df27c8 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -38,7 +38,7 @@ func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { } // DELETE /api/teams/:teamId -func DeleteTeamById(c *m.ReqContext) Response { +func DeleteTeamByID(c *m.ReqContext) Response { if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil { if err == m.ErrTeamNotFound { return ApiError(404, "Failed to delete Team. ID not found", nil) @@ -82,7 +82,7 @@ func SearchTeams(c *m.ReqContext) Response { } // GET /api/teams/:teamId -func GetTeamById(c *m.ReqContext) Response { +func GetTeamByID(c *m.ReqContext) Response { query := m.GetTeamByIdQuery{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/api/user.go b/pkg/api/user.go index b8483316b9d..4469fab2d29 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -14,12 +14,12 @@ func GetSignedInUser(c *m.ReqContext) Response { } // GET /api/users/:id -func GetUserById(c *m.ReqContext) Response { +func GetUserByID(c *m.ReqContext) Response { return getUserUserProfile(c.ParamsInt64(":id")) } -func getUserUserProfile(userId int64) Response { - query := m.GetUserProfileQuery{UserId: userId} +func getUserUserProfile(userID int64) Response { + query := m.GetUserProfileQuery{UserId: userID} if err := bus.Dispatch(&query); err != nil { if err == m.ErrUserNotFound { @@ -75,14 +75,14 @@ func UpdateUser(c *m.ReqContext, cmd m.UpdateUserCommand) Response { //POST /api/users/:id/using/:orgId func UpdateUserActiveOrg(c *m.ReqContext) Response { - userId := c.ParamsInt64(":id") - orgId := c.ParamsInt64(":orgId") + userID := c.ParamsInt64(":id") + orgID := c.ParamsInt64(":orgId") - if !validateUsingOrg(userId, orgId) { + if !validateUsingOrg(userID, orgID) { return ApiError(401, "Not a valid organization", nil) } - cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId} + cmd := m.SetUsingOrgCommand{UserId: userID, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { return ApiError(500, "Failed to change active organization", err) @@ -116,8 +116,8 @@ func GetUserOrgList(c *m.ReqContext) Response { return getUserOrgList(c.ParamsInt64(":id")) } -func getUserOrgList(userId int64) Response { - query := m.GetUserOrgListQuery{UserId: userId} +func getUserOrgList(userID int64) Response { + query := m.GetUserOrgListQuery{UserId: userID} if err := bus.Dispatch(&query); err != nil { return ApiError(500, "Failed to get user organizations", err) @@ -126,8 +126,8 @@ func getUserOrgList(userId int64) Response { return Json(200, query.Result) } -func validateUsingOrg(userId int64, orgId int64) bool { - query := m.GetUserOrgListQuery{UserId: userId} +func validateUsingOrg(userID int64, orgID int64) bool { + query := m.GetUserOrgListQuery{UserId: userID} if err := bus.Dispatch(&query); err != nil { return false @@ -136,7 +136,7 @@ func validateUsingOrg(userId int64, orgId int64) bool { // validate that the org id in the list valid := false for _, other := range query.Result { - if other.OrgId == orgId { + if other.OrgId == orgID { valid = true } } @@ -146,13 +146,13 @@ func validateUsingOrg(userId int64, orgId int64) bool { // POST /api/user/using/:id func UserSetUsingOrg(c *m.ReqContext) Response { - orgId := c.ParamsInt64(":id") + orgID := c.ParamsInt64(":id") - if !validateUsingOrg(c.UserId, orgId) { + if !validateUsingOrg(c.UserId, orgID) { return ApiError(401, "Not a valid organization", nil) } - cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId} + cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { return ApiError(500, "Failed to change active organization", err) @@ -163,13 +163,13 @@ func UserSetUsingOrg(c *m.ReqContext) Response { // GET /profile/switch-org/:id func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) { - orgId := c.ParamsInt64(":id") + orgID := c.ParamsInt64(":id") - if !validateUsingOrg(c.UserId, orgId) { + if !validateUsingOrg(c.UserId, orgID) { NotFoundHandler(c) } - cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId} + cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { NotFoundHandler(c) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 8ed3196e4ad..0338b3d6aa2 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -120,7 +120,7 @@ func (g *GrafanaServerImpl) initLogging() { } func (g *GrafanaServerImpl) startHttpServer() error { - g.httpServer = api.NewHttpServer() + g.httpServer = api.NewHTTPServer() err := g.httpServer.Start(g.context) diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index d6c377bc9ac..37e79c01071 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -17,10 +17,10 @@ type AuthOptions struct { } func getRequestUserId(c *m.ReqContext) int64 { - userId := c.Session.Get(session.SESS_KEY_USERID) + userID := c.Session.Get(session.SESS_KEY_USERID) - if userId != nil { - return userId.(int64) + if userID != nil { + return userID.(int64) } return 0 diff --git a/pkg/middleware/dashboard_redirect.go b/pkg/middleware/dashboard_redirect.go index 7c2af548a8f..024b112e154 100644 --- a/pkg/middleware/dashboard_redirect.go +++ b/pkg/middleware/dashboard_redirect.go @@ -20,7 +20,7 @@ func getDashboardUrlBySlug(orgId int64, slug string) (string, error) { return m.GetDashboardUrl(query.Result.Uid, query.Result.Slug), nil } -func RedirectFromLegacyDashboardUrl() macaron.Handler { +func RedirectFromLegacyDashboardURL() macaron.Handler { return func(c *m.ReqContext) { slug := c.Params("slug") diff --git a/pkg/middleware/dashboard_redirect_test.go b/pkg/middleware/dashboard_redirect_test.go index 0af06347ed0..24eab2d7b79 100644 --- a/pkg/middleware/dashboard_redirect_test.go +++ b/pkg/middleware/dashboard_redirect_test.go @@ -13,7 +13,7 @@ import ( func TestMiddlewareDashboardRedirect(t *testing.T) { Convey("Given the dashboard redirect middleware", t, func() { bus.ClearBusHandlers() - redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardUrl() + redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloUrl() fakeDash := m.NewDashboard("Child dash") @@ -34,9 +34,9 @@ func TestMiddlewareDashboardRedirect(t *testing.T) { Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { So(sc.resp.Code, ShouldEqual, 301) - redirectUrl, _ := sc.resp.Result().Location() - So(redirectUrl.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)) - So(len(redirectUrl.Query()), ShouldEqual, 2) + redirectURL, _ := sc.resp.Result().Location() + So(redirectURL.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)) + So(len(redirectURL.Query()), ShouldEqual, 2) }) }) @@ -47,11 +47,11 @@ func TestMiddlewareDashboardRedirect(t *testing.T) { Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { So(sc.resp.Code, ShouldEqual, 301) - redirectUrl, _ := sc.resp.Result().Location() - expectedUrl := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) - expectedUrl = strings.Replace(expectedUrl, "/d/", "/d-solo/", 1) - So(redirectUrl.Path, ShouldEqual, expectedUrl) - So(len(redirectUrl.Query()), ShouldEqual, 2) + redirectURL, _ := sc.resp.Result().Location() + expectedURL := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) + expectedURL = strings.Replace(expectedURL, "/d/", "/d-solo/", 1) + So(redirectURL.Path, ShouldEqual, expectedURL) + So(len(redirectURL.Query()), ShouldEqual, 2) }) }) }) diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index c63a0e81e57..4bbedbc3b21 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -14,10 +14,10 @@ import ( func TestRecoveryMiddleware(t *testing.T) { Convey("Given an api route that panics", t, func() { - apiUrl := "/api/whatever" - recoveryScenario("recovery middleware should return json", apiUrl, func(sc *scenarioContext) { + apiURL := "/api/whatever" + recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) { sc.handlerFunc = PanicHandler - sc.fakeReq("GET", apiUrl).exec() + sc.fakeReq("GET", apiURL).exec() sc.req.Header.Add("content-type", "application/json") So(sc.resp.Code, ShouldEqual, 500) @@ -27,10 +27,10 @@ func TestRecoveryMiddleware(t *testing.T) { }) Convey("Given a non-api route that panics", t, func() { - apiUrl := "/whatever" - recoveryScenario("recovery middleware should return html", apiUrl, func(sc *scenarioContext) { + apiURL := "/whatever" + recoveryScenario("recovery middleware should return html", apiURL, func(sc *scenarioContext) { sc.handlerFunc = PanicHandler - sc.fakeReq("GET", apiUrl).exec() + sc.fakeReq("GET", apiURL).exec() So(sc.resp.Code, ShouldEqual, 500) So(sc.resp.Header().Get("content-type"), ShouldEqual, "text/html; charset=UTF-8") From 0ffcea08c7188c6760322160b0bcfea1374a086f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 22 Mar 2018 15:18:05 +0300 Subject: [PATCH 55/78] dashboard version cleanup: more tests and refactor --- pkg/services/sqlstore/dashboard_version.go | 20 +++++++++---------- .../sqlstore/dashboard_version_test.go | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index d91b5727545..1f2850b2021 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -67,10 +67,10 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error { return nil } +const MAX_VERSIONS_TO_DELETE = 100 + func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { return inTransaction(func(sess *DBSession) error { - const MAX_VERSIONS_TO_DELETE = 100 - versionsToKeep := setting.DashboardVersionsToKeep if versionsToKeep < 1 { versionsToKeep = 1 @@ -80,27 +80,25 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { // min_version_to_keep = min_version + (versions_count - versions_to_keep) // where version stats is processed for each dashboard. This guarantees that we keep at least versions_to_keep // versions, but in some cases (when versions are sparse) this number may be more. - versionIdsToDeleteSubquery := `SELECT id + versionIdsToDeleteQuery := `SELECT id FROM dashboard_version, ( SELECT dashboard_id, count(version) as count, min(version) as min FROM dashboard_version GROUP BY dashboard_id - ) AS vtd - WHERE dashboard_version.dashboard_id=vtd.dashboard_id - AND version < vtd.min + vtd.count - ?` + ) AS vtd + WHERE dashboard_version.dashboard_id=vtd.dashboard_id + AND version < vtd.min + vtd.count - ?` var versionIdsToDelete []interface{} - err := sess.SQL(versionIdsToDeleteSubquery, versionsToKeep).Find(&versionIdsToDelete) + err := sess.SQL(versionIdsToDeleteQuery, versionsToKeep).Find(&versionIdsToDelete) if err != nil { return err } // Don't delete more than MAX_VERSIONS_TO_DELETE version per time - limit := MAX_VERSIONS_TO_DELETE - if len(versionIdsToDelete) < MAX_VERSIONS_TO_DELETE { - limit = len(versionIdsToDelete) + if len(versionIdsToDelete) > MAX_VERSIONS_TO_DELETE { + versionIdsToDelete = versionIdsToDelete[:MAX_VERSIONS_TO_DELETE] } - versionIdsToDelete = versionIdsToDelete[:limit] if len(versionIdsToDelete) > 0 { deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)` diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go index 151dc7c4be2..a6403755d05 100644 --- a/pkg/services/sqlstore/dashboard_version_test.go +++ b/pkg/services/sqlstore/dashboard_version_test.go @@ -141,5 +141,25 @@ func TestDeleteExpiredVersions(t *testing.T) { So(len(query.Result), ShouldEqual, versionsToWrite) }) + + Convey("Don't delete more than MAX_VERSIONS_TO_DELETE per iteration", func() { + versionsToWriteBigNumber := MAX_VERSIONS_TO_DELETE + versionsToWrite + for i := 0; i < versionsToWriteBigNumber-versionsToWrite; i++ { + updateTestDashboard(savedDash, map[string]interface{}{ + "tags": "different-tag", + }) + } + + err := DeleteExpiredVersions(&m.DeleteExpiredVersionsCommand{}) + So(err, ShouldBeNil) + + query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWriteBigNumber} + GetDashboardVersions(&query) + + // Ensure we have at least versionsToKeep versions + So(len(query.Result), ShouldBeGreaterThanOrEqualTo, versionsToKeep) + // Ensure we haven't deleted more than MAX_VERSIONS_TO_DELETE rows + So(versionsToWriteBigNumber-len(query.Result), ShouldBeLessThanOrEqualTo, MAX_VERSIONS_TO_DELETE) + }) }) } From 4916826364691ac5bd8332651c22c2f1d069e96c Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 22 Mar 2018 14:39:13 +0100 Subject: [PATCH 56/78] small screen legend right also work like legend under in render + set scrollbar to undefined in destroyScrollbar so it doesnt become disabled when toggeling between right and under --- public/app/plugins/panel/graph/legend.ts | 4 +++- public/sass/components/_panel_graph.scss | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index 7a9c75d4f1d..8a7248fea7f 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -111,6 +111,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } function render() { + let legendWidth = elem.width(); if (!ctrl.panel.legend.show) { elem.empty(); firstRender = true; @@ -163,7 +164,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } // render first time for getting proper legend height - if (!panel.legend.rightSide) { + if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== 10)) { renderLegendElement(tableHeaderElem); elem.empty(); } @@ -265,6 +266,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { function destroyScrollbar() { if (legendScrollbar) { legendScrollbar.destroy(); + legendScrollbar = undefined; } } }, diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 48d88872074..e15cd576367 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -11,19 +11,19 @@ flex: 0 1 10px; max-height: 100%; } - } - .graph-legend-series { - display: block; - padding-left: 0px; - } + .graph-legend-series { + display: block; + padding-left: 0px; + } - .graph-legend-table { - width: auto; - } + .graph-legend-table { + width: auto; + } - .graph-legend-table .graph-legend-series { - display: table-row; + .graph-legend-table .graph-legend-series { + display: table-row; + } } } } From 3ccadff800b350940cca0aae72cf35f2822bcc58 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Mar 2018 14:49:40 +0100 Subject: [PATCH 57/78] mssql: fix timeGroup macro so that it properly creates correct groups Earlier the division of interval was done using whole numbers resulting in that important information was lost/too many time series merged to the same group. Now using division of floating point and rounding up to solve the problem --- pkg/tsdb/mssql/macros.go | 2 +- pkg/tsdb/mssql/macros_test.go | 4 +- pkg/tsdb/mssql/mssql_test.go | 72 ++++++++++++++++++++--------------- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index 92c6ede148e..108faee85b4 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -113,7 +113,7 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er m.Query.Model.Set("fillValue", floatVal) } } - return fmt.Sprintf("cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s))/%.0f as int)*%.0f as int)", args[0], interval.Seconds(), interval.Seconds()), nil + return fmt.Sprintf("CAST(ROUND(DATEDIFF(second, '1970-01-01', %s)/%.1f, 0) as bigint)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index db1f5670924..c07bcbf498c 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -57,14 +57,14 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column))/300 as int)*300 as int)") + So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300") }) Convey("interpolate __timeGroup function with spaces around arguments", func() { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column))/300 as int)*300 as int)") + So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300") }) Convey("interpolate __timeGroup function with fill (value = NULL)", func() { diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 7ac135ec2f5..8e8b22d254f 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -211,22 +211,32 @@ func TestMSSQL(t *testing.T) { } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] So(err, ShouldBeNil) + queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) points := queryResult.Series[0].Points + So(len(points), ShouldEqual, 6) - So(len(points), ShouldEqual, 4) - actualValueFirst := points[0][0].Float64 - actualTimeFirst := time.Unix(int64(points[0][1].Float64)/1000, 0) - So(actualValueFirst, ShouldEqual, 15) - So(actualTimeFirst, ShouldEqual, fromStart) + dt := fromStart - actualValueLast := points[3][0].Float64 - actualTimeLast := time.Unix(int64(points[3][1].Float64)/1000, 0) - So(actualValueLast, ShouldEqual, 20) - So(actualTimeLast, ShouldEqual, fromStart.Add(25*time.Minute)) + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 3; i < 6; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } }) Convey("When doing a metric query using timeGroup with NULL fill enabled", func() { @@ -247,33 +257,34 @@ func TestMSSQL(t *testing.T) { } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] So(err, ShouldBeNil) + queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) points := queryResult.Series[0].Points - So(len(points), ShouldEqual, 7) - actualValueFirst := points[0][0].Float64 - actualTimeFirst := time.Unix(int64(points[0][1].Float64)/1000, 0) - So(actualValueFirst, ShouldEqual, 15) - So(actualTimeFirst, ShouldEqual, fromStart) - actualNullPoint := points[3][0] - actualNullTime := time.Unix(int64(points[3][1].Float64)/1000, 0) - So(actualNullPoint.Valid, ShouldBeFalse) - So(actualNullTime, ShouldEqual, fromStart.Add(15*time.Minute)) + dt := fromStart - actualValueLast := points[5][0].Float64 - actualTimeLast := time.Unix(int64(points[5][1].Float64)/1000, 0) - So(actualValueLast, ShouldEqual, 20) - So(actualTimeLast, ShouldEqual, fromStart.Add(25*time.Minute)) + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } - actualLastNullPoint := points[6][0] - actualLastNullTime := time.Unix(int64(points[6][1].Float64)/1000, 0) - So(actualLastNullPoint.Valid, ShouldBeFalse) - So(actualLastNullTime, ShouldEqual, fromStart.Add(30*time.Minute)) + So(points[3][0].Valid, ShouldBeFalse) + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 4; i < 7; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } }) Convey("When doing a metric query using timeGroup with float fill enabled", func() { @@ -294,13 +305,12 @@ func TestMSSQL(t *testing.T) { } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] So(err, ShouldBeNil) + queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) points := queryResult.Series[0].Points - - So(points[6][0].Float64, ShouldEqual, 1.5) + So(points[3][0].Float64, ShouldEqual, 1.5) }) }) From b0076d4f6500b7fd02c0d862fc5de586154ff076 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Mar 2018 14:55:44 +0100 Subject: [PATCH 58/78] mssql: remove UTC conversion in macro functions Removes the macro function . Macro functions should not do UTC/timezone conversion - they should work in the same way as postgres and mysql datasource implementations. Grafana and Microsft SQL Server should be run on servers with UTC timezones. --- pkg/tsdb/mssql/macros.go | 13 ++++-------- pkg/tsdb/mssql/macros_test.go | 21 +++++++------------ .../mssql/partials/annotations.editor.html | 15 +++++++------ .../mssql/partials/query.editor.html | 13 ++++++------ 4 files changed, 24 insertions(+), 38 deletions(-) diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index 108faee85b4..9d41cd03255 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -73,25 +73,20 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("missing time column argument for macro %v", name) } return fmt.Sprintf("%s AS time", args[0]), nil - case "__utcTime": - if len(args) == 0 { - return "", fmt.Errorf("missing time column argument for macro %v", name) - } - return fmt.Sprintf("DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s) AS time", args[0]), nil case "__timeEpoch": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s) ) AS time", args[0]), nil + return fmt.Sprintf("DATEDIFF(second, '1970-01-01', %s) AS time", args[0]), nil case "__timeFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s >= DATEADD(s, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND %s <= DATEADD(s, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + return fmt.Sprintf("%s >= DATEADD(s, %d, '1970-01-01') AND %s <= DATEADD(s, %d, '1970-01-01')", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil case "__timeFrom": - return fmt.Sprintf("DATEADD(second, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil + return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil case "__timeTo": - return fmt.Sprintf("DATEADD(second, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil case "__timeGroup": if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval", name) diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index c07bcbf498c..12a9b0d82be 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -25,32 +25,25 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, "select time_column AS time") }) - Convey("interpolate __utcTime function", func() { - sql, err := engine.Interpolate(query, nil, "select $__utcTime(time_column)") - So(err, ShouldBeNil) - - So(sql, ShouldEqual, "select DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) AS time") - }) - Convey("interpolate __timeEpoch function", func() { sql, err := engine.Interpolate(query, nil, "select $__timeEpoch(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, "select DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) AS time") + So(sql, ShouldEqual, "select DATEDIFF(second, '1970-01-01', time_column) AS time") }) Convey("interpolate __timeEpoch function wrapped in aggregation", func() { sql, err := engine.Interpolate(query, nil, "select min($__timeEpoch(time_column))") So(err, ShouldBeNil) - So(sql, ShouldEqual, "select min(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) AS time)") + So(sql, ShouldEqual, "select min(DATEDIFF(second, '1970-01-01', time_column) AS time)") }) Convey("interpolate __timeFilter function", func() { sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, "WHERE time_column >= DATEADD(s, 18446744066914186738+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND time_column <= DATEADD(s, 18446744066914187038+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')") + So(sql, ShouldEqual, "WHERE time_column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND time_column <= DATEADD(s, 18446744066914187038, '1970-01-01')") }) Convey("interpolate __timeGroup function", func() { @@ -97,21 +90,21 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, "select DATEADD(second, 18446744066914186738+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')") + So(sql, ShouldEqual, "select DATEADD(second, 18446744066914186738, '1970-01-01')") }) Convey("interpolate __timeTo function", func() { sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, "select DATEADD(second, 18446744066914187038+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')") + So(sql, ShouldEqual, "select DATEADD(second, 18446744066914187038, '1970-01-01')") }) Convey("interpolate __unixEpochFilter function", func() { - sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(18446744066914186738)") + sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, "select 18446744066914186738 >= 18446744066914186738 AND 18446744066914186738 <= 18446744066914187038") + So(sql, ShouldEqual, "select time_column >= 18446744066914186738 AND time_column <= 18446744066914187038") }) Convey("interpolate __unixEpochFrom function", func() { diff --git a/public/app/plugins/datasource/mssql/partials/annotations.editor.html b/public/app/plugins/datasource/mssql/partials/annotations.editor.html index ecdffd92d1e..185785ced4a 100644 --- a/public/app/plugins/datasource/mssql/partials/annotations.editor.html +++ b/public/app/plugins/datasource/mssql/partials/annotations.editor.html @@ -27,16 +27,15 @@ An annotation is an event that is overlayed on top of graphs. The query can have Macros: - $__time(column) -> column AS time -- $__utcTime(column) -> DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), column) AS time -- $__timeEpoch(column) -> DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), column) ) AS time -- $__timeFilter(column) -> column > DATEADD(s, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') AND column < DATEADD(s, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') -- $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877 +- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time +- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01') +- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 Or build your own conditionals using these macros which just return the values: -- $__timeFrom() -> DATEADD(second, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') -- $__timeTo() -> DATEADD(second, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') -- $__unixEpochFrom() -> 1492750877 -- $__unixEpochTo() -> 1492750877 +- $__timeFrom() -> DATEADD(second, 1492750877, '1970-01-01') +- $__timeTo() -> DATEADD(second, 1492750877, '1970-01-01') +- $__unixEpochFrom() -> 1492750877 +- $__unixEpochTo() -> 1492750877 diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html index e8f44c8c9f8..f8c7effb827 100644 --- a/public/app/plugins/datasource/mssql/partials/query.editor.html +++ b/public/app/plugins/datasource/mssql/partials/query.editor.html @@ -48,15 +48,14 @@ Table: Macros: - $__time(column) -> column AS time -- $__utcTime(column) -> DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), column) AS time -- $__timeEpoch(column) -> DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), column) ) AS time -- $__timeFilter(column) -> column > DATEADD(s, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') AND column < DATEADD(s, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') -- $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877 -- $__timeGroup(column, '5m'[, fillvalue]) -> cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), column))/300 as int)*300 as int). Providing a fillValue of NULL or floating value will automatically fill empty series in timerange with that value. +- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time +- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01') +- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 +- $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a fillValue of NULL or floating value will automatically fill empty series in timerange with that value. Or build your own conditionals using these macros which just return the values: -- $__timeFrom() -> DATEADD(second, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') -- $__timeTo() -> DATEADD(second, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') +- $__timeFrom() -> DATEADD(second, 1492750877, '1970-01-01') +- $__timeTo() -> DATEADD(second, 1492750877, '1970-01-01') - $__unixEpochFrom() -> 1492750877 - $__unixEpochTo() -> 1492750877 From b69ebee066ae3eea79a79213a2360e55be837726 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Mar 2018 15:23:12 +0100 Subject: [PATCH 59/78] mssql: fix precision for the time column in table/annotation query mode Use the ConvertSqlTimeColumnToEpochMs function to convert any native datetime data type or epoch time (millisecond precision). Additional tests and update of existing due to timezone issues running MSSQL on UTC and dev environment on non-utc. Update stored procedures test to handle more parameters. Update test dashboard. --- docker/blocks/mssql_tests/dashboard.json | 380 +++++++++++++++--- pkg/tsdb/mssql/mssql.go | 13 +- pkg/tsdb/mssql/mssql_test.go | 285 ++++++++++--- .../mssql/partials/annotations.editor.html | 2 +- .../datasource/mssql/response_parser.ts | 2 +- 5 files changed, 548 insertions(+), 134 deletions(-) diff --git a/docker/blocks/mssql_tests/dashboard.json b/docker/blocks/mssql_tests/dashboard.json index 323a61bb49a..20e3907b48b 100644 --- a/docker/blocks/mssql_tests/dashboard.json +++ b/docker/blocks/mssql_tests/dashboard.json @@ -53,7 +53,7 @@ "iconColor": "#6ed0e0", "limit": 100, "name": "Deploys", - "rawQuery": "SELECT\n time_sec as time,\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ", + "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ", "showIn": 0, "tags": [], "type": "tags" @@ -65,7 +65,7 @@ "iconColor": "rgba(255, 96, 96, 1)", "limit": 100, "name": "Tickets", - "rawQuery": "SELECT\n time_sec as time,\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ", + "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ", "showIn": 0, "tags": [], "type": "tags" @@ -76,8 +76,20 @@ "hide": false, "iconColor": "#7eb26d", "limit": 100, - "name": "Metric Values", - "rawQuery": "SELECT \n time, \n measurement as text, \n '' as tags\nFROM\n metric_values \nORDER BY 1", + "name": "Metric Values timeEpoch macro", + "rawQuery": "SELECT \n $__timeEpoch(time), \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_MSSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "#1f78c1", + "limit": 100, + "name": "Metric Values native time", + "rawQuery": "SELECT \n time, \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", "showIn": 0, "tags": [], "type": "tags" @@ -88,7 +100,7 @@ "gnetId": null, "graphTooltip": 0, "id": null, - "iteration": 1521481503341, + "iteration": 1521715844826, "links": [], "panels": [ { @@ -138,6 +150,222 @@ "transform": "table", "type": "table" }, + { + "columns": [], + "datasource": "${DS_MSSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 4 + }, + "id": 32, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as bigint) as time", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as bigint) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MSSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 4 + }, + "id": 33, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as datetime) as time", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as datetime) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MSSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 34, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT GETDATE() as time", + "refId": "A", + "target": "" + } + ], + "title": "GETDATE() as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MSSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 35, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT GETUTCDATE() as time", + "refId": "A", + "target": "" + } + ], + "title": "GETUTCDATE() as time", + "transform": "table", + "type": "table" + }, { "aliasColors": {}, "bars": false, @@ -149,7 +377,7 @@ "h": 9, "w": 8, "x": 0, - "y": 4 + "y": 7 }, "id": 7, "legend": { @@ -228,7 +456,7 @@ "h": 9, "w": 8, "x": 8, - "y": 4 + "y": 7 }, "id": 9, "legend": { @@ -307,7 +535,7 @@ "h": 9, "w": 8, "x": 16, - "y": 4 + "y": 7 }, "id": 10, "legend": { @@ -386,7 +614,7 @@ "h": 9, "w": 8, "x": 0, - "y": 13 + "y": 16 }, "id": 16, "legend": { @@ -465,7 +693,7 @@ "h": 9, "w": 8, "x": 8, - "y": 13 + "y": 16 }, "id": 12, "legend": { @@ -544,7 +772,7 @@ "h": 9, "w": 8, "x": 16, - "y": 13 + "y": 16 }, "id": 13, "legend": { @@ -623,7 +851,7 @@ "h": 8, "w": 12, "x": 0, - "y": 22 + "y": 25 }, "id": 27, "legend": { @@ -655,13 +883,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value one' as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value one' as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value two' as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value two' as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values\nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", "refId": "B" } ], @@ -712,7 +940,7 @@ "h": 8, "w": 12, "x": 12, - "y": 22 + "y": 25 }, "id": 5, "legend": { @@ -734,7 +962,19 @@ "pointradius": 3, "points": false, "renderer": "flot", - "seriesOverrides": [], + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], "spaceLength": 10, "stack": false, "steppedLine": false, @@ -742,8 +982,14 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nGROUP BY \n $__timeGroup(time, '$summarize')\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize')\nORDER BY 1", "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n time,\n avg(valueOne) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueOne,\n avg(valueTwo) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueTwo\nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nORDER BY 1", + "refId": "B" } ], "thresholds": [], @@ -793,7 +1039,7 @@ "h": 8, "w": 12, "x": 0, - "y": 30 + "y": 33 }, "id": 4, "legend": { @@ -825,13 +1071,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -882,7 +1128,7 @@ "h": 8, "w": 12, "x": 12, - "y": 30 + "y": 33 }, "id": 28, "legend": { @@ -963,7 +1209,7 @@ "h": 8, "w": 12, "x": 0, - "y": 38 + "y": 41 }, "id": 19, "legend": { @@ -995,13 +1241,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -1052,7 +1298,7 @@ "h": 8, "w": 12, "x": 12, - "y": 38 + "y": 41 }, "id": 18, "legend": { @@ -1082,7 +1328,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1133,7 +1379,7 @@ "h": 8, "w": 12, "x": 0, - "y": 46 + "y": 49 }, "id": 17, "legend": { @@ -1165,13 +1411,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -1222,7 +1468,7 @@ "h": 8, "w": 12, "x": 12, - "y": 46 + "y": 49 }, "id": 20, "legend": { @@ -1252,7 +1498,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1303,7 +1549,7 @@ "h": 8, "w": 12, "x": 0, - "y": 54 + "y": 57 }, "id": 29, "legend": { @@ -1335,7 +1581,7 @@ { "alias": "", "format": "time_series", - "rawSql": "DECLARE \n @from int = $__unixEpochFrom(),\n @to int = $__unixEpochTo()\n \nEXEC dbo.sp_test_epoch @from, @to", + "rawSql": "DECLARE\n @from int = $__unixEpochFrom(), \n @to int = $__unixEpochTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_epoch @from, @to, @interval, @metric", "refId": "A" } ], @@ -1386,7 +1632,7 @@ "h": 8, "w": 12, "x": 12, - "y": 54 + "y": 57 }, "id": 30, "legend": { @@ -1418,7 +1664,7 @@ { "alias": "", "format": "time_series", - "rawSql": "DECLARE \n @from datetime = $__timeFrom(),\n @to datetime = $__timeTo()\n \nEXEC dbo.sp_test_datetime @from, @to", + "rawSql": "DECLARE\n @from datetime = $__timeFrom(), \n @to datetime = $__timeTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_datetime @from, @to, @interval, @metric", "refId": "A" } ], @@ -1469,7 +1715,7 @@ "h": 8, "w": 12, "x": 0, - "y": 62 + "y": 65 }, "id": 14, "legend": { @@ -1499,13 +1745,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -1559,7 +1805,7 @@ "h": 8, "w": 12, "x": 12, - "y": 62 + "y": 65 }, "id": 15, "legend": { @@ -1589,7 +1835,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1642,7 +1888,7 @@ "h": 8, "w": 12, "x": 0, - "y": 70 + "y": 73 }, "id": 25, "legend": { @@ -1672,13 +1918,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -1732,7 +1978,7 @@ "h": 8, "w": 12, "x": 12, - "y": 70 + "y": 73 }, "id": 22, "legend": { @@ -1762,7 +2008,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1815,7 +2061,7 @@ "h": 8, "w": 12, "x": 0, - "y": 78 + "y": 81 }, "id": 21, "legend": { @@ -1845,13 +2091,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -1905,7 +2151,7 @@ "h": 8, "w": 12, "x": 12, - "y": 78 + "y": 81 }, "id": 26, "legend": { @@ -1935,7 +2181,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1988,7 +2234,7 @@ "h": 8, "w": 12, "x": 0, - "y": 86 + "y": 89 }, "id": 23, "legend": { @@ -2018,13 +2264,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -2078,7 +2324,7 @@ "h": 8, "w": 12, "x": 12, - "y": 86 + "y": 89 }, "id": 24, "legend": { @@ -2108,7 +2354,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -2157,6 +2403,26 @@ "tags": [], "templating": { "list": [ + { + "allValue": "'ALL'", + "current": {}, + "datasource": "${DS_MSSQL_TEST}", + "hide": 0, + "includeAll": true, + "label": "Metric", + "multi": false, + "name": "metric", + "options": [], + "query": "SELECT DISTINCT measurement FROM metric_values", + "refresh": 1, + "regex": "", + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, { "auto": false, "auto_count": 30, @@ -2208,7 +2474,7 @@ }, "time": { "from": "2018-03-15T12:30:00.000Z", - "to": "2018-03-15T13:55:00.000Z" + "to": "2018-03-15T13:55:01.000Z" }, "timepicker": { "refresh_intervals": [ @@ -2238,5 +2504,5 @@ "timezone": "", "title": "Microsoft SQL Server Data Source Test", "uid": "GlAqcPgmz", - "version": 37 + "version": 57 } \ No newline at end of file diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index 6da61d63e42..2638fd8bb40 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -119,15 +119,10 @@ func (e MssqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, return err } - // convert column named time to unix timestamp to make - // native datetime mssql types work in annotation queries - if timeIndex != -1 { - switch value := values[timeIndex].(type) { - case time.Time: - values[timeIndex] = (float64(value.Unix()) * 1000) + float64(value.Nanosecond()/1e6) // in case someone is trying to map times beyond 2262 :D - } - } - + // converts column named time to unix timestamp in milliseconds + // to make native mssql datetime types and epoch dates work in + // annotation and table queries. + tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) table.Rows = append(table.Rows, values) } diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 8e8b22d254f..4bd1e3a8ad7 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -19,6 +19,8 @@ import ( // and set up a MSSQL db named grafanatest and a user/password grafana/Password! // Use the docker/blocks/mssql_tests/docker-compose.yaml to spin up a // preconfigured MSSQL server suitable for running these tests. +// Thers's also a dashboard.json in same directory that you can import to Grafana +// once you've created a datasource for the test server/database. // If needed, change the variable below to the IP address of the database. var serverIP string = "localhost" @@ -37,7 +39,7 @@ func TestMSSQL(t *testing.T) { sess := x.NewSession() defer sess.Close() - fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC) + fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) Convey("Given a table with different native data types", func() { sql := ` @@ -186,14 +188,8 @@ func TestMSSQL(t *testing.T) { }) } - dtFormat := "2006-01-02 15:04:05.999999999" for _, s := range series { - sql = fmt.Sprintf(` - INSERT INTO metric (time, value) - VALUES(CAST('%s' AS DATETIME), %d) - `, s.Time.Format(dtFormat), s.Value) - - _, err = sess.Exec(sql) + _, err = sess.Insert(s) So(err, ShouldBeNil) } @@ -315,42 +311,34 @@ func TestMSSQL(t *testing.T) { }) Convey("Given a table with metrics having multiple values and measurements", func() { - sql := ` - IF OBJECT_ID('dbo.[metric_values]', 'U') IS NOT NULL - DROP TABLE dbo.[metric_values] - - CREATE TABLE [metric_values] ( - time datetime, - measurement nvarchar(100), - valueOne int, - valueTwo int, - ) - ` - - _, err := sess.Exec(sql) - So(err, ShouldBeNil) - - type metricValues struct { + type metric_values struct { Time time.Time Measurement string - ValueOne int64 - ValueTwo int64 + ValueOne int64 `xorm:"integer 'valueOne'"` + ValueTwo int64 `xorm:"integer 'valueTwo'"` } + if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(metric_values{}) + } + err := sess.CreateTable(metric_values{}) + So(err, ShouldBeNil) + rand.Seed(time.Now().Unix()) rnd := func(min, max int64) int64 { return rand.Int63n(max-min) + min } - series := []*metricValues{} + series := []*metric_values{} for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) { - series = append(series, &metricValues{ + series = append(series, &metric_values{ Time: t, Measurement: "Metric A", ValueOne: rnd(0, 100), ValueTwo: rnd(0, 100), }) - series = append(series, &metricValues{ + series = append(series, &metric_values{ Time: t, Measurement: "Metric B", ValueOne: rnd(0, 100), @@ -358,14 +346,8 @@ func TestMSSQL(t *testing.T) { }) } - dtFormat := "2006-01-02 15:04:05" for _, s := range series { - sql = fmt.Sprintf(` - INSERT metric_values (time, measurement, valueOne, valueTwo) - VALUES(CAST('%s' AS DATETIME), '%s', %d, %d) - `, s.Time.Format(dtFormat), s.Measurement, s.ValueOne, s.ValueTwo) - - _, err = sess.Exec(sql) + _, err = sess.Insert(s) So(err, ShouldBeNil) } @@ -383,8 +365,8 @@ func TestMSSQL(t *testing.T) { } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] So(err, ShouldBeNil) + queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) So(len(queryResult.Series), ShouldEqual, 2) @@ -406,8 +388,8 @@ func TestMSSQL(t *testing.T) { } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] So(err, ShouldBeNil) + queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) So(len(queryResult.Series), ShouldEqual, 2) @@ -426,32 +408,42 @@ func TestMSSQL(t *testing.T) { sql = ` CREATE PROCEDURE sp_test_epoch( - @from int, - @to int + @from int, + @to int, + @interval nvarchar(50) = '5m', + @metric nvarchar(200) = 'ALL' ) AS BEGIN + DECLARE @dInterval int + SELECT @dInterval = 300 + + IF @interval = '10m' + SELECT @dInterval = 600 + SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, measurement + ' - value one' as metric, avg(valueOne) as value FROM metric_values WHERE - time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND + (@metric = 'ALL' OR measurement = @metric) GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, measurement UNION ALL SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, measurement + ' - value two' as metric, avg(valueTwo) as value FROM metric_values WHERE - time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND + (@metric = 'ALL' OR measurement = @metric) GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, measurement ORDER BY 1 END @@ -484,6 +476,7 @@ func TestMSSQL(t *testing.T) { resp, err := endpoint.Query(nil, nil, query) queryResult := resp.Results["A"] So(err, ShouldBeNil) + fmt.Println("query", "sql", queryResult.Meta) So(queryResult.Error, ShouldBeNil) So(len(queryResult.Series), ShouldEqual, 4) @@ -505,32 +498,42 @@ func TestMSSQL(t *testing.T) { sql = ` CREATE PROCEDURE sp_test_datetime( - @from datetime, - @to datetime + @from datetime, + @to datetime, + @interval nvarchar(50) = '5m', + @metric nvarchar(200) = 'ALL' ) AS BEGIN + DECLARE @dInterval int + SELECT @dInterval = 300 + + IF @interval = '10m' + SELECT @dInterval = 600 + SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, measurement + ' - value one' as metric, avg(valueOne) as value FROM metric_values WHERE - time >= @from AND time <= @to + time BETWEEN @from AND @to AND + (@metric = 'ALL' OR measurement = @metric) GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, measurement UNION ALL SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, measurement + ' - value two' as metric, avg(valueTwo) as value FROM metric_values WHERE - time >= @from AND time <= @to + time BETWEEN @from AND @to AND + (@metric = 'ALL' OR measurement = @metric) GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, measurement ORDER BY 1 END @@ -580,7 +583,7 @@ func TestMSSQL(t *testing.T) { DROP TABLE dbo.[event] CREATE TABLE [event] ( - time_sec bigint, + time_sec int, description nvarchar(100), tags nvarchar(100), ) @@ -666,30 +669,180 @@ func TestMSSQL(t *testing.T) { }) Convey("When doing an annotation query with a time column in datetime format", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + dtFormat := "2006-01-02 15:04:05.999999999" + query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT DATEADD(s, time_sec, {d '1970-01-01'}) AS time, description as [text], tags FROM [event] WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC", + "rawSql": fmt.Sprintf(`SELECT + CAST('%s' AS DATETIME) as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Format(dtFormat)), "format": "table", }), - RefId: "Tickets", + RefId: "A", }, }, - TimeRange: &tsdb.TimeRange{ - From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), - To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), - }, } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["Tickets"] So(err, ShouldBeNil) - So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) columns := queryResult.Tables[0].Rows[0] //Should be in milliseconds - So(columns[0].(float64), ShouldBeGreaterThan, 1000000000000) + So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch second format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch second format (int) should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + cast(%d as int) as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()*1000), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column holding a bigint null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as bigint) as time, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) + }) + + Convey("When doing an annotation query with a time column holding a datetime null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as datetime) as time, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) }) }) }) @@ -697,6 +850,8 @@ func TestMSSQL(t *testing.T) { func InitMSSQLTestDB(t *testing.T) *xorm.Engine { x, err := xorm.NewEngine(sqlutil.TestDB_Mssql.DriverName, strings.Replace(sqlutil.TestDB_Mssql.ConnStr, "localhost", serverIP, 1)) + x.DatabaseTZ = time.UTC + x.TZLocation = time.UTC // x.ShowSQL() @@ -704,8 +859,6 @@ func InitMSSQLTestDB(t *testing.T) *xorm.Engine { t.Fatalf("Failed to init mssql db %v", err) } - sqlutil.CleanDB(x) - return x } diff --git a/public/app/plugins/datasource/mssql/partials/annotations.editor.html b/public/app/plugins/datasource/mssql/partials/annotations.editor.html index 185785ced4a..75eaa3ed1d9 100644 --- a/public/app/plugins/datasource/mssql/partials/annotations.editor.html +++ b/public/app/plugins/datasource/mssql/partials/annotations.editor.html @@ -20,7 +20,7 @@
Annotation Query Format
An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. -- column with alias: time for the annotation event time (in UTC). Use unix timestamp in seconds or any native date data type. +- column with alias: time for the annotation event time. Use epoch time or any native date data type. - column with alias: text for the annotation text. - column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2'. diff --git a/public/app/plugins/datasource/mssql/response_parser.ts b/public/app/plugins/datasource/mssql/response_parser.ts index c98a9652b0e..b6f538707b0 100644 --- a/public/app/plugins/datasource/mssql/response_parser.ts +++ b/public/app/plugins/datasource/mssql/response_parser.ts @@ -128,7 +128,7 @@ export default class ResponseParser { const row = table.rows[i]; list.push({ annotation: options.annotation, - time: row[timeColumnIndex], + time: Math.floor(row[timeColumnIndex]), text: row[textColumnIndex], tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [], }); From 66c03f84f59a493dd5110982070dbddb91ae48c2 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Mar 2018 15:27:12 +0100 Subject: [PATCH 60/78] postgres: fix precision for the time column in table/annotation query mode Use the ConvertSqlTimeColumnToEpochMs function to convert any native datetime data type or epoch time (millisecond precision). Additional tests and update of existing due to timezone issues running postgres on UTC and dev environment on non-utc. Added test dashboard. --- docker/blocks/postgres_tests/dashboard.json | 2324 +++++++++++++++++ pkg/tsdb/postgres/postgres.go | 15 +- pkg/tsdb/postgres/postgres_test.go | 668 ++++- .../postgres/partials/annotations.editor.html | 5 +- .../datasource/postgres/response_parser.ts | 2 +- 5 files changed, 2930 insertions(+), 84 deletions(-) create mode 100644 docker/blocks/postgres_tests/dashboard.json diff --git a/docker/blocks/postgres_tests/dashboard.json b/docker/blocks/postgres_tests/dashboard.json new file mode 100644 index 00000000000..eea95863716 --- /dev/null +++ b/docker/blocks/postgres_tests/dashboard.json @@ -0,0 +1,2324 @@ +{ + "__inputs": [ + { + "name": "DS_POSTGRES_TEST", + "label": "Postgres TEST", + "description": "", + "type": "datasource", + "pluginId": "postgres", + "pluginName": "PostgreSQL" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "5.0.0" + }, + { + "type": "datasource", + "id": "postgres", + "name": "PostgreSQL", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "5.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + }, + { + "datasource": "${DS_POSTGRES_TEST}", + "enable": false, + "hide": false, + "iconColor": "#6ed0e0", + "limit": 100, + "name": "Deploys", + "rawQuery": "SELECT \"time_sec\" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_POSTGRES_TEST}", + "enable": false, + "hide": false, + "iconColor": "rgba(255, 96, 96, 1)", + "limit": 100, + "name": "Tickets", + "rawQuery": "SELECT \"time_sec\" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_POSTGRES_TEST}", + "enable": false, + "hide": false, + "iconColor": "#7eb26d", + "limit": 100, + "name": "Metric Values timeEpoch macro", + "rawQuery": "SELECT \n $__timeEpoch(time), \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_POSTGRES_TEST}", + "enable": false, + "hide": false, + "iconColor": "#1f78c1", + "limit": 100, + "name": "Metric Values native time", + "rawQuery": "SELECT \n time, \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", + "showIn": 0, + "tags": [], + "type": "tags" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "iteration": 1521725946837, + "links": [], + "panels": [ + { + "columns": [], + "datasource": "${DS_POSTGRES_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 1, + "desc": false + }, + "styles": [ + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT * FROM postgres_types", + "refId": "A" + } + ], + "title": "Data types", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_POSTGRES_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 4 + }, + "id": 32, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as bigint) as time", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as bigint) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_POSTGRES_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 4 + }, + "id": 33, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as timestamp) as time", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as datetime) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_POSTGRES_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 34, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT localtimestamp as time", + "refId": "A", + "target": "" + } + ], + "title": "localtimestamp as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_POSTGRES_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 35, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT NOW() as time", + "refId": "A", + "target": "" + } + ], + "title": "NOW() as time", + "transform": "table", + "type": "table" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 7 + }, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m without fill", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 7 + }, + "id": 9, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(NULL) and null as zero", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 7 + }, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(10.0)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 16, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize without fill", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(NULL)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(100.0)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 27, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement || ' - value one' as metric, \n avg(\"valueOne\") as \"valueOne\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement || ' - value two' as metric, \n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column using timeGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 25 + }, + "id": 5, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column using timeGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 4, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 33 + }, + "id": 28, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 41 + }, + "id": 19, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 41 + }, + "id": 18, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 49 + }, + "id": 17, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 49 + }, + "id": 20, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 57 + }, + "id": 14, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 57 + }, + "id": 15, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 65 + }, + "id": 25, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 50, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 65 + }, + "id": 22, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values\nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 100, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 73 + }, + "id": 21, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 73 + }, + "id": 26, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 81 + }, + "id": 23, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 81 + }, + "id": 24, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": {}, + "datasource": "${DS_POSTGRES_TEST}", + "hide": 0, + "includeAll": true, + "label": "Metric", + "multi": true, + "name": "metric", + "options": [], + "query": "SELECT DISTINCT measurement FROM metric_values", + "refresh": 1, + "regex": "", + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "10m", + "value": "10m" + }, + "hide": 0, + "label": "Interval", + "name": "summarize", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "10s", + "value": "10s" + }, + { + "selected": false, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + }, + { + "selected": true, + "text": "10m", + "value": "10m" + } + ], + "query": "1s,10s,30s,1m,5m,10m", + "refresh": 2, + "type": "interval" + } + ] + }, + "time": { + "from": "2018-03-15T12:30:00.000Z", + "to": "2018-03-15T13:55:01.000Z" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Postgres Data Source Test", + "uid": "vHQdlVziz", + "version": 14 +} \ No newline at end of file diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index 6a084ad1237..5f6b56ebcf1 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -63,7 +63,6 @@ func (e *PostgresQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSo } func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - columnNames, err := rows.Columns() if err != nil { return err @@ -100,14 +99,10 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro return err } - // convert column named time to unix timestamp to make - // native datetime postgres types work in annotation queries - if timeIndex != -1 { - switch value := values[timeIndex].(type) { - case time.Time: - values[timeIndex] = float64(value.UnixNano() / 1e9) - } - } + // converts column named time to unix timestamp in milliseconds to make + // native postgres datetime types and epoch dates work in + // annotation and table queries. + tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) table.Rows = append(table.Rows, values) } @@ -118,7 +113,6 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro } func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) { - types, err := rows.ColumnTypes() if err != nil { return nil, err @@ -209,7 +203,6 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() fillValue.Valid = true } - } for rows.Next() { diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index 75e8cb77f2e..3f2203ac7a4 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -1,6 +1,8 @@ package postgres import ( + "fmt" + "math/rand" "testing" "time" @@ -14,7 +16,11 @@ import ( ) // To run this test, remove the Skip from SkipConvey -// and set up a PostgreSQL db named grafanatest and a user/password grafanatest/grafanatest +// and set up a PostgreSQL db named grafanatest and a user/password grafanatest/grafanatest! +// Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a +// preconfigured Postgres server suitable for running these tests. +// Thers's also a dashboard.json in same directory that you can import to Grafana +// once you've created a datasource for the test server/database. func TestPostgres(t *testing.T) { SkipConvey("PostgreSQL", t, func() { x := InitPostgresTestDB(t) @@ -30,88 +36,599 @@ func TestPostgres(t *testing.T) { sess := x.NewSession() defer sess.Close() - sql := ` - CREATE TABLE postgres_types( - c00_smallint smallint, - c01_integer integer, - c02_bigint bigint, + fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) - c03_real real, - c04_double double precision, - c05_decimal decimal(10,2), - c06_numeric numeric(10,2), + Convey("Given a table with different native data types", func() { + sql := ` + DROP TABLE IF EXISTS postgres_types; + CREATE TABLE postgres_types( + c00_smallint smallint, + c01_integer integer, + c02_bigint bigint, - c07_char char(10), - c08_varchar varchar(10), - c09_text text, + c03_real real, + c04_double double precision, + c05_decimal decimal(10,2), + c06_numeric numeric(10,2), - c10_timestamp timestamp without time zone, - c11_timestamptz timestamp with time zone, - c12_date date, - c13_time time without time zone, - c14_timetz time with time zone, - c15_interval interval - ); - ` - _, err := sess.Exec(sql) - So(err, ShouldBeNil) + c07_char char(10), + c08_varchar varchar(10), + c09_text text, - sql = ` - INSERT INTO postgres_types VALUES( - 1,2,3, - 4.5,6.7,1.1,1.2, - 'char10','varchar10','text', + c10_timestamp timestamp without time zone, + c11_timestamptz timestamp with time zone, + c12_date date, + c13_time time without time zone, + c14_timetz time with time zone, - now(),now(),now(),now(),now(),'15m'::interval - ); - ` - _, err = sess.Exec(sql) - So(err, ShouldBeNil) - - Convey("Query with Table format should map PostgreSQL column types to Go types", func() { - query := &tsdb.TsdbQuery{ - Queries: []*tsdb.Query{ - { - Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT * FROM postgres_types", - "format": "table", - }), - RefId: "A", - }, - }, - } - - resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] + c15_interval interval + ); + ` + _, err := sess.Exec(sql) So(err, ShouldBeNil) - column := queryResult.Tables[0].Rows[0] - So(column[0].(int64), ShouldEqual, 1) - So(column[1].(int64), ShouldEqual, 2) - So(column[2].(int64), ShouldEqual, 3) - So(column[3].(float64), ShouldEqual, 4.5) - So(column[4].(float64), ShouldEqual, 6.7) - // libpq doesnt properly convert decimal, numeric and char to go types but returns []uint8 instead - // So(column[5].(float64), ShouldEqual, 1.1) - // So(column[6].(float64), ShouldEqual, 1.2) - // So(column[7].(string), ShouldEqual, "char") - So(column[8].(string), ShouldEqual, "varchar10") - So(column[9].(string), ShouldEqual, "text") + sql = ` + INSERT INTO postgres_types VALUES( + 1,2,3, + 4.5,6.7,1.1,1.2, + 'char10','varchar10','text', - So(column[10].(time.Time), ShouldHaveSameTypeAs, time.Now()) - So(column[11].(time.Time), ShouldHaveSameTypeAs, time.Now()) - So(column[12].(time.Time), ShouldHaveSameTypeAs, time.Now()) - So(column[13].(time.Time), ShouldHaveSameTypeAs, time.Now()) - So(column[14].(time.Time), ShouldHaveSameTypeAs, time.Now()) + now(),now(),now(),now(),now(),'15m'::interval + ); + ` + _, err = sess.Exec(sql) + So(err, ShouldBeNil) - // libpq doesnt properly convert interval to go types but returns []uint8 instead - // So(column[15].(time.Time), ShouldHaveSameTypeAs, time.Now()) + Convey("When doing a table query should map Postgres column types to Go types", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT * FROM postgres_types", + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + column := queryResult.Tables[0].Rows[0] + So(column[0].(int64), ShouldEqual, 1) + So(column[1].(int64), ShouldEqual, 2) + So(column[2].(int64), ShouldEqual, 3) + + So(column[3].(float64), ShouldEqual, 4.5) + So(column[4].(float64), ShouldEqual, 6.7) + So(column[5].(float64), ShouldEqual, 1.1) + So(column[6].(float64), ShouldEqual, 1.2) + + So(column[7].(string), ShouldEqual, "char10 ") + So(column[8].(string), ShouldEqual, "varchar10") + So(column[9].(string), ShouldEqual, "text") + + So(column[10].(time.Time), ShouldHaveSameTypeAs, time.Now()) + So(column[11].(time.Time), ShouldHaveSameTypeAs, time.Now()) + So(column[12].(time.Time), ShouldHaveSameTypeAs, time.Now()) + So(column[13].(time.Time), ShouldHaveSameTypeAs, time.Now()) + So(column[14].(time.Time), ShouldHaveSameTypeAs, time.Now()) + + So(column[15].(string), ShouldEqual, "00:15:00") + }) + }) + + Convey("Given a table with metrics that lacks data for some series ", func() { + sql := ` + DROP TABLE IF EXISTS metric; + CREATE TABLE metric ( + time timestamp, + value integer + ) + ` + + _, err := sess.Exec(sql) + So(err, ShouldBeNil) + + type metric struct { + Time time.Time + Value int64 + } + + series := []*metric{} + firstRange := genTimeRangeByInterval(fromStart, 10*time.Minute, 10*time.Second) + secondRange := genTimeRangeByInterval(fromStart.Add(20*time.Minute), 10*time.Minute, 10*time.Second) + + for _, t := range firstRange { + series = append(series, &metric{ + Time: t, + Value: 15, + }) + } + + for _, t := range secondRange { + series = append(series, &metric{ + Time: t, + Value: 20, + }) + } + + for _, s := range series { + _, err = sess.Insert(s) + So(err, ShouldBeNil) + } + + Convey("When doing a metric query using timeGroup", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(len(points), ShouldEqual, 6) + + dt := fromStart + + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 3; i < 6; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + }) + + Convey("When doing a metric query using timeGroup with NULL fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(len(points), ShouldEqual, 7) + + dt := fromStart + + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + + So(points[3][0].Valid, ShouldBeFalse) + + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 4; i < 7; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + }) + + Convey("When doing a metric query using timeGroup with float fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', 1.5), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(points[3][0].Float64, ShouldEqual, 1.5) + }) + }) + + Convey("Given a table with metrics having multiple values and measurements", func() { + type metric_values struct { + Time time.Time + Measurement string + ValueOne int64 `xorm:"integer 'valueOne'"` + ValueTwo int64 `xorm:"integer 'valueTwo'"` + } + + if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(metric_values{}) + } + err := sess.CreateTable(metric_values{}) + So(err, ShouldBeNil) + + rand.Seed(time.Now().Unix()) + rnd := func(min, max int64) int64 { + return rand.Int63n(max-min) + min + } + + series := []*metric_values{} + for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) { + series = append(series, &metric_values{ + Time: t, + Measurement: "Metric A", + ValueOne: rnd(0, 100), + ValueTwo: rnd(0, 100), + }) + series = append(series, &metric_values{ + Time: t, + Measurement: "Metric B", + ValueOne: rnd(0, 100), + ValueTwo: rnd(0, 100), + }) + } + + for _, s := range series { + _, err := sess.Insert(s) + So(err, ShouldBeNil) + } + + Convey("When doing a metric query grouping by time and select metric column should return correct series", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT $__timeEpoch(time), measurement || ' - value one' as metric, "valueOne" FROM metric_values ORDER BY 1`, + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 2) + So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one") + So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") + }) + + Convey("When doing a metric query grouping by time should return correct series", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT $__timeEpoch(time), "valueOne", "valueTwo" FROM metric_values ORDER BY 1`, + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 2) + So(queryResult.Series[0].Name, ShouldEqual, "valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "valueTwo") + }) + }) + + Convey("Given a table with event data", func() { + type event struct { + TimeSec int64 + Description string + Tags string + } + + if exist, err := sess.IsTableExist(event{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(event{}) + } + err := sess.CreateTable(event{}) + So(err, ShouldBeNil) + + events := []*event{} + for _, t := range genTimeRangeByInterval(fromStart.Add(-20*time.Minute), 60*time.Minute, 25*time.Minute) { + events = append(events, &event{ + TimeSec: t.Unix(), + Description: "Someone deployed something", + Tags: "deploy", + }) + events = append(events, &event{ + TimeSec: t.Add(5 * time.Minute).Unix(), + Description: "New support ticket registered", + Tags: "ticket", + }) + } + + for _, e := range events { + _, err = sess.Insert(e) + So(err, ShouldBeNil) + } + + Convey("When doing an annotation query of deploy events should return expected result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC`, + "format": "table", + }), + RefId: "Deploys", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Deploys"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + }) + + Convey("When doing an annotation query of ticket events should return expected result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC`, + "format": "table", + }), + RefId: "Tickets", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Tickets"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + }) + + Convey("When doing an annotation query with a time column in datetime format", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + dtFormat := "2006-01-02 15:04:05.999999999" + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + CAST('%s' AS TIMESTAMP) as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Format(dtFormat)), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch second format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch second format (int) should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + cast(%d as bigint) as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()*1000), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, dt.Unix()*1000) + }) + + Convey("When doing an annotation query with a time column holding a bigint null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as bigint) as time, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) + }) + + Convey("When doing an annotation query with a time column holding a timestamp null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as timestamp) as time, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) + }) }) }) } func InitPostgresTestDB(t *testing.T) *xorm.Engine { x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr) + x.DatabaseTZ = time.UTC + x.TZLocation = time.UTC // x.ShowSQL() @@ -119,7 +636,18 @@ func InitPostgresTestDB(t *testing.T) *xorm.Engine { t.Fatalf("Failed to init postgres db %v", err) } - sqlutil.CleanDB(x) - return x } + +func genTimeRangeByInterval(from time.Time, duration time.Duration, interval time.Duration) []time.Time { + durationSec := int64(duration.Seconds()) + intervalSec := int64(interval.Seconds()) + timeRange := []time.Time{} + + for i := int64(0); i < durationSec; i += intervalSec { + timeRange = append(timeRange, from) + from = from.Add(time.Duration(int64(time.Second) * intervalSec)) + } + + return timeRange +} diff --git a/public/app/plugins/datasource/postgres/partials/annotations.editor.html b/public/app/plugins/datasource/postgres/partials/annotations.editor.html index b56f7523087..09232d6f8ed 100644 --- a/public/app/plugins/datasource/postgres/partials/annotations.editor.html +++ b/public/app/plugins/datasource/postgres/partials/annotations.editor.html @@ -18,15 +18,16 @@
Annotation Query Format
-An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. +An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. -- column with alias: time for the annotation event. Format is UTC in seconds, use extract(epoch from column) as "time" +- column with alias: time for the annotation event time. Use epoch time or any native date data type. - column with alias: text for the annotation text - column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' Macros: - $__time(column) -> column as "time" +- $__timeEpoch -> extract(epoch from column) as "time" - $__timeFilter(column) -> column ≥ to_timestamp(1492750877) AND column ≤ to_timestamp(1492750877) - $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877 diff --git a/public/app/plugins/datasource/postgres/response_parser.ts b/public/app/plugins/datasource/postgres/response_parser.ts index 620aba5fa7e..ebc9598468b 100644 --- a/public/app/plugins/datasource/postgres/response_parser.ts +++ b/public/app/plugins/datasource/postgres/response_parser.ts @@ -134,7 +134,7 @@ export default class ResponseParser { const row = table.rows[i]; list.push({ annotation: options.annotation, - time: Math.floor(row[timeColumnIndex]) * 1000, + time: Math.floor(row[timeColumnIndex]), title: row[titleColumnIndex], text: row[textColumnIndex], tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [], From f5654f88e21eba4b5418151737367e969f04025d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Mar 2018 15:40:46 +0100 Subject: [PATCH 61/78] mysql: fix precision for the time column in table/annotation query mode Use the ConvertSqlTimeColumnToEpochMs function to convert any native datetime data type or epoch time (millisecond precision). Refactored mysql implementation to make it more similar to postgres and mssql implementations. Added $__timeEpoch macro function with same implementation as $__time. Added possibility to use a time column named time in addition to the currectly supported time_sec. Additional tests and update of existing. Added test dashboard. --- docker/blocks/mysql_tests/dashboard.json | 2350 +++++++++++++++++ pkg/tsdb/mysql/macros.go | 2 +- pkg/tsdb/mysql/mysql.go | 205 +- pkg/tsdb/mysql/mysql_test.go | 718 ++++- .../mysql/partials/annotations.editor.html | 7 +- .../mysql/partials/query.editor.html | 7 +- .../datasource/mysql/response_parser.ts | 4 +- 7 files changed, 3088 insertions(+), 205 deletions(-) create mode 100644 docker/blocks/mysql_tests/dashboard.json diff --git a/docker/blocks/mysql_tests/dashboard.json b/docker/blocks/mysql_tests/dashboard.json new file mode 100644 index 00000000000..3ab08a7da35 --- /dev/null +++ b/docker/blocks/mysql_tests/dashboard.json @@ -0,0 +1,2350 @@ +{ + "__inputs": [ + { + "name": "DS_MYSQL_TEST", + "label": "MySQL TEST", + "description": "", + "type": "datasource", + "pluginId": "mysql", + "pluginName": "MySQL" + }, + { + "name": "DS_MSSQL_TEST", + "label": "MSSQL Test", + "description": "", + "type": "datasource", + "pluginId": "mssql", + "pluginName": "Microsoft SQL Server" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "5.0.0" + }, + { + "type": "datasource", + "id": "mssql", + "name": "Microsoft SQL Server", + "version": "1.0.0" + }, + { + "type": "datasource", + "id": "mysql", + "name": "MySQL", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "5.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + }, + { + "datasource": "${DS_MYSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "#6ed0e0", + "limit": 100, + "name": "Deploys", + "rawQuery": "SELECT\n time_sec,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_MYSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "rgba(255, 96, 96, 1)", + "limit": 100, + "name": "Tickets", + "rawQuery": "SELECT\n time_sec as time,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_MYSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "#7eb26d", + "limit": 100, + "name": "Metric Values timeEpoch macro", + "rawQuery": "SELECT \n $__timeEpoch(time), \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_MYSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "#1f78c1", + "limit": 100, + "name": "Metric Values native time", + "rawQuery": "SELECT \n time, \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", + "showIn": 0, + "tags": [], + "type": "tags" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "iteration": 1521715720483, + "links": [], + "panels": [ + { + "columns": [], + "datasource": "${DS_MYSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT * from mysql_types", + "refId": "A" + } + ], + "title": "Data types", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MYSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 4 + }, + "id": 32, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as unsigned integer) as time_sec", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as unsigned integer) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MYSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 4 + }, + "id": 33, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as datetime) as time_sec", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as datetime) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MYSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 34, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(NOW() as datetime) as time_sec", + "refId": "A", + "target": "" + } + ], + "title": "cast()NOW() as datetime) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MYSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 35, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT NOW() as time", + "refId": "A", + "target": "" + } + ], + "title": "NOW() as time", + "transform": "table", + "type": "table" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 7 + }, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m without fill", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 7 + }, + "id": 9, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(NULL) and null as zero", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 7 + }, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(10.0)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 16, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize without fill", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(NULL)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(100.0)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 27, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n CONCAT(measurement, ' - value one') as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n CONCAT(measurement, ' - value two') as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values\nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1,2\nORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column using timeGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 25 + }, + "id": 5, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n measurement IN($metric)\nGROUP BY 1\nORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column using timeGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 4, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 33 + }, + "id": 28, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 41 + }, + "id": 19, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 41 + }, + "id": 18, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 49 + }, + "id": 17, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 49 + }, + "id": 20, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 57 + }, + "id": 14, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 57 + }, + "id": 15, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 65 + }, + "id": 25, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 50, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 65 + }, + "id": 22, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 100, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 73 + }, + "id": 21, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 73 + }, + "id": 26, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 81 + }, + "id": 23, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 81 + }, + "id": 24, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": "", + "current": {}, + "datasource": "${DS_MYSQL_TEST}", + "hide": 0, + "includeAll": true, + "label": "Metric", + "multi": true, + "name": "metric", + "options": [], + "query": "SELECT DISTINCT measurement FROM metric_values", + "refresh": 1, + "regex": "", + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "10m", + "value": "10m" + }, + "hide": 0, + "label": "Interval", + "name": "summarize", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "10s", + "value": "10s" + }, + { + "selected": false, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + }, + { + "selected": true, + "text": "10m", + "value": "10m" + } + ], + "query": "1s,10s,30s,1m,5m,10m", + "refresh": 2, + "type": "interval" + } + ] + }, + "time": { + "from": "2018-03-15T11:30:00.000Z", + "to": "2018-03-15T12:55:01.000Z" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "MySQL Data Source Test", + "uid": "Hmf8FDkmz", + "version": 9 +} \ No newline at end of file diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index b0170070dcf..a292f209429 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -68,7 +68,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { - case "__time": + case "__timeEpoch", "__time": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index f3060e235e5..483974c55a4 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -81,7 +81,7 @@ func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, // check if there is a column named time for i, col := range columnNames { switch col { - case "time_sec": + case "time", "time_sec": timeIndex = i } } @@ -96,13 +96,10 @@ func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, return err } - // for annotations, convert to epoch - if timeIndex != -1 { - switch value := values[timeIndex].(type) { - case time.Time: - values[timeIndex] = float64(value.UnixNano() / 1e9) - } - } + // converts column named time to unix timestamp in milliseconds to make + // native mysql datetime types and epoch dates work in + // annotation and table queries. + tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) table.Rows = append(table.Rows, values) } @@ -185,9 +182,37 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. return err } - rowData := NewStringStringScan(columnNames) + columnTypes, err := rows.ColumnTypes() + if err != nil { + return err + } + rowLimit := 1000000 rowCount := 0 + timeIndex := -1 + metricIndex := -1 + + // check columns of resultset: a column named time is mandatory + // the first text column is treated as metric name unless a column named metric is present + for i, col := range columnNames { + switch col { + case "time", "time_sec": + timeIndex = i + case "metric": + metricIndex = i + default: + if metricIndex == -1 { + switch columnTypes[i].DatabaseTypeName() { + case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT": + metricIndex = i + } + } + } + } + + if timeIndex == -1 { + return fmt.Errorf("Found no column named time or time_sec") + } fillMissing := query.Model.Get("fill").MustBool(false) var fillInterval float64 @@ -198,53 +223,90 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() fillValue.Valid = true } - } - for ; rows.Next(); rowCount++ { + for rows.Next() { + var timestamp float64 + var value null.Float + var metric string + if rowCount > rowLimit { - return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit) + return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit) } - err := rowData.Update(rows.Rows) + values, err := e.getTypedRowData(rows) if err != nil { - e.log.Error("MySQL response parsing", "error", err) - return fmt.Errorf("MySQL response parsing error %v", err) + return err } - if rowData.metric == "" { - rowData.metric = "Unknown" + switch columnValue := values[timeIndex].(type) { + case int64: + timestamp = float64(columnValue * 1000) + case float64: + timestamp = columnValue * 1000 + case time.Time: + timestamp = float64(columnValue.UnixNano() / 1e6) + default: + return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue) } - if !rowData.time.Valid { - return fmt.Errorf("Found row with no time value") - } - - series, exist := pointsBySeries[rowData.metric] - if exist == false { - series = &tsdb.TimeSeries{Name: rowData.metric} - pointsBySeries[rowData.metric] = series - seriesByQueryOrder.PushBack(rowData.metric) - } - - if fillMissing { - var intervalStart float64 - if exist == false { - intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) + if metricIndex >= 0 { + if columnValue, ok := values[metricIndex].(string); ok == true { + metric = columnValue } else { - intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval - } - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - - for i := intervalStart; i < rowData.time.Float64; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ + return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex]) } } - series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time}) + for i, col := range columnNames { + if i == timeIndex || i == metricIndex { + continue + } + + switch columnValue := values[i].(type) { + case int64: + value = null.FloatFrom(float64(columnValue)) + case float64: + value = null.FloatFrom(columnValue) + case nil: + value.Valid = false + default: + return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue) + } + if metricIndex == -1 { + metric = col + } + + series, exist := pointsBySeries[metric] + if exist == false { + series = &tsdb.TimeSeries{Name: metric} + pointsBySeries[metric] = series + seriesByQueryOrder.PushBack(metric) + } + + if fillMissing { + var intervalStart float64 + if exist == false { + intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) + } else { + intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval + } + + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + + for i := intervalStart; i < timestamp; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } + } + + series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)}) + + e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value) + rowCount++ + + } } for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { @@ -269,62 +331,3 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. result.Meta.Set("rowCount", rowCount) return nil } - -type stringStringScan struct { - rowPtrs []interface{} - rowValues []string - columnNames []string - columnCount int - - time null.Float - value null.Float - metric string -} - -func NewStringStringScan(columnNames []string) *stringStringScan { - s := &stringStringScan{ - columnCount: len(columnNames), - columnNames: columnNames, - rowPtrs: make([]interface{}, len(columnNames)), - rowValues: make([]string, len(columnNames)), - } - - for i := 0; i < s.columnCount; i++ { - s.rowPtrs[i] = new(sql.RawBytes) - } - - return s -} - -func (s *stringStringScan) Update(rows *sql.Rows) error { - if err := rows.Scan(s.rowPtrs...); err != nil { - return err - } - - s.time = null.FloatFromPtr(nil) - s.value = null.FloatFromPtr(nil) - - for i := 0; i < s.columnCount; i++ { - if rb, ok := s.rowPtrs[i].(*sql.RawBytes); ok { - s.rowValues[i] = string(*rb) - - switch s.columnNames[i] { - case "time_sec": - if sec, err := strconv.ParseInt(s.rowValues[i], 10, 64); err == nil { - s.time = null.FloatFrom(float64(sec * 1000)) - } - case "value": - if value, err := strconv.ParseFloat(s.rowValues[i], 64); err == nil { - s.value = null.FloatFrom(value) - } - case "metric": - s.metric = s.rowValues[i] - } - - *rb = nil // reset pointer to discard current value to avoid a bug - } else { - return fmt.Errorf("Cannot convert index %d column %s to type *sql.RawBytes", i, s.columnNames[i]) - } - } - return nil -} diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index fe2c82223d2..668babd5ddf 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -1,6 +1,8 @@ package mysql import ( + "fmt" + "math/rand" "testing" "time" @@ -14,8 +16,12 @@ import ( // To run this test, remove the Skip from SkipConvey // and set up a MySQL db named grafana_tests and a user/password grafana/password +// Use the docker/blocks/mysql_tests/docker-compose.yaml to spin up a +// preconfigured MySQL server suitable for running these tests. +// Thers's also a dashboard.json in same directory that you can import to Grafana +// once you've created a datasource for the test server/database. func TestMySQL(t *testing.T) { - SkipConvey("MySQL", t, func() { + Convey("MySQL", t, func() { x := InitMySQLTestDB(t) endpoint := &MysqlQueryEndpoint{ @@ -29,110 +35,621 @@ func TestMySQL(t *testing.T) { sess := x.NewSession() defer sess.Close() - sql := "CREATE TABLE `mysql_types` (" - sql += "`atinyint` tinyint(1) NOT NULL," - sql += "`avarchar` varchar(3) NOT NULL," - sql += "`achar` char(3)," - sql += "`amediumint` mediumint NOT NULL," - sql += "`asmallint` smallint NOT NULL," - sql += "`abigint` bigint NOT NULL," - sql += "`aint` int(11) NOT NULL," - sql += "`adouble` double(10,2)," - sql += "`anewdecimal` decimal(10,2)," - sql += "`afloat` float(10,2) NOT NULL," - sql += "`atimestamp` timestamp NOT NULL," - sql += "`adatetime` datetime NOT NULL," - sql += "`atime` time NOT NULL," - // sql += "`ayear` year," // Crashes xorm when running cleandb - sql += "`abit` bit(1)," - sql += "`atinytext` tinytext," - sql += "`atinyblob` tinyblob," - sql += "`atext` text," - sql += "`ablob` blob," - sql += "`amediumtext` mediumtext," - sql += "`amediumblob` mediumblob," - sql += "`alongtext` longtext," - sql += "`alongblob` longblob," - sql += "`aenum` enum('val1', 'val2')," - sql += "`aset` set('a', 'b', 'c', 'd')," - sql += "`adate` date," - sql += "`time_sec` datetime(6)," - sql += "`aintnull` int(11)," - sql += "`afloatnull` float(10,2)," - sql += "`avarcharnull` varchar(3)," - sql += "`adecimalnull` decimal(10,2)" - sql += ") ENGINE=InnoDB DEFAULT CHARSET=latin1;" - _, err := sess.Exec(sql) - So(err, ShouldBeNil) + fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.Local) - sql = "INSERT INTO `mysql_types` " - sql += "(`atinyint`, `avarchar`, `achar`, `amediumint`, `asmallint`, `abigint`, `aint`, `adouble`, " - sql += "`anewdecimal`, `afloat`, `adatetime`, `atimestamp`, `atime`, `abit`, `atinytext`, " - sql += "`atinyblob`, `atext`, `ablob`, `amediumtext`, `amediumblob`, `alongtext`, `alongblob`, " - sql += "`aenum`, `aset`, `adate`, `time_sec`) " - sql += "VALUES(1, 'abc', 'def', 1, 10, 100, 1420070400, 1.11, " - sql += "2.22, 3.33, now(), current_timestamp(), '11:11:11', 1, 'tinytext', " - sql += "'tinyblob', 'text', 'blob', 'mediumtext', 'mediumblob', 'longtext', 'longblob', " - sql += "'val2', 'a,b', curdate(), '2018-01-01 00:01:01.123456');" - _, err = sess.Exec(sql) - So(err, ShouldBeNil) - - Convey("Query with Table format should map MySQL column types to Go types", func() { - query := &tsdb.TsdbQuery{ - Queries: []*tsdb.Query{ - { - Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT * FROM mysql_types", - "format": "table", - }), - RefId: "A", - }, - }, + Convey("Given a table with different native data types", func() { + if exists, err := sess.IsTableExist("mysql_types"); err != nil || exists { + So(err, ShouldBeNil) + sess.DropTable("mysql_types") } - resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] + sql := "CREATE TABLE `mysql_types` (" + sql += "`atinyint` tinyint(1) NOT NULL," + sql += "`avarchar` varchar(3) NOT NULL," + sql += "`achar` char(3)," + sql += "`amediumint` mediumint NOT NULL," + sql += "`asmallint` smallint NOT NULL," + sql += "`abigint` bigint NOT NULL," + sql += "`aint` int(11) NOT NULL," + sql += "`adouble` double(10,2)," + sql += "`anewdecimal` decimal(10,2)," + sql += "`afloat` float(10,2) NOT NULL," + sql += "`atimestamp` timestamp NOT NULL," + sql += "`adatetime` datetime NOT NULL," + sql += "`atime` time NOT NULL," + sql += "`ayear` year," // Crashes xorm when running cleandb + sql += "`abit` bit(1)," + sql += "`atinytext` tinytext," + sql += "`atinyblob` tinyblob," + sql += "`atext` text," + sql += "`ablob` blob," + sql += "`amediumtext` mediumtext," + sql += "`amediumblob` mediumblob," + sql += "`alongtext` longtext," + sql += "`alongblob` longblob," + sql += "`aenum` enum('val1', 'val2')," + sql += "`aset` set('a', 'b', 'c', 'd')," + sql += "`adate` date," + sql += "`time_sec` datetime(6)," + sql += "`aintnull` int(11)," + sql += "`afloatnull` float(10,2)," + sql += "`avarcharnull` varchar(3)," + sql += "`adecimalnull` decimal(10,2)" + sql += ") ENGINE=InnoDB DEFAULT CHARSET=latin1;" + _, err := sess.Exec(sql) So(err, ShouldBeNil) - column := queryResult.Tables[0].Rows[0] + sql = "INSERT INTO `mysql_types` " + sql += "(`atinyint`, `avarchar`, `achar`, `amediumint`, `asmallint`, `abigint`, `aint`, `adouble`, " + sql += "`anewdecimal`, `afloat`, `adatetime`, `atimestamp`, `atime`, `ayear`, `abit`, `atinytext`, " + sql += "`atinyblob`, `atext`, `ablob`, `amediumtext`, `amediumblob`, `alongtext`, `alongblob`, " + sql += "`aenum`, `aset`, `adate`, `time_sec`) " + sql += "VALUES(1, 'abc', 'def', 1, 10, 100, 1420070400, 1.11, " + sql += "2.22, 3.33, now(), current_timestamp(), '11:11:11', '2018', 1, 'tinytext', " + sql += "'tinyblob', 'text', 'blob', 'mediumtext', 'mediumblob', 'longtext', 'longblob', " + sql += "'val2', 'a,b', curdate(), '2018-01-01 00:01:01.123456');" + _, err = sess.Exec(sql) + So(err, ShouldBeNil) - So(*column[0].(*int8), ShouldEqual, 1) - So(column[1].(string), ShouldEqual, "abc") - So(column[2].(string), ShouldEqual, "def") - So(*column[3].(*int32), ShouldEqual, 1) - So(*column[4].(*int16), ShouldEqual, 10) - So(*column[5].(*int64), ShouldEqual, 100) - So(*column[6].(*int32), ShouldEqual, 1420070400) - So(column[7].(float64), ShouldEqual, 1.11) - So(column[8].(float64), ShouldEqual, 2.22) - So(*column[9].(*float32), ShouldEqual, 3.33) - _, offset := time.Now().Zone() - So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second)) - So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second)) - So(column[12].(string), ShouldEqual, "11:11:11") - So(*column[13].(*[]byte), ShouldHaveSameTypeAs, []byte{1}) - So(column[14].(string), ShouldEqual, "tinytext") - So(column[15].(string), ShouldEqual, "tinyblob") - So(column[16].(string), ShouldEqual, "text") - So(column[17].(string), ShouldEqual, "blob") - So(column[18].(string), ShouldEqual, "mediumtext") - So(column[19].(string), ShouldEqual, "mediumblob") - So(column[20].(string), ShouldEqual, "longtext") - So(column[21].(string), ShouldEqual, "longblob") - So(column[22].(string), ShouldEqual, "val2") - So(column[23].(string), ShouldEqual, "a,b") - So(column[24].(time.Time).Format("2006-01-02T00:00:00Z"), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z")) - So(column[25].(float64), ShouldEqual, 1514764861) - So(column[26], ShouldEqual, nil) - So(column[27], ShouldEqual, nil) - So(column[28], ShouldEqual, "") - So(column[29], ShouldEqual, nil) + Convey("Query with Table format should map MySQL column types to Go types", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT * FROM mysql_types", + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + column := queryResult.Tables[0].Rows[0] + + So(*column[0].(*int8), ShouldEqual, 1) + So(column[1].(string), ShouldEqual, "abc") + So(column[2].(string), ShouldEqual, "def") + So(*column[3].(*int32), ShouldEqual, 1) + So(*column[4].(*int16), ShouldEqual, 10) + So(*column[5].(*int64), ShouldEqual, 100) + So(*column[6].(*int32), ShouldEqual, 1420070400) + So(column[7].(float64), ShouldEqual, 1.11) + So(column[8].(float64), ShouldEqual, 2.22) + So(*column[9].(*float32), ShouldEqual, 3.33) + _, offset := time.Now().Zone() + So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second)) + So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second)) + So(column[12].(string), ShouldEqual, "11:11:11") + So(column[13].(int64), ShouldEqual, 2018) + So(*column[14].(*[]byte), ShouldHaveSameTypeAs, []byte{1}) + So(column[15].(string), ShouldEqual, "tinytext") + So(column[16].(string), ShouldEqual, "tinyblob") + So(column[17].(string), ShouldEqual, "text") + So(column[18].(string), ShouldEqual, "blob") + So(column[19].(string), ShouldEqual, "mediumtext") + So(column[20].(string), ShouldEqual, "mediumblob") + So(column[21].(string), ShouldEqual, "longtext") + So(column[22].(string), ShouldEqual, "longblob") + So(column[23].(string), ShouldEqual, "val2") + So(column[24].(string), ShouldEqual, "a,b") + So(column[25].(time.Time).Format("2006-01-02T00:00:00Z"), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z")) + So(column[26].(float64), ShouldEqual, float64(1514764861000)) + So(column[27], ShouldEqual, nil) + So(column[28], ShouldEqual, nil) + So(column[29], ShouldEqual, "") + So(column[30], ShouldEqual, nil) + }) + }) + + Convey("Given a table with metrics that lacks data for some series ", func() { + type metric struct { + Time time.Time + Value int64 + } + + if exist, err := sess.IsTableExist(metric{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(metric{}) + } + err := sess.CreateTable(metric{}) + So(err, ShouldBeNil) + + series := []*metric{} + firstRange := genTimeRangeByInterval(fromStart, 10*time.Minute, 10*time.Second) + secondRange := genTimeRangeByInterval(fromStart.Add(20*time.Minute), 10*time.Minute, 10*time.Second) + + for _, t := range firstRange { + series = append(series, &metric{ + Time: t, + Value: 15, + }) + } + + for _, t := range secondRange { + series = append(series, &metric{ + Time: t, + Value: 20, + }) + } + + for _, s := range series { + _, err = sess.Insert(s) + So(err, ShouldBeNil) + } + + Convey("When doing a metric query using timeGroup", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m') as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(len(points), ShouldEqual, 6) + + dt := fromStart + + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 3; i < 6; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + }) + + Convey("When doing a metric query using timeGroup with NULL fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(len(points), ShouldEqual, 7) + + dt := fromStart + + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + + So(points[3][0].Valid, ShouldBeFalse) + + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 4; i < 7; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + }) + + Convey("When doing a metric query using timeGroup with float fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', 1.5) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(points[3][0].Float64, ShouldEqual, 1.5) + }) + }) + + Convey("Given a table with metrics having multiple values and measurements", func() { + type metric_values struct { + Time time.Time + Measurement string + ValueOne int64 `xorm:"integer 'valueOne'"` + ValueTwo int64 `xorm:"integer 'valueTwo'"` + } + + if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(metric_values{}) + } + err := sess.CreateTable(metric_values{}) + So(err, ShouldBeNil) + + rand.Seed(time.Now().Unix()) + rnd := func(min, max int64) int64 { + return rand.Int63n(max-min) + min + } + + series := []*metric_values{} + for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) { + series = append(series, &metric_values{ + Time: t, + Measurement: "Metric A", + ValueOne: rnd(0, 100), + ValueTwo: rnd(0, 100), + }) + series = append(series, &metric_values{ + Time: t, + Measurement: "Metric B", + ValueOne: rnd(0, 100), + ValueTwo: rnd(0, 100), + }) + } + + for _, s := range series { + _, err := sess.Insert(s) + So(err, ShouldBeNil) + } + + Convey("When doing a metric query grouping by time and select metric column should return correct series", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values ORDER BY 1`, + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 2) + So(queryResult.Series[0].Name, ShouldEqual, "Metric B - value one") + So(queryResult.Series[1].Name, ShouldEqual, "Metric A - value one") + }) + + Convey("When doing a metric query grouping by time should return correct series", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT $__time(time), valueOne, valueTwo FROM metric_values ORDER BY 1`, + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 2) + So(queryResult.Series[0].Name, ShouldEqual, "valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "valueTwo") + }) + }) + + Convey("Given a table with event data", func() { + type event struct { + TimeSec int64 + Description string + Tags string + } + + if exist, err := sess.IsTableExist(event{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(event{}) + } + err := sess.CreateTable(event{}) + So(err, ShouldBeNil) + + events := []*event{} + for _, t := range genTimeRangeByInterval(fromStart.Add(-20*time.Minute), 60*time.Minute, 25*time.Minute) { + events = append(events, &event{ + TimeSec: t.Unix(), + Description: "Someone deployed something", + Tags: "deploy", + }) + events = append(events, &event{ + TimeSec: t.Add(5 * time.Minute).Unix(), + Description: "New support ticket registered", + Tags: "ticket", + }) + } + + for _, e := range events { + _, err = sess.Insert(e) + So(err, ShouldBeNil) + } + + Convey("When doing an annotation query of deploy events should return expected result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT time_sec, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC`, + "format": "table", + }), + RefId: "Deploys", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Deploys"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + }) + + Convey("When doing an annotation query of ticket events should return expected result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT time_sec, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC`, + "format": "table", + }), + RefId: "Tickets", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Tickets"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + }) + + Convey("When doing an annotation query with a time column in datetime format", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 0, time.UTC) + dtFormat := "2006-01-02 15:04:05.999999999" + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + CAST('%s' as datetime) as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Format(dtFormat)), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch second format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, dt.Unix()*1000) + }) + + Convey("When doing an annotation query with a time column in epoch second format (signed integer) should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 0, time.Local) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + CAST('%d' as signed integer) as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()*1000), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, dt.Unix()*1000) + }) + + Convey("When doing an annotation query with a time column holding a unsigned integer null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as unsigned integer) as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) + }) + + Convey("When doing an annotation query with a time column holding a DATETIME null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as DATETIME) as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) + }) }) }) } func InitMySQLTestDB(t *testing.T) *xorm.Engine { x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr+"&parseTime=true") + x.DatabaseTZ = time.Local + x.TZLocation = time.Local // x.ShowSQL() @@ -140,7 +657,18 @@ func InitMySQLTestDB(t *testing.T) *xorm.Engine { t.Fatalf("Failed to init mysql db %v", err) } - sqlutil.CleanDB(x) - return x } + +func genTimeRangeByInterval(from time.Time, duration time.Duration, interval time.Duration) []time.Time { + durationSec := int64(duration.Seconds()) + intervalSec := int64(interval.Seconds()) + timeRange := []time.Time{} + + for i := int64(0); i < durationSec; i += intervalSec { + timeRange = append(timeRange, from) + from = from.Add(time.Duration(int64(time.Second) * intervalSec)) + } + + return timeRange +} diff --git a/public/app/plugins/datasource/mysql/partials/annotations.editor.html b/public/app/plugins/datasource/mysql/partials/annotations.editor.html index b34eff5b011..d142e091fed 100644 --- a/public/app/plugins/datasource/mysql/partials/annotations.editor.html +++ b/public/app/plugins/datasource/mysql/partials/annotations.editor.html @@ -18,15 +18,16 @@
Annotation Query Format
-An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. +An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the time or time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. -- column with alias: time_sec for the annotation event. Format is UTC in seconds, use UNIX_TIMESTAMP(column) +- column with alias: time or time_sec for the annotation event time. Use epoch time or any native date data type. - column with alias: text for the annotation text - column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' Macros: -- $__time(column) -> UNIX_TIMESTAMP(column) as time_sec +- $__time(column) -> UNIX_TIMESTAMP(column) as time (or as time_sec) +- $__timeEpoch(column) -> UNIX_TIMESTAMP(column) as time (or as time_sec) - $__timeFilter(column) -> UNIX_TIMESTAMP(time_date_time) > 1492750877 AND UNIX_TIMESTAMP(time_date_time) < 1492750877 - $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index 22d64c9190f..9acf32405c1 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -38,15 +38,16 @@
Time series:
-- return column named time_sec (UTC in seconds), use UNIX_TIMESTAMP(column)
-- return column named value for the time point value
-- return column named metric to represent the series name
+- return column named time or time_sec (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.
+- return column(s) with numeric datatype as values
+- (Optional: return column named metric to represent the series name. If no column named metric is found the column name of the value column is used as series name)
 
 Table:
 - return any set of columns
 
 Macros:
 - $__time(column) -> UNIX_TIMESTAMP(column) as time_sec
+- $__timeEpoch(column) -> UNIX_TIMESTAMP(column) as time_sec
 - $__timeFilter(column) ->  UNIX_TIMESTAMP(time_date_time) ≥ 1492750877 AND UNIX_TIMESTAMP(time_date_time) ≤ 1492750877
 - $__unixEpochFilter(column) ->  time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877
 - $__timeGroup(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed)
diff --git a/public/app/plugins/datasource/mysql/response_parser.ts b/public/app/plugins/datasource/mysql/response_parser.ts
index 50683578035..e5d8ab79f2a 100644
--- a/public/app/plugins/datasource/mysql/response_parser.ts
+++ b/public/app/plugins/datasource/mysql/response_parser.ts
@@ -113,7 +113,7 @@ export default class ResponseParser {
     let tagsColumnIndex = -1;
 
     for (let i = 0; i < table.columns.length; i++) {
-      if (table.columns[i].text === 'time_sec') {
+      if (table.columns[i].text === 'time_sec' || table.columns[i].text === 'time') {
         timeColumnIndex = i;
       } else if (table.columns[i].text === 'title') {
         return this.$q.reject({
@@ -137,7 +137,7 @@ export default class ResponseParser {
       const row = table.rows[i];
       list.push({
         annotation: options.annotation,
-        time: Math.floor(row[timeColumnIndex]) * 1000,
+        time: Math.floor(row[timeColumnIndex]),
         text: row[textColumnIndex] ? row[textColumnIndex].toString() : '',
         tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [],
       });

From bd4ecaeac6e6737ac44043c90ba9a574cfea433f Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson 
Date: Thu, 22 Mar 2018 15:46:40 +0100
Subject: [PATCH 62/78] mssql: update query editor help

---
 .../plugins/datasource/mssql/partials/query.editor.html   | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html
index f8c7effb827..c7dc030be6e 100644
--- a/public/app/plugins/datasource/mssql/partials/query.editor.html
+++ b/public/app/plugins/datasource/mssql/partials/query.editor.html
@@ -53,6 +53,14 @@ Macros:
 - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
 - $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a fillValue of NULL or floating value will automatically fill empty series in timerange with that value.
 
+Example of group by and order by with $__timeGroup:
+SELECT
+  $__timeGroup(date_time_col, '1h') AS time,
+  sum(value) as value
+FROM yourtable
+GROUP BY $__timeGroup(date_time_col, '1h')
+ORDER BY 1
+
 Or build your own conditionals using these macros which just return the values:
 - $__timeFrom() -> DATEADD(second, 1492750877, '1970-01-01')
 - $__timeTo() -> DATEADD(second, 1492750877, '1970-01-01')

From e722732021bcd243411df2e6a982b7acae9c319d Mon Sep 17 00:00:00 2001
From: Gerben Meijer 
Date: Thu, 22 Mar 2018 16:27:24 +0100
Subject: [PATCH 63/78] Return actual user ID in UserProfileDTO

This fixes calls to /api/user where ID is currently always 0
---
 pkg/services/sqlstore/user.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go
index 73ea07f031f..f42ff5fb2ed 100644
--- a/pkg/services/sqlstore/user.go
+++ b/pkg/services/sqlstore/user.go
@@ -315,6 +315,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
 	}
 
 	query.Result = m.UserProfileDTO{
+		Id:             user.Id,
 		Name:           user.Name,
 		Email:          user.Email,
 		Login:          user.Login,

From f0f41c2a8ed87d9c07a4b178388cd2893beb0b9c Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson 
Date: Thu, 22 Mar 2018 16:44:55 +0100
Subject: [PATCH 64/78] mysql: skip tests by default

---
 pkg/tsdb/mysql/mysql_test.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go
index 668babd5ddf..750704c9965 100644
--- a/pkg/tsdb/mysql/mysql_test.go
+++ b/pkg/tsdb/mysql/mysql_test.go
@@ -21,7 +21,7 @@ import (
 // Thers's also a dashboard.json in same directory that you can import to Grafana
 // once you've created a datasource for the test server/database.
 func TestMySQL(t *testing.T) {
-	Convey("MySQL", t, func() {
+	SkipConvey("MySQL", t, func() {
 		x := InitMySQLTestDB(t)
 
 		endpoint := &MysqlQueryEndpoint{

From 823f9030488166f2036873d9567387f9a14777c3 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll 
Date: Thu, 22 Mar 2018 16:59:06 +0100
Subject: [PATCH 65/78] removed trash can icon from save buttons

---
 public/app/containers/ManageDashboards/FolderSettings.tsx   | 2 +-
 public/app/features/dashboard/partials/folder_settings.html | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx
index 586a8f05b4c..ff4e9cb417f 100644
--- a/public/app/containers/ManageDashboards/FolderSettings.tsx
+++ b/public/app/containers/ManageDashboards/FolderSettings.tsx
@@ -143,7 +143,7 @@ export class FolderSettings extends React.Component {
                   className="btn btn-success"
                   disabled={!folder.folder.canSave || !folder.folder.hasChanged}
                 >
-                   Save
+                  Save