@qualithm/arrow-flight-sql-js 0.0.1 → 0.1.0

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.

Potentially problematic release.


This version of @qualithm/arrow-flight-sql-js might be problematic. Click here for more details.

@@ -1,3 +1,733 @@
1
- "use strict";
2
- // Todo
1
+ /**
2
+ * Fluent SQL query builder for constructing type-safe queries.
3
+ *
4
+ * This module provides a query builder API that generates SQL strings
5
+ * compatible with the FlightSqlClient.query() and executeUpdate() methods.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { QueryBuilder } from "@qualithm/arrow-flight-sql-js"
10
+ *
11
+ * const query = new QueryBuilder()
12
+ * .select("id", "name", "email")
13
+ * .from("users")
14
+ * .where("status", "=", "active")
15
+ * .orderBy("created_at", "DESC")
16
+ * .limit(10)
17
+ * .build()
18
+ *
19
+ * const result = await client.query(query)
20
+ * ```
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+ // ============================================================================
25
+ // Value Escaping
26
+ // ============================================================================
27
+ /**
28
+ * Creates a raw SQL expression that won't be escaped.
29
+ *
30
+ * WARNING: Only use with trusted input. Raw expressions bypass escaping
31
+ * and can lead to SQL injection if used with untrusted data.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const query = new QueryBuilder()
36
+ * .select("*")
37
+ * .from("events")
38
+ * .where("created_at", ">", raw("NOW() - INTERVAL '1 day'"))
39
+ * .build()
40
+ * ```
41
+ */
42
+ export function raw(sql) {
43
+ return { __raw: true, sql };
44
+ }
45
+ /**
46
+ * Checks if a value is a raw SQL expression.
47
+ */
48
+ function isRaw(value) {
49
+ return typeof value === "object" && value !== null && "__raw" in value;
50
+ }
51
+ /**
52
+ * Escape a SQL identifier (table name, column name).
53
+ * Uses double quotes for standard SQL compliance.
54
+ */
55
+ export function escapeIdentifier(identifier) {
56
+ // Handle qualified names (schema.table, table.column)
57
+ if (identifier.includes(".")) {
58
+ return identifier
59
+ .split(".")
60
+ .map((part) => escapeIdentifier(part))
61
+ .join(".");
62
+ }
63
+ // Handle wildcards
64
+ if (identifier === "*") {
65
+ return "*";
66
+ }
67
+ // Handle expressions with parentheses (function calls like COUNT(*))
68
+ if (identifier.includes("(") || identifier.includes(")")) {
69
+ return identifier;
70
+ }
71
+ // Escape by doubling quotes and wrapping
72
+ return `"${identifier.replace(/"/g, '""')}"`;
73
+ }
74
+ /**
75
+ * Escape a SQL string value.
76
+ * Uses single quotes with proper escaping.
77
+ */
78
+ export function escapeString(value) {
79
+ // Escape single quotes by doubling them
80
+ return `'${value.replace(/'/g, "''")}'`;
81
+ }
82
+ /**
83
+ * Format a SQL value for inclusion in a query.
84
+ */
85
+ export function formatValue(value) {
86
+ if (value === null) {
87
+ return "NULL";
88
+ }
89
+ if (isRaw(value)) {
90
+ return value.sql;
91
+ }
92
+ if (typeof value === "string") {
93
+ return escapeString(value);
94
+ }
95
+ if (typeof value === "number") {
96
+ if (!Number.isFinite(value)) {
97
+ throw new Error(`Invalid numeric value: ${String(value)}`);
98
+ }
99
+ return String(value);
100
+ }
101
+ if (typeof value === "bigint") {
102
+ return String(value);
103
+ }
104
+ if (typeof value === "boolean") {
105
+ return value ? "TRUE" : "FALSE";
106
+ }
107
+ if (value instanceof Date) {
108
+ return `TIMESTAMP '${value.toISOString()}'`;
109
+ }
110
+ if (Array.isArray(value)) {
111
+ const formatted = value.map(formatValue);
112
+ return `(${formatted.join(", ")})`;
113
+ }
114
+ throw new Error(`Unsupported value type: ${typeof value}`);
115
+ }
116
+ // ============================================================================
117
+ // QueryBuilder Class
118
+ // ============================================================================
119
+ /**
120
+ * Fluent SQL query builder.
121
+ *
122
+ * Supports SELECT, INSERT, UPDATE, and DELETE operations with a chainable API.
123
+ *
124
+ * @example SELECT query
125
+ * ```typescript
126
+ * const sql = new QueryBuilder()
127
+ * .select("id", "name")
128
+ * .from("users")
129
+ * .where("active", "=", true)
130
+ * .limit(10)
131
+ * .build()
132
+ * ```
133
+ *
134
+ * @example INSERT query
135
+ * ```typescript
136
+ * const sql = new QueryBuilder()
137
+ * .insertInto("users")
138
+ * .columns("name", "email")
139
+ * .values("Alice", "alice@example.com")
140
+ * .build()
141
+ * ```
142
+ *
143
+ * @example UPDATE query
144
+ * ```typescript
145
+ * const sql = new QueryBuilder()
146
+ * .update("users")
147
+ * .set("status", "inactive")
148
+ * .where("last_login", "<", new Date("2024-01-01"))
149
+ * .build()
150
+ * ```
151
+ *
152
+ * @example DELETE query
153
+ * ```typescript
154
+ * const sql = new QueryBuilder()
155
+ * .deleteFrom("users")
156
+ * .where("status", "=", "deleted")
157
+ * .build()
158
+ * ```
159
+ */
160
+ export class QueryBuilder {
161
+ _operation = "SELECT";
162
+ _distinct = false;
163
+ _columns = [];
164
+ _table = "";
165
+ _tableAlias;
166
+ _joins = [];
167
+ _conditions = [];
168
+ _groupBy = [];
169
+ _having = [];
170
+ _orderBy = [];
171
+ _limit;
172
+ _offset;
173
+ _parameterized = false;
174
+ _params = [];
175
+ // For INSERT
176
+ _insertColumns = [];
177
+ _insertValues = [];
178
+ // For UPDATE
179
+ _setValues = new Map();
180
+ // ============================================================================
181
+ // SELECT Operations
182
+ // ============================================================================
183
+ /**
184
+ * Specify columns to select.
185
+ *
186
+ * @param columns - Column names or column specs with aliases
187
+ * @returns this for chaining
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * .select("id", "name")
192
+ * .select({ column: "email", alias: "user_email" })
193
+ * .select("*")
194
+ * ```
195
+ */
196
+ select(...columns) {
197
+ this._operation = "SELECT";
198
+ this._columns.push(...columns);
199
+ return this;
200
+ }
201
+ /**
202
+ * Select distinct rows only.
203
+ */
204
+ distinct() {
205
+ this._distinct = true;
206
+ return this;
207
+ }
208
+ /**
209
+ * Specify the table to query from.
210
+ *
211
+ * @param table - Table name
212
+ * @param alias - Optional table alias
213
+ */
214
+ from(table, alias) {
215
+ this._table = table;
216
+ this._tableAlias = alias;
217
+ return this;
218
+ }
219
+ // ============================================================================
220
+ // JOIN Operations
221
+ // ============================================================================
222
+ /**
223
+ * Add a JOIN clause.
224
+ *
225
+ * @param type - Join type (INNER, LEFT, RIGHT, FULL, CROSS)
226
+ * @param table - Table to join
227
+ * @param on - Join condition
228
+ * @param alias - Optional table alias
229
+ */
230
+ join(type, table, on, alias) {
231
+ this._joins.push({ type, table, alias, on });
232
+ return this;
233
+ }
234
+ /**
235
+ * Add an INNER JOIN clause.
236
+ */
237
+ innerJoin(table, on, alias) {
238
+ return this.join("INNER", table, on, alias);
239
+ }
240
+ /**
241
+ * Add a LEFT JOIN clause.
242
+ */
243
+ leftJoin(table, on, alias) {
244
+ return this.join("LEFT", table, on, alias);
245
+ }
246
+ /**
247
+ * Add a RIGHT JOIN clause.
248
+ */
249
+ rightJoin(table, on, alias) {
250
+ return this.join("RIGHT", table, on, alias);
251
+ }
252
+ // ============================================================================
253
+ // WHERE Operations
254
+ // ============================================================================
255
+ /**
256
+ * Add a WHERE condition.
257
+ *
258
+ * @param column - Column name
259
+ * @param operator - Comparison operator
260
+ * @param value - Value to compare against
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * .where("status", "=", "active")
265
+ * .where("age", ">=", 18)
266
+ * .where("role", "IN", ["admin", "moderator"])
267
+ * .where("deleted_at", "IS", null)
268
+ * ```
269
+ */
270
+ where(column, operator, value) {
271
+ this._conditions.push({
272
+ column,
273
+ operator,
274
+ value,
275
+ logical: "AND"
276
+ });
277
+ return this;
278
+ }
279
+ /**
280
+ * Add an OR WHERE condition.
281
+ */
282
+ orWhere(column, operator, value) {
283
+ this._conditions.push({
284
+ column,
285
+ operator,
286
+ value,
287
+ logical: "OR"
288
+ });
289
+ return this;
290
+ }
291
+ /**
292
+ * Add a WHERE BETWEEN condition.
293
+ */
294
+ whereBetween(column, min, max) {
295
+ // Use raw expression for BETWEEN
296
+ this._conditions.push({
297
+ column,
298
+ operator: "BETWEEN",
299
+ value: raw(`${formatValue(min)} AND ${formatValue(max)}`),
300
+ logical: "AND"
301
+ });
302
+ return this;
303
+ }
304
+ /**
305
+ * Add a WHERE IN condition.
306
+ */
307
+ whereIn(column, values) {
308
+ return this.where(column, "IN", values);
309
+ }
310
+ /**
311
+ * Add a WHERE IS NULL condition.
312
+ */
313
+ whereNull(column) {
314
+ return this.where(column, "IS", null);
315
+ }
316
+ /**
317
+ * Add a WHERE IS NOT NULL condition.
318
+ */
319
+ whereNotNull(column) {
320
+ return this.where(column, "IS NOT", null);
321
+ }
322
+ // ============================================================================
323
+ // GROUP BY / HAVING
324
+ // ============================================================================
325
+ /**
326
+ * Add GROUP BY columns.
327
+ */
328
+ groupBy(...columns) {
329
+ this._groupBy.push(...columns);
330
+ return this;
331
+ }
332
+ /**
333
+ * Add a HAVING condition.
334
+ */
335
+ having(column, operator, value) {
336
+ this._having.push({
337
+ column,
338
+ operator,
339
+ value,
340
+ logical: "AND"
341
+ });
342
+ return this;
343
+ }
344
+ // ============================================================================
345
+ // ORDER BY / LIMIT / OFFSET
346
+ // ============================================================================
347
+ /**
348
+ * Add ORDER BY clause.
349
+ *
350
+ * @param column - Column to order by
351
+ * @param direction - Sort direction (ASC or DESC)
352
+ * @param nulls - NULLS FIRST or NULLS LAST
353
+ */
354
+ orderBy(column, direction = "ASC", nulls) {
355
+ this._orderBy.push({ column, direction, nulls });
356
+ return this;
357
+ }
358
+ /**
359
+ * Set the maximum number of rows to return.
360
+ */
361
+ limit(count) {
362
+ if (!Number.isInteger(count) || count < 0) {
363
+ throw new Error("LIMIT must be a non-negative integer");
364
+ }
365
+ this._limit = count;
366
+ return this;
367
+ }
368
+ /**
369
+ * Set the number of rows to skip.
370
+ */
371
+ offset(count) {
372
+ if (!Number.isInteger(count) || count < 0) {
373
+ throw new Error("OFFSET must be a non-negative integer");
374
+ }
375
+ this._offset = count;
376
+ return this;
377
+ }
378
+ // ============================================================================
379
+ // INSERT Operations
380
+ // ============================================================================
381
+ /**
382
+ * Start an INSERT statement.
383
+ *
384
+ * @param table - Table to insert into
385
+ */
386
+ insertInto(table) {
387
+ this._operation = "INSERT";
388
+ this._table = table;
389
+ return this;
390
+ }
391
+ /**
392
+ * Specify columns for INSERT.
393
+ */
394
+ columns(...columns) {
395
+ this._insertColumns = columns;
396
+ return this;
397
+ }
398
+ /**
399
+ * Add values to INSERT.
400
+ * Can be called multiple times for multi-row inserts.
401
+ *
402
+ * @param values - Values corresponding to columns
403
+ */
404
+ values(...values) {
405
+ this._insertValues.push(values);
406
+ return this;
407
+ }
408
+ // ============================================================================
409
+ // UPDATE Operations
410
+ // ============================================================================
411
+ /**
412
+ * Start an UPDATE statement.
413
+ *
414
+ * @param table - Table to update
415
+ */
416
+ update(table) {
417
+ this._operation = "UPDATE";
418
+ this._table = table;
419
+ return this;
420
+ }
421
+ /**
422
+ * Set a column value for UPDATE.
423
+ *
424
+ * @param column - Column to update
425
+ * @param value - New value
426
+ */
427
+ set(column, value) {
428
+ this._setValues.set(column, value);
429
+ return this;
430
+ }
431
+ /**
432
+ * Set multiple column values for UPDATE.
433
+ *
434
+ * @param values - Object mapping column names to values
435
+ */
436
+ setMany(values) {
437
+ for (const [column, value] of Object.entries(values)) {
438
+ this._setValues.set(column, value);
439
+ }
440
+ return this;
441
+ }
442
+ // ============================================================================
443
+ // DELETE Operations
444
+ // ============================================================================
445
+ /**
446
+ * Start a DELETE statement.
447
+ *
448
+ * @param table - Table to delete from
449
+ */
450
+ deleteFrom(table) {
451
+ this._operation = "DELETE";
452
+ this._table = table;
453
+ return this;
454
+ }
455
+ // ============================================================================
456
+ // Build Methods
457
+ // ============================================================================
458
+ /**
459
+ * Build the SQL query string.
460
+ *
461
+ * @returns The complete SQL query string
462
+ */
463
+ build() {
464
+ switch (this._operation) {
465
+ case "SELECT":
466
+ return this.buildSelect();
467
+ case "INSERT":
468
+ return this.buildInsert();
469
+ case "UPDATE":
470
+ return this.buildUpdate();
471
+ case "DELETE":
472
+ return this.buildDelete();
473
+ }
474
+ }
475
+ /**
476
+ * Build query with parameter placeholders for prepared statements.
477
+ *
478
+ * @returns Object containing SQL with placeholders and parameter values
479
+ */
480
+ buildParameterized() {
481
+ this._parameterized = true;
482
+ this._params = [];
483
+ const sql = this.build();
484
+ return {
485
+ sql,
486
+ params: this._params
487
+ };
488
+ }
489
+ /**
490
+ * Create a copy of this query builder.
491
+ */
492
+ clone() {
493
+ const cloned = new QueryBuilder();
494
+ cloned._operation = this._operation;
495
+ cloned._distinct = this._distinct;
496
+ cloned._columns = [...this._columns];
497
+ cloned._table = this._table;
498
+ cloned._tableAlias = this._tableAlias;
499
+ cloned._joins = [...this._joins];
500
+ cloned._conditions = [...this._conditions];
501
+ cloned._groupBy = [...this._groupBy];
502
+ cloned._having = [...this._having];
503
+ cloned._orderBy = [...this._orderBy];
504
+ cloned._limit = this._limit;
505
+ cloned._offset = this._offset;
506
+ cloned._insertColumns = [...this._insertColumns];
507
+ cloned._insertValues = this._insertValues.map((v) => [...v]);
508
+ cloned._setValues = new Map(this._setValues);
509
+ return cloned;
510
+ }
511
+ /**
512
+ * Reset the builder to initial state.
513
+ */
514
+ reset() {
515
+ this._operation = "SELECT";
516
+ this._distinct = false;
517
+ this._columns = [];
518
+ this._table = "";
519
+ this._tableAlias = undefined;
520
+ this._joins = [];
521
+ this._conditions = [];
522
+ this._groupBy = [];
523
+ this._having = [];
524
+ this._orderBy = [];
525
+ this._limit = undefined;
526
+ this._offset = undefined;
527
+ this._parameterized = false;
528
+ this._params = [];
529
+ this._insertColumns = [];
530
+ this._insertValues = [];
531
+ this._setValues.clear();
532
+ return this;
533
+ }
534
+ // ============================================================================
535
+ // Private Build Methods
536
+ // ============================================================================
537
+ buildSelect() {
538
+ const parts = [];
539
+ // SELECT
540
+ parts.push("SELECT");
541
+ if (this._distinct) {
542
+ parts.push("DISTINCT");
543
+ }
544
+ // Columns
545
+ const columns = this._columns.length === 0
546
+ ? "*"
547
+ : this._columns
548
+ .map((col) => {
549
+ if (typeof col === "string") {
550
+ return escapeIdentifier(col);
551
+ }
552
+ return `${escapeIdentifier(col.column)} AS ${escapeIdentifier(col.alias)}`;
553
+ })
554
+ .join(", ");
555
+ parts.push(columns);
556
+ // FROM
557
+ if (this._table !== "") {
558
+ parts.push("FROM");
559
+ let tableRef = escapeIdentifier(this._table);
560
+ if (this._tableAlias !== undefined && this._tableAlias !== "") {
561
+ tableRef += ` AS ${escapeIdentifier(this._tableAlias)}`;
562
+ }
563
+ parts.push(tableRef);
564
+ }
565
+ // JOINs
566
+ for (const join of this._joins) {
567
+ let joinClause = `${join.type} JOIN ${escapeIdentifier(join.table)}`;
568
+ if (join.alias !== undefined && join.alias !== "") {
569
+ joinClause += ` AS ${escapeIdentifier(join.alias)}`;
570
+ }
571
+ if (join.type !== "CROSS") {
572
+ joinClause += ` ON ${join.on}`;
573
+ }
574
+ parts.push(joinClause);
575
+ }
576
+ // WHERE
577
+ if (this._conditions.length > 0) {
578
+ parts.push("WHERE");
579
+ parts.push(this.buildConditions(this._conditions));
580
+ }
581
+ // GROUP BY
582
+ if (this._groupBy.length > 0) {
583
+ parts.push("GROUP BY");
584
+ parts.push(this._groupBy.map(escapeIdentifier).join(", "));
585
+ }
586
+ // HAVING
587
+ if (this._having.length > 0) {
588
+ parts.push("HAVING");
589
+ parts.push(this.buildConditions(this._having));
590
+ }
591
+ // ORDER BY
592
+ if (this._orderBy.length > 0) {
593
+ parts.push("ORDER BY");
594
+ parts.push(this._orderBy
595
+ .map((spec) => {
596
+ let clause = `${escapeIdentifier(spec.column)} ${spec.direction}`;
597
+ if (spec.nulls) {
598
+ clause += ` NULLS ${spec.nulls}`;
599
+ }
600
+ return clause;
601
+ })
602
+ .join(", "));
603
+ }
604
+ // LIMIT
605
+ if (this._limit !== undefined) {
606
+ parts.push(`LIMIT ${String(this._limit)}`);
607
+ }
608
+ // OFFSET
609
+ if (this._offset !== undefined) {
610
+ parts.push(`OFFSET ${String(this._offset)}`);
611
+ }
612
+ return parts.join(" ");
613
+ }
614
+ buildInsert() {
615
+ if (!this._table) {
616
+ throw new Error("INSERT requires a table name");
617
+ }
618
+ if (this._insertColumns.length === 0) {
619
+ throw new Error("INSERT requires columns");
620
+ }
621
+ if (this._insertValues.length === 0) {
622
+ throw new Error("INSERT requires values");
623
+ }
624
+ const parts = [];
625
+ // INSERT INTO
626
+ parts.push(`INSERT INTO ${escapeIdentifier(this._table)}`);
627
+ // Columns
628
+ parts.push(`(${this._insertColumns.map(escapeIdentifier).join(", ")})`);
629
+ // VALUES
630
+ parts.push("VALUES");
631
+ const valueRows = this._insertValues.map((row) => {
632
+ const formattedValues = row.map((v) => this.formatOrParam(v));
633
+ return `(${formattedValues.join(", ")})`;
634
+ });
635
+ parts.push(valueRows.join(", "));
636
+ return parts.join(" ");
637
+ }
638
+ buildUpdate() {
639
+ if (!this._table) {
640
+ throw new Error("UPDATE requires a table name");
641
+ }
642
+ if (this._setValues.size === 0) {
643
+ throw new Error("UPDATE requires SET values");
644
+ }
645
+ const parts = [];
646
+ // UPDATE
647
+ parts.push(`UPDATE ${escapeIdentifier(this._table)}`);
648
+ // SET
649
+ parts.push("SET");
650
+ const setClauses = [];
651
+ for (const [column, value] of this._setValues) {
652
+ setClauses.push(`${escapeIdentifier(column)} = ${this.formatOrParam(value)}`);
653
+ }
654
+ parts.push(setClauses.join(", "));
655
+ // WHERE
656
+ if (this._conditions.length > 0) {
657
+ parts.push("WHERE");
658
+ parts.push(this.buildConditions(this._conditions));
659
+ }
660
+ return parts.join(" ");
661
+ }
662
+ buildDelete() {
663
+ if (!this._table) {
664
+ throw new Error("DELETE requires a table name");
665
+ }
666
+ const parts = [];
667
+ // DELETE FROM
668
+ parts.push(`DELETE FROM ${escapeIdentifier(this._table)}`);
669
+ // WHERE
670
+ if (this._conditions.length > 0) {
671
+ parts.push("WHERE");
672
+ parts.push(this.buildConditions(this._conditions));
673
+ }
674
+ return parts.join(" ");
675
+ }
676
+ buildConditions(conditions) {
677
+ return conditions
678
+ .map((cond, index) => {
679
+ const prefix = index === 0 ? "" : `${cond.logical} `;
680
+ const column = escapeIdentifier(cond.column);
681
+ const value = this.formatOrParam(cond.value);
682
+ return `${prefix}${column} ${cond.operator} ${value}`;
683
+ })
684
+ .join(" ");
685
+ }
686
+ formatOrParam(value) {
687
+ if (this._parameterized && !isRaw(value)) {
688
+ this._params.push(value);
689
+ return `$${String(this._params.length)}`;
690
+ }
691
+ return formatValue(value);
692
+ }
693
+ }
694
+ // ============================================================================
695
+ // Convenience Functions
696
+ // ============================================================================
697
+ /**
698
+ * Create a new QueryBuilder for a SELECT query.
699
+ *
700
+ * @param columns - Columns to select
701
+ * @returns A new QueryBuilder with SELECT initialized
702
+ */
703
+ export function select(...columns) {
704
+ return new QueryBuilder().select(...columns);
705
+ }
706
+ /**
707
+ * Create a new QueryBuilder for an INSERT query.
708
+ *
709
+ * @param table - Table to insert into
710
+ * @returns A new QueryBuilder with INSERT initialized
711
+ */
712
+ export function insertInto(table) {
713
+ return new QueryBuilder().insertInto(table);
714
+ }
715
+ /**
716
+ * Create a new QueryBuilder for an UPDATE query.
717
+ *
718
+ * @param table - Table to update
719
+ * @returns A new QueryBuilder with UPDATE initialized
720
+ */
721
+ export function update(table) {
722
+ return new QueryBuilder().update(table);
723
+ }
724
+ /**
725
+ * Create a new QueryBuilder for a DELETE query.
726
+ *
727
+ * @param table - Table to delete from
728
+ * @returns A new QueryBuilder with DELETE initialized
729
+ */
730
+ export function deleteFrom(table) {
731
+ return new QueryBuilder().deleteFrom(table);
732
+ }
3
733
  //# sourceMappingURL=query-builder.js.map