mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
feat(db): dialect helper is the one place SQL functions are spelled
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { raw, type RawQueryFragment } from '@mikro-orm/core';
|
||||
|
||||
/**
|
||||
* The only place a repository may spell a database function.
|
||||
*
|
||||
* Every helper takes an already-qualified column reference (`'t.created_at'`)
|
||||
* and returns a fragment for the *current* dialect. The ORM exists so a
|
||||
* Postgres driver can follow; when it does, this file gets a second
|
||||
* implementation and no repository changes. Values are never interpolated —
|
||||
* the day count in `dateAdd` is checked to be an integer before it is spelled.
|
||||
*/
|
||||
|
||||
function column(ref: string): string {
|
||||
if (!/^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)?$/i.test(ref)) {
|
||||
throw new Error(`sql-functions: not a column reference: ${ref}`);
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
/** The calendar date (`YYYY-MM-DD`) of a timestamp column. */
|
||||
export function dateOf(ref: string): RawQueryFragment {
|
||||
return raw(`date(${column(ref)})`);
|
||||
}
|
||||
|
||||
/** The timestamp column shifted by whole days, as a calendar date. */
|
||||
export function dateAdd(ref: string, days: number): RawQueryFragment {
|
||||
if (!Number.isInteger(days)) throw new Error(`sql-functions: dateAdd needs an integer day count, got ${days}`);
|
||||
const sign = days < 0 ? '-' : '+';
|
||||
return raw(`date(${column(ref)}, '${sign}${Math.abs(days)} days')`);
|
||||
}
|
||||
|
||||
/** The database clock, in the same text the column defaults produce. */
|
||||
export function currentTimestamp(): RawQueryFragment {
|
||||
return raw('CURRENT_TIMESTAMP');
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createTestDb, resetTestDb } from '../../../helpers/test-db';
|
||||
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
|
||||
import { createUser } from '../../../helpers/factories';
|
||||
import { Users } from '../../../../src/db/entities/Users.entity';
|
||||
import { currentTimestamp, dateAdd, dateOf } from '../../../../src/db/dialect/sql-functions';
|
||||
|
||||
const testDb = createTestDb();
|
||||
let t: TestOrm;
|
||||
|
||||
beforeAll(async () => { t = await createTestOrm(testDb); });
|
||||
beforeEach(() => { resetTestDb(testDb); t.clear(); });
|
||||
afterAll(async () => { await t.close(); testDb.close(); });
|
||||
|
||||
describe('sql-functions (sqlite)', () => {
|
||||
it('SQLF-001: dateOf yields the calendar date of a stored timestamp', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
testDb.prepare("UPDATE users SET created_at = '2026-09-21 13:05:09' WHERE id = ?").run(user.id);
|
||||
const row = await t.em.createQueryBuilder(Users, 'u')
|
||||
.select([dateOf('u.created_at').as('d')])
|
||||
.where({ id: user.id })
|
||||
.execute('get', false);
|
||||
expect(row).toEqual({ d: '2026-09-21' });
|
||||
});
|
||||
|
||||
it('SQLF-002: dateAdd shifts by whole days', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
testDb.prepare("UPDATE users SET created_at = '2026-09-21 13:05:09' WHERE id = ?").run(user.id);
|
||||
const row = await t.em.createQueryBuilder(Users, 'u')
|
||||
.select([dateAdd('u.created_at', 10).as('d')])
|
||||
.where({ id: user.id })
|
||||
.execute('get', false);
|
||||
expect(row).toEqual({ d: '2026-10-01' });
|
||||
});
|
||||
|
||||
it('SQLF-003: currentTimestamp is the DB clock in the wire format', async () => {
|
||||
createUser(testDb);
|
||||
const row = await t.em.createQueryBuilder(Users, 'u')
|
||||
.select([currentTimestamp().as('now')])
|
||||
.limit(1)
|
||||
.execute('get', false);
|
||||
expect(String((row as { now: string }).now)).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user