This comes up often enough that it is worth writing down. Note that I used AI to gather and generate much of the content below using Epicor’s “Knowlege” from help, documentation and also from the web (including EpiUsers
). I have read the results, BUT I have not tested/verified every fact. If you see any errors below, Please lets correct it together. This post can become a resource for everyone in the future.
Dates and times look simple until they are not. Sometimes you have a true date. Sometimes you have a true time. Sometimes you have a datetime. Sometimes the database field is text that only looks like a date. Sometimes the time portion matters. Sometimes it absolutely should not.
The first rule is simple…
Know what you actually have before you convert it.
A date-only value is not the same thing as a datetime at midnight. A time-only value is not the same thing as a duration. A string is not a date, even if it looks like one.
And for BAQs, there is one more important point…
Most of the time, you are not writing a whole SQL statement.
You are writing an expression inside a calculated field, a condition, or a parameterized filter. The BAQ designer builds the outer SQL. You provide the pieces.
Common SQL Server Date and Time Types
| SQL Type | Meaning | Example |
|---|---|---|
date |
Calendar date only | 2026-06-16 |
time |
Time of day only | 14:30:00 |
datetime |
Older date plus time type | 2026-06-16 14:30:00.000 |
datetime2 |
Preferred modern date plus time type | 2026-06-16 14:30:00.1234567 |
datetimeoffset |
Date plus time plus offset | 2026-06-16 14:30:00 -06:00 |
varchar / nvarchar |
Text, not a date | '06/16/2026' |
For new SQL work, I generally prefer datetime2 over datetime unless there is a compatibility reason not to. If the offset matters, use datetimeoffset. If only the date matters, use date. If only the time-of-day matters, use time.
For BAQs, you usually do not get to redesign the underlying table. You deal with the field you have. So the practical skill is knowing how to shape the value correctly in calculated fields and criteria.
SQL in BAQs Is Usually an Expression
In a BAQ, do not think in terms of full SQL statements like this:
SELECT CAST(OrderHed.OrderDate AS date)
FROM Erp.OrderHed
WHERE OrderHed.OrderDate >= '2026-06-16'
That is not usually what you enter in BAQ Designer.
In a calculated field, you usually write only the expression:
CAST(OrderHed.OrderDate AS date)
In criteria, you usually define field/operator/value logic that amounts to something like this:
OrderHed.OrderDate >= @StartDate
or this:
OrderHed.OrderDate < DATEADD(day, 1, @EndDate)
That is the practical BAQ mindset.
The BAQ designer builds the query. You supply the expression.
BAQ Calculated Field Examples
DateTime to Date
Use this when the source field has a date and time, but you only want the date portion.
Calculated field expression:
CAST(OrderHed.OrderDate AS date)
or:
CONVERT(date, OrderHed.OrderDate)
Typical calculated field setup:
| Property | Value |
|---|---|
| Field Name | OrderDateOnly |
| Data Type | date |
| Expression | CAST(OrderHed.OrderDate AS date) |
The important point is that this returns a real date, not a formatted string.
That means it still sorts like a date, filters like a date, and behaves like a date.
DateTime to Time
Use this when you only want the time-of-day portion.
CAST(SomeTable.SomeDateTimeField AS time)
or:
CONVERT(time, SomeTable.SomeDateTimeField)
Example:
CAST(LaborDtl.ClockInDateTime_c AS time)
This is a time-of-day. It is not elapsed time. That distinction matters.
Date to Text
Use this when you need a display-friendly value.
CONVERT(varchar(10), OrderHed.OrderDate, 23)
Returns:
2026-06-16
Style 23 is ISO date format:
yyyy-mm-dd
This is fine for display. It is not ideal for further date logic because the result is now text.
DateTime to Text
For a display value with milliseconds:
CONVERT(varchar(23), SomeTable.SomeDateTimeField, 121)
Returns:
2026-06-16 14:30:45.123
For ISO-style datetime:
CONVERT(varchar(30), SomeTable.SomeDateTimeField, 126)
Returns:
2026-06-16T14:30:45.123
Useful SQL CONVERT styles:
| Style | Output |
|---|---|
23 |
yyyy-mm-dd |
108 |
hh:mi:ss |
112 |
yyyymmdd |
120 |
yyyy-mm-dd hh:mi:ss |
121 |
yyyy-mm-dd hh:mi:ss.mmm |
126 |
yyyy-mm-ddThh:mi:ss.mmm |
There are many more style codes, but these are the ones I would normally reach for first. Prefer four-digit years. Avoid ambiguous regional formats unless you have no choice.
Time to Text
If the source is a datetime field and you want just the time as text:
CONVERT(varchar(8), SomeTable.SomeDateTimeField, 108)
Returns:
14:30:45
If the field is already a SQL time field:
CONVERT(varchar(8), SomeTable.SomeTimeField, 108)
Again, this is display logic. Once converted to text, it is no longer a time value.
Text to Date
Sometimes you inherit a UD field or imported value that contains a date stored as text.
If the text is in ISO format:
2026-06-16
You can convert it like this:
CONVERT(date, UD01.ShortChar01, 23)
The safer BAQ version is usually:
TRY_CONVERT(date, UD01.ShortChar01, 23)
If the value cannot be converted, TRY_CONVERT returns NULL instead of failing the entire query.
That is usually what you want in a BAQ. One bad row should not take down the whole query.
Text to DateTime
If the text value looks like this:
2026-06-16T14:30:45
or this:
2026-06-16T14:30:45.123
Use:
TRY_CONVERT(datetime2, UD01.Character01, 126)
The 126 style is for ISO-style datetime text.
Find Bad Text Dates
This is useful when cleaning up old UD fields where someone stored dates as text.
Calculated field expression:
CASE
WHEN UD01.ShortChar01 IS NULL THEN 'Blank'
WHEN TRY_CONVERT(date, UD01.ShortChar01, 23) IS NULL THEN 'Invalid'
ELSE 'Valid'
END
You could also use this kind of expression as part of a cleanup BAQ to identify bad rows before trying to convert or migrate the data.
Date Difference
Days between two dates:
DATEDIFF(day, OrderHed.OrderDate, OrderHed.NeedByDate)
Hours between two datetimes:
DATEDIFF(hour, LaborDtl.ClockInDateTime_c, LaborDtl.ClockOutDateTime_c)
Minutes between two datetimes:
DATEDIFF(minute, LaborDtl.ClockInDateTime_c, LaborDtl.ClockOutDateTime_c)
A more precise labor-hours-style expression:
DATEDIFF(minute, LaborDtl.ClockInDateTime_c, LaborDtl.ClockOutDateTime_c) / 60.0
The / 60.0 matters. If you use / 60, SQL may treat it as integer division depending on the expression.
Add Days, Hours, or Minutes
Add seven days:
DATEADD(day, 7, OrderHed.OrderDate)
Subtract thirty days:
DATEADD(day, -30, OrderHed.OrderDate)
Add two hours:
DATEADD(hour, 2, SomeTable.SomeDateTimeField)
Add fifteen minutes:
DATEADD(minute, 15, SomeTable.SomeDateTimeField)
This is useful in calculated fields, but also in BAQ criteria when comparing against relative dates.
Build a Date from Parts
Sometimes you have year, month, and day stored separately.
DATEFROMPARTS(SomeTable.YearNum, SomeTable.MonthNum, SomeTable.DayNum)
Example:
DATEFROMPARTS(FiscalPer.FiscalYear, FiscalPer.FiscalPeriod, 1)
Use this only when the pieces really are valid date parts. If the source values are questionable, validate them first.
Build a DateTime from Parts
DATETIME2FROMPARTS
(
SomeTable.YearNum,
SomeTable.MonthNum,
SomeTable.DayNum,
SomeTable.HourNum,
SomeTable.MinuteNum,
0,
0,
0
)
This is not common in normal BAQs, but it is useful when integrating ugly legacy data.
BAQ Criteria Examples
This is where users get tripped up the most.
Filtering a DateTime Field for One Day
This is the tempting pattern:
CAST(OrderHed.OrderDate AS date) = @TargetDate
It is simple, and it often works.
But it applies a function to the database column. That can make the query less efficient because SQL has to compute that expression against the field.
The better pattern is a range:
OrderHed.OrderDate >= @TargetDate
and:
OrderHed.OrderDate < DATEADD(day, 1, @TargetDate)
In BAQ Designer, that usually means two criteria rows:
| Field | Operation | Value |
|---|---|---|
OrderHed.OrderDate |
>= |
@TargetDate |
OrderHed.OrderDate |
< |
DATEADD(day, 1, @TargetDate) |
This gets every record on the target date, regardless of the time portion.
Date Range with Parameters
For a start date and end date:
OrderHed.OrderDate >= @StartDate
and:
OrderHed.OrderDate < DATEADD(day, 1, @EndDate)
This handles the full end date.
Be careful with this:
OrderHed.OrderDate <= @EndDate
If @EndDate is 2026-06-16, that usually means:
2026-06-16 00:00:00
So records later that day may not qualify.
This is one of the classic datetime bugs. The query looks right, but it drops records from the end date because the time portion was ignored.
Last 30 Days
If you want the last 30 days from the current moment:
OrderHed.OrderDate >= DATEADD(day, -30, GETDATE())
If you want from midnight 30 days ago:
OrderHed.OrderDate >= CAST(DATEADD(day, -30, GETDATE()) AS date)
Those are not the same thing.
The first one means 30 days ago at the current time.
The second one means the beginning of the day 30 days ago.
Today Only
For a datetime column, use a range.
Start of today:
OrderHed.OrderDate >= CAST(GETDATE() AS date)
Start of tomorrow:
OrderHed.OrderDate < DATEADD(day, 1, CAST(GETDATE() AS date))
Together, those return today’s records.
Avoid this pattern when you can:
CAST(OrderHed.OrderDate AS date) = CAST(GETDATE() AS date)
It is readable, but it wraps the database column in a function. The range version is usually the better habit.
This Month
Start of current month:
DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1)
Start of next month:
DATEADD(month, 1, DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1))
Criteria pattern:
OrderHed.OrderDate >= DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1)
and:
OrderHed.OrderDate < DATEADD(month, 1, DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1))
That gives you the current month without needing to care how many days are in the month.
BAQ CASE Expressions
Calculated fields often need to classify dates.
Past Due Flag
CASE
WHEN OrderHed.NeedByDate < CAST(GETDATE() AS date) THEN 1
ELSE 0
END
Past Due Text
CASE
WHEN OrderHed.NeedByDate < CAST(GETDATE() AS date) THEN 'Past Due'
WHEN OrderHed.NeedByDate = CAST(GETDATE() AS date) THEN 'Due Today'
ELSE 'Future'
END
Null-Safe Date Display
CASE
WHEN OrderHed.NeedByDate IS NULL THEN ''
ELSE CONVERT(varchar(10), OrderHed.NeedByDate, 23)
END
Null-Safe Date Math
CASE
WHEN OrderHed.OrderDate IS NULL OR OrderHed.NeedByDate IS NULL THEN NULL
ELSE DATEDIFF(day, OrderHed.OrderDate, OrderHed.NeedByDate)
END
That kind of null handling matters in BAQs. A calculated field that works for 99% of rows but fails or misleads on the remaining 1% is still a problem.
C# and LINQ Equivalents
The C# side has the same conceptual problem. The names are different, but the same distinction matters.
| C# Type | Meaning |
|---|---|
DateTime |
Date plus time, with a Kind value |
DateTimeOffset |
Date plus time plus offset |
DateOnly |
Date only |
TimeOnly |
Time of day only |
TimeSpan |
Duration, not a time-of-day |
DateOnly and TimeOnly are the better semantic match when the value is truly date-only or time-only, but they are not available everywhere. If you are in older .NET Framework code, you are probably still dealing mostly with DateTime.
DateTime to DateOnly and TimeOnly
DateTime dt = new DateTime(2026, 6, 16, 14, 30, 45);
DateOnly d = DateOnly.FromDateTime(dt);
TimeOnly t = TimeOnly.FromDateTime(dt);
DateOnly plus TimeOnly to DateTime
DateOnly d = new DateOnly(2026, 6, 16);
TimeOnly t = new TimeOnly(14, 30, 45);
DateTime dt = d.ToDateTime(t);
DateTime to Formatted String
DateTime dt = new DateTime(2026, 6, 16, 14, 30, 45);
string dateOnly = dt.ToString("yyyy-MM-dd");
string timeOnly = dt.ToString("HH:mm:ss");
string dateTime = dt.ToString("yyyy-MM-ddTHH:mm:ss");
This is display logic. It creates strings. Do not do this early if you still need to sort, compare, filter, or perform date math.
String to DateTime
This works, but it depends on parsing rules:
string value = "2026-06-16T14:30:45";
DateTime dt = DateTime.Parse(value);
Better when you know the exact format:
using System.Globalization;
string value = "2026-06-16T14:30:45";
DateTime dt = DateTime.ParseExact(
value,
"yyyy-MM-ddTHH:mm:ss",
CultureInfo.InvariantCulture
);
Best when the data might be bad:
using System.Globalization;
string value = "2026-06-16";
bool ok = DateTime.TryParseExact(
value,
"yyyy-MM-dd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out DateTime parsed
);
if (ok)
{
// use parsed
}
else
{
// handle bad input
}
Same concept as SQL TRY_CONVERT. Bad input should be handled intentionally.
LINQ Examples
Assume this shape:
var targetDate = new DateTime(2026, 6, 16);
var nextDate = targetDate.AddDays(1);
Filter a DateTime Column for a Specific Day
Avoid this when the query is going to SQL:
var rows = db.Orders
.Where(o => o.OrderDate.Date == targetDate.Date);
It may translate, but it applies date conversion logic to the column.
Prefer the range pattern:
var rows = db.Orders
.Where(o => o.OrderDate >= targetDate &&
o.OrderDate < nextDate);
That is the same idea as the BAQ criteria pattern.
Keep the column clean. Put the boundary logic on the values.
Extract Date or Time in LINQ
var rows = db.Orders
.Select(o => new
{
o.OrderNum,
OrderDateOnly = o.OrderDate.Date,
OrderTimeOnly = o.OrderDate.TimeOfDay
});
This can be fine for projection or display, but I would still avoid using .Date casually in filters when a range comparison would be cleaner.
Add Days, Hours, or Minutes
var rows = db.Orders
.Where(o => o.OrderDate >= DateTime.Today.AddDays(-30));
var rows = db.Jobs
.Where(j => j.StartDateTime.AddHours(2) < DateTime.Now);
Use this when it represents the actual business rule. Be careful with DateTime.Now versus DateTime.Today versus UTC. They are not interchangeable.
DateOnly and TimeOnly in Newer Code
If your stack supports it, this is clearer:
DateOnly targetDate = new DateOnly(2026, 6, 16);
var rows = db.Orders
.Where(o => o.RequiredDate == targetDate);
For time-only logic:
TimeOnly start = new TimeOnly(8, 0);
TimeOnly end = new TimeOnly(17, 0);
var rows = db.Shifts
.Where(s => s.StartTime >= start &&
s.StartTime < end);
This is more explicit than forcing everything through DateTime.
Practical Rules
1. Do not store dates as text unless you have no choice
Text dates always become somebody else’s parsing problem later.
If you inherit text dates, use defensive conversion:
TRY_CONVERT(date, UD01.ShortChar01, 23)
or in C#:
DateTime.TryParseExact(
value,
"yyyy-MM-dd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out DateTime parsed
);
2. Use the narrowest correct type
If it is only a date, use a date.
If it is only a time-of-day, use a time.
If it is a timestamp, use a datetime.
If the offset matters, use something that preserves the offset.
Do not collapse different meanings into one field just because it is convenient today. That is how small shortcuts become long-term data quality problems.
3. Use ISO-style text when text is unavoidable
Prefer:
yyyy-MM-dd
and:
yyyy-MM-ddTHH:mm:ss
Avoid ambiguous text like:
06/16/2026
That may look harmless, but regional formatting assumptions can turn harmless text into bad data.
4. For datetime filtering, use ranges
This is the most important BAQ pattern:
SomeDateTimeField >= @StartDate
and:
SomeDateTimeField < DATEADD(day, 1, @EndDate)
This is better than converting the column to a date inside the criteria.
5. Avoid formatting too early
This is fine for display:
CONVERT(varchar(10), OrderHed.OrderDate, 23)
But this is better for date behavior:
CAST(OrderHed.OrderDate AS date)
Once you convert a date to text, it is text. You gave up date sorting, date filtering, and date math.
6. Do not confuse time-of-day with duration
This is a time:
14:30
This is a duration:
14.5 hours
They are not the same concept.
In SQL, time is a time-of-day. For elapsed time, you usually calculate a difference:
DATEDIFF(minute, LaborDtl.ClockInDateTime_c, LaborDtl.ClockOutDateTime_c) / 60.0
7. Be explicit about local time, UTC, and offsets
If the value crosses users, plants, servers, integrations, APIs, or time zones, ambiguity becomes a defect.
A date on a report may be local business context.
A system event timestamp may need UTC.
A customer-facing appointment may need an offset.
Those are different problems. Treat them differently.
Final Thought
Dates are not hard because the syntax is hard.
They are hard because systems blur different concepts into one field.
A date-only value, a time-of-day, a timestamp, a duration, a formatted string, and a timezone-aware value are all different things.
Keep the meaning clean, and the conversions get much easier.