This commit is contained in:
2021-09-02 19:09:19 +00:00
parent 733dfa0cfd
commit 7767c7d67c

View File

@@ -886,884 +886,124 @@ namespace AyaNova.DataList
{
StringBuilder sb = new StringBuilder();
//Column name
sb.Append(SqlColumnNameToFilter);
sb.Append(" ");
//handle null values separately
if (DataType != UiFieldDataType.Tags && sValue == "*NULL*")
{
switch (DataType)
{
//ALL TEXT TYPES
case UiFieldDataType.Text:
case UiFieldDataType.PhoneNumber:
case UiFieldDataType.EmailAddress:
case UiFieldDataType.HTTP:
{
if (sOperator == DataListFilterComparisonOperator.Equality)
{
// sb.Append(SqlColumnNameToFilter);
// sb.Append(" ");
sb.Append("Is Null");
sb.Append(" OR ");
sb.Append(SqlColumnNameToFilter);
sb.Append(" = ''");
}
else
sb.Append(" <> ''");
}
break;
default:
{
if (sOperator == DataListFilterComparisonOperator.Equality)
sb.Append("Is Null");
else
sb.Append("Is Not Null");
}
break;
}
}
else
{
//non null value
//Special addition to handle nulls
if (DataType != UiFieldDataType.Tags)
{
switch (sOperator)
//Build tags filter fragment
//https://www.postgresql.org/docs/current/functions-array.html
//https://www.postgresql.org/docs/9.1/functions-array.html
/*
For these reasons, am requesting the ability to filter on the Tag column to include:
**(no value) - to filter to show records that have no tags
**(has value) - to filter to show records that have any tags
**Not equal to - to filter to show records that do NOT have a specific tag
**Contains - to filter to show records that have tags with this text as part of the tag (i.e. be able to type in the text workordercategory so would return ALL workorder records that have one or more tags that have the text workordercategory in it - such as "inspection.workordercategory" or "service.workordercategory" or any one of the unit service types that now are a tag)
**NotContains MY ADDITION see below
//NOTE: all tags are lowercase so case is not an issue really with any of these queries
array operators for queries and examples:
where 'red'=ANY(tags) //case sensitive, matches any single item in array, must be in that order or it will bomb if ANY is first
//contains operator is the one I've been using: sbTags.Append("@> array[");
Filter ui builder allows multiple tag selection just like any other tag selection and sends the filter to the server like this:
filter "[{\"column\":\"customertags\",\"any\":false,\"items\":[{\"op\":\"=\",\"value\":\"completed.reminder,burnaby.dispatchzone,cabling-fibre.user-skill\"}]}]"
Comma separated list.
Current equalto filter doesn't work with this type of query as it directly compares it as if it was one string instead of does it contain all those terms
### HOW IT SHOULD WORK ###
Equality: Exactly equal (aside from order) compare entire search term array to tag array in db - all terms in search exactly present and no others in db record
contains and also length is same as search terms length, in this example 1 is the number of search terms as the search is for {'green'}
WHERE ARRAY['green'::varchar(255)] <@ tags and array_length(tags,1) = 1
Not equal: Not EXACTLY Equal (aside from order) All terms in search term array NOT present in All terms in db record. IOW if db record has all search terms but also one more tag then it's NOT equal, Order doesn't matter
Specifically exclude rows that exactly match all the search terms
WHERE NOT (ARRAY['green'::varchar(255)] <@ tags and array_length(tags,1) = 1)
NO value: Empty tag array in db record
db record tags has NO value
where tags = '{}'
Has value: Non empty tag array in db record
db record tags has value
where tags <> '{}'
Contains: All terms in search query present in db record, db record may have other tags but that's ok as long as it has the search term ones (order insensitive)
db record Tags contains search tags (and maybe also other tags)
WHERE ARRAY['red','green'::varchar(255)] <@ tags
Not contains: All terms in search query *NOT* present in db record, don't care what else is in db record as long as it's not the search terms (order insensitive)
db record Tags DOES NOT contain search tags
WHERE NOT ARRAY['red','green'::varchar(255)] <@ tags
This method and choices ensures users don't need to make a separate row for each one they can just treat the entire tag collection like they treat a single word match in a text field
*/
switch (sOperator)
{
case DataListFilterComparisonOperator.Equality:
{
case DataListFilterComparisonOperator.Equality:
//no change on equals for nulls
break;
case DataListFilterComparisonOperator.GreaterThan:
//no change on greater than for nulls
//(nulls are going to be assumed to be always at the
//less than end of the scale)
break;
case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
//no change on greater than for nulls
//(nulls are going to be assumed to be always at the
//less than end of the scale)
break;
case DataListFilterComparisonOperator.LessThan:
sb.Append("Is Null OR ");
sb.Append(SqlColumnNameToFilter);
sb.Append(" ");
break;
case DataListFilterComparisonOperator.LessThanOrEqualTo:
sb.Append("Is Null OR ");
sb.Append(SqlColumnNameToFilter);
sb.Append(" ");
break;
case DataListFilterComparisonOperator.NotEqual:
//This is the big one:
sb.Append("Is Null OR ");
sb.Append(SqlColumnNameToFilter);
sb.Append(" ");
break;
StringBuilder sbTags = new StringBuilder();
sbTags.Append("@> array[");
//Note: with listOptions change to split filter and view the tags are now in sValue as a string of comma delimited strings so split them out here
List<string> normalizedTags = TagBiz.NormalizeTags(sValue.Split(',').ToList<string>());
foreach (string s in normalizedTags)
{
sbTags.Append($"'{s}',");
}
sb.Append(sbTags.ToString().TrimEnd(','));
sb.Append("::varchar(255)]");
}
}
break;
case DataListFilterComparisonOperator.NotEqual:
sb.Append("<>'");
sb.Append(sValue);
sb.Append("'");
break;
#region Build for specific type
switch (DataType)
{
//ALL TEXT TYPES
case UiFieldDataType.Text:
case UiFieldDataType.PhoneNumber:
case UiFieldDataType.EmailAddress:
case UiFieldDataType.HTTP:
//escape any pre-existing apostrophes
//i.e. "O'Flaherty's pub"
sValue = sValue.Replace("'", "''");
case DataListFilterComparisonOperator.NotContains:
sb.Append("Not Like '%");
sb.Append(sValue);
sb.Append("%'");
break;
//case 1951 - unescape ampersands
sValue = sValue.Replace("&amp;", "&");
case DataListFilterComparisonOperator.Contains:
//RAVEN NOTE: Decided not to implement TEXT token criteria for now (TTM, probably not useful)
//meaning the A-H etc group ranges by alpha bet chunk
//TagSpecificWhereFragment = $"(array_to_string({tagColumn.GetSqlValueColumnName()},',') like '%{tagSpecificQuery}%')";
sb.Append("Like '%");
sb.Append(sValue);
sb.Append("%'");
break;
#region Build TEXT OPS criteria
switch (sOperator)
{
case DataListFilterComparisonOperator.Equality:
sb.Append("='");
sb.Append(sValue);
sb.Append("'");
break;
default:
throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] IN TAGS");
case DataListFilterComparisonOperator.GreaterThan:
sb.Append(">'");
sb.Append(sValue);
sb.Append("'");
break;
}
case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
sb.Append(">='");
sb.Append(sValue);
sb.Append("'");
break;
//NOTE: tag filter OPERATOR is ignored, query matches if all tags are found in a tag collection that could have other tags as well
case DataListFilterComparisonOperator.LessThan:
sb.Append("<'");
sb.Append(sValue);
sb.Append("'");
break;
//for initial release a tag filter is inclusive of all tags only
//in other words all tags presented must be in record to match (simple AND)
//select * from awidget where awidget.tags @> array['blah','blah3'::varchar(255)]
case DataListFilterComparisonOperator.LessThanOrEqualTo:
sb.Append("<='");
sb.Append(sValue);
sb.Append("'");
break;
//NOTE: After coding this discovered *can* do a LIKE query with tags like this:
//(array_to_string(awidget.tags,',') like '%zo%')
//implemented that way in picklistquery builder
//also ilike is a postgres case insensitive like but works on current locale of server which
//sounds like it might fuck up when using other languages so...
case DataListFilterComparisonOperator.NotEqual:
sb.Append("<>'");
sb.Append(sValue);
sb.Append("'");
break;
/* NOTE: CASE - I decided to NOT do case insensitive for now for datalists like I did for picklists because it's a bit of a different situation
I would need to make big changes to the select builder and this criteria builder so it's TTM at the moment because it's all tested and working
however also the use is a bit different in that it's much more necessary to be hyper accurate here since this drives reporting and potentially a lot
of important business data. Matching two different clients inadvertantly because of a case issue would be a bit of a kerfuffle potentially so I'd rather err on the side of accuracy
and also I'm not certain how the case code will work with other languages so it's a bit more risky here, if a picklist doesn't work I get a support call but if a report is missing data then maybe
the user doesn't realize it and has bad reports.
code from picklist in case I decide to do it later
if (ServerGlobalBizSettings.SearchCaseSensitiveOnly)
sWhere = $"({valueColumnName} like '%{autoCompleteQuery}%')";
else
sWhere = $"(lower({valueColumnName}) like lower('%{autoCompleteQuery}%'))";
*/
//Following 7 operators added 14-June-2006
case DataListFilterComparisonOperator.NotContains:
sb.Append("Not Like '%");
sb.Append(sValue);
sb.Append("%'");
break;
case DataListFilterComparisonOperator.Contains:
sb.Append("Like '%");
sb.Append(sValue);
sb.Append("%'");
break;
case DataListFilterComparisonOperator.StartsWith:
sb.Append("Like '");
sb.Append(sValue);
sb.Append("%'");
break;
case DataListFilterComparisonOperator.EndsWith:
sb.Append("Like '%");
sb.Append(sValue);
sb.Append("'");
break;
default:
throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] IN STRING");
}
#endregion build text ops criteria
break;
case UiFieldDataType.Bool:
{
switch (sOperator)
{
case DataListFilterComparisonOperator.Equality:
sb.Append("= ");
if (sValue.ToLowerInvariant() == "true")
sb.Append("true");
else
sb.Append("false");
break;
case DataListFilterComparisonOperator.NotEqual:
sb.Append("<> ");
if (sValue.ToLowerInvariant() == "true")
sb.Append("true");
else
sb.Append("false");
break;
default:
throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] in BOOL");
}
}
break;
//Note there are three types here for display purposes but all are stored in the db as a timestamp the same with date and time components
case UiFieldDataType.Date:
case UiFieldDataType.DateTime:
case UiFieldDataType.Time:
{
if (sValue.StartsWith("*") && sValue.EndsWith("*"))
{
//Note: due to date token relativity, all below calcs done with Client provided current date/time and converted to UTC only at the end for sql
DateTime ClientToday = clientTimeStamp.Date;
DateTime ClientNow = clientTimeStamp.DateTime;
#region Build criteria for date RANGE TOKEN specified
//Used as the basis point
System.DateTime dtAfter;
System.DateTime dtBefore;
switch (sValue)
{
//Case 402
case "*yesterday*":
//Between Day before yesterday at midnight and yesterday at midnight
dtAfter = ClientToday.AddDays(-1);
dtAfter = dtAfter.AddSeconds(-1);
dtBefore = ClientToday;
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*today*":
//Between yesterday at midnight and tommorow at midnight
dtAfter = ClientToday.AddSeconds(-1);
dtBefore = ClientToday.AddDays(1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*tomorrow*":
//Between Tonight at midnight and day after tommorow at midnight
dtAfter = ClientToday.AddDays(1).AddSeconds(-1);
dtBefore = ClientToday.AddDays(2);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*lastweek*":
//Between two Sundays ago at midnight and last sunday at midnight
dtAfter = ClientToday;
//go back a week
dtAfter = dtAfter.AddDays(-7);
//go backwards to Sunday
while (dtAfter.DayOfWeek != DayOfWeek.Sunday)
dtAfter = dtAfter.AddDays(-1);
//go to very start of eighth dayahead
dtBefore = dtAfter.AddDays(8);
dtAfter = dtAfter.AddSeconds(-1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*thisweek*":
//Between Sunday at midnight and Next sunday at midnight
dtAfter = ClientToday;
//go backwards to monday
while (dtAfter.DayOfWeek != DayOfWeek.Monday)
dtAfter = dtAfter.AddDays(-1);
//Now go back to sunday last second
dtAfter = dtAfter.AddSeconds(-1);
dtBefore = ClientToday;
//go forwards to monday
if (ClientToday.DayOfWeek == DayOfWeek.Monday)
{
//Monday today? then go to next monday
dtBefore = dtBefore.AddDays(7);
}
else
{
while (dtBefore.DayOfWeek != DayOfWeek.Monday)
dtBefore = dtBefore.AddDays(1);
}
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*nextweek*":
//Between Next Sunday at midnight and Next Next sunday at midnight
dtAfter = ClientToday;
//If today is monday skip over it first
if (dtAfter.DayOfWeek == DayOfWeek.Monday)
dtAfter = dtAfter.AddDays(1);
//go forwards to next monday
while (dtAfter.DayOfWeek != DayOfWeek.Monday)
dtAfter = dtAfter.AddDays(1);
//Now go back to sunday last second
dtAfter = dtAfter.AddDays(-1);
//go seven days ahead
dtBefore = dtAfter.AddDays(7);
dtAfter = dtAfter.AddSeconds(-1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*lastmonth*":
//start with the first day of this month
dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);
//subtract a Month
dtAfter = dtAfter.AddMonths(-1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.AddMonths(1);
//case 1155
dtAfter = dtAfter.AddSeconds(-1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*thismonth*":
//start with the first day of this month
dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.AddMonths(1);
//case 1155
dtAfter = dtAfter.AddSeconds(-1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*nextmonth*":
//start with the first day of this month
dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);
//Add a Month
dtAfter = dtAfter.AddMonths(1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.AddMonths(1);
//case 1155
dtAfter = dtAfter.AddSeconds(-1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*14daywindow*":
//start with today zero hour
dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, ClientToday.Day);
dtAfter = dtAfter.AddDays(-7);
//Add 15 days to get end date (zero hour so not really 15 full days)
dtBefore = dtAfter.AddDays(15);
//case 1155
dtAfter = dtAfter.AddSeconds(-1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*past*":
//Forever up to Now
dtAfter = new DateTime(1753, 1, 2);//this was for sql server but even then was probably outdated good enough though for our purposes
dtBefore = ClientNow;
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*future*":
//From Now to forever (999 years from now)
dtAfter = ClientNow;
dtBefore = dtAfter.AddYears(999);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*lastyear*":
//From zero hour january 1 a year ago
dtAfter = new DateTime(ClientNow.AddYears(-1).Year, 1, 1, 0, 0, 00).AddSeconds(-1);
//To zero hour January 1 this year
dtBefore = new DateTime(ClientNow.Year, 1, 1, 0, 0, 00);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*thisyear*":
//From zero hour january 1 this year
dtAfter = new DateTime(ClientNow.Year, 1, 1).AddSeconds(-1);
//To zero hour Jan 1 next year
dtBefore = new DateTime(ClientNow.AddYears(1).Year, 1, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*last3months*":
//From Now minus 3 months
dtAfter = ClientNow.AddMonths(-3).AddSeconds(-1);
//To Now
dtBefore = ClientNow;
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*last6months*":
//From Now minus 6 months
dtAfter = ClientNow.AddMonths(-6).AddSeconds(-1);
//To Now
dtBefore = ClientNow;
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*pastyear*": //within the prior 365 days before today
//From Now minus 365 days
dtAfter = ClientNow.AddDays(-365).AddSeconds(-1);
//To Now
dtBefore = ClientNow;
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*past90days*":
//From Now minus 90 days
dtAfter = ClientNow.AddDays(-90).AddSeconds(-1);
//To Now
dtBefore = ClientNow;
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*past30days*":
//From Now minus 30 days
dtAfter = ClientNow.AddDays(-30).AddSeconds(-1);
//To Now
dtBefore = ClientNow;
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*past7days*":
//From Now minus 7 days
dtAfter = ClientNow.AddDays(-7).AddSeconds(-1);
//To Now
dtBefore = ClientNow;
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*past24hours*":
//From Now minus 24 hours
dtAfter = ClientNow.AddHours(-24).AddSeconds(-1);
//To Now
dtBefore = ClientNow;
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*past6hours*":
//From Now minus 6 hours
dtAfter = ClientNow.AddHours(-6).AddSeconds(-1);
//To Now
dtBefore = ClientNow;
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*january*":
//From zero hour january 1 this year
dtAfter = new DateTime(ClientNow.Year, 1, 1).AddSeconds(-1);
//To zero hour feb 1 this year
dtBefore = new DateTime(ClientNow.Year, 2, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*february*":
dtAfter = new DateTime(ClientNow.Year, 2, 1).AddSeconds(-1);
dtBefore = new DateTime(ClientNow.Year, 3, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*march*":
dtAfter = new DateTime(ClientNow.Year, 3, 1).AddSeconds(-1);
dtBefore = new DateTime(ClientNow.Year, 4, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*april*":
dtAfter = new DateTime(ClientNow.Year, 4, 1).AddSeconds(-1);
dtBefore = new DateTime(ClientNow.Year, 5, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*may*":
dtAfter = new DateTime(ClientNow.Year, 5, 1).AddSeconds(-1);
dtBefore = new DateTime(ClientNow.Year, 6, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*june*":
dtAfter = new DateTime(ClientNow.Year, 6, 1).AddSeconds(-1);
dtBefore = new DateTime(ClientNow.Year, 7, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*july*":
dtAfter = new DateTime(ClientNow.Year, 7, 1).AddSeconds(-1);
dtBefore = new DateTime(ClientNow.Year, 8, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*august*":
dtAfter = new DateTime(ClientNow.Year, 8, 1).AddSeconds(-1);
dtBefore = new DateTime(ClientNow.Year, 9, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*september*":
dtAfter = new DateTime(ClientNow.Year, 9, 1).AddSeconds(-1);
dtBefore = new DateTime(ClientNow.Year, 10, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*october*":
dtAfter = new DateTime(ClientNow.Year, 10, 1).AddSeconds(-1);
dtBefore = new DateTime(ClientNow.Year, 11, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*november*":
dtAfter = new DateTime(ClientNow.Year, 11, 1).AddSeconds(-1);
dtBefore = new DateTime(ClientNow.Year, 12, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*december*":
//From zero hour dec 1 this year
dtAfter = new DateTime(ClientNow.Year, 12, 1).AddSeconds(-1);
//To zero hour Jan 1 next year
dtBefore = new DateTime(ClientNow.AddYears(1).Year, 1, 1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*lastyearlastmonth*":
//start with the first day of this month
dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);
//subtract a year and a Month
dtAfter = dtAfter.AddYears(-1).AddMonths(-1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.AddMonths(1);
dtAfter = dtAfter.AddSeconds(-1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*lastyearthismonth*":
//start with the first day of this month
dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);
//subtract a year
dtAfter = dtAfter.AddYears(-1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.AddMonths(1);
dtAfter = dtAfter.AddSeconds(-1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
case "*lastyearnextmonth*":
//start with the first day of this month
dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);
//subtract a year
dtAfter = dtAfter.AddYears(-1);
//Add a Month
dtAfter = dtAfter.AddMonths(1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.AddMonths(1);
dtAfter = dtAfter.AddSeconds(-1);
BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
break;
default:
throw new System.ArgumentOutOfRangeException("TOKEN", sOperator, "DataListSqlFilterCriteriaBuilder invalid filter TOKEN type [" + sValue + "] IN DATE_TIME");
//-----
}
#endregion
}
else
{
#region Build criteria for date specified
//dates come in iso8601 UTC format from the client
//suitable for the database to handle as all database dates are in UTC
//Local display and parsing will be considered a CLIENT issue at all times
//comes in UTC, parse converts it to local but touniversal puts it back in utc, weird but only thing that consistently work
System.DateTime dtData = DateTime.Parse(sValue).ToUniversalTime();
//Filter time resolution is in minutes only, you can't select seconds in a filter
//so the filter code below must construct a filter time
//that falls within the 0th second, 0th millisecond and 59th second and 999th millisecond of the specified time
//to ensure it matches the users selections
string sDateValueWithZeroSeconds = PostgresDateFormat(ZeroSeconds(dtData));
string sDateValueWithMaxSeconds = PostgresDateFormat(MaxSeconds(dtData));
switch (sOperator)
{
case DataListFilterComparisonOperator.Equality:
sb.Append(">='");
sb.Append(sDateValueWithZeroSeconds);
sb.Append("' AND ");
sb.Append(SqlColumnNameToFilter);
sb.Append(" ");
sb.Append("<='");
sb.Append(sDateValueWithMaxSeconds);
sb.Append("'");
break;
case DataListFilterComparisonOperator.GreaterThan:
sb.Append(">'");
sb.Append(sDateValueWithMaxSeconds);
sb.Append("'");
break;
case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
sb.Append(">='");
sb.Append(sDateValueWithMaxSeconds);
sb.Append("'");
break;
case DataListFilterComparisonOperator.LessThan:
sb.Append("<'");
sb.Append(sDateValueWithMaxSeconds);
sb.Append("'");
break;
case DataListFilterComparisonOperator.LessThanOrEqualTo:
sb.Append("<='");
sb.Append(sDateValueWithMaxSeconds);
sb.Append("'");
break;
case DataListFilterComparisonOperator.NotEqual:
sb.Append("<'");
sb.Append(sDateValueWithMaxSeconds);
sb.Append("' OR ");
sb.Append(SqlColumnNameToFilter);
sb.Append(" ");
sb.Append(">'");
sb.Append(sDateValueWithMaxSeconds);
sb.Append("'");
break;
default:
throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] IN DATE_TIME");
}
#endregion
}
}
break;
case UiFieldDataType.Enum://enums are just ints to the db, but it's a special type so the client can recognize it
case UiFieldDataType.MemorySize://memory / file size, just a long but this type is for display formatting only
case UiFieldDataType.Decimal:
case UiFieldDataType.Currency:
case UiFieldDataType.InternalId:
case UiFieldDataType.Integer: //whole numbers, not only integer
{
//case 1795 - it's numeric, convert to translation independant format
//RAVEN NOTE: no numbers are coming from the client in any culture format other than xx,xxx.00 but this is just insurance for api users
NumberFormatInfo nfi = System.Globalization.CultureInfo.CurrentCulture.NumberFormat;
switch (DataType)
{
case UiFieldDataType.Decimal:
case UiFieldDataType.Currency:
{
if (nfi.CurrencyDecimalSeparator != ".")
{
sValue = sValue.Replace(nfi.CurrencyGroupSeparator, "");
sValue = sValue.Replace(nfi.CurrencyDecimalSeparator, ".");
}
}
break;
case UiFieldDataType.Integer:
{
if (nfi.NumberDecimalSeparator != ".")
{
sValue = sValue.Replace(nfi.NumberGroupSeparator, "");
sValue = sValue.Replace(nfi.NumberDecimalSeparator, ".");
}
}
break;
case UiFieldDataType.InternalId:
{
//do nothing, it's a simple number
}
break;
}
switch (sOperator)
{
case DataListFilterComparisonOperator.Equality:
sb.Append("=");
sb.Append(sValue);
break;
case DataListFilterComparisonOperator.GreaterThan:
sb.Append(">");
sb.Append(sValue);
break;
case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
sb.Append(">=");
sb.Append(sValue);
break;
case DataListFilterComparisonOperator.LessThan:
sb.Append("<");
sb.Append(sValue);
break;
case DataListFilterComparisonOperator.LessThanOrEqualTo:
sb.Append("<=");
sb.Append(sValue);
break;
case DataListFilterComparisonOperator.NotEqual:
sb.Append("<>");
sb.Append(sValue);
break;
default:
throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] IN NUMBER");
}
break;
}
case UiFieldDataType.Tags:
{
//Build tags filter fragment
//https://www.postgresql.org/docs/current/functions-array.html
//https://www.postgresql.org/docs/9.1/functions-array.html
/*
For these reasons, am requesting the ability to filter on the Tag column to include:
**(no value) - to filter to show records that have no tags
**(has value) - to filter to show records that have any tags
**Not equal to - to filter to show records that do NOT have a specific tag
**Contains - to filter to show records that have tags with this text as part of the tag (i.e. be able to type in the text workordercategory so would return ALL workorder records that have one or more tags that have the text workordercategory in it - such as "inspection.workordercategory" or "service.workordercategory" or any one of the unit service types that now are a tag)
**NotContains MY ADDITION see below
//NOTE: all tags are lowercase so case is not an issue really with any of these queries
array operators for queries and examples:
where 'red'=ANY(tags) //case sensitive, matches any single item in array, must be in that order or it will bomb if ANY is first
//contains operator is the one I've been using: sbTags.Append("@> array[");
Filter ui builder allows multiple tag selection just like any other tag selection and sends the filter to the server like this:
filter "[{\"column\":\"customertags\",\"any\":false,\"items\":[{\"op\":\"=\",\"value\":\"completed.reminder,burnaby.dispatchzone,cabling-fibre.user-skill\"}]}]"
Comma separated list.
Current equalto filter doesn't work with this type of query as it directly compares it as if it was one string instead of does it contain all those terms
### HOW IT SHOULD WORK ###
Equality: Exactly equal (aside from order) compare entire search term array to tag array in db - all terms in search exactly present and no others in db record
contains and also length is same as search terms length, in this example 1 is the number of search terms as the search is for {'green'}
WHERE ARRAY['green'::varchar(255)] <@ tags and array_length(tags,1) = 1
Not equal: Not EXACTLY Equal (aside from order) All terms in search term array NOT present in All terms in db record. IOW if db record has all search terms but also one more tag then it's NOT equal, Order doesn't matter
Specifically exclude rows that exactly match all the search terms
WHERE NOT (ARRAY['green'::varchar(255)] <@ tags and array_length(tags,1) = 1)
NO value: Empty tag array in db record
db record tags has NO value
where tags = '{}'
Has value: Non empty tag array in db record
db record tags has value
where tags <> '{}'
Contains: All terms in search query present in db record, db record may have other tags but that's ok as long as it has the search term ones (order insensitive)
db record Tags contains search tags (and maybe also other tags)
WHERE ARRAY['red','green'::varchar(255)] <@ tags
Not contains: All terms in search query *NOT* present in db record, don't care what else is in db record as long as it's not the search terms (order insensitive)
db record Tags DOES NOT contain search tags
WHERE NOT ARRAY['red','green'::varchar(255)] <@ tags
This method and choices ensures users don't need to make a separate row for each one they can just treat the entire tag collection like they treat a single word match in a text field
*/
switch (sOperator)
{
case DataListFilterComparisonOperator.Equality:
{
StringBuilder sbTags = new StringBuilder();
sbTags.Append("@> array[");
//Note: with listOptions change to split filter and view the tags are now in sValue as a string of comma delimited strings so split them out here
List<string> normalizedTags = TagBiz.NormalizeTags(sValue.Split(',').ToList<string>());
foreach (string s in normalizedTags)
{
sbTags.Append($"'{s}',");
}
sb.Append(sbTags.ToString().TrimEnd(','));
sb.Append("::varchar(255)]");
}
break;
case DataListFilterComparisonOperator.NotEqual:
sb.Append("<>'");
sb.Append(sValue);
sb.Append("'");
break;
case DataListFilterComparisonOperator.NotContains:
sb.Append("Not Like '%");
sb.Append(sValue);
sb.Append("%'");
break;
case DataListFilterComparisonOperator.Contains:
//TagSpecificWhereFragment = $"(array_to_string({tagColumn.GetSqlValueColumnName()},',') like '%{tagSpecificQuery}%')";
sb.Append("Like '%");
sb.Append(sValue);
sb.Append("%'");
break;
default:
throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] IN TAGS");
}
//NOTE: tag filter OPERATOR is ignored, query matches if all tags are found in a tag collection that could have other tags as well
//for initial release a tag filter is inclusive of all tags only
//in other words all tags presented must be in record to match (simple AND)
//select * from awidget where awidget.tags @> array['blah','blah3'::varchar(255)]
//NOTE: After coding this discovered *can* do a LIKE query with tags like this:
//(array_to_string(awidget.tags,',') like '%zo%')
//implemented that way in picklistquery builder
//also ilike is a postgres case insensitive like but works on current locale of server which
//sounds like it might fuck up when using other languages so...
}
break;
case UiFieldDataType.TimeSpan: //TIMESPAN / DURATION
{
sValue = TimeSpanToPostgresInterval(sValue);
/*
{ name: vm.$ay.t("GridRowFilterDropDownGreaterThan"), id: ">" },
{
name: vm.$ay.t("GridRowFilterDropDownGreaterThanOrEqualTo"),
id: ">="
},
{ name: vm.$ay.t("GridRowFilterDropDownLessThan"), id: "<" },
{ name: vm.$ay.t("GridRowFilterDropDownLessThanOrEqualTo"), id: "<=" },
{ name: vm.$ay.t("GridRowFilterDropDownNotEquals"), id: "!=" }
*/
switch (sOperator)
{
case DataListFilterComparisonOperator.Equality:
sb.Append("=");
sb.Append(sValue);
break;
case DataListFilterComparisonOperator.GreaterThan:
sb.Append(">");
sb.Append(sValue);
break;
case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
sb.Append(">=");
sb.Append(sValue);
break;
case DataListFilterComparisonOperator.LessThan:
sb.Append("<");
sb.Append(sValue);
break;
case DataListFilterComparisonOperator.LessThanOrEqualTo:
sb.Append("<=");
sb.Append(sValue);
break;
case DataListFilterComparisonOperator.NotEqual:
sb.Append("<>");
sb.Append(sValue);
break;
default:
throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] IN TIMESPAN / DURATION");
}
break;
}
default:
throw new System.ArgumentOutOfRangeException("DATA_TYPE", DataType, "DataListSqlFilterCriteriaBuilder unhandled data type[" + DataType + "]");
}
#endregion
}//end of nonnull path
return sb.ToString();