Skip to content

Commit 05c7329

Browse files
committed
feat: enhance datetime parsing in generated column queries with guard conditions
1 parent c3f924a commit 05c7329

File tree

4 files changed

+124
-4
lines changed

4 files changed

+124
-4
lines changed

‎apps/nestjs-backend/src/db-provider/generated-column-query/__snapshots__/formula-query.spec.ts.snap‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ exports[`GeneratedColumnQuery > PostgreSQL Generated Column Functions > Date Fun
6060

6161
exports[`GeneratedColumnQuery > PostgreSQL Generated Column Functions > Date Functions > should implement datetimeFormat function with parameters 1`] = `"TO_CHAR(column_a::timestamp, 'YYYY-MM-DD')"`;
6262

63-
exports[`GeneratedColumnQuery > PostgreSQL Generated Column Functions > Date Functions > should implement datetimeParse function with parameters 1`] = `"TO_TIMESTAMP(column_a, 'YYYY-MM-DD')"`;
63+
exports[`GeneratedColumnQuery > PostgreSQL Generated Column Functions > Date Functions > should implement datetimeParse function with parameters 1`] = `"(CASE WHEN (column_a) IS NULL THEN NULL WHEN (column_a)::text = '' THEN NULL WHEN (column_a)::text ~ '^\\d{4}\\-\\d{2}\\-\\d{2}$' THEN TO_TIMESTAMP(column_a, 'YYYY-MM-DD') ELSE NULL END)"`;
6464

6565
exports[`GeneratedColumnQuery > PostgreSQL Generated Column Functions > Date Functions > should implement day function 1`] = `"EXTRACT(DAY FROM column_a::timestamp)"`;
6666

‎apps/nestjs-backend/src/db-provider/generated-column-query/__snapshots__/generated-column-query.spec.ts.snap‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ exports[`GeneratedColumnQuery > PostgreSQL Generated Column Functions > Date Fun
6060

6161
exports[`GeneratedColumnQuery > PostgreSQL Generated Column Functions > Date Functions > should implement datetimeFormat function with parameters 1`] = `"TO_CHAR(column_a::timestamp, 'YYYY-MM-DD')"`;
6262

63-
exports[`GeneratedColumnQuery > PostgreSQL Generated Column Functions > Date Functions > should implement datetimeParse function with parameters 1`] = `"TO_TIMESTAMP(column_a, 'YYYY-MM-DD')"`;
63+
exports[`GeneratedColumnQuery > PostgreSQL Generated Column Functions > Date Functions > should implement datetimeParse function with parameters 1`] = `"(CASE WHEN (column_a) IS NULL THEN NULL WHEN (column_a)::text = '' THEN NULL WHEN (column_a)::text ~ '^\\d{4}\\-\\d{2}\\-\\d{2}$' THEN TO_TIMESTAMP(column_a, 'YYYY-MM-DD') ELSE NULL END)"`;
6464

6565
exports[`GeneratedColumnQuery > PostgreSQL Generated Column Functions > Date Functions > should implement day function 1`] = `"EXTRACT(DAY FROM column_a::timestamp)"`;
6666

‎apps/nestjs-backend/src/db-provider/generated-column-query/postgres/generated-column-query.postgres.ts‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,14 @@ export class GeneratedColumnQueryPostgres extends GeneratedColumnQueryAbstract {
504504
if (!normalized || normalized === 'undefined' || normalized.toLowerCase() === 'null') {
505505
return dateString;
506506
}
507-
return `TO_TIMESTAMP(${dateString}, ${format})`;
507+
const guardPattern = this.buildDatetimeParseGuardRegex(normalized);
508+
if (!guardPattern) {
509+
return `TO_TIMESTAMP(${dateString}, ${format})`;
510+
}
511+
const valueExpr = `(${dateString})`;
512+
const textExpr = `${valueExpr}::text`;
513+
const escapedPattern = guardPattern.replace(/'/g, "''");
514+
return `(CASE WHEN ${valueExpr} IS NULL THEN NULL WHEN ${textExpr} = '' THEN NULL WHEN ${textExpr} ~ '${escapedPattern}' THEN TO_TIMESTAMP(${dateString}, ${format}) ELSE NULL END)`;
508515
}
509516

510517
day(date: string): string {
@@ -750,4 +757,57 @@ export class GeneratedColumnQueryPostgres extends GeneratedColumnQueryAbstract {
750757
protected escapeIdentifier(identifier: string): string {
751758
return `"${identifier.replace(/"/g, '""')}"`;
752759
}
760+
761+
private buildDatetimeParseGuardRegex(formatLiteral: string): string | null {
762+
if (!formatLiteral.startsWith("'") || !formatLiteral.endsWith("'")) {
763+
return null;
764+
}
765+
const literal = formatLiteral.slice(1, -1);
766+
const tokenPatterns: Array<[string, string]> = [
767+
['HH24', '\\d{2}'],
768+
['HH12', '\\d{2}'],
769+
['HH', '\\d{2}'],
770+
['MI', '\\d{2}'],
771+
['SS', '\\d{2}'],
772+
['MS', '\\d{1,3}'],
773+
['YYYY', '\\d{4}'],
774+
['YYY', '\\d{3}'],
775+
['YY', '\\d{2}'],
776+
['Y', '\\d'],
777+
['MM', '\\d{2}'],
778+
['DD', '\\d{2}'],
779+
];
780+
const optionalTokens = new Set(['FM', 'TM', 'TH']);
781+
let pattern = '^';
782+
for (let i = 0; i < literal.length; ) {
783+
let matched = false;
784+
const remaining = literal.slice(i);
785+
const upperRemaining = remaining.toUpperCase();
786+
for (const [token, tokenPattern] of tokenPatterns) {
787+
if (upperRemaining.startsWith(token)) {
788+
pattern += tokenPattern;
789+
i += token.length;
790+
matched = true;
791+
break;
792+
}
793+
}
794+
if (matched) {
795+
continue;
796+
}
797+
const optionalToken = upperRemaining.slice(0, 2);
798+
if (optionalTokens.has(optionalToken)) {
799+
i += optionalToken.length;
800+
continue;
801+
}
802+
const currentChar = literal[i];
803+
if (/\s/.test(currentChar)) {
804+
pattern += '\\s';
805+
} else {
806+
pattern += currentChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
807+
}
808+
i += 1;
809+
}
810+
pattern += '$';
811+
return pattern;
812+
}
753813
}

‎apps/nestjs-backend/src/db-provider/select-query/postgres/select-query.postgres.ts‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,14 @@ export class SelectQueryPostgres extends SelectQueryAbstract {
472472
if (!normalized || normalized === 'undefined' || normalized.toLowerCase() === 'null') {
473473
return dateString;
474474
}
475-
return `TO_TIMESTAMP(${dateString}, ${format})`;
475+
const guardPattern = this.buildDatetimeParseGuardRegex(normalized);
476+
if (!guardPattern) {
477+
return `TO_TIMESTAMP(${dateString}, ${format})`;
478+
}
479+
const valueExpr = `(${dateString})`;
480+
const textExpr = `${valueExpr}::text`;
481+
const escapedPattern = guardPattern.replace(/'/g, "''");
482+
return `(CASE WHEN ${valueExpr} IS NULL THEN NULL WHEN ${textExpr} = '' THEN NULL WHEN ${textExpr} ~ '${escapedPattern}' THEN TO_TIMESTAMP(${dateString}, ${format}) ELSE NULL END)`;
476483
}
477484

478485
day(date: string): string {
@@ -864,4 +871,57 @@ export class SelectQueryPostgres extends SelectQueryAbstract {
864871
parentheses(expression: string): string {
865872
return `(${expression})`;
866873
}
874+
875+
private buildDatetimeParseGuardRegex(formatLiteral: string): string | null {
876+
if (!formatLiteral.startsWith("'") || !formatLiteral.endsWith("'")) {
877+
return null;
878+
}
879+
const literal = formatLiteral.slice(1, -1);
880+
const tokenPatterns: Array<[string, string]> = [
881+
['HH24', '\\d{2}'],
882+
['HH12', '\\d{2}'],
883+
['HH', '\\d{2}'],
884+
['MI', '\\d{2}'],
885+
['SS', '\\d{2}'],
886+
['MS', '\\d{1,3}'],
887+
['YYYY', '\\d{4}'],
888+
['YYY', '\\d{3}'],
889+
['YY', '\\d{2}'],
890+
['Y', '\\d'],
891+
['MM', '\\d{2}'],
892+
['DD', '\\d{2}'],
893+
];
894+
const optionalTokens = new Set(['FM', 'TM', 'TH']);
895+
let pattern = '^';
896+
for (let i = 0; i < literal.length; ) {
897+
let matched = false;
898+
const remaining = literal.slice(i);
899+
const upperRemaining = remaining.toUpperCase();
900+
for (const [token, tokenPattern] of tokenPatterns) {
901+
if (upperRemaining.startsWith(token)) {
902+
pattern += tokenPattern;
903+
i += token.length;
904+
matched = true;
905+
break;
906+
}
907+
}
908+
if (matched) {
909+
continue;
910+
}
911+
const optionalToken = upperRemaining.slice(0, 2);
912+
if (optionalTokens.has(optionalToken)) {
913+
i += optionalToken.length;
914+
continue;
915+
}
916+
const currentChar = literal[i];
917+
if (/\s/.test(currentChar)) {
918+
pattern += '\\s';
919+
} else {
920+
pattern += currentChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
921+
}
922+
i += 1;
923+
}
924+
pattern += '$';
925+
return pattern;
926+
}
867927
}

0 commit comments

Comments
 (0)