Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/query/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface QueryState<T> {
readonly conditions: readonly string[]
readonly orderFields: readonly { field: string; direction: SortDir }[]
readonly groupFields: readonly string[]
readonly groupAll: boolean
readonly limitValue: number | null
readonly offsetValue: number | null
readonly data: Record<string, unknown> | null
Expand All @@ -47,6 +48,7 @@ function defaultState<T>(): QueryState<T> {
conditions: [],
orderFields: [],
groupFields: [],
groupAll: false,
limitValue: null,
offsetValue: null,
data: null,
Expand Down Expand Up @@ -112,6 +114,11 @@ export class Query<T = Record<string, unknown>> {
return this.with({ groupFields: [...this.state.groupFields, ...fields] })
}

/** Add GROUP ALL (aggregate entire result set without grouping fields) */
groupAll(): Query<T> {
return this.with({ groupAll: true })
}

/** Set LIMIT */
limit(n: number): Query<T> {
return this.with({ limitValue: n })
Expand Down Expand Up @@ -235,7 +242,9 @@ export class Query<T = Record<string, unknown>> {
sql += ` WHERE ${this.state.conditions.join(' AND ')}`
}

if (this.state.groupFields.length > 0) {
if (this.state.groupAll) {
sql += ' GROUP ALL'
} else if (this.state.groupFields.length > 0) {
sql += ` GROUP BY ${this.state.groupFields.join(', ')}`
}

Expand Down
85 changes: 85 additions & 0 deletions src/query/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,26 @@ export function max_(expr: Expression): FunctionExpression {
return func('math::max', expr)
}

/** math::mean aggregate (alias for avg) */
export function mathMean(expr: Expression): FunctionExpression {
return func('math::mean', expr)
}

/** math::sum aggregate (alias for sum_) */
export function mathSum(expr: Expression): FunctionExpression {
return func('math::sum', expr)
}

/** math::max aggregate (alias for max_) */
export function mathMax(expr: Expression): FunctionExpression {
return func('math::max', expr)
}

/** math::min aggregate (alias for min_) */
export function mathMin(expr: Expression): FunctionExpression {
return func('math::min', expr)
}

/** ABS */
export function abs_(expr: Expression): FunctionExpression {
return func('math::abs', expr)
Expand Down Expand Up @@ -216,3 +236,68 @@ export function cast(expr: Expression, typeName: string): Expression {
},
})
}

/**
* Create a type::record() reference for linking to a specific record.
* Generates: type::record('table', 'id') or type::record('table:id')
*
* @param table - Table name
* @param id - Optional record ID (if omitted, table is treated as a full record string)
*/
export function recordRef(table: string, id?: string): Expression {
validateIdentifier(table)
if (id !== undefined) {
return Object.freeze({
toSurQL(): string {
return `type::record('${table}:${id}')`
},
})
}
return Object.freeze({
toSurQL(): string {
return `type::record('${table}')`
},
})
}

/**
* Marker interface for SurrealDB server-side function values.
* When used in create/update data, these render as raw SurrealQL
* instead of being parameterized.
*/
export interface SurrealFnValue {
readonly __surqlFn: true
readonly surql: string
toSurQL(): string
}

/**
* Create a SurrealDB server-side function reference for use in field values.
* When passed as a value in create/update operations, it will be rendered
* as raw SurrealQL rather than parameterized.
*
* @param name - Fully qualified function name (e.g. 'time::now', 'math::floor')
* @param args - Optional arguments as SurrealQL strings
*/
export function surqlFn(name: string, ...args: string[]): SurrealFnValue {
const argsStr = args.join(', ')
return Object.freeze({
__surqlFn: true as const,
surql: `${name}(${argsStr})`,
toSurQL(): string {
return this.surql
},
})
}

/**
* Type guard to check if a value is a SurrealFnValue
*/
export function isSurqlFn(value: unknown): value is SurrealFnValue {
return (
typeof value === 'object' &&
value !== null &&
'__surqlFn' in value &&
(value as SurrealFnValue).__surqlFn === true
)
}
13 changes: 12 additions & 1 deletion src/query/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ export enum ReturnFormat {
export type VectorDistanceType = 'COSINE' | 'EUCLIDEAN' | 'MANHATTAN' | 'MINKOWSKI' | 'CHEBYSHEV' | 'HAMMING'

/**
* Quote a value for safe SurrealQL embedding
* Quote a value for safe SurrealQL embedding.
* Values with a `__surqlFn` marker are rendered as raw SurrealQL (server-side functions).
*/
export function quoteValue(value: unknown): string {
if (value === null || value === undefined) return 'NONE'
// Handle SurrealFnValue - render as raw SurrealQL, not parameterized
if (
typeof value === 'object' &&
value !== null &&
'__surqlFn' in value &&
(value as { __surqlFn: boolean }).__surqlFn === true &&
'surql' in value
) {
return (value as { surql: string }).surql
}
if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`
if (typeof value === 'boolean') return value ? 'true' : 'false'
if (typeof value === 'number') return String(value)
Expand Down
8 changes: 8 additions & 0 deletions src/query/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,22 @@ export {
floor,
func,
type FunctionExpression,
isSurqlFn,
lower,
mathMax,
mathMean,
mathMin,
mathSum,
max_,
min_,
raw,
type RawExpression,
recordRef,
round_,
stringLength,
sum_,
surqlFn,
type SurrealFnValue,
timeFormat,
timeNow,
typeIs,
Expand Down
20 changes: 15 additions & 5 deletions src/query/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,23 @@ export function extractOne<T>(raw: unknown): T | null {
}

/**
* Extract a scalar value from results
*/
export function extractScalar<T>(raw: unknown): T | null {
* Extract a scalar value from results.
* When `key` is provided, extracts that specific field.
* When `defaultValue` is provided, returns it instead of null on missing data.
*
* @param raw - Raw SurrealDB response
* @param key - Optional field key to extract
* @param defaultValue - Optional fallback value when result is missing
*/
export function extractScalar<T>(raw: unknown, key?: string, defaultValue?: T): T | null {
const item = extractOne<Record<string, unknown>>(raw)
if (!item) return null
if (!item) return defaultValue ?? null
if (key !== undefined) {
const val = item[key]
return val !== undefined ? (val as T) : (defaultValue ?? null)
}
const values = Object.values(item)
return values.length > 0 ? (values[0] as T) : null
return values.length > 0 ? (values[0] as T) : (defaultValue ?? null)
}

/**
Expand Down
Loading
Loading