Unverified 提交 0bed9d12 authored 作者: Evgenij Ryazanov's avatar Evgenij Ryazanov 提交者: GitHub

Merge pull request #1667 from katzyn/window

Detect NULL values and overflow in window frame bounds
......@@ -5559,6 +5559,7 @@ OVER windowNameOrSpecification
","
Returns the ratio of a value to the sum of all values.
If argument is NULL or sum of all values is 0, then the value of function is NULL.
Window frame clause is not allowed for this function.
Window functions are currently experimental in H2 and should be used with caution.
They also may require a lot of memory for large queries.
......
......@@ -21,6 +21,10 @@ Change Log
<h2>Next Version (unreleased)</h2>
<ul>
<li>Issue #1604: TestCrashAPI: PreparedStatement.getGeneratedKeys() is already closed
</li>
<li>PR #1667: Detect NULL values and overflow in window frame bounds
</li>
<li>PR #1664: Allow any expressions in window frames
</li>
<li>Issue #1576: H2 Console should not display precision and scale for data types that don't have them
......
......@@ -8,6 +8,7 @@
22004=Numeric value out of range: {0} in column {1}
22007=Cannot parse {0} constant {1}
22012=Division by zero: {0}
22013=Invalid PRECEDING or FOLLOWING size in window function: {0}
22018=Data conversion error converting {0}
22025=Error in LIKE ESCAPE: {0}
22030=Value not permitted for column {0}: {1}
......
......@@ -133,6 +133,15 @@ public class ErrorCode {
*/
public static final int DIVISION_BY_ZERO_1 = 22012;
/**
* The error with code <code>22013</code> is thrown when preceding or
* following size in a window function is null or negative. Example:
* <pre>
* FIRST_VALUE(N) OVER(ORDER BY N ROWS -1 PRECEDING)
* </pre>
*/
public static final int INVALID_PRECEDING_OR_FOLLOWING_1 = 22013;
/**
* The error with code <code>22018</code> is thrown when
* trying to convert a value to a data type where the conversion is
......
......@@ -162,26 +162,43 @@ public abstract class DataAnalysisOperation extends Expression {
WindowFrame frame = over.getWindowFrame();
if (frame != null) {
int index = getNumExpressions();
int orderBySize = 0;
if (orderBy != null) {
index += orderBy.size();
orderBySize = orderBy.size();
index += orderBySize;
}
int n = 0;
WindowFrameBound bound = frame.getStarting();
if (bound.isParameterized()) {
if (orderBySize != 1) {
throw getSingleSortKeyException();
}
if (bound.isVariable()) {
bound.setExpressionIndex(index);
n++;
}
}
bound = frame.getFollowing();
if (bound != null && bound.isVariable()) {
if (bound != null && bound.isParameterized()) {
if (orderBySize != 1) {
throw getSingleSortKeyException();
}
if (bound.isVariable()) {
bound.setExpressionIndex(index + n);
n++;
}
}
numFrameExpressions = n;
}
}
return this;
}
private DbException getSingleSortKeyException() {
String sql = getSQL();
return DbException.getSyntaxError(sql, sql.length() - 1, "exactly one sort key is required for RANGE units");
}
@Override
public void setEvaluatable(TableFilter tableFilter, boolean b) {
if (over != null) {
......
......@@ -10,15 +10,17 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import org.h2.api.ErrorCode;
import org.h2.engine.Session;
import org.h2.expression.BinaryOperation;
import org.h2.expression.Expression;
import org.h2.expression.BinaryOperation.OpType;
import org.h2.expression.Expression;
import org.h2.expression.ValueExpression;
import org.h2.message.DbException;
import org.h2.result.SortOrder;
import org.h2.table.ColumnResolver;
import org.h2.value.Value;
import org.h2.value.ValueNull;
/**
* Window frame clause.
......@@ -290,29 +292,94 @@ public final class WindowFrame {
private static int getIntOffset(WindowFrameBound bound, Value[] values, Session session) {
Value v = bound.isVariable() ? values[bound.getExpressionIndex()] : bound.getValue().getValue(session);
int value = v.getInt();
if (value < 0) {
throw DbException.getInvalidValueException("unsigned", value);
if (v == ValueNull.INSTANCE || value < 0) {
throw DbException.get(ErrorCode.INVALID_PRECEDING_OR_FOLLOWING_1, v.getTraceSQL());
}
return value;
}
/**
* Appends bound value to the current row and produces row for comparison
* operations.
*
* @param session
* the session
* @param orderedRows
* rows in partition
* @param sortOrder
* the sort order
* @param currentRow
* index of the current row
* @param bound
* window frame bound
* @param add
* false for PRECEDING, true for FOLLOWING
* @return row for comparison operations, or null if result is out of range
* and should be treated as UNLIMITED
*/
private static Value[] getCompareRow(Session session, ArrayList<Value[]> orderedRows, SortOrder sortOrder,
int currentRow, WindowFrameBound bound, boolean add) {
int sortIndex = sortOrder.getQueryColumnIndexes()[0];
OpType opType = add ^ (sortOrder.getSortTypes()[0] & SortOrder.DESCENDING) != 0 ? OpType.PLUS : OpType.MINUS;
Value[] row = orderedRows.get(currentRow);
Value currentValue = row[sortIndex];
int type = currentValue.getType();
Value newValue;
Value range = getValueOffset(bound, orderedRows.get(currentRow), session);
switch (type) {
case Value.NULL:
newValue = ValueNull.INSTANCE;
break;
case Value.BYTE:
case Value.SHORT:
case Value.INT:
case Value.LONG:
case Value.DECIMAL:
case Value.DOUBLE:
case Value.FLOAT:
case Value.TIME:
case Value.DATE:
case Value.TIMESTAMP:
case Value.TIMESTAMP_TZ:
case Value.INTERVAL_YEAR:
case Value.INTERVAL_MONTH:
case Value.INTERVAL_DAY:
case Value.INTERVAL_HOUR:
case Value.INTERVAL_MINUTE:
case Value.INTERVAL_SECOND:
case Value.INTERVAL_YEAR_TO_MONTH:
case Value.INTERVAL_DAY_TO_HOUR:
case Value.INTERVAL_DAY_TO_MINUTE:
case Value.INTERVAL_DAY_TO_SECOND:
case Value.INTERVAL_HOUR_TO_MINUTE:
case Value.INTERVAL_HOUR_TO_SECOND:
case Value.INTERVAL_MINUTE_TO_SECOND:
OpType opType = add ^ (sortOrder.getSortTypes()[0] & SortOrder.DESCENDING) != 0 ? OpType.PLUS
: OpType.MINUS;
try {
newValue = new BinaryOperation(opType, ValueExpression.get(currentValue), ValueExpression.get(range))
.optimize(session).getValue(session).convertTo(type);
} catch (DbException ex) {
switch (ex.getErrorCode()) {
case ErrorCode.NUMERIC_VALUE_OUT_OF_RANGE_1:
case ErrorCode.NUMERIC_VALUE_OUT_OF_RANGE_2:
return null;
}
throw ex;
}
break;
default:
throw DbException.getInvalidValueException("unsupported type of sort key for RANGE units",
currentValue.getTraceSQL());
}
Value[] newRow = row.clone();
newRow[sortIndex] = new BinaryOperation(opType, //
ValueExpression.get(row[sortIndex]),
ValueExpression.get(getValueOffset(bound, orderedRows.get(currentRow), session))) //
.optimize(session).getValue(session);
newRow[sortIndex] = newValue;
return newRow;
}
private static Value getValueOffset(WindowFrameBound bound, Value[] values, Session session) {
Value value = bound.isVariable() ? values[bound.getExpressionIndex()] : bound.getValue().getValue(session);
if (value.getSignum() < 0) {
throw DbException.getInvalidValueException("unsigned", value.getTraceSQL());
if (value == ValueNull.INSTANCE || value.getSignum() < 0) {
throw DbException.get(ErrorCode.INVALID_PRECEDING_OR_FOLLOWING_1, value.getTraceSQL());
}
return value;
}
......@@ -544,6 +611,25 @@ public final class WindowFrame {
return endIndex;
}
/**
* Returns starting or ending index of a window frame.
*
* @param session
* the session
* @param orderedRows
* rows in partition
* @param sortOrder
* the sort order
* @param currentRow
* index of the current row
* @param bound
* window frame bound
* @param forFollowing
* false for start index, true for end index
* @return starting or ending index of a window frame (inclusive), can be 0
* or be equal to the number of rows if frame is not limited from
* that side
*/
private int getIndex(Session session, ArrayList<Value[]> orderedRows, SortOrder sortOrder, int currentRow,
WindowFrameBound bound, boolean forFollowing) {
int size = orderedRows.size();
......@@ -587,6 +673,7 @@ public final class WindowFrame {
case RANGE: {
index = currentRow;
Value[] row = getCompareRow(session, orderedRows, sortOrder, index, bound, false);
if (row != null) {
index = Collections.binarySearch(orderedRows, row, sortOrder);
if (index >= 0) {
if (!forFollowing) {
......@@ -608,6 +695,9 @@ public final class WindowFrame {
index--;
}
}
} else {
index = -1;
}
break;
}
default:
......@@ -663,6 +753,7 @@ public final class WindowFrame {
case RANGE: {
index = currentRow;
Value[] row = getCompareRow(session, orderedRows, sortOrder, index, bound, true);
if (row != null) {
index = Collections.binarySearch(orderedRows, row, sortOrder);
if (index >= 0) {
if (forFollowing) {
......@@ -682,6 +773,9 @@ public final class WindowFrame {
}
}
}
} else {
index = size;
}
break;
}
default:
......
......@@ -57,6 +57,15 @@ public class WindowFrameBound {
return value;
}
/**
* Returns whether bound is defined as n PRECEDING or n FOLLOWING.
*
* @return whether bound is defined as n PRECEDING or n FOLLOWING
*/
public boolean isParameterized() {
return type == WindowFrameBoundType.PRECEDING || type == WindowFrameBoundType.FOLLOWING;
}
/**
* Returns whether bound is defined with a variable. This method may be used
* only after {@link #optimize(Session)} invocation.
......
......@@ -1309,12 +1309,17 @@ public class JdbcPreparedStatement extends JdbcStatement implements
@Override
public ResultSet getGeneratedKeys() throws SQLException {
if (batchIdentities != null) {
try {
int id = getNextId(TraceObject.RESULT_SET);
if (isDebugEnabled()) {
debugCodeAssign("ResultSet", TraceObject.RESULT_SET, id, "getGeneratedKeys()");
}
checkClosed();
generatedKeys = new JdbcResultSet(conn, this, null, batchIdentities.getResult(), id, false, true, false);
generatedKeys = new JdbcResultSet(conn, this, null, batchIdentities.getResult(), id, false, true,
false);
} catch (Exception e) {
throw logAndConvert(e);
}
}
return super.getGeneratedKeys();
}
......
......@@ -8,6 +8,7 @@
22004=#Numeric value out of range: {0} in column {1}
22007=Nelze zpracovat konstantu {0} {1}
22012=Dělení nulou: {0}
22013=#Invalid PRECEDING or FOLLOWING size in window function: {0}
22018=Chyba při převodu dat {0}
22025=Chyba v LIKE escapování: {0}
22030=#Value not permitted for column {0}: {1}
......
......@@ -8,6 +8,7 @@
22004=Numerischer Wert ausserhalb des Bereichs: {0} in Feld {1}
22007=Kann {0} {1} nicht umwandeln
22012=Division durch 0: {0}
22013=#Invalid PRECEDING or FOLLOWING size in window function: {0}
22018=Datenumwandlungsfehler beim Umwandeln von {0}
22025=Fehler in LIKE ESCAPE: {0}
22030=Wert nicht erlaubt für Feld {0}: {1}
......
......@@ -8,6 +8,7 @@
22004=Numeric value out of range: {0} in column {1}
22007=Cannot parse {0} constant {1}
22012=Division by zero: {0}
22013=Invalid PRECEDING or FOLLOWING size in window function: {0}
22018=Data conversion error converting {0}
22025=Error in LIKE ESCAPE: {0}
22030=Value not permitted for column {0}: {1}
......
......@@ -8,6 +8,7 @@
22004=#Numeric value out of range: {0} in column {1}
22007=Imposible interpretar la constante {0} {1}
22012=División por cero: {0}
22013=#Invalid PRECEDING or FOLLOWING size in window function: {0}
22018=Conversión de datos fallida, convirtiendo {0}
22025=Error en LIKE ESCAPE: {0}
22030=Valor no permitido para la columna {0}: {1}
......
......@@ -8,6 +8,7 @@
22004=#Numeric value out of range: {0} in column {1}
22007=Impossible d''analyser {0} constante {1}
22012=Division par zéro: {0}
22013=#Invalid PRECEDING or FOLLOWING size in window function: {0}
22018=Erreur lors de la conversion de données {0}
22025=Erreur dans LIKE ESCAPE: {0}
22030=Valeur non permise pour la colonne {0}: {1}
......
......@@ -8,6 +8,7 @@
22004=#Numeric value out of range: {0} in column {1}
22007={0} 定数 {1} を解析できません
22012=ゼロで除算しました: {0}
22013=#Invalid PRECEDING or FOLLOWING size in window function: {0}
22018=データ変換中にエラーが発生しました {0}
22025=LIKE ESCAPE にエラーがあります: {0}
22030=#Value not permitted for column {0}: {1}
......
......@@ -8,6 +8,7 @@
22004=#Numeric value out of range: {0} in column {1}
22007=Nie można odczytać {0} jako {1}
22012=Dzielenie przez zero: {0}
22013=#Invalid PRECEDING or FOLLOWING size in window function: {0}
22018=Błąd konwersji danych {0}
22025=Błąd w LIKE ESCAPE: {0}
22030=#Value not permitted for column {0}: {1}
......
......@@ -8,6 +8,7 @@
22004=#Numeric value out of range: {0} in column {1}
22007=Não é possível converter {1} para {0}
22012=Divisão por zero: {0}
22013=#Invalid PRECEDING or FOLLOWING size in window function: {0}
22018=Erro na conversão de dado, convertendo {0}
22025=Erro em LIKE ESCAPE: {0}
22030=#Value not permitted for column {0}: {1}
......
......@@ -8,6 +8,7 @@
22004=Численное значение вне допустимого диапазона: {0} в столбце {1}
22007=Невозможно преобразование строки {1} в тип {0}
22012=Деление на ноль: {0}
22013=Недопустимое значение PRECEDING или FOLLOWING в оконной функции: {0}
22018=Ошибка преобразования данных при конвертации {0}
22025=Ошибка в LIKE ESCAPE: {0}
22030=Недопустимое значение для столбца {0}: {1}
......
......@@ -8,6 +8,7 @@
22004=#Numeric value out of range: {0} in column {1}
22007=Nemožem rozobrať {0} konštantu {1}
22012=Delenie nulou: {0}
22013=#Invalid PRECEDING or FOLLOWING size in window function: {0}
22018=Chyba konverzie dát pre {0}
22025=Chyba v LIKE ESCAPE: {0}
22030=#Value not permitted for column {0}: {1}
......
......@@ -8,6 +8,7 @@
22004=#Numeric value out of range: {0} in column {1}
22007=不能解析字段 {0} 的数值 :{1}
22012=除数为零: {0}
22013=#Invalid PRECEDING or FOLLOWING size in window function: {0}
22018=转换数据{0}期间出现转换错误
22025=LIKE ESCAPE(转义符)存在错误: {0}
22030=#Value not permitted for column {0}: {1}
......
......@@ -345,7 +345,7 @@ SELECT *,
> rows: 8
SELECT *, ARRAY_AGG(ID) OVER (ORDER BY VALUE ROWS -1 PRECEDING) FROM TEST;
> exception INVALID_VALUE_2
> exception INVALID_PRECEDING_OR_FOLLOWING_1
SELECT *, ARRAY_AGG(ID) OVER (ORDER BY ID ROWS BETWEEN 2 PRECEDING AND 1 PRECEDING) FROM TEST FETCH FIRST 4 ROWS ONLY;
> ID VALUE ARRAY_AGG(ID) OVER (ORDER BY ID ROWS BETWEEN 2 PRECEDING AND 1 PRECEDING)
......
......@@ -109,5 +109,121 @@ SELECT ROW_NUMBER() OVER (ORDER BY CATEGORY), SUM(ID) FROM TEST GROUP BY CATEGOR
> 1 12
> rows: 1
SELECT SUM(ID) OVER (ORDER BY ID ROWS NULL PRECEDING) P FROM TEST;
> exception INVALID_PRECEDING_OR_FOLLOWING_1
SELECT SUM(ID) OVER (ORDER BY ID RANGE NULL PRECEDING) P FROM TEST;
> exception INVALID_PRECEDING_OR_FOLLOWING_1
SELECT ARRAY_AGG(ID) OVER (ORDER BY V NULLS FIRST RANGE BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) A,
ID, V FROM VALUES (1, 1), (2, NULL), (3, 2) T(ID, V) ORDER BY V NULLS FIRST;
> A ID V
> --------- -- ----
> [3, 1, 2] 2 null
> [3, 1] 1 1
> [3, 1] 3 2
> rows (ordered): 3
SELECT ARRAY_AGG(ID) OVER (ORDER BY V NULLS LAST RANGE BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) A,
ID, V FROM VALUES (1, 1), (2, NULL), (3, 2) T(ID, V) ORDER BY V NULLS LAST;
> A ID V
> --------- -- ----
> [2, 3, 1] 1 1
> [2, 3, 1] 3 2
> [2] 2 null
> rows (ordered): 3
SELECT ARRAY_AGG(ID) OVER (ORDER BY V NULLS FIRST RANGE BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) A,
ID, V FROM VALUES (1, 1), (2, NULL), (3, 2) T(ID, V) ORDER BY V NULLS FIRST;
> A ID V
> --------- -- ----
> [3, 1, 2] 2 null
> [3] 1 1
> null 3 2
> rows (ordered): 3
SELECT ARRAY_AGG(ID) OVER (ORDER BY V NULLS LAST RANGE BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) A,
ID, V FROM VALUES (1, 1), (2, NULL), (3, 2) T(ID, V) ORDER BY V NULLS LAST;
> A ID V
> ------ -- ----
> [2, 3] 1 1
> [2] 3 2
> [2] 2 null
> rows (ordered): 3
SELECT ARRAY_AGG(ID) OVER (ORDER BY V NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) A,
ID, V FROM VALUES (1, 1), (2, NULL), (3, 2) T(ID, V) ORDER BY V NULLS FIRST;
> A ID V
> --------- -- ----
> [2] 2 null
> [2, 1, 3] 1 1
> [2, 1, 3] 3 2
> rows (ordered): 3
SELECT ARRAY_AGG(ID) OVER (ORDER BY V NULLS LAST RANGE BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) A,
ID, V FROM VALUES (1, 1), (2, NULL), (3, 2) T(ID, V) ORDER BY V NULLS LAST;
> A ID V
> --------- -- ----
> [1, 3] 1 1
> [1, 3] 3 2
> [1, 3, 2] 2 null
> rows (ordered): 3
SELECT ARRAY_AGG(ID) OVER (ORDER BY V NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) A,
ID, V FROM VALUES (1, 1), (2, NULL), (3, 2) T(ID, V) ORDER BY V NULLS FIRST;
> A ID V
> ------ -- ----
> [2] 2 null
> [2] 1 1
> [2, 1] 3 2
> rows (ordered): 3
SELECT ARRAY_AGG(ID) OVER (ORDER BY V NULLS LAST RANGE BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) A,
ID, V FROM VALUES (1, 1), (2, NULL), (3, 2) T(ID, V) ORDER BY V NULLS LAST;
> A ID V
> --------- -- ----
> null 1 1
> [1] 3 2
> [1, 3, 2] 2 null
> rows (ordered): 3
SELECT SUM(V) OVER (ORDER BY V RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM VALUES (TRUE) T(V);
> exception INVALID_VALUE_2
SELECT
SUM(ID) OVER (ORDER BY ID RANGE BETWEEN 10000000000 PRECEDING AND CURRENT ROW) P,
SUM(ID) OVER (ORDER BY ID RANGE BETWEEN 10000000001 PRECEDING AND 10000000000 PRECEDING) P2,
SUM(ID) OVER (ORDER BY ID RANGE BETWEEN CURRENT ROW AND 2147483647 FOLLOWING) F,
SUM(ID) OVER (ORDER BY ID RANGE BETWEEN 2147483647 FOLLOWING AND 2147483648 FOLLOWING) F2,
ID FROM TEST ORDER BY ID;
> P P2 F F2 ID
> -- ---- -- ---- --
> 1 null 63 null 1
> 3 null 62 null 2
> 7 null 60 null 4
> 15 null 56 null 8
> 31 null 48 null 16
> 63 null 32 null 32
> rows (ordered): 6
DROP TABLE TEST;
> ok
SELECT
ARRAY_AGG(T) OVER (ORDER BY T RANGE BETWEEN INTERVAL 1 DAY PRECEDING AND CURRENT ROW) C,
ARRAY_AGG(T) OVER (ORDER BY T RANGE BETWEEN INTERVAL 2 HOUR PRECEDING AND INTERVAL 1 HOUR PRECEDING) P,
T FROM VALUES (TIME '00:00:00'), (TIME '01:30:00') TEST(T) ORDER BY T;
> C P T
> -------------------- ---------- --------
> [00:00:00] null 00:00:00
> [00:00:00, 01:30:00] [00:00:00] 01:30:00
> rows (ordered): 2
SELECT SUM(A) OVER (ORDER BY A, B RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) S FROM VALUES (1, 2) T(A, B);
>> 1
SELECT SUM(A) OVER (ORDER BY A, B RANGE BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) S FROM VALUES (1, 2) T(A, B);
> exception SYNTAX_ERROR_2
SELECT SUM(A) OVER (ORDER BY A, B RANGE BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) S FROM VALUES (1, 2) T(A, B);
> exception SYNTAX_ERROR_2
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论