feat(base): add baseSchemaName helper for duckdb schema naming

This commit is contained in:
Philipinho
2026-04-23 16:05:45 +01:00
parent b04bcb5b0c
commit 08711791d6
2 changed files with 65 additions and 0 deletions
@@ -0,0 +1,34 @@
import { baseSchemaName } from './schema-name';
describe('baseSchemaName', () => {
it('converts a uuid to a DuckDB-safe identifier with a b_ prefix', () => {
expect(baseSchemaName('019c69a5-1d84-7985-a7f6-8ee2871d8669')).toBe(
'b_019c69a51d847985a7f68ee2871d8669',
);
});
it('rejects a non-uuid string (preserves the quoting contract)', () => {
expect(() => baseSchemaName('not-a-uuid')).toThrow(/invalid base id/i);
expect(() => baseSchemaName('')).toThrow(/invalid base id/i);
expect(() => baseSchemaName('b_019c69a5; DROP TABLE rows; --')).toThrow(
/invalid base id/i,
);
});
it('is deterministic', () => {
const id = '019c70b3-dd47-7014-8b87-ec8f167577ee';
expect(baseSchemaName(id)).toBe(baseSchemaName(id));
});
it('accepts mixed-case hex and normalises to lowercase', () => {
expect(baseSchemaName('019C69A5-1D84-7985-A7F6-8EE2871D8669')).toBe(
'b_019c69a51d847985a7f68ee2871d8669',
);
});
it('produces names that parse as SQL identifiers without quoting', () => {
const name = baseSchemaName('019c69a5-1d84-7985-a7f6-8ee2871d8669');
// Must match DuckDB's unquoted-identifier grammar: [a-zA-Z_][a-zA-Z0-9_]*
expect(name).toMatch(/^[a-zA-Z_][a-zA-Z0-9_]*$/);
});
});
@@ -0,0 +1,31 @@
// Matches the UUID regex pattern in `loader-sql.ts`. We use a handwritten
// regex rather than importing `validate` from the `uuid` package because
// that package is ESM-only and Jest's ts-jest config cannot transform it
// in this repo.
const UUID =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
const UUID_DASHES = /-/g;
/*
* Turns a base UUID into a DuckDB-safe schema name.
*
* '019c69a5-1d84-7985-a7f6-8ee2871d8669'
* -> 'b_019c69a51d847985a7f68ee2871d8669'
*
* The `b_` prefix is required because DuckDB unquoted identifiers must start
* with a letter or underscore — a bare hex UUID starts with a digit and would
* have to be double-quoted everywhere. The strip-dashes step makes the rest
* of the identifier hex-only, which is always safe.
*
* All attached database names, `DETACH DATABASE` targets, and schema-qualified
* references (`<schema>.rows`) run through this function. Validation is
* strict: if the input isn't a real UUID, we throw rather than produce a
* "safe-looking" identifier that might leak through to user-facing SQL.
*/
export function baseSchemaName(baseId: string): string {
if (!UUID.test(baseId)) {
throw new Error(`Invalid base id "${baseId}"`);
}
return `b_${baseId.toLowerCase().replace(UUID_DASHES, '')}`;
}