case 4690 - data-list date filters: >= and < from the start of the minute, != excludes the whole minute; null check first

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 20:38:23 -07:00
parent 892ef59608
commit 9af1034a88
2 changed files with 13 additions and 11 deletions

View File

@@ -119,7 +119,7 @@ in this branch or a follow-up release. UPDATE: John has decided these all go in
- [x] **Drop-and-recreate database.** Fixed: before the `DROP`, `DropAndRecreateDbAsync` checks `rolcreatedb OR rolsuper` for the current user; without it, boot stops with an E1000 message naming the user and the setting, and the database is untouched. Confirmed: `DbUtil.cs:342-345` drops, then creates; triggered by `AYANOVA_PERMANENTLY_ERASE_DATABASE` (`Startup.cs:567-570`). Without `CREATEDB` it drops the database and then fails to create it. Test the permission before dropping, or change the order - [x] **Drop-and-recreate database.** Fixed: before the `DROP`, `DropAndRecreateDbAsync` checks `rolcreatedb OR rolsuper` for the current user; without it, boot stops with an E1000 message naming the user and the setting, and the database is untouched. Confirmed: `DbUtil.cs:342-345` drops, then creates; triggered by `AYANOVA_PERMANENTLY_ERASE_DATABASE` (`Startup.cs:567-570`). Without `CREATEDB` it drops the database and then fails to create it. Test the permission before dropping, or change the order
- [x] **Report browser download location.** Fixed: downloads into `<AYANOVA_DATA_PATH>/report-browser` (derived `ServerBootConfig.REPORT_RENDER_BROWSER_DOWNLOAD_PATH`, not a new setting; empty when no data path is set, then beside the app as before) through `BrowserFetcherOptions.Path`, and launches the downloaded executable explicitly. `AYANOVA_REPORT_RENDER_BROWSER_PATH` still takes precedence. Existing installs keep an unused old download beside the app until it is removed. Confirmed: `new BrowserFetcher()` with the default path beside the app (`ReportBiz.cs:800`). Point it under the data directory in code. Beside the app it is read-only under a hardened systemd unit (`ProtectSystem=strict`), and a redeploy that replaces the app folder deletes it - [x] **Report browser download location.** Fixed: downloads into `<AYANOVA_DATA_PATH>/report-browser` (derived `ServerBootConfig.REPORT_RENDER_BROWSER_DOWNLOAD_PATH`, not a new setting; empty when no data path is set, then beside the app as before) through `BrowserFetcherOptions.Path`, and launches the downloaded executable explicitly. `AYANOVA_REPORT_RENDER_BROWSER_PATH` still takes precedence. Existing installs keep an unused old download beside the app until it is removed. Confirmed: `new BrowserFetcher()` with the default path beside the app (`ReportBiz.cs:800`). Point it under the data directory in code. Beside the app it is read-only under a hardened systemd unit (`ProtectSystem=strict`), and a redeploy that replaces the app folder deletes it
- [x] **Report failures: return a specific error.** Done: download and launch each in their own `try`; on failure `HandleBrowserFailure` logs at Error (was Debug via the job processor) and writes `rendererror.exception` ("Report rendering browser could not be downloaded to … / started from … with arguments …: (reason)"), which the client already shows as `JobFailed: …`; no client change. The Sockeye crash is n/a: rendering runs as a job, and `JobsBiz.ProcessJobAsync` catches every exception, logs it through NLog and marks the job Failed (`JobsBiz.cs:390-439`), so the process survives. What remains: a browser download or launch failure (`ReportBiz.cs:800`, `:838`, outside the render `try`) reaches the client only as a generic job failure. Catch it in `DoRenderJob` and return a render error that says what failed - [x] **Report failures: return a specific error.** Done: download and launch each in their own `try`; on failure `HandleBrowserFailure` logs at Error (was Debug via the job processor) and writes `rendererror.exception` ("Report rendering browser could not be downloaded to … / started from … with arguments …: (reason)"), which the client already shows as `JobFailed: …`; no client change. The Sockeye crash is n/a: rendering runs as a job, and `JobsBiz.ProcessJobAsync` catches every exception, logs it through NLog and marks the job Failed (`JobsBiz.cs:390-439`), so the process survives. What remains: a browser download or launch failure (`ReportBiz.cs:800`, `:838`, outside the render `try`) reaches the client only as a generic job failure. Catch it in `DoRenderJob` and return a render error that says what failed
- [ ] **Data-list date filters, small fixes.** Confirmed: `>=` (`:790-793`) and `<` (`:795-798`) use `MaxSeconds` and should use `ZeroSeconds`; date `NotEqual` (`:806-814`) uses `MaxSeconds` on both sides and should exclude the whole minute (`< ZeroSeconds OR > MaxSeconds`). `DataListField` is dereferenced at `:24` and `:27` before the DEBUG-only null check at `:33`, so **release builds** throw a NullReferenceException on an unknown column, not only DEBUG builds - [x] **Data-list date filters, small fixes.** Fixed: `>=` and `<` use `ZeroSeconds`; `!=` is `< ZeroSeconds OR > MaxSeconds`; the `DataListField` null check runs first and in all builds, throwing `ArgumentException` naming the column. (An unknown column sent to `/data-list` is in practice rejected earlier by `DataListSqlSelectBuilder`, with its own "DEV ERROR" message; unchanged.) Sockeye 8.1.0 still has the old code. Confirmed: `>=` (`:790-793`) and `<` (`:795-798`) use `MaxSeconds` and should use `ZeroSeconds`; date `NotEqual` (`:806-814`) uses `MaxSeconds` on both sides and should exclude the whole minute (`< ZeroSeconds OR > MaxSeconds`). `DataListField` is dereferenced at `:24` and `:27` before the DEBUG-only null check at `:33`, so **release builds** throw a NullReferenceException on an unknown column, not only DEBUG builds
- [ ] **Relative date tokens use the server's time zone** (`*today*`, `*yesterday*` …). Confirmed: `ClientToday = clientTimeStamp.Date` (`:399-400`) has `Kind=Unspecified`, and `ToUniversalTime()` (`:1060`, `:1065`) converts it with the server's zone. Wrong for any user in a different zone from the server. The fix builds the range from `clientTimeStamp.Offset`, with care for ranges that cross a DST change. **This changes behaviour** and affects the integration tests' `TIME_ZONE_ADJUSTMENT`. Probably better as a follow-up release - [ ] **Relative date tokens use the server's time zone** (`*today*`, `*yesterday*` …). Confirmed: `ClientToday = clientTimeStamp.Date` (`:399-400`) has `Kind=Unspecified`, and `ToUniversalTime()` (`:1060`, `:1065`) converts it with the server's zone. Wrong for any user in a different zone from the server. The fix builds the range from `clientTimeStamp.Offset`, with care for ranges that cross a DST change. **This changes behaviour** and affects the integration tests' `TIME_ZONE_ADJUSTMENT`. Probably better as a follow-up release
Not port items, listed so they are not forgotten: the three server bugs the integration Not port items, listed so they are not forgotten: the three server bugs the integration
@@ -211,3 +211,4 @@ Found during the port, out of scope. One line each.
| 2026-10-22 | Drop-and-recreate: CREATEDB check before the drop | Throwaway non-superuser role `ayatest_nocreatedb` owning throwaway database `ayatest_erase`, server data path in scratch (the erase also clears the attachments folder). Without CREATEDB: boot stops with the new E1000 message, database still there. Old sequence by hand as the same role: `DROP DATABASE` succeeds, `CREATE DATABASE` fails "permission denied", database gone. With CREATEDB: erased and recreated (oid 145595 → 145596), schema 20 built, boot complete. Database and role dropped afterwards. Integration suite not rerun: the change is only reached with the erase setting | | 2026-10-22 | Drop-and-recreate: CREATEDB check before the drop | Throwaway non-superuser role `ayatest_nocreatedb` owning throwaway database `ayatest_erase`, server data path in scratch (the erase also clears the attachments folder). Without CREATEDB: boot stops with the new E1000 message, database still there. Old sequence by hand as the same role: `DROP DATABASE` succeeds, `CREATE DATABASE` fails "permission denied", database gone. With CREATEDB: erased and recreated (oid 145595 → 145596), schema 20 built, boot complete. Database and role dropped afterwards. Integration suite not rerun: the change is only reached with the erase setting |
| 2026-10-22 | Report browser downloaded under the data path | Browsers in `bin` moved aside; server with an empty scratch data folder. 22/22 reports render; Chrome and ChromeHeadlessShell 155 downloaded into `<data>/report-browser`, launched from there (logged), nothing written to `bin`. Second run: first render 1.6 s (was 25 s with the download), no new files: download reused | | 2026-10-22 | Report browser downloaded under the data path | Browsers in `bin` moved aside; server with an empty scratch data folder. 22/22 reports render; Chrome and ChromeHeadlessShell 155 downloaded into `<data>/report-browser`, launched from there (logged), nothing written to `bin`. Second run: first render 1.6 s (was 25 s with the download), no new files: download reused |
| 2026-10-22 | Specific error for report browser download / launch failures | Download blocked (a file named `report-browser` in the data folder): job failed with `rendererror.exception` "…could not be downloaded to "…\report-browser" (check internet access and that the folder is writable): … Cannot create …", Error in the log, server still up. Launch with `AYANOVA_REPORT_RENDER_BROWSER_PATH` pointing nowhere: "…could not be started from "C:\no-such-folder\chrome.exe" with arguments '--no-sandbox --lang=en-CA' …: Browser was not found…", server still up. Normal: 22/22 reports, page counts = PuppeteerSharp 17; integration suite = baseline | | 2026-10-22 | Specific error for report browser download / launch failures | Download blocked (a file named `report-browser` in the data folder): job failed with `rendererror.exception` "…could not be downloaded to "…\report-browser" (check internet access and that the folder is writable): … Cannot create …", Error in the log, server still up. Launch with `AYANOVA_REPORT_RENDER_BROWSER_PATH` pointing nowhere: "…could not be started from "C:\no-such-folder\chrome.exe" with arguments '--no-sandbox --lang=en-CA' …: Browser was not found…", server still up. Normal: 22/22 reports, page counts = PuppeteerSharp 17; integration suite = baseline |
| 2026-10-22 | Data-list date filters `>=`, `<`, `!=`; null check | `WorkOrderServiceDate` through `/data-list`, 590 work orders. Minute with 6 records off second 0, before: `>=` 142 (= `>`), `<` 448 (= `<=`), `!=` 590 (all). After: `>=` 148, `<` 442, `!=` 584; identities `>=` = `>`+K, `<` = `<=`−K, `!=` = all−K hold. Same identities hold for a minute with 5 records exactly on second 0. Integration suite = baseline |

