mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat(base): schema-qualified query builder for single-instance duckdb
This commit is contained in:
@@ -2,6 +2,8 @@ import { buildColumnSpecs } from './column-types';
|
|||||||
import { buildDuckDbListQuery } from './duckdb-query-builder';
|
import { buildDuckDbListQuery } from './duckdb-query-builder';
|
||||||
import { BasePropertyType } from '../base.schemas';
|
import { BasePropertyType } from '../base.schemas';
|
||||||
|
|
||||||
|
const SCHEMA = 'b_019c69a3dd4770148b87ec8f1675aaaa';
|
||||||
|
|
||||||
const numericProp = {
|
const numericProp = {
|
||||||
id: '00000000-0000-0000-0000-000000000001',
|
id: '00000000-0000-0000-0000-000000000001',
|
||||||
type: BasePropertyType.NUMBER,
|
type: BasePropertyType.NUMBER,
|
||||||
@@ -18,10 +20,11 @@ const columns = buildColumnSpecs([numericProp, textProp]);
|
|||||||
describe('buildDuckDbListQuery', () => {
|
describe('buildDuckDbListQuery', () => {
|
||||||
it('renders no-filter, no-sort, no-search as live-rows-paginated-by-position', () => {
|
it('renders no-filter, no-sort, no-search as live-rows-paginated-by-position', () => {
|
||||||
const { sql, params } = buildDuckDbListQuery({
|
const { sql, params } = buildDuckDbListQuery({
|
||||||
|
schema: SCHEMA,
|
||||||
columns,
|
columns,
|
||||||
pagination: { limit: 100 },
|
pagination: { limit: 100 },
|
||||||
});
|
});
|
||||||
expect(sql).toMatch(/FROM rows/);
|
expect(sql).toContain(`FROM ${SCHEMA}.rows`);
|
||||||
expect(sql).toMatch(/deleted_at IS NULL/);
|
expect(sql).toMatch(/deleted_at IS NULL/);
|
||||||
expect(sql).toMatch(/ORDER BY position ASC, id ASC/);
|
expect(sql).toMatch(/ORDER BY position ASC, id ASC/);
|
||||||
expect(sql).toMatch(/LIMIT 101/);
|
expect(sql).toMatch(/LIMIT 101/);
|
||||||
@@ -30,6 +33,7 @@ describe('buildDuckDbListQuery', () => {
|
|||||||
|
|
||||||
it('renders numeric gt filter with parameterized value', () => {
|
it('renders numeric gt filter with parameterized value', () => {
|
||||||
const { sql, params } = buildDuckDbListQuery({
|
const { sql, params } = buildDuckDbListQuery({
|
||||||
|
schema: SCHEMA,
|
||||||
columns,
|
columns,
|
||||||
filter: {
|
filter: {
|
||||||
op: 'and',
|
op: 'and',
|
||||||
@@ -43,6 +47,7 @@ describe('buildDuckDbListQuery', () => {
|
|||||||
|
|
||||||
it('renders text contains with ILIKE and escaped wildcards', () => {
|
it('renders text contains with ILIKE and escaped wildcards', () => {
|
||||||
const { sql, params } = buildDuckDbListQuery({
|
const { sql, params } = buildDuckDbListQuery({
|
||||||
|
schema: SCHEMA,
|
||||||
columns,
|
columns,
|
||||||
filter: {
|
filter: {
|
||||||
op: 'and',
|
op: 'and',
|
||||||
@@ -56,6 +61,7 @@ describe('buildDuckDbListQuery', () => {
|
|||||||
|
|
||||||
it('renders sort with sentinel wrapping and cursor keyset', () => {
|
it('renders sort with sentinel wrapping and cursor keyset', () => {
|
||||||
const { sql } = buildDuckDbListQuery({
|
const { sql } = buildDuckDbListQuery({
|
||||||
|
schema: SCHEMA,
|
||||||
columns,
|
columns,
|
||||||
sorts: [{ propertyId: numericProp.id, direction: 'asc' }],
|
sorts: [{ propertyId: numericProp.id, direction: 'asc' }],
|
||||||
pagination: {
|
pagination: {
|
||||||
@@ -71,6 +77,7 @@ describe('buildDuckDbListQuery', () => {
|
|||||||
|
|
||||||
it('renders search in trgm mode as ILIKE on search_text', () => {
|
it('renders search in trgm mode as ILIKE on search_text', () => {
|
||||||
const { sql, params } = buildDuckDbListQuery({
|
const { sql, params } = buildDuckDbListQuery({
|
||||||
|
schema: SCHEMA,
|
||||||
columns,
|
columns,
|
||||||
search: { mode: 'trgm', query: 'hello' },
|
search: { mode: 'trgm', query: 'hello' },
|
||||||
pagination: { limit: 10 },
|
pagination: { limit: 10 },
|
||||||
@@ -89,6 +96,7 @@ describe('buildDuckDbListQuery', () => {
|
|||||||
const choiceA = 'choice-uuid-aaa';
|
const choiceA = 'choice-uuid-aaa';
|
||||||
const choiceB = 'choice-uuid-bbb';
|
const choiceB = 'choice-uuid-bbb';
|
||||||
const { sql, params } = buildDuckDbListQuery({
|
const { sql, params } = buildDuckDbListQuery({
|
||||||
|
schema: SCHEMA,
|
||||||
columns: cols,
|
columns: cols,
|
||||||
filter: {
|
filter: {
|
||||||
op: 'and',
|
op: 'and',
|
||||||
@@ -104,6 +112,7 @@ describe('buildDuckDbListQuery', () => {
|
|||||||
|
|
||||||
it('renders nested AND/OR groups with correct parentheses', () => {
|
it('renders nested AND/OR groups with correct parentheses', () => {
|
||||||
const { sql } = buildDuckDbListQuery({
|
const { sql } = buildDuckDbListQuery({
|
||||||
|
schema: SCHEMA,
|
||||||
columns,
|
columns,
|
||||||
filter: {
|
filter: {
|
||||||
op: 'or',
|
op: 'or',
|
||||||
@@ -119,6 +128,7 @@ describe('buildDuckDbListQuery', () => {
|
|||||||
|
|
||||||
it('handles empty filter group without emitting WHERE on it', () => {
|
it('handles empty filter group without emitting WHERE on it', () => {
|
||||||
const { sql, params } = buildDuckDbListQuery({
|
const { sql, params } = buildDuckDbListQuery({
|
||||||
|
schema: SCHEMA,
|
||||||
columns,
|
columns,
|
||||||
filter: { op: 'and', children: [] },
|
filter: { op: 'and', children: [] },
|
||||||
pagination: { limit: 100 },
|
pagination: { limit: 100 },
|
||||||
@@ -130,6 +140,7 @@ describe('buildDuckDbListQuery', () => {
|
|||||||
|
|
||||||
it('renders multi-sort keyset with s0, s1, position, id chain', () => {
|
it('renders multi-sort keyset with s0, s1, position, id chain', () => {
|
||||||
const { sql } = buildDuckDbListQuery({
|
const { sql } = buildDuckDbListQuery({
|
||||||
|
schema: SCHEMA,
|
||||||
columns,
|
columns,
|
||||||
sorts: [
|
sorts: [
|
||||||
{ propertyId: numericProp.id, direction: 'asc' },
|
{ propertyId: numericProp.id, direction: 'asc' },
|
||||||
@@ -149,6 +160,7 @@ describe('buildDuckDbListQuery', () => {
|
|||||||
|
|
||||||
it('renders text isEmpty as IS NULL OR = empty-string', () => {
|
it('renders text isEmpty as IS NULL OR = empty-string', () => {
|
||||||
const { sql } = buildDuckDbListQuery({
|
const { sql } = buildDuckDbListQuery({
|
||||||
|
schema: SCHEMA,
|
||||||
columns,
|
columns,
|
||||||
filter: {
|
filter: {
|
||||||
op: 'and',
|
op: 'and',
|
||||||
@@ -158,4 +170,14 @@ describe('buildDuckDbListQuery', () => {
|
|||||||
});
|
});
|
||||||
expect(sql).toMatch(new RegExp(`"${textProp.id}" IS NULL`));
|
expect(sql).toMatch(new RegExp(`"${textProp.id}" IS NULL`));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects invalid schema name', () => {
|
||||||
|
expect(() =>
|
||||||
|
buildDuckDbListQuery({
|
||||||
|
schema: 'bad name',
|
||||||
|
columns: [],
|
||||||
|
pagination: { limit: 10 },
|
||||||
|
}),
|
||||||
|
).toThrow(/invalid schema/i);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ColumnSpec } from './query-cache.types';
|
|||||||
export type AfterKeys = Record<string, unknown>;
|
export type AfterKeys = Record<string, unknown>;
|
||||||
|
|
||||||
export type DuckDbListQueryOpts = {
|
export type DuckDbListQueryOpts = {
|
||||||
|
schema: string;
|
||||||
columns: ColumnSpec[];
|
columns: ColumnSpec[];
|
||||||
filter?: FilterNode;
|
filter?: FilterNode;
|
||||||
sorts?: SortSpec[];
|
sorts?: SortSpec[];
|
||||||
@@ -54,6 +55,10 @@ const SYSTEM_COLUMN_DUCK: Record<string, 'created_at' | 'updated_at' | 'last_upd
|
|||||||
export function buildDuckDbListQuery(
|
export function buildDuckDbListQuery(
|
||||||
opts: DuckDbListQueryOpts,
|
opts: DuckDbListQueryOpts,
|
||||||
): DuckDbListQuery {
|
): DuckDbListQuery {
|
||||||
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(opts.schema)) {
|
||||||
|
throw new Error(`Invalid schema name "${opts.schema}"`);
|
||||||
|
}
|
||||||
|
const rowsTable = `${opts.schema}.rows`;
|
||||||
const index = indexColumns(opts.columns);
|
const index = indexColumns(opts.columns);
|
||||||
const params: unknown[] = [];
|
const params: unknown[] = [];
|
||||||
|
|
||||||
@@ -86,7 +91,7 @@ export function buildDuckDbListQuery(
|
|||||||
|
|
||||||
const sql =
|
const sql =
|
||||||
`SELECT ${selectParts.join(', ')}` +
|
`SELECT ${selectParts.join(', ')}` +
|
||||||
` FROM rows` +
|
` FROM ${rowsTable}` +
|
||||||
` WHERE ${whereClauses.join(' AND ')}` +
|
` WHERE ${whereClauses.join(' AND ')}` +
|
||||||
` ORDER BY ${orderByParts.join(', ')}` +
|
` ORDER BY ${orderByParts.join(', ')}` +
|
||||||
` LIMIT ${opts.pagination.limit + 1}`;
|
` LIMIT ${opts.pagination.limit + 1}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user