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

rogerpadilla / uql / 23572402971

26 Mar 2026 01:08AM UTC coverage: 94.888% (-0.2%) from 95.086%
23572402971

push

github

rogerpadilla
chore: update version to 0.7.1 in CHANGELOG.md to reflect the addition of Bun SQL support and related enhancements

2890 of 3210 branches covered (90.03%)

Branch coverage included in aggregate %.

5129 of 5241 relevant lines covered (97.86%)

348.18 hits per line

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

95.0
/packages/uql-orm/src/util/sql.util.ts
1
import type { Key, QueryUpdateResult, RawRow } from '../type/index.js';
2
import type { PrimaryKey } from '../type/utility.js';
3
import { getKeys, hasKeys } from './object.util.js';
4

5
/** Pre-computed regex for each SQL identifier escape character to avoid per-call allocation. */
6
const escapeIdRegexCache = { '`': /`/g, '"': /"/g } as const satisfies Record<string, RegExp>;
72✔
7

8
export function flatObject<E extends object>(obj: E, pre?: string): E {
9
  return getKeys(obj).reduce(
111✔
10
    (acc, key) => flatObjectEntry(acc, key, obj[key as Key<E>], typeof obj[key as Key<E>] === 'object' ? '' : pre),
158✔
11
    {} as E,
12
  );
13
}
14

15
function flatObjectEntry<E>(map: E, key: string, val: unknown, pre?: string): E {
16
  const prefix = pre ? `${pre}.${key}` : key;
169✔
17
  if (typeof val === 'object' && val !== null) {
169✔
18
    return getKeys(val).reduce(
10✔
19
      (acc, prop) => flatObjectEntry(acc, prop, (val as Record<string, unknown>)[prop], prefix),
11✔
20
      map,
21
    );
22
  }
23
  (map as Record<string, unknown>)[prefix] = val;
159✔
24
  return map;
159✔
25
}
26

27
export function unflatObjects<T extends object>(objects: RawRow[]): T[] {
28
  if (!Array.isArray(objects) || !objects.length) {
207✔
29
    return objects as T[];
42✔
30
  }
31

32
  const attrsPaths = obtainAttrsPaths(objects[0]);
165✔
33

34
  if (!hasKeys(attrsPaths)) {
165✔
35
    return objects as T[];
139✔
36
  }
37

38
  return objects.map((row) => unflatObject<T>(row, attrsPaths));
39✔
39
}
40

41
/**
42
 * Unflattens a single raw row using pre-computed attribute paths.
43
 * Use this for streaming to avoid per-row array allocations.
44
 */
45
export function unflatObject<T extends object>(row: RawRow, attrsPaths: Record<string, string[]>): T {
46
  const dto = {} as T;
61✔
47

48
  for (const col in row) {
61✔
49
    if (row[col] === null) {
488✔
50
      continue;
149✔
51
    }
52
    const attrPath = attrsPaths[col];
339✔
53
    if (attrPath) {
339✔
54
      let target = dto as Record<string, unknown>;
161✔
55
      for (let i = 0; i < attrPath.length - 1; i++) {
161✔
56
        const seg = attrPath[i];
181✔
57
        if (typeof target[seg] !== 'object') {
181✔
58
          target[seg] = {};
50✔
59
        }
60
        target = target[seg] as Record<string, unknown>;
181✔
61
      }
62
      target[attrPath[attrPath.length - 1]] = row[col];
161✔
63
    } else {
64
      (dto as RawRow)[col] = row[col];
178✔
65
    }
66
  }
67

68
  return dto;
61✔
69
}
70

71
export function obtainAttrsPaths<T extends object>(row: T) {
72
  const paths: { [k: string]: string[] } = {};
183✔
73
  for (const col in row) {
183✔
74
    if (col.includes('.')) {
901✔
75
      paths[col] = col.split('.');
170✔
76
    }
77
  }
78
  return paths;
182✔
79
}
80

81
/**
82
 * Escape a SQL identifier (table name, column name, etc.)
83
 * @param val the identifier to escape
84
 * @param escapeIdChar the escape character to use (e.g. ` or ")
85
 * @param forbidQualified whether to forbid qualified identifiers (containing dots)
86
 * @param addDot whether to add a dot suffix
87
 */
88
export function escapeSqlId(
89
  val: string,
90
  escapeIdChar: '`' | '"' = '`',
22,934✔
91
  forbidQualified?: boolean,
92
  addDot?: boolean,
93
): string {
94
  if (!val) {
22,934✔
95
    return '';
5,215✔
96
  }
97

98
  if (!forbidQualified && val.includes('.')) {
17,719✔
99
    const result = val
12✔
100
      .split('.')
101
      .map((it) => escapeSqlId(it, escapeIdChar, true))
24✔
102
      .join('.');
103
    return addDot ? result + '.' : result;
12✔
104
  }
105

106
  const escaped =
107
    escapeIdChar + val.replace(escapeIdRegexCache[escapeIdChar], escapeIdChar + escapeIdChar) + escapeIdChar;
17,707✔
108

109
  const suffix = addDot ? '.' : '';
17,707✔
110

111
  return escaped + suffix;
22,934✔
112
}
113

114
/**
115
 * Payload for building a QueryUpdateResult.
116
 */
117
export interface BuildUpdateResultPayload {
118
  /** The count of rows affected by the statement. */
119
  changes?: number;
120
  /** The raw rows returned by the query (for RETURNING clauses). */
121
  rows?: RawRow[];
122
  /** The first (MySQL) or last (SQLite) auto-generated ID from the driver header. */
123
  id?: PrimaryKey;
124
  /** The ID strategy: 'first' (MySQL/MariaDB) or 'last' (SQLite/LibSQL/D1). */
125
  insertIdStrategy?: 'first' | 'last';
126
  /**
127
   * Driver-specific upsert detection from the result header.
128
   * MySQL/MariaDB `ON DUPLICATE KEY UPDATE` convention: 1 = insert, 2 = update, 0 = no-op.
129
   */
130
  upsertStatus?: number;
131
}
132

133
/**
134
 * Unified utility to build a QueryUpdateResult from driver-specific results.
135
 *
136
 * UQL's SQL dialects always alias the entity's ID column to `id` in RETURNING clauses,
137
 * so the result rows always contain an `id` property regardless of the entity's @Id() key name.
138
 */
139
export function buildUpdateResult(payload: BuildUpdateResultPayload): QueryUpdateResult {
140
  const { rows, id, insertIdStrategy, upsertStatus } = payload;
2,615✔
141
  const changes = payload.changes ?? rows?.length ?? 0;
2,615✔
142

143
  // 1. ID Mapping
144
  let firstId: PrimaryKey | undefined;
145
  const rowId = rows?.[0]?.['id'] as PrimaryKey | undefined;
2,615✔
146
  if (isPrimaryKey(rowId)) {
2,615✔
147
    firstId = rowId;
150✔
148
  } else if (isPrimaryKey(id)) {
2,465✔
149
    const offset = changes - 1;
2,020✔
150
    if (insertIdStrategy === 'last') {
2,020✔
151
      if (typeof id === 'bigint') {
1,734✔
152
        firstId = id - BigInt(offset);
2✔
153
      } else if (typeof id === 'number') {
1,732!
154
        firstId = id - offset;
1,732✔
155
      } else {
156
        firstId = id;
×
157
      }
158
    } else {
159
      firstId = id;
286✔
160
    }
161
  }
162

163
  let ids: PrimaryKey[] = [];
2,615✔
164

165
  if (rows?.length) {
2,615✔
166
    ids = rows.map((r) => r['id'] as PrimaryKey);
215✔
167
  } else if (isPrimaryKey(firstId)) {
2,465✔
168
    if (typeof firstId === 'bigint' || typeof firstId === 'number') {
2,020!
169
      ids = Array.from({ length: changes }, (_, i) =>
2,020✔
170
        typeof firstId === 'bigint' ? firstId + BigInt(i) : firstId + i,
558✔
171
      );
172
    } else if (changes === 1) {
×
173
      ids = [firstId];
×
174
    }
175
  }
176

177
  // 2. Creation Status
178
  // PostgreSQL: `(xmax = 0) AS "_created"` in the RETURNING clause provides a boolean per row.
179
  // MySQL/MariaDB: `affectedRows` convention — 1 = insert, 2 = update, 0 = no-op.
180
  const created =
2,615✔
181
    (rows?.length === 1 ? (rows[0]?.['_created'] as boolean | undefined) : undefined) ??
2,615✔
182
    (typeof upsertStatus === 'number' && upsertStatus >= 0 && upsertStatus <= 2 ? upsertStatus === 1 : undefined);
6,236✔
183

184
  return { changes, ids, firstId: ids?.[0], created };
2,615✔
185
}
186

187
/**
188
 * Checks if a value is of a primary key type (string, number, or bigint).
189
 */
190
export function isPrimaryKey(val: unknown): val is PrimaryKey {
191
  return typeof val === 'string' || typeof val === 'number' || typeof val === 'bigint';
7,555✔
192
}
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