Fix date-only searching.

This commit is contained in:
Sam Rose 2025-01-09 17:05:44 +00:00
parent b85b2426ed
commit 26c0861e68
No known key found for this signature in database
6 changed files with 196 additions and 13 deletions

View File

@ -32,8 +32,12 @@ export async function errorHandling(ctx: any, next: any) {
} }
if (environment.isTest() && ctx.headers["x-budibase-include-stacktrace"]) { if (environment.isTest() && ctx.headers["x-budibase-include-stacktrace"]) {
let rootErr = err
while (rootErr.cause) {
rootErr = rootErr.cause
}
// @ts-ignore // @ts-ignore
error.stack = err.stack error.stack = rootErr.stack
} }
ctx.body = error ctx.body = error

View File

@ -816,14 +816,29 @@ class InternalBuilder {
filters.oneOf, filters.oneOf,
ArrayOperator.ONE_OF, ArrayOperator.ONE_OF,
(q, key: string, array) => { (q, key: string, array) => {
const schema = this.getFieldSchema(key)
const values = Array.isArray(array) ? array : [array]
if (shouldOr) { if (shouldOr) {
q = q.or q = q.or
} }
if (this.client === SqlClient.ORACLE) { if (this.client === SqlClient.ORACLE) {
// @ts-ignore // @ts-ignore
key = this.convertClobs(key) key = this.convertClobs(key)
} else if (
this.client === SqlClient.SQL_LITE &&
schema?.type === FieldType.DATETIME &&
schema.dateOnly
) {
for (const value of values) {
if (value != null) {
q = q.or.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
} else {
q = q.or.whereNull(key)
}
}
return q
} }
return q.whereIn(key, Array.isArray(array) ? array : [array]) return q.whereIn(key, values)
}, },
(q, key: string[], array) => { (q, key: string[], array) => {
if (shouldOr) { if (shouldOr) {
@ -882,6 +897,19 @@ class InternalBuilder {
let high = value.high let high = value.high
let low = value.low let low = value.low
if (
this.client === SqlClient.SQL_LITE &&
schema?.type === FieldType.DATETIME &&
schema.dateOnly
) {
if (high != null) {
high = `${high.toISOString().slice(0, 10)}T23:59:59.999Z`
}
if (low != null) {
low = low.toISOString().slice(0, 10)
}
}
if (this.client === SqlClient.ORACLE) { if (this.client === SqlClient.ORACLE) {
rawKey = this.convertClobs(key) rawKey = this.convertClobs(key)
} else if ( } else if (
@ -914,6 +942,7 @@ class InternalBuilder {
} }
if (filters.equal) { if (filters.equal) {
iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => { iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => {
const schema = this.getFieldSchema(key)
if (shouldOr) { if (shouldOr) {
q = q.or q = q.or
} }
@ -928,6 +957,16 @@ class InternalBuilder {
// @ts-expect-error knex types are wrong, raw is fine here // @ts-expect-error knex types are wrong, raw is fine here
subq.whereNotNull(identifier).andWhere(identifier, value) subq.whereNotNull(identifier).andWhere(identifier, value)
) )
} else if (
this.client === SqlClient.SQL_LITE &&
schema?.type === FieldType.DATETIME &&
schema.dateOnly
) {
if (value != null) {
return q.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
} else {
return q.whereNull(key)
}
} else { } else {
return q.whereRaw(`COALESCE(?? = ?, FALSE)`, [ return q.whereRaw(`COALESCE(?? = ?, FALSE)`, [
this.rawQuotedIdentifier(key), this.rawQuotedIdentifier(key),
@ -938,6 +977,7 @@ class InternalBuilder {
} }
if (filters.notEqual) { if (filters.notEqual) {
iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => { iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => {
const schema = this.getFieldSchema(key)
if (shouldOr) { if (shouldOr) {
q = q.or q = q.or
} }
@ -959,6 +999,18 @@ class InternalBuilder {
// @ts-expect-error knex types are wrong, raw is fine here // @ts-expect-error knex types are wrong, raw is fine here
.or.whereNull(identifier) .or.whereNull(identifier)
) )
} else if (
this.client === SqlClient.SQL_LITE &&
schema?.type === FieldType.DATETIME &&
schema.dateOnly
) {
if (value != null) {
return q.not
.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
.or.whereNull(key)
} else {
return q.not.whereNull(key)
}
} else { } else {
return q.whereRaw(`COALESCE(?? != ?, TRUE)`, [ return q.whereRaw(`COALESCE(?? != ?, TRUE)`, [
this.rawQuotedIdentifier(key), this.rawQuotedIdentifier(key),

View File

@ -14,7 +14,7 @@ import environment from "../environment"
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}` const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
const ROW_ID_REGEX = /^\[.*]$/g const ROW_ID_REGEX = /^\[.*]$/g
const ENCODED_SPACE = encodeURIComponent(" ") const ENCODED_SPACE = encodeURIComponent(" ")
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/ const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}.\d{3}Z)?$/
const TIME_REGEX = /^(?:\d{2}:)?(?:\d{2}:)(?:\d{2})$/ const TIME_REGEX = /^(?:\d{2}:)?(?:\d{2}:)(?:\d{2})$/
export function isExternalTableID(tableId: string) { export function isExternalTableID(tableId: string) {
@ -149,15 +149,7 @@ export function isInvalidISODateString(str: string) {
} }
export function isValidISODateString(str: string) { export function isValidISODateString(str: string) {
const trimmedValue = str.trim() return ISO_DATE_REGEX.test(str.trim())
if (!ISO_DATE_REGEX.test(trimmedValue)) {
return false
}
let d = new Date(trimmedValue)
if (isNaN(d.getTime())) {
return false
}
return d.toISOString() === trimmedValue
} }
export function isValidFilter(value: any) { export function isValidFilter(value: any) {

View File

@ -1683,6 +1683,112 @@ if (descriptions.length) {
}) })
}) })
describe.only("datetime - date only", () => {
const JAN_1ST = "2020-01-01"
const JAN_10TH = "2020-01-10"
const JAN_30TH = "2020-01-30"
const UNEXISTING_DATE = "2020-01-03"
const NULL_DATE__ID = `null_date__id`
beforeAll(async () => {
tableOrViewId = await createTableOrView({
dateid: { name: "dateid", type: FieldType.STRING },
date: {
name: "date",
type: FieldType.DATETIME,
dateOnly: true,
},
})
await createRows([
{ dateid: NULL_DATE__ID, date: null },
{ date: JAN_1ST },
{ date: `${JAN_10TH}T00:00:00.000Z` },
])
})
describe("equal", () => {
it("successfully finds a row", async () => {
await expectQuery({
equal: { date: JAN_1ST },
}).toContainExactly([{ date: JAN_1ST }])
})
it("successfully finds an ISO8601 row", async () => {
await expectQuery({
equal: { date: JAN_10TH },
}).toContainExactly([{ date: JAN_10TH }])
})
it("finds a row with ISO8601 timestamp", async () => {
await expectQuery({
equal: { date: `${JAN_1ST}T00:00:00.000Z` },
}).toContainExactly([{ date: JAN_1ST }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({
equal: { date: UNEXISTING_DATE },
}).toFindNothing()
})
})
describe("notEqual", () => {
it("successfully finds a row", async () => {
await expectQuery({
notEqual: { date: JAN_1ST },
}).toContainExactly([
{ date: JAN_10TH },
{ dateid: NULL_DATE__ID },
])
})
it("fails to find nonexistent row", async () => {
await expectQuery({
notEqual: { date: JAN_30TH },
}).toContainExactly([
{ date: JAN_1ST },
{ date: JAN_10TH },
{ dateid: NULL_DATE__ID },
])
})
})
describe("oneOf", () => {
it("successfully finds a row", async () => {
await expectQuery({
oneOf: { date: [JAN_1ST] },
}).toContainExactly([{ date: JAN_1ST }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({
oneOf: { date: [UNEXISTING_DATE] },
}).toFindNothing()
})
})
describe("range", () => {
it("successfully finds a row", async () => {
await expectQuery({
range: { date: { low: JAN_1ST, high: JAN_1ST } },
}).toContainExactly([{ date: JAN_1ST }])
})
it("successfully finds multiple rows", async () => {
await expectQuery({
range: { date: { low: JAN_1ST, high: JAN_10TH } },
}).toContainExactly([{ date: JAN_1ST }, { date: JAN_10TH }])
})
it("successfully finds no rows", async () => {
await expectQuery({
range: { date: { low: JAN_30TH, high: JAN_30TH } },
}).toFindNothing()
})
})
})
isInternal && isInternal &&
!isInMemory && !isInMemory &&
describe("AI Column", () => { describe("AI Column", () => {

View File

@ -411,6 +411,15 @@ export async function coreOutputProcessing(
row[property] = `${hours}:${minutes}:${seconds}` row[property] = `${hours}:${minutes}:${seconds}`
} }
} }
} else if (column.type === FieldType.DATETIME && column.dateOnly) {
for (const row of rows) {
if (typeof row[property] === "string") {
row[property] = new Date(row[property])
}
if (row[property] instanceof Date) {
row[property] = row[property].toISOString().slice(0, 10)
}
}
} else if (column.type === FieldType.LINK) { } else if (column.type === FieldType.LINK) {
for (let row of rows) { for (let row of rows) {
// if relationship is empty - remove the array, this has been part of the API for some time // if relationship is empty - remove the array, this has been part of the API for some time

View File

@ -699,7 +699,27 @@ export function runQuery<T extends Record<string, any>>(
return docValue._id === testValue return docValue._id === testValue
} }
return docValue === testValue if (docValue === testValue) {
return true
}
if (docValue == null && testValue != null) {
return false
}
if (docValue != null && testValue == null) {
return false
}
const leftDate = dayjs(docValue)
if (leftDate.isValid()) {
const rightDate = dayjs(testValue)
if (rightDate.isValid()) {
return leftDate.isSame(rightDate)
}
}
return false
} }
const not = const not =