View File

@@ -21,6 +21,11 @@ namespace AyaNova.DataList
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
DataListFieldDefinition DataListField = objectFieldsList.FirstOrDefault(z => z.FieldKey == f.Column); DataListFieldDefinition DataListField = objectFieldsList.FirstOrDefault(z => z.FieldKey == f.Column);
//checked before first use and in all builds (was DEBUG only and after use, so an unknown column was a NullReferenceException)
if (DataListField == null)
{
throw new System.ArgumentException($"DataListSqlFilterCriteriaBuilder: filter column {f.Column} is not a field of this data list");
}
var dataType = DataListField.UiFieldDataType; var dataType = DataListField.UiFieldDataType;
//No filtering on custom fields! //No filtering on custom fields!
@@ -28,13 +33,6 @@ namespace AyaNova.DataList
{ {
continue; continue;
} }
#if (DEBUG)
//Developers little helper
if (DataListField == null)
{
throw new System.ArgumentNullException($"DEV ERROR in DataListSqlFilterCriteriaBuilder.cs: field {f.Column} specified in template was NOT found in ObjectFields list");
}
#endif
//Iterate filter items building this WHERE segment //Iterate filter items building this WHERE segment
bool ThisIsTheFirstFilterItemForThisColumn = true; bool ThisIsTheFirstFilterItemForThisColumn = true;
@@ -794,13 +792,15 @@ namespace AyaNova.DataList
break; break;
case DataListFilterComparisonOperator.GreaterThanOrEqualTo: case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
//from the start of the selected minute (MaxSeconds here dropped everything in that minute but its last millisecond)
sb.Append(">='"); sb.Append(">='");
sb.Append(sDateValueWithMaxSeconds); sb.Append(sDateValueWithZeroSeconds);
sb.Append("'"); sb.Append("'");
break; break;
case DataListFilterComparisonOperator.LessThan: case DataListFilterComparisonOperator.LessThan:
//before the start of the selected minute (MaxSeconds here included almost all of that minute)
sb.Append("<'"); sb.Append("<'");
sb.Append(sDateValueWithMaxSeconds); sb.Append(sDateValueWithZeroSeconds);
sb.Append("'"); sb.Append("'");
break; break;
case DataListFilterComparisonOperator.LessThanOrEqualTo: case DataListFilterComparisonOperator.LessThanOrEqualTo:
@@ -810,8 +810,9 @@ namespace AyaNova.DataList
break; break;
case DataListFilterComparisonOperator.NotEqual: case DataListFilterComparisonOperator.NotEqual:
//outside the whole selected minute, the opposite of Equality above (was < MaxSeconds, which excluded only the last millisecond)
sb.Append("<'"); sb.Append("<'");
sb.Append(sDateValueWithMaxSeconds); sb.Append(sDateValueWithZeroSeconds);
sb.Append("' OR "); sb.Append("' OR ");
sb.Append(SqlColumnNameToFilter); sb.Append(SqlColumnNameToFilter);
sb.Append(" "); sb.Append(" ");