mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[sql] improvements
This commit is contained in:
@@ -292,10 +292,27 @@ export const SEARCH_LIMIT = 20
|
||||
/** Escape LIKE wildcards so user input is matched literally (using `\` as the escape char). */
|
||||
const escapeLike = (s: string): string => s.replace(/[\\%_]/g, '\\$&')
|
||||
|
||||
/**
|
||||
* The smallest string that sorts after every string beginning with `prefix` under
|
||||
* SQLite's default BINARY (byte-wise) collation: the prefix followed by U+10FFFF, whose
|
||||
* UTF-8 encoding (F4 8F BF BF) is the highest valid sequence. The pair
|
||||
* `>= prefix AND < prefixUpperBound(prefix)` is the prefix match as a range.
|
||||
*/
|
||||
const prefixUpperBound = (prefix: string): string => `${prefix}\u{10FFFF}`
|
||||
|
||||
/**
|
||||
* Prefix-search accounts by username (case-insensitive, "begins with"), ordered
|
||||
* alphabetically. Backed by the indexed `username_lower` generated column, so the
|
||||
* `name%` LIKE stays index-friendly. Returns up to `limit` matches.
|
||||
* alphabetically. Returns up to `limit` matches.
|
||||
*
|
||||
* The prefix is applied as a RANGE on the indexed `username_lower` generated column
|
||||
* (`>= name AND < name+U+10FFFF`), not as `LIKE 'name%'`. SQLite only turns a LIKE into
|
||||
* an index seek when the LIKE is case-sensitive or the index is NOCASE, and never when
|
||||
* it carries an ESCAPE clause, so the LIKE form was a full index scan on every call —
|
||||
* 48k rows read per keystroke in the site's search bar, tens of millions a day on D1's
|
||||
* meter. The range is a seek (`SEARCH … (username_lower>? AND username_lower<?)`) and
|
||||
* walks the index in `ORDER BY` order, so the LIMIT stops it after `limit` rows. The
|
||||
* LIKE stays as a residual filter on those rows so the match is exactly "begins with",
|
||||
* with the user's `%`/`_` taken literally.
|
||||
*/
|
||||
export async function searchAccounts(
|
||||
db: D1Database,
|
||||
@@ -306,9 +323,11 @@ export async function searchAccounts(
|
||||
if (q === '') return []
|
||||
const { results } = await db
|
||||
.prepare(
|
||||
`SELECT data FROM account WHERE username_lower LIKE ?1 ESCAPE '\\' ORDER BY username_lower LIMIT ?2`
|
||||
`SELECT data FROM account
|
||||
WHERE username_lower >= ?1 AND username_lower < ?2 AND username_lower LIKE ?3 ESCAPE '\\'
|
||||
ORDER BY username_lower LIMIT ?4`
|
||||
)
|
||||
.bind(`${escapeLike(q)}%`, limit)
|
||||
.bind(q, prefixUpperBound(q), `${escapeLike(q)}%`, limit)
|
||||
.all<AccountRow>()
|
||||
return parseAll(results)
|
||||
}
|
||||
|
||||
@@ -2846,27 +2846,11 @@ async function hydrateRooms(db: D1Database, rooms: Room[]): Promise<Room[]> {
|
||||
return rooms
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `custom_avatar_item` has been seen on this database. API owns that table's migration
|
||||
* and room deployments can precede it, so {@link attachBadges} probes for it — but a table
|
||||
* never goes away once created, so a hit is remembered and the probe (a scan of
|
||||
* `sqlite_master`, on every room page) runs only until the first hit.
|
||||
*/
|
||||
let badgeTableSeen = false
|
||||
|
||||
/** Published room-linked image badges; drafts remain on the creator's Studio shelf. */
|
||||
async function attachBadges(db: D1Database, rooms: Room[]): Promise<void> {
|
||||
if (!rooms.length) return
|
||||
if (!badgeTableSeen) {
|
||||
const exists = await db
|
||||
.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='custom_avatar_item'")
|
||||
.first()
|
||||
if (!exists) {
|
||||
for (const room of rooms) room.Badges = []
|
||||
return
|
||||
}
|
||||
badgeTableSeen = true
|
||||
}
|
||||
// `custom_avatar_item` is owned by api's migrations (0015, badge index 0035), both long
|
||||
// applied to the shared database; this no longer probes `sqlite_master` for it.
|
||||
// Served by `idx_custom_avatar_item_badge_room` (api migrations/0035), a PARTIAL index over
|
||||
// `outfit_type = 1000` keyed by the `json_extract(data,'$.CustomBadgeMetadata.RoomId')`
|
||||
// expression. SQLite matches both only as written: drop or rephrase either term and this
|
||||
|
||||
Reference in New Issue
Block a user