Skip to content

Postgres dialect and PIVOT fixes surfaced by the SNPRC migration - #7993

Open
labkey-bpatel wants to merge 17 commits into
release26.3-SNAPSHOTfrom
26.3_fb_sql_server_migration_2
Open

labkey-bpatel wants to merge 17 commits into
release26.3-SNAPSHOTfrom
26.3_fb_sql_server_migration_2

Conversation

@labkey-bpatel

@labkey-bpatel labkey-bpatel commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Rationale

Migrating the SNPRC EHR deployment from SQL Server to Postgres called for a handful of adjustments in shared platform code so that LabKey SQL produces matching results on both databases. Where the two backends legitimately differ, nothing shared is redefined: week() still defers to the driver, with its two possible numbering schemes now available as explicit functions, and isnumeric() keeps its 1/0 contract. The PIVOT changes concern how a pivot value reaches the generated SQL rather than either database in particular. These came up through SNPRC's queries rather than through general use, and each change is a no-op for queries that already produce matching results.

Related Pull Requests

Changes

  • Two explicit week numbering functionsweekiso() (ISO 8601, 1 to 53) and weekus() (US, 1 to 54), defined by LabKey on both dialects instead of by whichever expression a driver picks for {fn week(x)}. week() itself is untouched: it has been a passthrough since 2010, and the shared ehr_lookups.dateRange, ehr_lookups.next30Days and ldk.dateRange lookups all read it, so redefining it would have silently shifted WeekOfYear for every existing Postgres deployment. SNPRC overrides its own copies of those three lookups to call weekus(). The SQL Server implementations live in premiumModules.
  • isnumeric() returns 1/0 on both databases — SQL Server resolves it to the mssqlMethods passthrough, Postgres to a regex-based CASE, so callers compare it with = 1. Matching changes in premiumModules and snprcEHRModules.
  • A pivot value is emitted as a dialect-quoted literal — the value is a constant, so it writes itself into the generated SQL the way every other expression node does, rather than having its LabKey SQL source text spliced in on the assumption that anything which parsed is safe to emit.
  • A pivot no longer stores a null column_makePivotedAggColumn() returns null whenever parse errors are present, and that null was going into the column map.

labkey-bpatel and others added 8 commits July 9, 2026 23:05
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
QueryPivot.getSql() bound each pivot value using the constant's own JdbcType, but wrapConstant() never produces QDate/QTimestamp -- a date or timestamp pivot column arrives here as a QString, so the parameter was bound as VARCHAR and Postgres rejected the comparison with "operator does not exist: timestamp without time zone = character varying". SQL Server converts implicitly, so only Postgres was affected. Prefer the pivot column's own type and convert the value to it, falling back to the constant's type and value when that conversion doesn't hold, since the IN (SELECT ...) form can supply values from an expression other than the pivot column. Where the two types already match -- every string pivot -- the emitted SQL and bound value are unchanged.
_makePivotedAggColumn() returns null whenever parse errors are present, so QueryPivot.getAllColumns() silently omitted every <value>::<agg> column. The TableInfo's column set is built from getAllColumns() through initializeColumns(), and that path never reaches getSql(), so a failure such as "Pivot query unauthorized" produced a successfully-constructed table missing all of its pivoted columns with no error raised anywhere -- the schema browser, getQueryDetails and the custom view field picker all show the query as simply having no pivoted output. Throw the underlying parse error the way getSql() and getColMembers() already do, and discard the partially-built _columns map first so a later call re-derives it rather than serving the short list from cache. Only fires when a column was actually dropped, so the complete-column path is unaffected.

@labkey-adam labkey-adam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any tests for these changes?

Covers the Postgres dialect override that emits US-style week numbers. The dates are chosen to exercise both divergent rules and the year boundary; a mid-year sample in a Mon-Thu year passes with the bug fully present.
@labkey-bpatel

Copy link
Copy Markdown
Contributor Author

Comment thread api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java Outdated
week() is a passthrough, so overriding it on Postgres moved WeekOfYear for every existing caller, including the shared EHR and LDK date range lookups. The two new functions are defined by LabKey and return the same number on either dialect, so callers pick a numbering instead of inheriting the driver's.
@labkey-matthewb
labkey-matthewb self-requested a review September 15, 2026 17:10
This is diagnostics rather than a correctness fix, and it changes behavior every TableInfo runs through, so it moves to its own PR along with the AbstractTableInfo init-guard fix it needs to be safe.
isnumeric() began as a SQL Server passthrough returning 1/0, so callers compare it with = 1. PostgreSQL returns 1/0 again and SQL Server resolves to that passthrough, restoring the one spelling that works on both.
labkey-bpatel added a commit to LabKey/snprcEHRModules that referenced this pull request Sep 15, 2026
isnumeric() returns 1/0, not a boolean. The comparison was dropped to match a platform change that is reverted in LabKey/platform#7993.
The formula needs the argument twice, so it was spelled out twice, duplicating the SQL, its bound parameters and any side effects. A VALUES subquery names it instead.
The value was bound as a typed parameter, which needed its JdbcType patched from the pivot column and spent one bind per value per aggregate. Letting the constant emit itself quotes through the dialect instead, with no parameters and no conversion.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants