fix(db): add gte and lt to the self-hosted query builder so synastry reports can be created
createSynastryFullReport counted today's reports with .gte(), which the self-hosted client did not implement; every create threw before any credit was reserved and surfaced as a generic 500 (BUG-1399). The synastry create path now uses the shared DataQueryBuilder type, and a scan test requires every builder method used in src to exist on the self-hosted client. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cb3bc330eb
commit
503a16fb92
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-21 — 婚恋合盘点「开始合盘」不再直接报错
|
||||
|
||||
- 自托管数据库客户端补上「大于等于 / 小于」两种查询,合盘报告可以正常创建(BUG-1399)。Skill 版本不变。
|
||||
|
||||
## 2026-10-20 — 婚恋合盘选人页按已确认原型排版
|
||||
|
||||
- 选人页、新增星盘、生成中和报告页按已确认原型排版。新增是一张卡片;生成中是轨道和四步;报告有返回、日期和中英文切换。Skill 版本不变。
|
||||
|
||||
@@ -36952,3 +36952,19 @@
|
||||
- 相关记录:BUG-275(商品保存已发布 id 被折叠成同一句错误)。
|
||||
- 复发自:无。同型问题曾出现在商品目录,模型保存未一起改。
|
||||
- 修复版本:分支 `codex/model-admin-draft-delete-20261020`,未提交。
|
||||
|
||||
## BUG-1399 | 婚恋合盘点「开始合盘」就报「报告生成暂时不可用」,一份都生成不了
|
||||
|
||||
- 状态:resolved(待 staging 真机复测)
|
||||
- 首次发现:2026-10-21(产品负责人真机)
|
||||
- 最近更新:2026-10-21
|
||||
- 影响面:`POST /api/reports`(`reportType: synastry_full`)→ `createSynastryFullReport`;自托管数据库客户端 `frontend/src/lib/db/local-postgres-client-core.ts`
|
||||
- 用户现象:选好两个人点「开始合盘」,按钮下出现「报告生成暂时不可用」。不扣点。
|
||||
- 触发条件:任意一次新建合盘报告。
|
||||
- 根因:`createSynastryFullReport` 数「今天已生成几份」时用了 `.gte("created_at", …)`。自托管客户端的查询构造器没有 `gte` 方法(也没有 `lt`),内部类型和比较符表里有、方法没实现,编译分支也只认 `neq / gt / lte / like`。调用抛 TypeError,被 `app/api/reports/route.ts` 外层 `catch` 吞成通用 500。单测用合盘文件里手写的 `Query` 类型和桩,桩里自带 `gte`,所以测试与门禁全绿;共享接口 `DataQueryBuilder` 也没有 `gte`,但这段代码绕开了它。与 BUG-990 同类:兼容层没实现的写法只在真库上暴露。
|
||||
- 修复:构造器补 `gte`、`lt`,比较符统一走 `SQL_CMP`;`DataQueryBuilder` 接口补这两个方法;`synastry-full-create.ts` 删掉手写 `Query` 类型,改用 `DataQueryBuilder`,以后缺方法会在类型检查时报错。
|
||||
- 验证:`tests/local-postgres-roundtrips.test.ts` 新增一条(假连接池断言生成 `>=` / `<` 且值走参数);`tests/database-range-filters.test.ts`(真库,门禁跑:`gte` / `lt` 过滤真实行,并跑合盘「今日计数」同一条查询);`tests/db-query-builder-coverage.test.ts` 扫描 `frontend/src` 里用到的查询构造器方法,要求自托管构造器都有实现(临时删掉 `gte` 时该测试会红,已验证)。前端全量 5089 条,失败 25 条与基线同名单(均为无 Docker 的库测试),无测试消失;tsc 0;lint 0 error;构建 `/` Static,首屏 gzip 与基线同为 643,725 字节。
|
||||
- 防复发:上面的扫描测试;业务代码不得为数据库查询另写本地类型绕开 `DataQueryBuilder`。
|
||||
- 相关记录:BUG-990(同类:兼容层缺 `not(col,"is",null)`);issue #82。
|
||||
- 复发自:BUG-990 同类问题。BUG-990 的防复发是「新写法须 test:db 真查或部署后 smoke」,#82 的创建路径没有真库测试,验收也没在 staging 点过一次,所以没拦住。
|
||||
- 修复版本:分支 `codex/synastry-gte-fix-20261021`。
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# 进度:婚恋合盘「开始合盘」即失败(P0,Claude 直接执行)
|
||||
|
||||
- 任务书:`docs/tasks/TASK-synastry-staging-fix-20261021.md` 的 T1、T2(产品负责人 10-21:「P0 你改吧,其余交给执行方」)
|
||||
- 基线:`origin/staging` `cb3bc330e`(代码同已部署的 `63984d4fc`)
|
||||
- 分支:`codex/synastry-gte-fix-20261021`
|
||||
- 记录:BUG-1399
|
||||
|
||||
## 做了什么
|
||||
|
||||
- `frontend/src/lib/db/local-postgres-client-core.ts`:查询构造器补 `gte`、`lt`;比较符统一用 `SQL_CMP`。
|
||||
- `frontend/src/lib/db/data-client.ts`:`DataQueryBuilder` 补 `gte`、`lt`。
|
||||
- `frontend/src/lib/synastry-full-create.ts`:删掉手写 `Query` 类型,改用 `DataQueryBuilder`。
|
||||
|
||||
## 测试
|
||||
|
||||
| 检查 | 结果 |
|
||||
| --- | --- |
|
||||
| `tsc --noEmit` | 0 错 |
|
||||
| `npm run lint` | 0 error(122 条 warning,与基线相同) |
|
||||
| `npm test` | 5089 条,失败 25 条,与 `63984d4fc` 失败名单逐条相同(均为本机无 Docker 的库测试);无测试名消失,新增 4 条 |
|
||||
| 新增扫描测试的反向验证 | 临时删掉 `gte` 方法,扫描测试报出 `src/lib/synastry-full-create.ts:131 .gte()`,恢复后通过 |
|
||||
| `npm run build` | `/` 仍 Static;首屏 gzip 643,725 字节,与基线相同 |
|
||||
| 真库测试 `database-range-filters.test.ts` | 本机无 Docker 跳过;由 staging 门禁的真 PostgreSQL 跑(推送后核对门禁结果) |
|
||||
|
||||
## 留给执行方(同一任务书 T3~T6 与红线 3)
|
||||
|
||||
合盘创建主路径的完整真库测试(走到预留成功、任务入队)、时间线起止年月、英文分析逻辑、删模型后旧对话改用默认模型、选人页开关文案与闪烁、删重复的 BUG-1397。
|
||||
|
||||
## 真机复测(产品负责人)
|
||||
|
||||
部署后在 staging:婚恋合盘选两个人 → 开始合盘 → 应进入等待页(不再出现「报告生成暂时不可用」)→ 几分钟后报告揭幕。
|
||||
@@ -509,5 +509,5 @@
|
||||
| `TASK-niryaana-shoola-pvr-20261018.md` | `PROGRESS-niryaana-shoola-pvr-20261018.md` | #84 Niryaana Shoola 改 PVR 口径(我们的表与 Shoola 逐行相同;推翻 BUG-1283 所选 profile;占星师 7a773d13) | 待验收 | `codex/niryaana-shoola-pvr-20261018` |
|
||||
| `TASK-synastry-full-report-20261018.md` | #82 | 婚恋合盘:选人页 + 完整合盘报告(关系画像九节:结论+折叠的分析逻辑+关系时间线+技术依据摘要;英文按需;计费键 synastry.full) | 排队(等 #78、#79、#81) | `codex/synastry-full-report-20261018`;动表 |
|
||||
| `TASK-synastry-report-chat-20261018.md` | `PROGRESS-synastry-report-chat-20261018.md` | 从合盘报告某一节进入对话问依据(绑定报告与节、对话里可用术语、只依据已存计算结果并逐条核对、计费照普通对话) | 已验收 | `549e37b99`;动表 |
|
||||
| `TASK-synastry-staging-fix-20261021.md` | — | 婚恋合盘 staging 点「开始合盘」即失败(兼容层缺 `.gte`)+ 时间线无起止、英文缺分析逻辑、删模型后旧对话改用默认模型(BUG-1399 起) | 待提 issue | `codex/synastry-staging-fix-20261021`;动表 |
|
||||
| `TASK-synastry-staging-fix-20261021.md` | — | 婚恋合盘 staging 点「开始合盘」即失败(兼容层缺 `.gte`,P0 已由 Claude 修,BUG-1399)+ 时间线无起止、英文缺分析逻辑、删模型后旧对话改用默认模型(执行方从 T3 做起,BUG-1400 起) | P0 已推 staging;其余待提 issue | `codex/synastry-staging-fix-20261021`;动表 |
|
||||
| `TASK-synastry-report-chat-fix-20261020.md` | `PROGRESS-synastry-report-chat-20261018.md` | 大运核对要认报告里的「小运」和英文 mahadasha / antardasha;预填文案只留一句 | 已验收 | `549e37b99` |
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
| T5 | K1:对话解析不到绑定的模型时,改用当前默认模型继续,并把会话的 `model_id / model_config_version` 更新成新模型;没有任何可用模型时才报现有的「模型不可用」 | 单测:被删版本的会话下一轮用默认模型;无默认模型时仍报错 |
|
||||
| T6 | 选人页:开关读取失败显示「合盘服务暂时连不上,请稍后再试」;开关结果回来之前不渲染选人区(只渲染标题和副标题);删掉 BUG_HISTORY 里重复的 BUG-1397 一段;CHANGELOG、PROGRESS(含真机复测步骤:选两人生成一份、等待页、报告揭幕、切英文、从末尾进对话问一节) | 渲染测试;文档齐 |
|
||||
|
||||
## 已完成(Claude 直接执行,产品负责人 10-21 授权「P0 你改吧,其余交给执行方」)
|
||||
|
||||
T1、T2 已由 Claude 在 `codex/synastry-gte-fix-20261021` 完成并推 staging(BUG-1399,进度见 `PROGRESS-synastry-gte-fix-20261021.md`):构造器补 `gte` / `lt`、`DataQueryBuilder` 补方法、合盘创建改用共享类型、假连接池 SQL 测试、真库过滤测试、扫描测试。**执行方从 T3 做起**,另外仍要补红线 3 的「合盘创建主路径真库测试」(Claude 只做了查询级的真库测试)。
|
||||
|
||||
## 让步顺序
|
||||
|
||||
T1、T2 最先做,单独一个提交先推 staging(门禁绿、部署后告知产品负责人可以复测生成);T3~T6 随后。
|
||||
@@ -63,4 +67,4 @@ grep -n "BUG-1[0-9][0-9][0-9]" docs/BUG_HISTORY.md | tail -3
|
||||
|
||||
## BUG 编号
|
||||
|
||||
从 **BUG-1399** 起(10-21 staging 最大 BUG-1398),开工时核对。
|
||||
BUG-1399 已由 Claude 的 P0 使用;执行方从 **BUG-1400** 起,开工时核对。
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface DataQueryBuilder extends PromiseLike<DataQueryResult> {
|
||||
eq(column: string, value: unknown): DataQueryBuilder;
|
||||
neq(column: string, value: unknown): DataQueryBuilder;
|
||||
gt(column: string, value: unknown): DataQueryBuilder;
|
||||
gte(column: string, value: unknown): DataQueryBuilder;
|
||||
lt(column: string, value: unknown): DataQueryBuilder;
|
||||
lte(column: string, value: unknown): DataQueryBuilder;
|
||||
like(column: string, value: unknown): DataQueryBuilder;
|
||||
in(column: string, value: readonly unknown[]): DataQueryBuilder;
|
||||
|
||||
@@ -17,6 +17,8 @@ type Filter =
|
||||
| Readonly<{ kind: "eq"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "neq"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "gt"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "gte"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "lt"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "lte"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "like"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "in"; column: string; value: readonly unknown[] }>
|
||||
@@ -534,6 +536,18 @@ class LocalPostgresQueryBuilder implements DataQueryBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
gte(column: string, value: unknown) {
|
||||
identifier(column);
|
||||
this.filters.push({ kind: "gte", column, value });
|
||||
return this;
|
||||
}
|
||||
|
||||
lt(column: string, value: unknown) {
|
||||
identifier(column);
|
||||
this.filters.push({ kind: "lt", column, value });
|
||||
return this;
|
||||
}
|
||||
|
||||
lte(column: string, value: unknown) {
|
||||
identifier(column);
|
||||
this.filters.push({ kind: "lte", column, value });
|
||||
@@ -650,16 +664,7 @@ class LocalPostgresQueryBuilder implements DataQueryBuilder {
|
||||
return `not (${column} @> $${parameters.length})`;
|
||||
}
|
||||
parameters.push(databaseValue(types.get(filter.column), filter.value));
|
||||
const operator =
|
||||
filter.kind === "neq"
|
||||
? "<>"
|
||||
: filter.kind === "gt"
|
||||
? ">"
|
||||
: filter.kind === "lte"
|
||||
? "<="
|
||||
: filter.kind === "like"
|
||||
? "like"
|
||||
: "=";
|
||||
const operator = filter.kind === "like" ? "like" : SQL_CMP[filter.kind];
|
||||
return `${column} ${operator} $${parameters.length}`;
|
||||
});
|
||||
return ` where ${parts.join(" and ")}`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
|
||||
import { authorizeUsage, releaseUsage } from "@/lib/consultation-billing";
|
||||
import type { DataQueryBuilder } from "@/lib/db/data-client";
|
||||
import { consultationSubjectFailureResponse } from "@/lib/consultation-subject-resolver";
|
||||
import { FeaturePricingError, resolveFeaturePricing } from "@/lib/feature-pricing";
|
||||
import { MARRIAGE_MATCH_RULES_VERSION, marriageMatchFingerprint } from "@/lib/marriage-match-service";
|
||||
@@ -27,15 +28,9 @@ export type SynastryCreateResponse = {
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
type QueryResult = { data: Row[] | null; error: { message?: string } | null };
|
||||
|
||||
type Query = PromiseLike<QueryResult> & {
|
||||
eq(column: string, value: unknown): Query;
|
||||
in(column: string, values: readonly string[]): Query;
|
||||
gte(column: string, value: string): Query;
|
||||
limit(count: number): Query;
|
||||
maybeSingle(): Promise<{ data: Row | null; error: { message?: string } | null }>;
|
||||
};
|
||||
// The shared builder type, not a local stub: a method the self-hosted client
|
||||
// lacks must fail type-checking here (BUG-1399).
|
||||
type Query = DataQueryBuilder;
|
||||
|
||||
function subjectId(value: unknown): string | null {
|
||||
if (value === "self") return "self";
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
import { closeLocalPostgresDataPool, createLocalPostgresDataClient } from "../src/lib/db/local-postgres-client-core.ts";
|
||||
import { startPostgresFixture, type PostgresFixture } from "./helpers/postgres-fixture.ts";
|
||||
|
||||
// BUG-1399: `.gte()` was missing from the self-hosted client, so the synastry
|
||||
// report's "reports created today" count threw before anything was reserved.
|
||||
// This runs the same filters against a real database. Fictional rows only.
|
||||
const runner = fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url));
|
||||
const docker = spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], { stdio: "ignore" }).status === 0;
|
||||
const SERVICE_URL_ROLE = ["service_runtime", "service-runtime-test-password"] as const;
|
||||
|
||||
function applyMigrations(fixture: PostgresFixture): void {
|
||||
const result = spawnSync(process.execPath, [runner], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", "schema-owner-test-password"),
|
||||
},
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
}
|
||||
|
||||
test("gte and lt filter real rows, and the synastry daily count query runs", {
|
||||
skip: docker ? false : "docker unavailable",
|
||||
}, async () => {
|
||||
const fixture = startPostgresFixture();
|
||||
const serviceUrl = fixture.connectionUrl(...SERVICE_URL_ROLE);
|
||||
try {
|
||||
applyMigrations(fixture);
|
||||
const owner = randomUUID();
|
||||
const older = randomUUID();
|
||||
const newer = randomUUID();
|
||||
fixture.psql(`insert into identity.users (id, name, email, email_verified, email_verified_at)
|
||||
values ('${owner}', 'Fictional owner', '${owner}@example.invalid', true, now());`);
|
||||
for (const [id, updatedAt, name] of [
|
||||
[older, "2026-10-01T00:00:00Z", "虚构甲"],
|
||||
[newer, "2026-10-21T08:00:00Z", "虚构乙"],
|
||||
] as const) {
|
||||
fixture.psql(`insert into public.chart_profiles (
|
||||
id, user_id, role, profile, updated_at, name, birth_date, reported_birth_time,
|
||||
birth_time_source, latitude, longitude, timezone_id, timezone_offset,
|
||||
birth_place_label, ayanamsa, birth_time_status
|
||||
) values (
|
||||
'${id}', '${owner}', 'other', '{"name":"${name}"}'::jsonb, '${updatedAt}', '${name}', '1990-01-01', '08:00',
|
||||
'family_exact', 22.3, 114.1, 'Asia/Shanghai', 8, '虚构港', 'lahiri', 'reported'
|
||||
);`);
|
||||
}
|
||||
|
||||
const admin = createLocalPostgresDataClient(serviceUrl, null, "service_role");
|
||||
const since = await admin.from("chart_profiles").select("id")
|
||||
.eq("user_id", owner)
|
||||
.gte("updated_at", "2026-10-21T00:00:00.000Z");
|
||||
assert.equal(since.error, null, since.error?.message ?? "gte failed");
|
||||
assert.deepEqual((since.data as Array<{ id: string }>).map((row) => row.id), [newer]);
|
||||
|
||||
const before = await admin.from("chart_profiles").select("id")
|
||||
.eq("user_id", owner)
|
||||
.lt("updated_at", "2026-10-21T00:00:00.000Z");
|
||||
assert.equal(before.error, null, before.error?.message ?? "lt failed");
|
||||
assert.deepEqual((before.data as Array<{ id: string }>).map((row) => row.id), [older]);
|
||||
|
||||
// The exact chain createSynastryFullReport uses for today's count.
|
||||
const today = await admin.from("personal_reports").select("created_at")
|
||||
.eq("user_id", owner)
|
||||
.gte("created_at", "2026-10-21T00:00:00.000Z");
|
||||
assert.equal(today.error, null, today.error?.message ?? "personal_reports gte failed");
|
||||
assert.deepEqual(today.data, []);
|
||||
} finally {
|
||||
await closeLocalPostgresDataPool(serviceUrl);
|
||||
fixture.stop();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// BUG-1399: a feature called `.gte()` on the self-hosted database client, which
|
||||
// did not implement it. Unit tests passed because they used a hand-written query
|
||||
// stub that had the method. This test reads every PostgREST-style filter or
|
||||
// modifier call in frontend/src and requires the self-hosted builder to declare it.
|
||||
const frontendDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const srcDir = path.join(frontendDir, "src");
|
||||
|
||||
const POSTGREST_METHODS = [
|
||||
"eq", "neq", "gt", "gte", "lt", "lte", "like", "ilike", "is", "in", "not", "or",
|
||||
"contains", "containedBy", "overlaps", "range", "rangeGt", "rangeGte", "rangeLt",
|
||||
"rangeLte", "rangeAdjacent", "textSearch", "match", "filter", "order", "limit",
|
||||
"single", "maybeSingle", "csv", "abortSignal", "returns", "throwOnError",
|
||||
] as const;
|
||||
|
||||
function sourceFiles(dir: string): string[] {
|
||||
return readdirSync(dir).flatMap((name) => {
|
||||
const full = path.join(dir, name);
|
||||
if (statSync(full).isDirectory()) return sourceFiles(full);
|
||||
return /\.(ts|tsx)$/.test(name) ? [full] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function builderMethods(): Set<string> {
|
||||
const source = readFileSync(path.join(srcDir, "lib/db/local-postgres-client-core.ts"), "utf8");
|
||||
const start = source.indexOf("class LocalPostgresQueryBuilder");
|
||||
const end = source.indexOf("\nexport class LocalPostgresDataClient", start);
|
||||
assert.ok(start >= 0 && end > start, "LocalPostgresQueryBuilder class not found");
|
||||
return new Set([...source.slice(start, end).matchAll(/^ {2}(?:async )?([a-zA-Z]+)\(/gm)].map((match) => match[1]));
|
||||
}
|
||||
|
||||
// Lines that are clearly not database query chains: zod schemas and plain arrays.
|
||||
// `.contains(` is left out of the scan: DOM `element.contains()` uses the same name.
|
||||
function isDatabaseChain(line: string): boolean {
|
||||
if (/\bz\.|\.number\(\)|\.string\(\)|\.array\(|\.date\(\)/.test(line)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
test("every PostgREST method used in frontend/src exists on the self-hosted query builder", () => {
|
||||
const implemented = builderMethods();
|
||||
const missing: string[] = [];
|
||||
const builderOnly = new Set<string>(["gte", "lt", "lte", "gt", "neq", "ilike", "maybeSingle", "containedBy", "overlaps", "rangeGt", "rangeGte", "rangeLt", "rangeLte", "rangeAdjacent", "textSearch", "csv", "throwOnError"]);
|
||||
for (const file of sourceFiles(srcDir)) {
|
||||
const lines = readFileSync(file, "utf8").split("\n");
|
||||
lines.forEach((line, index) => {
|
||||
if (!isDatabaseChain(line)) return;
|
||||
for (const method of POSTGREST_METHODS) {
|
||||
if (!builderOnly.has(method)) continue;
|
||||
if (new RegExp(`\\.${method}\\(`).test(line) && !implemented.has(method)) {
|
||||
missing.push(`${path.relative(frontendDir, file)}:${index + 1} .${method}()`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
assert.deepEqual(missing, [], `self-hosted builder lacks methods used in src:\n${missing.join("\n")}`);
|
||||
});
|
||||
|
||||
test("the self-hosted builder declares gte and lt (BUG-1399)", () => {
|
||||
const implemented = builderMethods();
|
||||
assert.ok(implemented.has("gte"));
|
||||
assert.ok(implemented.has("lt"));
|
||||
});
|
||||
@@ -284,3 +284,30 @@ test("closing the pool drops that url from the metadata cache", async () => {
|
||||
assert.ok(calls.length > 0);
|
||||
});
|
||||
});
|
||||
|
||||
test("gte and lt compile to >= and < with the value bound as a parameter (BUG-1399)", async () => {
|
||||
await withSwitches({}, async () => {
|
||||
const url = "postgresql://range-filters@127.0.0.1:1/jyotisha";
|
||||
const calls = installFakePool(url, {
|
||||
columns: [
|
||||
{ column_name: "id", udt_name: "uuid" },
|
||||
{ column_name: "user_id", udt_name: "uuid" },
|
||||
{ column_name: "created_at", udt_name: "timestamptz" },
|
||||
],
|
||||
});
|
||||
const client = clientFor(url, "service_role");
|
||||
const since = "2026-10-21T00:00:00.000Z";
|
||||
const until = "2026-10-22T00:00:00.000Z";
|
||||
const result = await client.from("personal_reports").select("created_at")
|
||||
.eq("user_id", identity.id)
|
||||
.gte("created_at", since)
|
||||
.lt("created_at", until);
|
||||
assert.equal(result.error, null);
|
||||
const business = calls.filter((call) => tag(call.sql) === "business");
|
||||
assert.equal(business.length, 1);
|
||||
const sql = business[0].sql.replace(/\s+/g, " ");
|
||||
assert.match(sql, /"created_at" >= \$2/);
|
||||
assert.match(sql, /"created_at" < \$3/);
|
||||
assert.deepEqual(business[0].params.slice(0, 3).map(String), [identity.id, since, until].map(String));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user