mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-28 00:18:58 -07:00
[auth] optimize ban query
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
-- Expose the account's `signupIp` and `deviceId` (both in the JSON blob) as indexed
|
||||||
|
-- generated columns, so the signup caps (`countAccountsBySignupIp`,
|
||||||
|
-- `countAccountsByDeviceId` in @repo/domain's accounts-db.ts) are an index lookup
|
||||||
|
-- instead of running json_extract over every account. The IP count alone was a third
|
||||||
|
-- of D1's query time, and both run on every account creation.
|
||||||
|
--
|
||||||
|
-- Same construction as `has_plus` (0009) and `is_influencer` (0012), but a full index
|
||||||
|
-- rather than a partial one: the lookup is by value, not by a fixed flag. Kept in sync
|
||||||
|
-- with SCHEMA_DDL in @repo/domain's accounts-db.ts (and its mirror in
|
||||||
|
-- apps/econ/src/avatar-db.ts).
|
||||||
|
|
||||||
|
ALTER TABLE account ADD COLUMN signup_ip TEXT GENERATED ALWAYS AS (json_extract(data, '$.signupIp')) VIRTUAL;
|
||||||
|
ALTER TABLE account ADD COLUMN device_id TEXT GENERATED ALWAYS AS (json_extract(data, '$.deviceId')) VIRTUAL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_account_signup_ip ON account (signup_ip);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_account_device_id ON account (device_id);
|
||||||
@@ -17,11 +17,15 @@ export const SCHEMA_DDL: string[] = [
|
|||||||
account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.accountId')) VIRTUAL,
|
account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.accountId')) VIRTUAL,
|
||||||
username_lower TEXT GENERATED ALWAYS AS (lower(json_extract(data, '$.username'))) VIRTUAL,
|
username_lower TEXT GENERATED ALWAYS AS (lower(json_extract(data, '$.username'))) VIRTUAL,
|
||||||
has_plus INTEGER GENERATED ALWAYS AS (json_extract(data, '$.hasPlus')) VIRTUAL,
|
has_plus INTEGER GENERATED ALWAYS AS (json_extract(data, '$.hasPlus')) VIRTUAL,
|
||||||
is_influencer INTEGER GENERATED ALWAYS AS (json_extract(data, '$.isInfluencer')) VIRTUAL
|
is_influencer INTEGER GENERATED ALWAYS AS (json_extract(data, '$.isInfluencer')) VIRTUAL,
|
||||||
|
signup_ip TEXT GENERATED ALWAYS AS (json_extract(data, '$.signupIp')) VIRTUAL,
|
||||||
|
device_id TEXT GENERATED ALWAYS AS (json_extract(data, '$.deviceId')) VIRTUAL
|
||||||
)`,
|
)`,
|
||||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_account_id ON account (account_id)`,
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_account_id ON account (account_id)`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_account_has_plus ON account (has_plus) WHERE has_plus = 1`,
|
`CREATE INDEX IF NOT EXISTS idx_account_has_plus ON account (has_plus) WHERE has_plus = 1`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_account_is_influencer ON account (is_influencer) WHERE is_influencer = 1`,
|
`CREATE INDEX IF NOT EXISTS idx_account_is_influencer ON account (is_influencer) WHERE is_influencer = 1`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_account_signup_ip ON account (signup_ip)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_account_device_id ON account (device_id)`,
|
||||||
]
|
]
|
||||||
|
|
||||||
/** The stored avatar payload — opaque JSON the client sets and reads back. */
|
/** The stored avatar payload — opaque JSON the client sets and reads back. */
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import { bindPlaceholders, chunkForBinds } from './d1-binds'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema DDL — the head schema, i.e. what the table looks like after every migration
|
* Schema DDL — the head schema, i.e. what the table looks like after every migration
|
||||||
* (0001_accounts + 0002_avatar + 0009_account_has_plus + 0012_account_is_influencer, sans
|
* (0001_accounts + 0002_avatar + 0009_account_has_plus + 0012_account_is_influencer +
|
||||||
|
* 0013_account_signup_ip_device_id, sans
|
||||||
* seed INSERTs; 0004 added a `platform_id` generated column and 0008 dropped it again, so it
|
* seed INSERTs; 0004 added a `platform_id` generated column and 0008 dropped it again, so it
|
||||||
* appears here in neither form).
|
* appears here in neither form).
|
||||||
*
|
*
|
||||||
@@ -24,6 +25,7 @@ import { bindPlaceholders, chunkForBinds } from './d1-binds'
|
|||||||
* subscriber through its partial index instead of scanning every account's JSON. It is 1 for
|
* subscriber through its partial index instead of scanning every account's JSON. It is 1 for
|
||||||
* JSON true, 0 for false and NULL when the key is absent; the index holds only the 1s.
|
* JSON true, 0 for false and NULL when the key is absent; the index holds only the 1s.
|
||||||
* `is_influencer` does the same for `isInfluencer`, for econ's influencer list.
|
* `is_influencer` does the same for `isInfluencer`, for econ's influencer list.
|
||||||
|
* `signup_ip` and `device_id` (full indexes) back the signup caps' counts.
|
||||||
*/
|
*/
|
||||||
export const SCHEMA_DDL: string[] = [
|
export const SCHEMA_DDL: string[] = [
|
||||||
`CREATE TABLE IF NOT EXISTS account (
|
`CREATE TABLE IF NOT EXISTS account (
|
||||||
@@ -32,12 +34,16 @@ export const SCHEMA_DDL: string[] = [
|
|||||||
account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.accountId')) VIRTUAL,
|
account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.accountId')) VIRTUAL,
|
||||||
username_lower TEXT GENERATED ALWAYS AS (lower(json_extract(data, '$.username'))) VIRTUAL,
|
username_lower TEXT GENERATED ALWAYS AS (lower(json_extract(data, '$.username'))) VIRTUAL,
|
||||||
has_plus INTEGER GENERATED ALWAYS AS (json_extract(data, '$.hasPlus')) VIRTUAL,
|
has_plus INTEGER GENERATED ALWAYS AS (json_extract(data, '$.hasPlus')) VIRTUAL,
|
||||||
is_influencer INTEGER GENERATED ALWAYS AS (json_extract(data, '$.isInfluencer')) VIRTUAL
|
is_influencer INTEGER GENERATED ALWAYS AS (json_extract(data, '$.isInfluencer')) VIRTUAL,
|
||||||
|
signup_ip TEXT GENERATED ALWAYS AS (json_extract(data, '$.signupIp')) VIRTUAL,
|
||||||
|
device_id TEXT GENERATED ALWAYS AS (json_extract(data, '$.deviceId')) VIRTUAL
|
||||||
)`,
|
)`,
|
||||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_account_id ON account (account_id)`,
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_account_id ON account (account_id)`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_accounts_username_lower ON account (username_lower)`,
|
`CREATE INDEX IF NOT EXISTS idx_accounts_username_lower ON account (username_lower)`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_account_has_plus ON account (has_plus) WHERE has_plus = 1`,
|
`CREATE INDEX IF NOT EXISTS idx_account_has_plus ON account (has_plus) WHERE has_plus = 1`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_account_is_influencer ON account (is_influencer) WHERE is_influencer = 1`,
|
`CREATE INDEX IF NOT EXISTS idx_account_is_influencer ON account (is_influencer) WHERE is_influencer = 1`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_account_signup_ip ON account (signup_ip)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_account_device_id ON account (device_id)`,
|
||||||
]
|
]
|
||||||
|
|
||||||
/** Client-facing account shape (camelCase, exactly as the client's AccountDTO). */
|
/** Client-facing account shape (camelCase, exactly as the client's AccountDTO). */
|
||||||
@@ -358,11 +364,8 @@ export async function searchAccounts(
|
|||||||
* at login). An empty id yields no matches (avoids matching every account with no
|
* at login). An empty id yields no matches (avoids matching every account with no
|
||||||
* device recorded).
|
* device recorded).
|
||||||
*
|
*
|
||||||
* Reads `deviceId` straight out of the JSON blob, so this is a table scan — no
|
* Reads the indexed `device_id` generated column (migration 0013, added for the
|
||||||
* generated column, no migration. Fine at our account count and for the occasional
|
* per-device signup cap), not the JSON blob, so it is an index lookup.
|
||||||
* linkup lookup this exists for; if it ever gets hot, promote `deviceId` to an indexed
|
|
||||||
* generated column (migration 0004 did that for `platformId`, and 0008 undid it once
|
|
||||||
* nothing queried it — that pair is the recipe both ways).
|
|
||||||
*
|
*
|
||||||
* The device id is unverified client input, so treat a match as a *hint* (these
|
* The device id is unverified client input, so treat a match as a *hint* (these
|
||||||
* accounts share a device) and never as proof of identity.
|
* accounts share a device) and never as proof of identity.
|
||||||
@@ -370,7 +373,7 @@ export async function searchAccounts(
|
|||||||
export async function getAccountsByDeviceId(db: D1Database, deviceId: string): Promise<Account[]> {
|
export async function getAccountsByDeviceId(db: D1Database, deviceId: string): Promise<Account[]> {
|
||||||
if (deviceId === '') return []
|
if (deviceId === '') return []
|
||||||
const { results } = await db
|
const { results } = await db
|
||||||
.prepare("SELECT data FROM account WHERE json_extract(data, '$.deviceId') = ?1")
|
.prepare('SELECT data FROM account WHERE device_id = ?1')
|
||||||
.bind(deviceId)
|
.bind(deviceId)
|
||||||
.all<AccountRow>()
|
.all<AccountRow>()
|
||||||
return parseAll(results)
|
return parseAll(results)
|
||||||
@@ -393,7 +396,7 @@ export async function getAccountsByDeviceId(db: D1Database, deviceId: string): P
|
|||||||
export async function countAccountsByDeviceId(db: D1Database, deviceId: string): Promise<number> {
|
export async function countAccountsByDeviceId(db: D1Database, deviceId: string): Promise<number> {
|
||||||
if (deviceId === '') return 0
|
if (deviceId === '') return 0
|
||||||
const row = await db
|
const row = await db
|
||||||
.prepare("SELECT COUNT(*) AS n FROM account WHERE json_extract(data, '$.deviceId') = ?1")
|
.prepare('SELECT COUNT(*) AS n FROM account WHERE device_id = ?1')
|
||||||
.bind(deviceId)
|
.bind(deviceId)
|
||||||
.first<{ n: number }>()
|
.first<{ n: number }>()
|
||||||
return row?.n ?? 0
|
return row?.n ?? 0
|
||||||
@@ -462,7 +465,7 @@ export async function setLoginContext(
|
|||||||
export async function countAccountsBySignupIp(db: D1Database, ip: string): Promise<number> {
|
export async function countAccountsBySignupIp(db: D1Database, ip: string): Promise<number> {
|
||||||
if (ip === '') return 0
|
if (ip === '') return 0
|
||||||
const row = await db
|
const row = await db
|
||||||
.prepare("SELECT COUNT(*) AS n FROM account WHERE json_extract(data, '$.signupIp') = ?1")
|
.prepare('SELECT COUNT(*) AS n FROM account WHERE signup_ip = ?1')
|
||||||
.bind(ip)
|
.bind(ip)
|
||||||
.first<{ n: number }>()
|
.first<{ n: number }>()
|
||||||
return row?.n ?? 0
|
return row?.n ?? 0
|
||||||
|
|||||||
Reference in New Issue
Block a user