• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 19029600099

03 Nov 2025 09:18AM UTC coverage: 74.778% (-0.1%) from 74.874%
19029600099

push

github

web-flow
Merge pull request #2071 from teableio/fix/formula-issue

fix/formula issue

10481 of 11291 branches covered (92.83%)

167 of 204 new or added lines in 9 files covered. (81.86%)

1213 existing lines in 31 files now uncovered.

51694 of 69130 relevant lines covered (74.78%)

4380.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.68
/apps/nestjs-backend/src/db-provider/sort-query/function/sort-function.abstract.ts
1
import { InternalServerErrorException } from '@nestjs/common';
2✔
2
import type { FieldCore } from '@teable/core';
3
import { SortFunc } from '@teable/core';
4
import type { Knex } from 'knex';
5
import type { IRecordQuerySortContext } from '../../../features/record/query-builder/record-query-builder.interface';
6
import type { ISortFunctionInterface } from './sort-function.interface';
7

8
export abstract class AbstractSortFunction implements ISortFunctionInterface {
2✔
9
  protected columnName?: string;
101✔
10

11
  constructor(
101✔
12
    protected readonly knex: Knex,
101✔
13
    protected readonly field: FieldCore,
101✔
14
    protected readonly context?: IRecordQuerySortContext
101✔
15
  ) {
101✔
16
    const { dbFieldName, id } = field;
101✔
17

18
    const selection = context?.selectionMap.get(id);
101✔
19
    const normalizedSelection =
101✔
20
      selection !== undefined && selection !== null
101✔
21
        ? this.normalizeSelection(selection)
97✔
22
        : undefined;
4✔
23
    if (this.isNullConstant(normalizedSelection)) {
101!
UNCOV
24
      this.columnName = undefined;
×
UNCOV
25
      return;
×
UNCOV
26
    }
×
27
    if (normalizedSelection) {
101✔
28
      this.columnName = normalizedSelection;
97✔
29
      return;
97✔
30
    }
97✔
31
    const quotedIdentifier = this.quoteIdentifier(dbFieldName);
4✔
32
    this.columnName = this.isNullConstant(quotedIdentifier) ? undefined : quotedIdentifier;
101✔
33
  }
101✔
34

35
  compiler(builderClient: Knex.QueryBuilder, sortFunc: SortFunc) {
101✔
36
    const functionHandlers = {
101✔
37
      [SortFunc.Asc]: this.asc,
101✔
38
      [SortFunc.Desc]: this.desc,
101✔
39
    };
101✔
40
    const chosenHandler = functionHandlers[sortFunc].bind(this);
101✔
41

42
    if (!chosenHandler) {
101!
43
      throw new InternalServerErrorException(`Unknown function ${sortFunc} for sort`);
×
44
    }
×
45

46
    return chosenHandler(builderClient);
101✔
47
  }
101✔
48

49
  generateSQL(sortFunc: SortFunc): string | undefined {
101✔
UNCOV
50
    const functionHandlers = {
×
51
      [SortFunc.Asc]: this.getAscSQL,
×
52
      [SortFunc.Desc]: this.getDescSQL,
×
UNCOV
53
    };
×
UNCOV
54
    const chosenHandler = functionHandlers[sortFunc].bind(this);
×
55

56
    if (!chosenHandler) {
×
57
      throw new InternalServerErrorException(`Unknown function ${sortFunc} for sort`);
×
UNCOV
58
    }
×
59

60
    return chosenHandler();
×
61
  }
×
62

63
  asc(builderClient: Knex.QueryBuilder): Knex.QueryBuilder {
101✔
UNCOV
64
    if (!this.columnName) {
×
65
      return builderClient;
×
66
    }
×
UNCOV
67
    builderClient.orderByRaw(`${this.columnName} ASC NULLS FIRST`);
×
UNCOV
68
    return builderClient;
×
69
  }
×
70

71
  desc(builderClient: Knex.QueryBuilder): Knex.QueryBuilder {
101✔
UNCOV
72
    if (!this.columnName) {
×
UNCOV
73
      return builderClient;
×
UNCOV
74
    }
×
UNCOV
75
    builderClient.orderByRaw(`${this.columnName} DESC NULLS LAST`);
×
UNCOV
76
    return builderClient;
×
UNCOV
77
  }
×
78

79
  getAscSQL() {
101✔
UNCOV
80
    if (!this.columnName) {
×
81
      return undefined;
×
82
    }
×
UNCOV
83
    return this.knex.raw(`${this.columnName} ASC NULLS FIRST`).toQuery();
×
84
  }
×
85

86
  getDescSQL() {
101✔
87
    if (!this.columnName) {
×
88
      return undefined;
×
89
    }
×
90
    return this.knex.raw(`${this.columnName} DESC NULLS LAST`).toQuery();
×
UNCOV
91
  }
×
92

93
  protected createSqlPlaceholders(values: unknown[]): string {
101✔
94
    return values.map(() => '?').join(',');
11✔
95
  }
11✔
96

97
  private normalizeSelection(selection: unknown): string | undefined {
101✔
98
    if (typeof selection === 'string') {
97✔
99
      return selection;
97✔
100
    }
97!
101
    if (selection && typeof (selection as Knex.Raw).toQuery === 'function') {
97!
UNCOV
102
      return (selection as Knex.Raw).toQuery();
×
UNCOV
103
    }
×
104
    if (selection && typeof (selection as Knex.Raw).toSQL === 'function') {
97!
UNCOV
105
      const { sql } = (selection as Knex.Raw).toSQL();
×
UNCOV
106
      if (sql) {
×
UNCOV
107
        return sql;
×
UNCOV
108
      }
×
UNCOV
109
    }
×
UNCOV
110
    return undefined;
×
UNCOV
111
  }
×
112

113
  private quoteIdentifier(identifier: string): string {
101✔
114
    if (!identifier) {
4!
UNCOV
115
      return identifier;
×
UNCOV
116
    }
×
117
    if (identifier.startsWith('"') && identifier.endsWith('"')) {
4✔
118
      return identifier;
4✔
119
    }
4!
UNCOV
120
    const escaped = identifier.replace(/"/g, '""');
×
UNCOV
121
    return `"${escaped}"`;
×
UNCOV
122
  }
×
123

124
  private isNullConstant(selection?: string): boolean {
101✔
125
    if (!selection) {
105✔
126
      return false;
4✔
127
    }
4✔
128
    const trimmed = selection.trim().toUpperCase();
101✔
129
    if (trimmed === 'NULL') {
105!
UNCOV
130
      return true;
×
UNCOV
131
    }
✔
132
    return trimmed.startsWith('NULL::');
101✔
133
  }
101✔
134
}
101✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc