turbine-orm 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/adapters/index.d.ts +3 -2
- package/dist/cjs/cli/index.js +26 -4
- package/dist/cjs/cli/loader.js +62 -7
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +54 -76
- package/dist/cjs/client.js +8 -0
- package/dist/cjs/query/builder.js +261 -51
- package/dist/cli/index.js +28 -6
- package/dist/cli/loader.d.ts +22 -5
- package/dist/cli/loader.js +61 -7
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +9 -4
- package/dist/cli/studio.js +54 -76
- package/dist/client.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/query/builder.d.ts +35 -0
- package/dist/query/builder.js +261 -51
- package/dist/query/index.d.ts +1 -1
- package/package.json +3 -3
- package/dist/cjs/query.js +0 -2711
- package/dist/query.d.ts +0 -878
- package/dist/query.js +0 -2705
package/dist/cli/studio.d.ts
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* turbine-orm CLI — Studio
|
|
3
3
|
*
|
|
4
4
|
* A local, read-only web UI for browsing databases, exploring relations,
|
|
5
|
-
* and
|
|
6
|
-
*
|
|
5
|
+
* and composing queries visually. ORM-native since v0.19: there is no
|
|
6
|
+
* raw-SQL input surface — the Query tab builds `findMany` args that are
|
|
7
|
+
* validated against introspected metadata and compiled by QueryInterface
|
|
8
|
+
* (`/api/builder`). Pure Node (built-in `http` module), no runtime
|
|
9
|
+
* dependencies beyond `pg`, bound to 127.0.0.1 only.
|
|
7
10
|
*
|
|
8
11
|
* Security model:
|
|
9
12
|
* • Bind 127.0.0.1 only (never 0.0.0.0 — no LAN exposure)
|
|
10
13
|
* • Random auth token generated per process, required in Cookie header
|
|
11
|
-
* •
|
|
14
|
+
* • No SQL input surface at all — every identifier in a builder request is
|
|
15
|
+
* validated against the introspected schema; all values are $N params
|
|
12
16
|
* • Every query runs in a READ ONLY transaction (belt-and-suspenders)
|
|
13
|
-
* • 30s statement timeout
|
|
17
|
+
* • 30s statement timeout via parameterized set_config()
|
|
18
|
+
* • Per-session rate limiting, CSP + security headers, cross-origin refusal
|
|
14
19
|
*
|
|
15
20
|
* Not implemented (deliberately): row editing, DDL, destructive operations.
|
|
16
21
|
* Studio is for inspection. Use the CLI, migrate, or raw SQL for writes.
|
package/dist/cli/studio.js
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* turbine-orm CLI — Studio
|
|
3
3
|
*
|
|
4
4
|
* A local, read-only web UI for browsing databases, exploring relations,
|
|
5
|
-
* and
|
|
6
|
-
*
|
|
5
|
+
* and composing queries visually. ORM-native since v0.19: there is no
|
|
6
|
+
* raw-SQL input surface — the Query tab builds `findMany` args that are
|
|
7
|
+
* validated against introspected metadata and compiled by QueryInterface
|
|
8
|
+
* (`/api/builder`). Pure Node (built-in `http` module), no runtime
|
|
9
|
+
* dependencies beyond `pg`, bound to 127.0.0.1 only.
|
|
7
10
|
*
|
|
8
11
|
* Security model:
|
|
9
12
|
* • Bind 127.0.0.1 only (never 0.0.0.0 — no LAN exposure)
|
|
10
13
|
* • Random auth token generated per process, required in Cookie header
|
|
11
|
-
* •
|
|
14
|
+
* • No SQL input surface at all — every identifier in a builder request is
|
|
15
|
+
* validated against the introspected schema; all values are $N params
|
|
12
16
|
* • Every query runs in a READ ONLY transaction (belt-and-suspenders)
|
|
13
|
-
* • 30s statement timeout
|
|
17
|
+
* • 30s statement timeout via parameterized set_config()
|
|
18
|
+
* • Per-session rate limiting, CSP + security headers, cross-origin refusal
|
|
14
19
|
*
|
|
15
20
|
* Not implemented (deliberately): row editing, DDL, destructive operations.
|
|
16
21
|
* Studio is for inspection. Use the CLI, migrate, or raw SQL for writes.
|
|
@@ -140,6 +145,13 @@ async function handleRequest(req, res, ctx) {
|
|
|
140
145
|
sendHtml(res, 200, STUDIO_HTML);
|
|
141
146
|
return;
|
|
142
147
|
}
|
|
148
|
+
// Favicon — answered before the auth gate so the browser's automatic request
|
|
149
|
+
// doesn't 401/404 on every load. No icon body needed (204).
|
|
150
|
+
if (pathname === '/favicon.ico') {
|
|
151
|
+
res.writeHead(204, { 'Content-Length': '0' });
|
|
152
|
+
res.end();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
143
155
|
// API routes — all require auth.
|
|
144
156
|
if (!isAuthorized(req, ctx.authToken)) {
|
|
145
157
|
sendJson(res, 401, { error: 'unauthorized — use the URL printed in the terminal' });
|
|
@@ -160,9 +172,6 @@ async function handleRequest(req, res, ctx) {
|
|
|
160
172
|
const rawName = decodeURIComponent(pathname.slice('/api/tables/'.length));
|
|
161
173
|
return apiTableRows(res, ctx, rawName, url.searchParams);
|
|
162
174
|
}
|
|
163
|
-
if (pathname === '/api/query' && req.method === 'POST') {
|
|
164
|
-
return apiQuery(req, res, ctx);
|
|
165
|
-
}
|
|
166
175
|
if (pathname === '/api/builder' && req.method === 'POST') {
|
|
167
176
|
return apiBuilder(req, res, ctx);
|
|
168
177
|
}
|
|
@@ -222,6 +231,15 @@ function constantTimeEqual(a, b) {
|
|
|
222
231
|
}
|
|
223
232
|
return result === 0;
|
|
224
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Build a helpful "unknown table" error that lists the available tables so the
|
|
236
|
+
* caller can spot a typo or schema mismatch immediately.
|
|
237
|
+
*/
|
|
238
|
+
function unknownTableMessage(name, ctx) {
|
|
239
|
+
const available = Object.keys(ctx.metadata.tables);
|
|
240
|
+
const list = available.length ? available.join(', ') : '(none)';
|
|
241
|
+
return `[turbine] Unknown table "${name}" in schema "${ctx.options.schema}". Available: ${list}`;
|
|
242
|
+
}
|
|
225
243
|
// ---------------------------------------------------------------------------
|
|
226
244
|
// API: /api/schema
|
|
227
245
|
// ---------------------------------------------------------------------------
|
|
@@ -254,7 +272,9 @@ async function apiSchema(res, ctx) {
|
|
|
254
272
|
WHERE n.nspname = $1 AND c.relkind = 'r'`, [ctx.options.schema]);
|
|
255
273
|
const counts = new Map();
|
|
256
274
|
for (const row of countsResult.rows) {
|
|
257
|
-
|
|
275
|
+
// pg_class.reltuples is -1 on PG14+ until a table is ANALYZEd; clamp so the
|
|
276
|
+
// sidebar never shows a negative estimate.
|
|
277
|
+
counts.set(row.relname, Math.max(0, Number(row.reltuples)));
|
|
258
278
|
}
|
|
259
279
|
sendJson(res, 200, {
|
|
260
280
|
schema: ctx.options.schema,
|
|
@@ -268,7 +288,7 @@ async function apiSchema(res, ctx) {
|
|
|
268
288
|
export async function apiTableRows(res, ctx, rawTableName, params) {
|
|
269
289
|
const table = ctx.metadata.tables[rawTableName];
|
|
270
290
|
if (!table) {
|
|
271
|
-
sendJson(res, 404, { error:
|
|
291
|
+
sendJson(res, 404, { error: unknownTableMessage(rawTableName, ctx) });
|
|
272
292
|
return;
|
|
273
293
|
}
|
|
274
294
|
const limit = clampInt(params.get('limit'), 50, 1, 500);
|
|
@@ -363,54 +383,6 @@ export function escapeLikePattern(s) {
|
|
|
363
383
|
return s.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
364
384
|
}
|
|
365
385
|
// ---------------------------------------------------------------------------
|
|
366
|
-
// API: /api/query — read-only SELECT/WITH runner
|
|
367
|
-
// ---------------------------------------------------------------------------
|
|
368
|
-
async function apiQuery(req, res, ctx) {
|
|
369
|
-
const body = await readJsonBody(req);
|
|
370
|
-
const rawSql = typeof body?.sql === 'string' ? body.sql.trim() : '';
|
|
371
|
-
if (!rawSql) {
|
|
372
|
-
sendJson(res, 400, { error: 'missing sql' });
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
if (rawSql.length > 10_000) {
|
|
376
|
-
sendJson(res, 400, { error: 'query too long — maximum 10,000 characters allowed' });
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
if (!isReadOnlyStatement(rawSql)) {
|
|
380
|
-
sendJson(res, 400, {
|
|
381
|
-
error: 'only SELECT / WITH statements are allowed in Studio — use the CLI for writes',
|
|
382
|
-
});
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
const client = await ctx.pool.connect();
|
|
386
|
-
try {
|
|
387
|
-
await client.query('BEGIN READ ONLY');
|
|
388
|
-
await client.query(ctx.statementTimeout.sql, ctx.statementTimeout.params);
|
|
389
|
-
const started = Date.now();
|
|
390
|
-
const result = await client.query(rawSql);
|
|
391
|
-
const elapsedMs = Date.now() - started;
|
|
392
|
-
await client.query('COMMIT');
|
|
393
|
-
sendJson(res, 200, {
|
|
394
|
-
columns: result.fields.map((f) => ({ name: f.name, dataTypeID: f.dataTypeID })),
|
|
395
|
-
rows: result.rows.map((r) => serializeRow(r)),
|
|
396
|
-
rowCount: result.rowCount ?? result.rows.length,
|
|
397
|
-
elapsedMs,
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
catch (err) {
|
|
401
|
-
try {
|
|
402
|
-
await client.query('ROLLBACK');
|
|
403
|
-
}
|
|
404
|
-
catch {
|
|
405
|
-
/* ignore */
|
|
406
|
-
}
|
|
407
|
-
sendJson(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
408
|
-
}
|
|
409
|
-
finally {
|
|
410
|
-
client.release();
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
// ---------------------------------------------------------------------------
|
|
414
386
|
// API: /api/builder — Turbine ORM findMany spec runner
|
|
415
387
|
// ---------------------------------------------------------------------------
|
|
416
388
|
export async function apiBuilder(req, res, ctx) {
|
|
@@ -418,7 +390,7 @@ export async function apiBuilder(req, res, ctx) {
|
|
|
418
390
|
const tableName = typeof body?.table === 'string' ? body.table : '';
|
|
419
391
|
const args = (body?.args ?? {});
|
|
420
392
|
if (!tableName || !ctx.metadata.tables[tableName]) {
|
|
421
|
-
sendJson(res, 400, { error:
|
|
393
|
+
sendJson(res, 400, { error: unknownTableMessage(tableName, ctx) });
|
|
422
394
|
return;
|
|
423
395
|
}
|
|
424
396
|
let deferred;
|
|
@@ -438,6 +410,11 @@ export async function apiBuilder(req, res, ctx) {
|
|
|
438
410
|
try {
|
|
439
411
|
await client.query('BEGIN READ ONLY');
|
|
440
412
|
await client.query(ctx.statementTimeout.sql, ctx.statementTimeout.params);
|
|
413
|
+
// QueryInterface emits unqualified table identifiers, which resolve via
|
|
414
|
+
// the connection's search_path. Pin it to the configured --schema so the
|
|
415
|
+
// Query tab reads the same schema as the Data tab (set_config is
|
|
416
|
+
// transaction-local and fully parameterized).
|
|
417
|
+
await client.query(`SELECT set_config('search_path', $1, true)`, [ctx.options.schema]);
|
|
441
418
|
const started = Date.now();
|
|
442
419
|
const result = await client.query(deferred.sql, deferred.params);
|
|
443
420
|
const elapsedMs = Date.now() - started;
|
|
@@ -466,6 +443,8 @@ export async function apiBuilder(req, res, ctx) {
|
|
|
466
443
|
function savedQueriesPath(ctx) {
|
|
467
444
|
return pathResolve(ctx.stateDir, 'studio-queries.json');
|
|
468
445
|
}
|
|
446
|
+
/** One-shot flag so the legacy saved-query notice isn't logged on every request. */
|
|
447
|
+
let legacyDropNoticeShown = false;
|
|
469
448
|
function loadSavedQueries(ctx) {
|
|
470
449
|
const file = savedQueriesPath(ctx);
|
|
471
450
|
if (!existsSync(file))
|
|
@@ -475,7 +454,17 @@ function loadSavedQueries(ctx) {
|
|
|
475
454
|
const parsed = JSON.parse(raw);
|
|
476
455
|
if (!parsed.queries || !Array.isArray(parsed.queries))
|
|
477
456
|
return { version: 1, queries: [] };
|
|
478
|
-
|
|
457
|
+
// Drop any legacy raw-SQL entries — Studio is builder-only now. Tell the
|
|
458
|
+
// user instead of silently discarding their saved work (the file on disk
|
|
459
|
+
// is only rewritten when a new query is saved, so this is recoverable).
|
|
460
|
+
const queries = parsed.queries.filter((q) => q && q.kind === 'builder');
|
|
461
|
+
const dropped = parsed.queries.length - queries.length;
|
|
462
|
+
if (dropped > 0 && !legacyDropNoticeShown) {
|
|
463
|
+
legacyDropNoticeShown = true;
|
|
464
|
+
console.warn(`[turbine studio] Ignoring ${dropped} legacy raw-SQL saved quer${dropped === 1 ? 'y' : 'ies'} in ${file} — ` +
|
|
465
|
+
'Studio is builder-only since v0.19. The entries remain in the file until a new query is saved.');
|
|
466
|
+
}
|
|
467
|
+
return { version: 1, queries };
|
|
479
468
|
}
|
|
480
469
|
catch {
|
|
481
470
|
return { version: 1, queries: [] };
|
|
@@ -498,27 +487,17 @@ export async function apiCreateSavedQuery(req, res, ctx) {
|
|
|
498
487
|
const body = await readJsonBody(req);
|
|
499
488
|
const table = typeof body?.table === 'string' ? body.table : '';
|
|
500
489
|
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
|
501
|
-
const kind = body?.kind === 'builder' ? 'builder' : body?.kind === 'sql' ? 'sql' : null;
|
|
502
490
|
if (!table || !ctx.metadata.tables[table]) {
|
|
503
|
-
sendJson(res, 400, { error:
|
|
491
|
+
sendJson(res, 400, { error: unknownTableMessage(table, ctx) });
|
|
504
492
|
return;
|
|
505
493
|
}
|
|
506
494
|
if (!name) {
|
|
507
495
|
sendJson(res, 400, { error: 'name is required' });
|
|
508
496
|
return;
|
|
509
497
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
const sql = kind === 'sql' && typeof body?.sql === 'string' ? body.sql : undefined;
|
|
515
|
-
const args = kind === 'builder' ? body?.args : undefined;
|
|
516
|
-
if (kind === 'sql' && !sql) {
|
|
517
|
-
sendJson(res, 400, { error: 'sql is required for kind=sql' });
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
if (kind === 'sql' && sql && !isReadOnlyStatement(sql)) {
|
|
521
|
-
sendJson(res, 400, { error: 'saved sql must be SELECT/WITH only' });
|
|
498
|
+
// Studio only persists visual-builder queries (no raw SQL surface).
|
|
499
|
+
if (body?.kind !== 'builder') {
|
|
500
|
+
sendJson(res, 400, { error: 'kind must be "builder"' });
|
|
522
501
|
return;
|
|
523
502
|
}
|
|
524
503
|
const data = loadSavedQueries(ctx);
|
|
@@ -526,9 +505,8 @@ export async function apiCreateSavedQuery(req, res, ctx) {
|
|
|
526
505
|
id: randomUUID(),
|
|
527
506
|
table,
|
|
528
507
|
name,
|
|
529
|
-
kind,
|
|
530
|
-
|
|
531
|
-
args,
|
|
508
|
+
kind: 'builder',
|
|
509
|
+
args: body?.args,
|
|
532
510
|
createdAt: new Date().toISOString(),
|
|
533
511
|
};
|
|
534
512
|
data.queries.push(entry);
|
package/dist/client.js
CHANGED
|
@@ -202,6 +202,14 @@ export class TurbineClient {
|
|
|
202
202
|
/** Active LISTEN subscriptions — torn down on disconnect() so it never hangs */
|
|
203
203
|
activeSubscriptions = new Set();
|
|
204
204
|
constructor(config = {}, schema) {
|
|
205
|
+
// Constructing without schema metadata previously crashed deep in the
|
|
206
|
+
// constructor with an opaque "Cannot read properties of undefined
|
|
207
|
+
// (reading 'tables')". Fail fast with an actionable message instead.
|
|
208
|
+
if (!schema || typeof schema !== 'object' || !schema.tables) {
|
|
209
|
+
throw new ValidationError('[turbine] TurbineClient requires schema metadata as its second argument. ' +
|
|
210
|
+
'Run `npx turbine generate` and use the generated client (`turbine()` from your output dir), ' +
|
|
211
|
+
'or pass the generated `schemaMetadata` object: new TurbineClient(config, schemaMetadata).');
|
|
212
|
+
}
|
|
205
213
|
/**
|
|
206
214
|
* Parse int8 (bigint, OID 20) as JavaScript number instead of string.
|
|
207
215
|
* Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
|
package/dist/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export { type IntrospectOptions, introspect } from './introspect.js';
|
|
|
43
43
|
export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
|
|
44
44
|
export type { ObserveConfig, ObserveHandle } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
|
-
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type JsonFilter, type NestedCreateOp, type NestedUpdateOp, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
46
|
+
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type HavingClause, type JsonFilter, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
47
47
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
48
48
|
export type { ColumnMetadata, IndexMetadata, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
49
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -402,6 +402,41 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
402
402
|
* Uses the target table's column mapping to resolve field names.
|
|
403
403
|
*/
|
|
404
404
|
private buildSubWhereForRelation;
|
|
405
|
+
/**
|
|
406
|
+
* Resolve a column's Postgres type from an arbitrary table's metadata
|
|
407
|
+
* (relation targets, not just `this.table`).
|
|
408
|
+
*/
|
|
409
|
+
private pgTypeForColumn;
|
|
410
|
+
/**
|
|
411
|
+
* Equality-fallthrough guard shared by every SQL-build path AND every
|
|
412
|
+
* cache-hit param-collect path. A plain object literal that matched no known
|
|
413
|
+
* filter shape on a non-JSON column is almost always a misspelled operator
|
|
414
|
+
* (`startWith` for `startsWith`); binding it as `col = $1` silently returns
|
|
415
|
+
* wrong rows. Class instances (Buffer for bytea, Decimal wrappers, ...) are
|
|
416
|
+
* legitimate bind values and pass through, as do objects on json/jsonb
|
|
417
|
+
* columns (object equality).
|
|
418
|
+
*/
|
|
419
|
+
private assertBindableEqualityValue;
|
|
420
|
+
/**
|
|
421
|
+
* Build the user-supplied `where` filter of a relation `with` clause against
|
|
422
|
+
* the relation's table alias. Supports the same scalar surface as the
|
|
423
|
+
* top-level WHERE builder — equality, IS NULL, operator objects (incl.
|
|
424
|
+
* `mode: 'insensitive'`), and OR/AND/NOT combinators. Unknown operator
|
|
425
|
+
* objects throw via {@link assertBindableEqualityValue}.
|
|
426
|
+
*
|
|
427
|
+
* Param push order MUST mirror {@link collectAliasWhereParams} exactly, or
|
|
428
|
+
* cache hits and pipeline batching will desync.
|
|
429
|
+
*/
|
|
430
|
+
private buildAliasWhere;
|
|
431
|
+
/** Mirrors {@link buildAliasWhere} param-push order for the cache-hit collect path. */
|
|
432
|
+
private collectAliasWhereParams;
|
|
433
|
+
/**
|
|
434
|
+
* Value-invariant, shape-aware fingerprint for a relation `with` clause's
|
|
435
|
+
* `where` filter. Must distinguish every SQL shape {@link buildAliasWhere}
|
|
436
|
+
* can emit — equality vs null vs operator sets vs combinators — or two
|
|
437
|
+
* differently-shaped wheres would share one cached SQL string.
|
|
438
|
+
*/
|
|
439
|
+
private fingerprintAliasWhere;
|
|
405
440
|
/**
|
|
406
441
|
* Build SQL clauses for a single operator object on a column.
|
|
407
442
|
* Each operator key becomes its own clause, all ANDed together.
|