Skip to content

Commit 07e2bbe

Browse files
committed
SERVER-6189 Support Dates before 1970 in aggregation
This required moving away from Date_t as it asserts on dates before 1970 Some tests are disabled due to SERVER-6666 and SERVER-6679
1 parent 672a6f7 commit 07e2bbe

File tree

5 files changed

+162
-76
lines changed

5 files changed

+162
-76
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// server6189 - Support date operators with dates before 1970
2+
3+
c = db.c;
4+
function test(date, testSynthetics) {
5+
c.drop();
6+
c.save( {date: date} );
7+
8+
res = c.aggregate( { $project:{ _id: 0
9+
, year:{ $year: '$date' }
10+
, month:{ $month: '$date' }
11+
, dayOfMonth:{ $dayOfMonth: '$date' }
12+
, hour:{ $hour: '$date' }
13+
, minute:{ $minute: '$date' }
14+
, second:{ $second: '$date' }
15+
//, millisecond:{ $millisecond: '$date' } // server-6666
16+
17+
// $substr will call coerceToString
18+
//, string: {$substr: ['$date', 0,1000]} // server-6679
19+
}} );
20+
assert.commandWorked(res);
21+
assert.eq(res.result[0], { year: date.getUTCFullYear()
22+
, month: date.getUTCMonth() + 1 // jan == 1
23+
, dayOfMonth: date.getUTCDate()
24+
, hour: date.getUTCHours()
25+
, minute: date.getUTCMinutes()
26+
, second: date.getUTCSeconds()
27+
//, millisecond: date.getUTCMilliseconds() // server-6666
28+
//, string: date.tojson().split('"')[1] // server-6679
29+
} );
30+
31+
if (testSynthetics) {
32+
// Tests with this set all have the same value for these fields
33+
res = c.aggregate( { $project:{ _id: 0
34+
, week:{ $week: '$date' }
35+
, dayOfWeek:{ $dayOfWeek: '$date' }
36+
, dayOfYear:{ $dayOfYear: '$date' }
37+
} } );
38+
39+
assert.commandWorked(res);
40+
assert.eq(res.result[0], { week: 0
41+
, dayOfWeek: 7
42+
, dayOfYear: 2
43+
} );
44+
}
45+
}
46+
47+
48+
// Basic test
49+
test(ISODate('1960-01-02 03:04:05.006Z'), true);
50+
51+
// Testing special rounding rules for seconds
52+
test(ISODate('1960-01-02 03:04:04.999Z'), false); // second = 4
53+
test(ISODate('1960-01-02 03:04:05.000Z'), true); // second = 5
54+
test(ISODate('1960-01-02 03:04:05.001Z'), true); // second = 5
55+
test(ISODate('1960-01-02 03:04:05.999Z'), true); // second = 5
56+
57+
// Test date before 1900 (negative tm_year values from gmtime)
58+
test(ISODate('1860-01-02 03:04:05.006Z'), false);
59+
60+
// Test with time_t == -1 and 0
61+
test(new Date(-1000), false);
62+
test(new Date(0), false);
63+
64+
// Test date > 2000 for completeness (using now)
65+
test(new Date(), false);

src/mongo/db/pipeline/expression.cpp

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -850,8 +850,7 @@ namespace mongo {
850850
const intrusive_ptr<Document> &pDocument) const {
851851
checkArgCount(1);
852852
intrusive_ptr<const Value> pDate(vpOperand[0]->evaluate(pDocument));
853-
tm date;
854-
(pDate->coerceToDate()).toTm(&date);
853+
tm date = pDate->coerceToTm();
855854
return Value::createInt(date.tm_mday);
856855
}
857856

@@ -882,8 +881,7 @@ namespace mongo {
882881
const intrusive_ptr<Document> &pDocument) const {
883882
checkArgCount(1);
884883
intrusive_ptr<const Value> pDate(vpOperand[0]->evaluate(pDocument));
885-
tm date;
886-
(pDate->coerceToDate()).toTm(&date);
884+
tm date = pDate->coerceToTm();
887885
return Value::createInt(date.tm_wday+1); // MySQL uses 1-7 tm uses 0-6
888886
}
889887

@@ -914,8 +912,7 @@ namespace mongo {
914912
const intrusive_ptr<Document> &pDocument) const {
915913
checkArgCount(1);
916914
intrusive_ptr<const Value> pDate(vpOperand[0]->evaluate(pDocument));
917-
tm date;
918-
(pDate->coerceToDate()).toTm(&date);
915+
tm date = pDate->coerceToTm();
919916
return Value::createInt(date.tm_yday+1); // MySQL uses 1-366 tm uses 0-365
920917
}
921918

@@ -1757,8 +1754,7 @@ namespace mongo {
17571754
const intrusive_ptr<Document> &pDocument) const {
17581755
checkArgCount(1);
17591756
intrusive_ptr<const Value> pDate(vpOperand[0]->evaluate(pDocument));
1760-
tm date;
1761-
(pDate->coerceToDate()).toTm(&date);
1757+
tm date = pDate->coerceToTm();
17621758
return Value::createInt(date.tm_min);
17631759
}
17641760

@@ -1858,8 +1854,7 @@ namespace mongo {
18581854
const intrusive_ptr<Document> &pDocument) const {
18591855
checkArgCount(1);
18601856
intrusive_ptr<const Value> pDate(vpOperand[0]->evaluate(pDocument));
1861-
tm date;
1862-
(pDate->coerceToDate()).toTm(&date);
1857+
tm date = pDate->coerceToTm();
18631858
return Value::createInt(date.tm_mon + 1); // MySQL uses 1-12 tm uses 0-11
18641859
}
18651860

@@ -1945,8 +1940,7 @@ namespace mongo {
19451940
const intrusive_ptr<Document> &pDocument) const {
19461941
checkArgCount(1);
19471942
intrusive_ptr<const Value> pDate(vpOperand[0]->evaluate(pDocument));
1948-
tm date;
1949-
(pDate->coerceToDate()).toTm(&date);
1943+
tm date = pDate->coerceToTm();
19501944
return Value::createInt(date.tm_hour);
19511945
}
19521946

@@ -2373,8 +2367,7 @@ namespace mongo {
23732367
const intrusive_ptr<Document> &pDocument) const {
23742368
checkArgCount(1);
23752369
intrusive_ptr<const Value> pDate(vpOperand[0]->evaluate(pDocument));
2376-
tm date;
2377-
(pDate->coerceToDate()).toTm(&date);
2370+
tm date = pDate->coerceToTm();
23782371
return Value::createInt(date.tm_sec);
23792372
}
23802373

@@ -2620,8 +2613,7 @@ namespace mongo {
26202613
const intrusive_ptr<Document> &pDocument) const {
26212614
checkArgCount(1);
26222615
intrusive_ptr<const Value> pDate(vpOperand[0]->evaluate(pDocument));
2623-
tm date;
2624-
(pDate->coerceToDate()).toTm(&date);
2616+
tm date = pDate->coerceToTm();
26252617
int dayOfWeek = date.tm_wday;
26262618
int dayOfYear = date.tm_yday;
26272619
int prevSundayDayOfYear = dayOfYear - dayOfWeek; // may be negative
@@ -2669,8 +2661,7 @@ namespace mongo {
26692661
const intrusive_ptr<Document> &pDocument) const {
26702662
checkArgCount(1);
26712663
intrusive_ptr<const Value> pDate(vpOperand[0]->evaluate(pDocument));
2672-
tm date;
2673-
(pDate->coerceToDate()).toTm(&date);
2664+
tm date = pDate->coerceToTm();
26742665
return Value::createInt(date.tm_year + 1900); // tm_year is years since 1900
26752666
}
26762667

src/mongo/db/pipeline/value.cpp

100755100644
Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -38,49 +38,40 @@ namespace mongo {
3838
Value::~Value() {
3939
}
4040

41-
Value::Value():
42-
type(jstNULL),
43-
oidValue(),
44-
dateValue(),
45-
stringValue(),
46-
pDocumentValue(),
47-
vpValue() {
48-
}
41+
Value::Value(): type(jstNULL) {}
4942

50-
Value::Value(BSONType theType):
51-
type(theType),
52-
oidValue(),
53-
dateValue(),
54-
stringValue(),
55-
pDocumentValue(),
56-
vpValue() {
43+
Value::Value(BSONType theType): type(theType) {
5744
switch(type) {
5845
case Undefined:
5946
case jstNULL:
6047
case Object: // empty
6148
case Array: // empty
6249
break;
6350

64-
case NumberDouble:
65-
doubleValue = 0;
66-
break;
67-
6851
case Bool:
6952
boolValue = false;
7053
break;
7154

72-
case NumberInt:
73-
intValue = 0;
55+
case NumberDouble:
56+
doubleValue = 0;
7457
break;
7558

76-
case Timestamp:
77-
timestampValue = OpTime();
59+
case NumberInt:
60+
intValue = 0;
7861
break;
7962

8063
case NumberLong:
8164
longValue = 0;
8265
break;
8366

67+
case Date:
68+
dateValue = 0;
69+
break;
70+
71+
case Timestamp:
72+
timestampValue = OpTime();
73+
break;
74+
8475
default:
8576
// nothing else is allowed
8677
uassert(16001, str::stream() <<
@@ -153,7 +144,8 @@ namespace mongo {
153144
break;
154145

155146
case Date:
156-
dateValue = pBsonElement->Date();
147+
// this is really signed but typed as unsigned for historical reasons
148+
dateValue = static_cast<long long>(pBsonElement->Date().millis);
157149
break;
158150

159151
case RegEx:
@@ -233,15 +225,10 @@ namespace mongo {
233225
return pValue;
234226
}
235227

236-
Value::Value(const Date_t &value):
237-
type(Date),
238-
pDocumentValue(),
239-
vpValue() {
240-
dateValue = value;
241-
}
242-
243-
intrusive_ptr<const Value> Value::createDate(const Date_t &value) {
244-
intrusive_ptr<const Value> pValue(new Value(value));
228+
intrusive_ptr<const Value> Value::createDate(const long long &value) {
229+
// Can't directly construct because constructor would clash with createLong
230+
intrusive_ptr<Value> pValue(new Value(Date));
231+
pValue->dateValue = value;
245232
return pValue;
246233
}
247234

@@ -353,7 +340,7 @@ namespace mongo {
353340
return boolValue;
354341
}
355342

356-
Date_t Value::getDate() const {
343+
long long Value::getDate() const {
357344
verify(getType() == Date);
358345
return dateValue;
359346
}
@@ -431,7 +418,7 @@ namespace mongo {
431418
break;
432419

433420
case Date:
434-
pBuilder->append(getDate());
421+
pBuilder->append(Date_t(getDate()));
435422
break;
436423

437424
case RegEx:
@@ -622,14 +609,14 @@ namespace mongo {
622609
return (double)0;
623610
}
624611

625-
Date_t Value::coerceToDate() const {
612+
long long Value::coerceToDate() const {
626613
switch(type) {
627614

628615
case Date:
629616
return dateValue;
630617

631618
case Timestamp:
632-
return Date_t(timestampValue.getSecs() * 1000ULL);
619+
return timestampValue.getSecs() * 1000LL;
633620

634621
default:
635622
uassert(16006, str::stream() <<
@@ -638,6 +625,49 @@ namespace mongo {
638625
} // switch(type)
639626
}
640627

628+
time_t Value::coerceToTimeT() const {
629+
long long millis = coerceToDate();
630+
if (millis < 0) {
631+
// We want the division below to truncate toward -inf rather than 0
632+
// eg Dec 31, 1969 23:59:58.001 should be -2 seconds rather than -1
633+
// This is needed to get the correct values from coerceToTM
634+
if ( -1999 / 1000 != -2) { // this is implementation defined
635+
millis -= 1000-1;
636+
}
637+
}
638+
const long long seconds = millis / 1000;
639+
640+
uassert(16421, "Can't handle date values outside of time_t range",
641+
seconds >= std::numeric_limits<time_t>::min() &&
642+
seconds <= std::numeric_limits<time_t>::max());
643+
644+
return static_cast<time_t>(seconds);
645+
}
646+
tm Value::coerceToTm() const {
647+
// See implementation in Date_t.
648+
// Can't reuse that here because it doesn't support times before 1970
649+
time_t dtime = coerceToTimeT();
650+
tm out;
651+
652+
#if defined(_WIN32) // Both the argument order and the return values differ
653+
bool itWorked = gmtime_s(&out, &dtime) == 0;
654+
#else
655+
bool itWorked = gmtime_r(&dtime, &out) != NULL;
656+
#endif
657+
658+
if (!itWorked) {
659+
if (dtime < 0) {
660+
// Windows docs say it doesn't support these, but empirically it seems to work
661+
uasserted(16422, "gmtime failed - your system doesn't support dates before 1970");
662+
}
663+
else {
664+
uasserted(16423, str::stream() << "gmtime failed to convert time_t of " << dtime);
665+
}
666+
}
667+
668+
return out;
669+
}
670+
641671
string Value::coerceToString() const {
642672
stringstream ss;
643673
switch(type) {
@@ -661,7 +691,7 @@ namespace mongo {
661691
return ss.str();
662692

663693
case Date:
664-
return dateValue.toString();
694+
return time_t_to_String(coerceToTimeT());
665695

666696
case jstNULL:
667697
case Undefined:
@@ -842,10 +872,8 @@ namespace mongo {
842872
return -1;
843873

844874
case Date: {
845-
// need to convert to long long to handle dates before 1970
846-
// see BSONElement::compareElementValues
847-
long long l = static_cast<long long>(rL->dateValue.millis);
848-
long long r = static_cast<long long>(rR->dateValue.millis);
875+
long long l = rL->dateValue;
876+
long long r = rR->dateValue;
849877
if (l < r)
850878
return -1;
851879
if (l > r)
@@ -938,7 +966,7 @@ namespace mongo {
938966
break;
939967

940968
case Date:
941-
boost::hash_combine(seed, (unsigned long long)dateValue);
969+
boost::hash_combine(seed, dateValue);
942970
break;
943971

944972
case RegEx:

0 commit comments

Comments
 (0)