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

rogerpadilla / uql / 29158059857

11 Jul 2026 03:30PM UTC coverage: 94.962% (+0.03%) from 94.932%
29158059857

push

github

rogerpadilla
feat(cockroachdb): add integration tests and full vector search support

Adds live-DB integration test coverage for CockroachDB (previously
untested), which surfaced and fixed several real bugs:

- upsertOne/upsertMany were completely broken (relied on Postgres's
  xmax system column, which CockroachDB doesn't have)
- auto-generated IDs used SERIAL (CockroachDB's unique_rowid()),
  producing values that overflow Number.MAX_SAFE_INTEGER and get
  silently corrupted as JS numbers; now uses a sequential IDENTITY
  column instead
- $text full-text search never worked; now generates
  to_tsvector/to_tsquery, which CockroachDB supports
- $merge/$push on a JSONB column via the Bun SQL driver could
  corrupt the value or throw, since CockroachDB didn't get the same
  wire-encoding fix Postgres has on that driver

Also adds full vector search support for CockroachDB: $sort/$vector
uses the same pgvector-compatible distance operators as Postgres,
and schema generation emits CockroachDB's native CREATE VECTOR INDEX
syntax.

Refactors the dialect hierarchy so CockroachDialect and
PostgresDialect share a common PgLikeSqlDialect base (mirroring the
existing MySqlLikeSqlDialect pattern) instead of CockroachDialect
extending PostgresDialect directly.

Also fixes MariaDB's upsert `created` detection on the Bun SQL
driver, which could report false on a fresh insert instead of
undefined.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

3264 of 3613 branches covered (90.34%)

Branch coverage included in aggregate %.

115 of 118 new or added lines in 5 files covered. (97.46%)

5652 of 5776 relevant lines covered (97.85%)

460.82 hits per line

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

92.44
/packages/uql-orm/src/dialect/pgLikeSqlDialect.ts
1
import { getMeta } from '../entity/index.js';
2
import {
3
  type DialectFeatures,
4
  type EntityMeta,
5
  type FieldOptions,
6
  type JsonColumnType,
7
  type JsonUpdateOp,
8
  type QueryComparisonOptions,
9
  type QueryConflictPaths,
10
  type QueryContext,
11
  type QueryOptions,
12
  QueryRaw,
13
  type QuerySizeComparisonOps,
14
  type QueryTextSearchOptions,
15
  type QueryVectorSearch,
16
  type Type,
17
  type VectorDistance,
18
} from '../type/index.js';
19
import { escapeAnsiSqlLiteral } from '../util/ansiSqlLiteral.js';
20
import { hasKeys, isJsonType } from '../util/index.js';
21
import { AbstractSqlDialect } from './abstractSqlDialect.js';
22
import { buildElemMatchConditions } from './jsonArrayElemMatchUtils.js';
23

24
/**
25
 * Shared AST/quoting/JSONB/full-text-search/vector-search implementation between Postgres and
26
 * CockroachDB (wire- and SQL-compatible for everything below, including `to_tsvector`/`to_tsquery`
27
 * and pgvector's `<=>`/`<->`/`<#>` distance operators, which CockroachDB implements natively).
28
 * `xmax`-based upsert `created` detection is Postgres-only (CockroachDB has no `xmax`/`ctid`) and
29
 * stays in {@link PostgresDialect}, along with the `vectorExtension`/`vectorIndexStyle` values that
30
 * differ (Postgres needs `CREATE EXTENSION vector` and pgvector's `USING ivfflat/hnsw` index
31
 * syntax; CockroachDB's vector type and `CREATE VECTOR INDEX` syntax are both native).
32
 */
33
export abstract class PgLikeSqlDialect extends AbstractSqlDialect {
34
  /** Default {@link DialectFeatures} for Postgres-wire dialects. */
35
  protected override readonly featureDefaults: DialectFeatures = {
229✔
36
    explicitJsonCast: false,
37
    nativeArrays: true,
38
    supportsJsonb: true,
39
    ifNotExists: true,
40
    indexIfNotExists: true,
41
    dropTableCascade: true,
42
    renameColumn: true,
43
    foreignKeyAlter: true,
44
    columnComment: false,
45
    vectorIndexStyle: 'create',
46
    vectorSupportsLength: true,
47
    supportsTimestamptz: true,
48
    defaultStringAsText: true,
49
  };
50

51
  override readonly quoteChar = '"';
229✔
52
  // Shared default for both dialects. CockroachDB docs flag sequential PKs as a hotspotting risk
53
  // under heavy concurrent insert load (writes concentrate on one range); this default still beats
54
  // `SERIAL` (CockroachDB's `unique_rowid()`, a ~64-bit value that overflows JS's safe-integer
55
  // range). High-throughput CockroachDB users should override this per-entity with a UUID PK.
56
  override readonly serialPrimaryKey: string = 'BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY';
229✔
57
  override readonly tableOptions = '';
229✔
58
  override readonly beginTransactionCommand = 'BEGIN';
229✔
59
  override readonly commitTransactionCommand = 'COMMIT';
229✔
60
  override readonly rollbackTransactionCommand = 'ROLLBACK';
229✔
61
  override readonly alterColumnStrategy = 'separate-clauses';
229✔
62
  override readonly insertIdSource = 'returning';
229✔
63
  override readonly maxBindValues: number = 65535;
229✔
64

65
  override readonly vectorOpsClass: Readonly<Record<VectorDistance, string>> | undefined = {
229✔
66
    cosine: 'vector_cosine_ops',
67
    l2: 'vector_l2_ops',
68
    inner: 'vector_ip_ops',
69
    l1: 'vector_l1_ops',
70
    hamming: 'bit_hamming_ops',
71
  };
72

73
  override normalizeValue(value: unknown): unknown {
74
    if (value != null && typeof value === 'object' && Array.isArray(value)) {
2,971✔
75
      return this.features.nativeArrays ? value : toPgArray(value);
449✔
76
    }
77
    return super.normalizeValue(value);
2,522✔
78
  }
79

80
  override placeholder(index: number): string {
81
    return `$${index}`;
1,371✔
82
  }
83

84
  override insert<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[], opts?: QueryOptions): void {
85
    super.insert(ctx, entity, payload, opts);
171✔
86
    ctx.append(' ' + this.returningId(entity));
171✔
87
  }
88

89
  override upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void {
90
    this.buildUpsertOnConflict(ctx, entity, conflictPaths, payload);
5✔
91
  }
92

93
  /**
94
   * Shared `INSERT ... ON CONFLICT (...) DO UPDATE/NOTHING RETURNING ...` builder, mirroring
95
   * {@link AbstractSqlDialect.onConflictUpsert}'s "assemble everything, don't hand back fragments"
96
   * shape for the `$N`-placeholder Postgres-wire dialects. `extraReturning` lets {@link PostgresDialect}
97
   * append `(xmax = 0) AS "_created"` to detect insert-vs-update; CockroachDB has no `xmax`/`ctid`
98
   * system columns, so it uses the default (empty) and `created` stays `undefined` in the result.
99
   */
100
  protected buildUpsertOnConflict<E>(
101
    ctx: QueryContext,
102
    entity: Type<E>,
103
    conflictPaths: QueryConflictPaths<E>,
104
    payload: E | E[],
105
    extraReturning = '',
18✔
106
  ): void {
107
    const meta = getMeta(entity);
18✔
108
    const update = this.getUpsertUpdateAssignments(ctx, meta, conflictPaths, payload, (name) => `EXCLUDED.${name}`);
18✔
109
    const keysStr = this.getUpsertConflictPathsStr(meta, conflictPaths);
18✔
110
    const onConflict = update ? `DO UPDATE SET ${update}` : 'DO NOTHING';
18✔
111
    super.insert(ctx, entity, payload);
18✔
112
    ctx.append(` ON CONFLICT (${keysStr}) ${onConflict} ${this.returningId(entity)}${extraReturning}`);
18✔
113
  }
114

115
  /** Full-text search: both dialects support `to_tsvector(...) @@ to_tsquery(...)`. */
116
  override compare<E>(
117
    ctx: QueryContext,
118
    entity: Type<E>,
119
    key: string,
120
    val: unknown,
121
    opts: QueryComparisonOptions = {},
740✔
122
  ): void {
123
    if (key === '$text') {
740✔
124
      const meta = getMeta(entity);
3✔
125
      const search = val as QueryTextSearchOptions<E>;
3✔
126
      const fields = (search.$fields ?? [])
3!
127
        .map((fKey) => {
128
          const field = meta.fields[fKey];
5✔
129
          const columnName = this.resolveColumnName(fKey, field!);
5✔
130
          return this.escapeId(columnName);
5✔
131
        })
132
        .join(` || ' ' || `);
133
      ctx.append(`to_tsvector(${fields}) @@ to_tsquery(`);
3✔
134
      ctx.addValue(search.$value);
3✔
135
      ctx.append(')');
3✔
136
      return;
3✔
137
    }
138
    super.compare(ctx, entity, key, val, opts);
737✔
139
  }
140

141
  protected override jsonAll(ctx: QueryContext, jsonField: string, value: unknown): string {
142
    return `${jsonField} @> ${this.jsonVal(ctx, value)}`;
3✔
143
  }
144

145
  protected override jsonSize(ctx: QueryContext, jsonField: string, value: number | QuerySizeComparisonOps): string {
146
    const tmpCtx = this.createContext();
6✔
147
    this.buildSizeComparison(tmpCtx, () => tmpCtx.append(`jsonb_array_length(${jsonField})`), value);
7✔
148
    ctx.pushValue(...tmpCtx.values);
6✔
149
    return tmpCtx.sql;
6✔
150
  }
151

152
  protected override jsonElemMatch(ctx: QueryContext, jsonField: string, match: Record<string, unknown>): string {
153
    // Primitive element match: keys are operators (e.g. { $startsWith: 'ad' } on a string[])
154
    const isPrimitiveElement = Object.keys(match).some((k) => k.startsWith('$'));
22✔
155
    if (isPrimitiveElement) {
11✔
156
      const conditions = Object.entries(match).map(([op, opVal]) =>
2✔
157
        this.buildJsonFieldCondition(ctx, () => 'elem', '', op, opVal),
2✔
158
      );
159
      return `EXISTS (SELECT 1 FROM jsonb_array_elements_text(${jsonField}) AS elem WHERE ${conditions.join(' AND ')})`;
2✔
160
    }
161

162
    const hasOperators = Object.values(match).some(
9✔
163
      (v) => v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).some((k) => k.startsWith('$')),
11✔
164
    );
165

166
    if (!hasOperators) {
9✔
167
      return `${jsonField} @> ${this.jsonVal(ctx, [match])}`;
1✔
168
    }
169

170
    const conditions = buildElemMatchConditions(
8✔
171
      match,
172
      (field, op, opVal) =>
173
        this.buildJsonFieldCondition(ctx, (f) => `elem->>'${this.escapeJsonKey(f)}'`, field, op, opVal),
17✔
174
      (field, val) => `elem->>'${this.escapeJsonKey(field)}' = ${this.addValue(ctx.values, val)}`,
1✔
175
    );
176

177
    return `EXISTS (SELECT 1 FROM jsonb_array_elements(${jsonField}) AS elem WHERE ${conditions.join(' AND ')})`;
8✔
178
  }
179

180
  protected override get regexpOp(): string {
181
    return '~';
2✔
182
  }
183

184
  protected override ilikeExpr(f: string, ph: string): string {
185
    return `${f} ILIKE ${ph}`;
13✔
186
  }
187

188
  protected override get neOp(): string {
189
    return 'IS DISTINCT FROM';
7✔
190
  }
191

192
  protected override formatIn(ctx: QueryContext, values: unknown[], negate: boolean): string {
193
    if (values.length === 0) return negate ? ' NOT IN (NULL)' : ' IN (NULL)';
227!
194
    const ph = this.addValue(ctx.values, values);
227✔
195
    return negate ? ` <> ALL(${ph})` : ` = ANY(${ph})`;
227✔
196
  }
197

198
  protected override numericCast(expr: string): string {
199
    return `(${expr})::numeric`;
8✔
200
  }
201

202
  protected override formatPersistableValue<E>(ctx: QueryContext, field: FieldOptions, value: unknown): void {
203
    if (value instanceof QueryRaw) {
865✔
204
      super.formatPersistableValue(ctx, field, value);
1✔
205
      return;
1✔
206
    }
207
    if (isJsonType(field.type)) {
864✔
208
      ctx.append(this.jsonVal(ctx, value, field.type as JsonColumnType));
8✔
209
      return;
8✔
210
    }
211
    if (field.type === 'vector' && Array.isArray(value)) {
856✔
212
      ctx.addValue(`[${value.join(',')}]`);
29✔
213
      ctx.append('::vector');
29✔
214
      return;
29✔
215
    }
216
    super.formatPersistableValue(ctx, field, value);
827✔
217
  }
218

219
  protected override formatJsonUpdate<E>(ctx: QueryContext, escapedCol: string, value: JsonUpdateOp<E>): void {
220
    let expr = escapedCol;
28✔
221
    if (hasKeys(value.$merge)) {
28✔
222
      expr = `COALESCE(${escapedCol}, '{}'::jsonb) || ${this.jsonVal(ctx, value.$merge)}`;
15✔
223
    }
224
    if (hasKeys(value.$push)) {
28✔
225
      const push = value.$push as Record<string, unknown>;
12✔
226
      for (const [key, v] of Object.entries(push)) {
12✔
227
        const currentExpr = expr;
12✔
228
        const ph = this.jsonVal(ctx, v);
12✔
229
        expr = `jsonb_set(${currentExpr}, '{${this.escapeJsonKey(key)}}', COALESCE((${currentExpr})->'${this.escapeJsonKey(
12✔
230
          key,
231
        )}', '[]'::jsonb) || jsonb_build_array(${ph}))`;
232
      }
233
    }
234
    if (value.$unset?.length) {
28✔
235
      for (const key of value.$unset) {
8✔
236
        expr = `(${expr}) - '${this.escapeJsonKey(key)}'`;
9✔
237
      }
238
    }
239
    ctx.append(`${escapedCol} = ${expr}`);
28✔
240
  }
241

242
  /**
243
   * Helper to add a JSON value to context with appropriate stringification and cast.
244
   */
245
  private jsonVal(ctx: QueryContext, value: unknown, type: JsonColumnType = 'jsonb'): string {
39✔
246
    if (value instanceof QueryRaw) return this.addValue(ctx.values, value);
39!
247
    if (value == null) return `${this.addValue(ctx.values, null)}::${type}`;
39!
248

249
    const json = JSON.stringify(value);
39✔
250
    const ph = this.addValue(ctx.values, json);
39✔
251
    return this.features.explicitJsonCast ? `(${ph}::text)::${type}` : `${ph}::${type}`;
39✔
252
  }
253

254
  override escape(value: unknown): string {
255
    return escapeAnsiSqlLiteral(value);
1✔
256
  }
257

258
  /** pgvector distance operators (also implemented natively by CockroachDB). */
259
  private static readonly VECTOR_OPS: Record<VectorDistance, string> = {
42✔
260
    cosine: '<=>',
261
    l2: '<->',
262
    inner: '<#>',
263
    l1: '<+>',
264
    hamming: '<~>',
265
  };
266

267
  /** Emit a pgvector-style distance expression: `"col" <op> $N::<vectorType>`. */
268
  protected override appendVectorSort<E>(
269
    ctx: QueryContext,
270
    meta: EntityMeta<E>,
271
    key: string,
272
    search: QueryVectorSearch,
273
  ): void {
274
    const { colName, distance, vectorCast } = this.resolveVectorSortParams(meta, key, search);
21✔
275
    const op = PgLikeSqlDialect.VECTOR_OPS[distance];
21✔
276
    ctx.append(`${this.escapeId(colName)} ${op} `);
21✔
277
    ctx.addValue(`[${search.$vector.join(',')}]`);
21✔
278
    ctx.append(`::${vectorCast}`);
21✔
279
  }
280
}
281

282
/**
283
 * Converts a JS array to a Postgres array literal string: `{"val1","val2"}`.
284
 * Safely handles nesting and escaping of special characters.
285
 */
286
function toPgArray(arr: unknown[]): string {
287
  const elements = arr.map((val) => {
4✔
288
    if (val == null) return 'NULL';
9✔
289
    if (Array.isArray(val)) return toPgArray(val);
8✔
290
    if (typeof val === 'boolean') return val ? 'true' : 'false';
7!
291
    if (val instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(val))) {
7!
NEW
292
      const hex = Array.from(val)
×
NEW
293
        .map((b) => b.toString(16).padStart(2, '0'))
×
294
        .join('');
NEW
295
      return `"\\\\x${hex}"`;
×
296
    }
297
    const str = String(val);
7✔
298
    const escaped = str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
7✔
299
    return `"${escaped}"`;
7✔
300
  });
301
  return `{${elements.join(',')}}`;
4✔
302
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc