3566 Alexander Barkov 2011-11-18
WL#946: Bug#13399082 ABARKOV ASSERTION `MON > 0 && MON < 13' FAILED IN TZTIME.CC:794 | SEC_SINCE_EPOCH
modified:
mysql-test/r/timezone2.result
mysql-test/r/type_temporal_fractional.result
mysql-test/t/timezone2.test
mysql-test/t/type_temporal_fractional.test
sql/field.cc
sql/field.h
sql/item.cc
sql/item.h
sql/item_timefunc.cc
sql/sql_time.cc
sql/sql_time.h
3565 Alexander Barkov 2011-11-17
WL#946: Bug#13401354 ITEM->FIELD_TYPE() == MYSQL_TYPE_TIME || ITEM->FIELD_TYPE() == MYSQL_TYPE_NULL
modified:
mysql-test/r/type_temporal_fractional.result
mysql-test/t/type_temporal_fractional.test
sql/item_cmpfunc.cc
=== modified file 'mysql-test/r/timezone2.result'
--- a/mysql-test/r/timezone2.result 2010-08-06 19:29:37 +0000
+++ b/mysql-test/r/timezone2.result 2011-11-18 09:52:00 +0000
@@ -309,3 +309,33 @@ CONVERT_TZ(1, 1, a)
NULL
DROP TABLE t1;
End of 5.1 tests
+#
+# Start of 5.6 tests
+#
+SET time_zone='Europe/Moscow';
+CREATE TABLE t1 (a TIMESTAMP, b VARCHAR(30));
+INSERT INTO t1 VALUES
+('2003-03-30 01:59:59', 'Before the gap'),
+('2003-03-30 02:30:00', 'Inside the gap'),
+('2003-03-30 03:00:00', 'After the gap');
+Warnings:
+Warning 1299 Invalid TIMESTAMP value in column 'a' at row 2
+SELECT a, UNIX_TIMESTAMP(a), b FROM t1;
+a UNIX_TIMESTAMP(a) b
+2003-03-30 01:59:59 1048978799 Before the gap
+2003-03-30 03:00:00 1048978800 Inside the gap
+2003-03-30 03:00:00 1048978800 After the gap
+DROP TABLE t1;
+SELECT UNIX_TIMESTAMP('2003-03-30 01:59:59'), 'Before the gap' AS b;
+UNIX_TIMESTAMP('2003-03-30 01:59:59') b
+1048978799 Before the gap
+SELECT UNIX_TIMESTAMP('2003-03-30 02:30:00'), 'Inside the gap' AS b;
+UNIX_TIMESTAMP('2003-03-30 02:30:00') b
+1048978800 Inside the gap
+SELECT UNIX_TIMESTAMP('2003-03-30 03:00:00'), 'After the gap' AS b;
+UNIX_TIMESTAMP('2003-03-30 03:00:00') b
+1048978800 After the gap
+SET time_zone=DEFAULT;
+#
+# End of 5.6 tests
+#
=== modified file 'mysql-test/r/type_temporal_fractional.result'
--- a/mysql-test/r/type_temporal_fractional.result 2011-11-17 18:31:32 +0000
+++ b/mysql-test/r/type_temporal_fractional.result 2011-11-18 09:52:00 +0000
@@ -16689,3 +16689,70 @@ Warnings:
Warning 1411 Incorrect datetime value: '00-0' for function str_to_date
Warning 1411 Incorrect datetime value: '00-0' for function str_to_date
DROP TABLE t1;
+#
+# Bug#13399082 - ASSERTION `MON > 0 && MON < 13' FAILED IN TZTIME.CC | SEC_SINCE_EPOCH
+#
+SET TIME_ZONE='+02:00';
+SELECT UNIX_TIMESTAMP(STR_TO_DATE('2020','%Y'));
+UNIX_TIMESTAMP(STR_TO_DATE('2020','%Y'))
+0
+SET TIME_ZONE=DEFAULT;
+# This should return NULL
+CREATE TABLE t1 (a INT);
+INSERT INTO t1 VALUES(NULL);
+SELECT UNIX_TIMESTAMP(a) FROM t1;
+UNIX_TIMESTAMP(a)
+NULL
+DROP TABLE t1;
+SELECT UNIX_TIMESTAMP(NULL);
+UNIX_TIMESTAMP(NULL)
+NULL
+SELECT UNIX_TIMESTAMP(DATE(NULL));
+UNIX_TIMESTAMP(DATE(NULL))
+NULL
+SELECT UNIX_TIMESTAMP(TIME(NULL));
+UNIX_TIMESTAMP(TIME(NULL))
+NULL
+SELECT UNIX_TIMESTAMP(TIMESTAMP(NULL));
+UNIX_TIMESTAMP(TIMESTAMP(NULL))
+NULL
+# This should return 0
+CREATE TABLE t1 (a VARCHAR(30));
+INSERT INTO t1 VALUES
+('0000-01-01 00:00:00'),('2001-00-01 00:00:01'),('2001-01-00 00:00:00'),
+('0000-00-00 00:00:01'),('0000-00-00 00:00:00.1');
+SELECT UNIX_TIMESTAMP(a) FROM t1;
+UNIX_TIMESTAMP(a)
+0.000000
+0.000000
+0.000000
+0.000000
+0.000000
+DROP TABLE t1;
+SELECT UNIX_TIMESTAMP('0000-01-01 00:00:00');
+UNIX_TIMESTAMP('0000-01-01 00:00:00')
+0
+SELECT UNIX_TIMESTAMP('2001-00-01 00:00:00');
+UNIX_TIMESTAMP('2001-00-01 00:00:00')
+0
+SELECT UNIX_TIMESTAMP('2001-01-00 00:00:00');
+UNIX_TIMESTAMP('2001-01-00 00:00:00')
+0
+SELECT UNIX_TIMESTAMP('0000-00-00 10:10:20');
+UNIX_TIMESTAMP('0000-00-00 10:10:20')
+0
+SELECT UNIX_TIMESTAMP('0000-00-00 00:00:00.1');
+UNIX_TIMESTAMP('0000-00-00 00:00:00.1')
+0.0
+SELECT UNIX_TIMESTAMP(TIMESTAMP'0000-00-00 10:10:20');
+UNIX_TIMESTAMP(TIMESTAMP'0000-00-00 10:10:20')
+0
+SELECT UNIX_TIMESTAMP(TIMESTAMP'0000-00-00 00:00:00.1');
+UNIX_TIMESTAMP(TIMESTAMP'0000-00-00 00:00:00.1')
+0.0
+SELECT UNIX_TIMESTAMP(TIMESTAMP('0000-00-00 10:10:20'));
+UNIX_TIMESTAMP(TIMESTAMP('0000-00-00 10:10:20'))
+0
+SELECT UNIX_TIMESTAMP(TIMESTAMP('0000-00-00 00:00:00.1'));
+UNIX_TIMESTAMP(TIMESTAMP('0000-00-00 00:00:00.1'))
+0.0
=== modified file 'mysql-test/t/timezone2.test'
--- a/mysql-test/t/timezone2.test 2010-08-06 19:29:37 +0000
+++ b/mysql-test/t/timezone2.test 2011-11-18 09:52:00 +0000
@@ -284,3 +284,25 @@ SELECT CONVERT_TZ(1, 1, a) FROM t1;
DROP TABLE t1;
--echo End of 5.1 tests
+
+--echo #
+--echo # Start of 5.6 tests
+--echo #
+SET time_zone='Europe/Moscow';
+CREATE TABLE t1 (a TIMESTAMP, b VARCHAR(30));
+# Testing values around winter/summer time change
+# Expect non-zero UNIX_TIMESTAMP value for all of them
+INSERT INTO t1 VALUES
+('2003-03-30 01:59:59', 'Before the gap'),
+('2003-03-30 02:30:00', 'Inside the gap'),
+('2003-03-30 03:00:00', 'After the gap');
+SELECT a, UNIX_TIMESTAMP(a), b FROM t1;
+DROP TABLE t1;
+SELECT UNIX_TIMESTAMP('2003-03-30 01:59:59'), 'Before the gap' AS b;
+SELECT UNIX_TIMESTAMP('2003-03-30 02:30:00'), 'Inside the gap' AS b;
+SELECT UNIX_TIMESTAMP('2003-03-30 03:00:00'), 'After the gap' AS b;
+SET time_zone=DEFAULT;
+
+--echo #
+--echo # End of 5.6 tests
+--echo #
=== modified file 'mysql-test/t/type_temporal_fractional.test'
--- a/mysql-test/t/type_temporal_fractional.test 2011-11-17 18:31:32 +0000
+++ b/mysql-test/t/type_temporal_fractional.test 2011-11-18 09:52:00 +0000
@@ -7261,6 +7261,41 @@ AND
col_timestamp_3_not_null_key < col_datetime;
DROP TABLE t1;
+
+--echo #
+--echo # Bug#13399082 - ASSERTION `MON > 0 && MON < 13' FAILED IN TZTIME.CC | SEC_SINCE_EPOCH
+--echo #
+SET TIME_ZONE='+02:00';
+SELECT UNIX_TIMESTAMP(STR_TO_DATE('2020','%Y'));
+SET TIME_ZONE=DEFAULT;
+
+--echo # This should return NULL
+CREATE TABLE t1 (a INT);
+INSERT INTO t1 VALUES(NULL);
+SELECT UNIX_TIMESTAMP(a) FROM t1;
+DROP TABLE t1;
+SELECT UNIX_TIMESTAMP(NULL);
+SELECT UNIX_TIMESTAMP(DATE(NULL));
+SELECT UNIX_TIMESTAMP(TIME(NULL));
+SELECT UNIX_TIMESTAMP(TIMESTAMP(NULL));
+
+--echo # This should return 0
+CREATE TABLE t1 (a VARCHAR(30));
+INSERT INTO t1 VALUES
+('0000-01-01 00:00:00'),('2001-00-01 00:00:01'),('2001-01-00 00:00:00'),
+('0000-00-00 00:00:01'),('0000-00-00 00:00:00.1');
+SELECT UNIX_TIMESTAMP(a) FROM t1;
+DROP TABLE t1;
+SELECT UNIX_TIMESTAMP('0000-01-01 00:00:00');
+SELECT UNIX_TIMESTAMP('2001-00-01 00:00:00');
+SELECT UNIX_TIMESTAMP('2001-01-00 00:00:00');
+SELECT UNIX_TIMESTAMP('0000-00-00 10:10:20');
+SELECT UNIX_TIMESTAMP('0000-00-00 00:00:00.1');
+SELECT UNIX_TIMESTAMP(TIMESTAMP'0000-00-00 10:10:20');
+SELECT UNIX_TIMESTAMP(TIMESTAMP'0000-00-00 00:00:00.1');
+SELECT UNIX_TIMESTAMP(TIMESTAMP('0000-00-00 10:10:20'));
+SELECT UNIX_TIMESTAMP(TIMESTAMP('0000-00-00 00:00:00.1'));
+
## TS-TODO: SELECT CAST('00:00:00' AS DATETIME) -> should it use curdate?
## TS-TODO: reuse count_datetime_length()/count_string_result_length()
=== modified file 'sql/field.cc'
--- a/sql/field.cc 2011-11-07 14:35:16 +0000
+++ b/sql/field.cc 2011-11-18 09:52:00 +0000
@@ -1794,15 +1794,12 @@ bool Field::get_time(MYSQL_TIME *ltime)
}
-bool Field::get_timestamp(struct timeval *tm)
+bool Field::get_timestamp(struct timeval *tm, int *warnings)
{
MYSQL_TIME ltime;
- if (get_date(<ime, TIME_FUZZY_DATE))
- return true;
- my_bool not_used;
- tm->tv_sec= TIME_to_timestamp(current_thd, <ime, ¬_used);
- tm->tv_usec= ltime.second_part;
- return false;
+ DBUG_ASSERT(!is_null());
+ return get_date(<ime, TIME_FUZZY_DATE) ||
+ datetime_to_timeval(current_thd, <ime, tm, warnings);
}
@@ -5156,30 +5153,14 @@ Field_temporal_with_date_and_time::conve
struct timeval *tm,
int *warnings)
{
- my_bool in_dst_time_gap;
- /* Only convert a correct date (not a zero date) */
- if (!ltime->month)
+ /*
+ No needs to do check_date(TIME_NO_ZERO_IN_DATE),
+ because it has been done earlier in
+ store_time(), number_to_datetime() or str_to_datetime().
+ */
+ if (datetime_with_no_zero_in_date_to_timeval(thd, ltime, tm, warnings))
{
tm->tv_sec= tm->tv_usec= 0;
- if (ltime->second_part)
- {
- /* Don't allow zero timestamp with microseconds */
- *warnings|= MYSQL_TIME_WARN_TRUNCATED;
- return true;
- }
- return false;
- }
-
- tm->tv_usec= ltime->second_part;
-
- if (!(tm->tv_sec= TIME_to_timestamp(thd, ltime, &in_dst_time_gap)))
- {
- *warnings|= MYSQL_TIME_WARN_OUT_OF_RANGE;
- return true;
- }
- else if (in_dst_time_gap)
- {
- *warnings|= MYSQL_TIME_WARN_INVALID_TIMESTAMP;
return true;
}
return false;
@@ -5358,7 +5339,7 @@ bool Field_timestamp::get_date_internal(
/**
Get TIMESTAMP field value as seconds since begging of Unix Epoch
*/
-bool Field_timestamp::get_timestamp(struct timeval *tm)
+bool Field_timestamp::get_timestamp(struct timeval *tm, int *warnings)
{
if (is_null())
return true;
@@ -5567,12 +5548,11 @@ Field_timestampf::get_date_internal(MYSQ
}
-bool Field_timestampf::get_timestamp(struct timeval *tm)
+bool Field_timestampf::get_timestamp(struct timeval *tm, int *warnings)
{
THD *thd= table ? table->in_use : current_thd;
thd->time_zone_used= 1;
- if (is_null())
- return true;
+ DBUG_ASSERT(!is_null());
my_timestamp_from_binary(tm, ptr, dec);
return false;
}
=== modified file 'sql/field.h'
--- a/sql/field.h 2011-11-07 14:35:16 +0000
+++ b/sql/field.h 2011-11-18 09:52:00 +0000
@@ -535,7 +535,7 @@ public:
This method is used in "SELECT UNIX_TIMESTAMP(field)"
to avoid conversion from timestamp to MYSQL_TIME and back.
*/
- virtual bool get_timestamp(struct timeval *tm);
+ virtual bool get_timestamp(struct timeval *tm, int *warnings);
/**
Stores a timestamp value in "struct timeval" format into a field.
Note, store_timestamp(), get_timestamp() and store_time()
@@ -2170,7 +2170,7 @@ public:
Field::set_default();
}
/* Get TIMESTAMP field value as seconds since begging of Unix Epoch */
- bool get_timestamp(struct timeval *tm);
+ bool get_timestamp(struct timeval *tm, int *warnings);
bool get_date(MYSQL_TIME *ltime,uint fuzzydate);
Field_timestamp *clone(MEM_ROOT *mem_root) const {
DBUG_ASSERT(type() == MYSQL_TYPE_TIMESTAMP);
@@ -2265,7 +2265,7 @@ public:
else
Field::set_default();
}
- bool get_timestamp(struct timeval *tm);
+ bool get_timestamp(struct timeval *tm, int *warnings);
};
=== modified file 'sql/item.cc'
--- a/sql/item.cc 2011-11-16 14:58:34 +0000
+++ b/sql/item.cc 2011-11-18 09:52:00 +0000
@@ -1372,17 +1372,14 @@ bool Item::get_time_from_non_temporal(MY
}
return (null_value= true); // Impossible result type
}
-
-bool Item::get_timeval(struct timeval *tm)
+
+bool Item::get_timeval(struct timeval *tm, int *warnings)
{
MYSQL_TIME ltime;
- if (get_date(<ime, 0))
- return true;
- my_bool not_used;
- tm->tv_sec= TIME_to_timestamp(current_thd, <ime, ¬_used);
- tm->tv_usec= ltime.second_part;
- return false;
+ return (null_value=
+ (get_date(<ime, TIME_FUZZY_DATE) ||
+ datetime_to_timeval(current_thd, <ime, tm, warnings)));
}
@@ -2674,9 +2671,10 @@ bool Item_field::get_time(MYSQL_TIME *lt
return 0;
}
-bool Item_field::get_timeval(struct timeval *tm)
+bool Item_field::get_timeval(struct timeval *tm, int *warnings)
{
- return ((null_value= field->get_timestamp(tm)));
+ return (null_value= (field->is_null() ||
+ field->get_timestamp(tm, warnings)));
}
double Item_field::val_result()
=== modified file 'sql/item.h'
--- a/sql/item.h 2011-11-10 14:58:23 +0000
+++ b/sql/item.h 2011-11-18 09:52:00 +0000
@@ -1082,7 +1082,7 @@ public:
@retval false on success
@retval true on error
*/
- virtual bool get_timeval(struct timeval *tm);
+ virtual bool get_timeval(struct timeval *tm, int *warnings);
virtual bool get_date_result(MYSQL_TIME *ltime,uint fuzzydate)
{ return get_date(ltime,fuzzydate); }
/*
@@ -1989,7 +1989,7 @@ public:
bool get_date(MYSQL_TIME *ltime,uint fuzzydate);
bool get_date_result(MYSQL_TIME *ltime,uint fuzzydate);
bool get_time(MYSQL_TIME *ltime);
- bool get_timeval(struct timeval *tm);
+ bool get_timeval(struct timeval *tm, int *warnings);
bool is_null() { return field->is_null(); }
void update_null_value();
Item *get_tmp_table_item(THD *thd);
=== modified file 'sql/item_timefunc.cc'
--- a/sql/item_timefunc.cc 2011-11-16 13:45:51 +0000
+++ b/sql/item_timefunc.cc 2011-11-18 09:52:00 +0000
@@ -1471,14 +1471,19 @@ bool Item_func_unix_timestamp::val_timev
tm->tv_usec= 0;
return false; // no args: null_value is set in constructor and is always 0.
}
- if (args[0]->get_timeval(tm)) // Don't set null_value here
+ int warnings= 0;
+ if (args[0]->get_timeval(tm, &warnings)) // Don't set null_value here
{
- tm->tv_sec= tm->tv_usec= 0;
/*
- We set null_value only if args[0] returned null.
+ We set null_value only if args[0]->get_date() invoked
+ inside args[0]->get_timeval() returned null,
+ which is indicated by "warnings == 0".
In case of wrong non-null datetime parameter we return 0.
+ "warnings" will not be equal to 0 in this case.
*/
- return (null_value= args[0]->null_value);
+ if (warnings == 0)
+ return (null_value= true);
+ tm->tv_sec= tm->tv_usec= 0;
}
return (null_value= false);
}
@@ -1499,10 +1504,9 @@ longlong Item_func_unix_timestamp::val_i
DBUG_ASSERT(arg_count == 1 &&
args[0]->type() == Item::FIELD_ITEM &&
args[0]->field_type() == MYSQL_TYPE_TIMESTAMP);
- Field *field=((Item_field*) args[0])->field;
/* Leave the incl_endp intact */
struct timeval tm;
- return (null_value= field->get_timestamp(&tm)) ? 0 : tm.tv_sec;
+ return val_timeval(&tm) ? 0 : tm.tv_sec;
}
=== modified file 'sql/sql_time.cc'
--- a/sql/sql_time.cc 2011-11-03 09:33:45 +0000
+++ b/sql/sql_time.cc 2011-11-18 09:52:00 +0000
@@ -637,6 +637,116 @@ my_time_t TIME_to_timestamp(THD *thd, co
}
+/**
+ Convert a datetime MYSQL_TIME representation
+ to corresponding "struct timeval" value.
+
+ ltime must previously be checked for TIME_NO_ZERO_IN_DATE.
+ Things like '0000-01-01', '2000-00-01', '2000-01-00' are not allowed
+ and asserted.
+
+ Things like '0000-00-00 10:30:30' or '0000-00-00 00:00:00.123456'
+ (i.e. empty date with non-empty time) return error.
+
+ Zero datetime '0000-00-00 00:00:00.000000'
+ is allowed and is mapper to {tv_sec=0, tv_usec=0}.
+
+ Note: In case of error, tm value is not initialized.
+
+ Note: "warnings" is not initialized to zero,
+ so new warnings are added to the old ones.
+ Caller must make sure to initialize "warnings".
+
+ @param IN thd current thd
+ @param IN ltime datetime value
+ @param OUT tm timeval value
+ @param OUT warnings pointer to warnings vector
+ @return
+ @retval false on success
+ @retval true on error
+*/
+bool datetime_with_no_zero_in_date_to_timeval(THD *thd,
+ const MYSQL_TIME *ltime,
+ struct timeval *tm,
+ int *warnings)
+{
+ if (!ltime->month) /* Zero date */
+ {
+ DBUG_ASSERT(!ltime->year && !ltime->day);
+ if (non_zero_time(ltime))
+ {
+ /*
+ Return error for zero date with non-zero time, e.g.:
+ '0000-00-00 10:20:30' or '0000-00-00 00:00:00.123456'
+ */
+ *warnings|= MYSQL_TIME_WARN_TRUNCATED;
+ return true;
+ }
+ tm->tv_sec= tm->tv_usec= 0; // '0000-00-00 00:00:00.000000'
+ return false;
+ }
+
+ my_bool in_dst_time_gap;
+ if (!(tm->tv_sec= TIME_to_timestamp(current_thd, ltime, &in_dst_time_gap)))
+ {
+ /*
+ Date was outside of the supported timestamp range.
+ For example: '3001-01-01 00:00:00' or '1000-01-01 00:00:00'
+ */
+ *warnings|= MYSQL_TIME_WARN_OUT_OF_RANGE;
+ return true;
+ }
+ else if (in_dst_time_gap)
+ {
+ /*
+ Set MYSQL_TIME_WARN_INVALID_TIMESTAMP warning to indicate
+ that date was fine but pointed to winter/summer time switch gap.
+ In this case tm is set to the fist second after gap.
+ For example: '2003-03-30 02:30:00 MSK' -> '2003-03-30 03:00:00 MSK'
+ */
+ *warnings|= MYSQL_TIME_WARN_INVALID_TIMESTAMP;
+ }
+ tm->tv_usec= ltime->second_part;
+ return false;
+}
+
+
+/**
+ Convert a datetime MYSQL_TIME representation
+ to corresponding "struct timeval" value.
+
+ Things like '0000-01-01', '2000-00-01', '2000-01-00'
+ (i.e. incomplete date) return error.
+
+ Things like '0000-00-00 10:30:30' or '0000-00-00 00:00:00.123456'
+ (i.e. empty date with non-empty time) return error.
+
+ Zero datetime '0000-00-00 00:00:00.000000'
+ is allowed and is mapper to {tv_sec=0, tv_usec=0}.
+
+ Note: In case of error, tm value is not initialized.
+
+ Note: "warnings" is not initialized to zero,
+ so new warnings are added to the old ones.
+ Caller must make sure to initialize "warnings".
+
+ @param IN thd current thd
+ @param IN ltime datetime value
+ @param OUT tm timeval value
+ @param OUT warnings pointer to warnings vector
+ @return
+ @retval false on success
+ @retval true on error
+*/
+bool datetime_to_timeval(THD *thd, const MYSQL_TIME *ltime,
+ struct timeval *tm, int *warnings)
+{
+ return
+ check_date(ltime, non_zero_date(ltime), TIME_NO_ZERO_IN_DATE, warnings) ||
+ datetime_with_no_zero_in_date_to_timeval(current_thd, ltime, tm, warnings);
+}
+
+
/*
Convert a time string to a MYSQL_TIME struct and produce a warning
if string was cut during conversion.
=== modified file 'sql/sql_time.h'
--- a/sql/sql_time.h 2011-11-07 11:50:35 +0000
+++ b/sql/sql_time.h 2011-11-18 09:52:00 +0000
@@ -36,6 +36,11 @@ ulong convert_month_to_period(ulong mont
void mix_date_and_time(MYSQL_TIME *ldate, const MYSQL_TIME *ltime);
void get_date_from_daynr(long daynr,uint *year, uint *month, uint *day);
my_time_t TIME_to_timestamp(THD *thd, const MYSQL_TIME *t, my_bool *not_exist);
+bool datetime_with_no_zero_in_date_to_timeval(THD *thd, const MYSQL_TIME *t,
+ struct timeval *tm,
+ int *warnings);
+bool datetime_to_timeval(THD *thd, const MYSQL_TIME *t,
+ struct timeval *tm, int *warnings);
bool str_to_datetime_with_warn(String *str, MYSQL_TIME *l_time, uint flags);
bool my_decimal_to_datetime_with_warn(const my_decimal *decimal,
MYSQL_TIME *ltime, uint flags);
No bundle (reason: useless for push emails).
| Thread |
|---|
| • bzr push into mysql-trunk branch (alexander.barkov:3565 to 3566)Bug#13399082 WL#946 | Alexander Barkov | 21 Nov |