@jakub.knejzlik/ts-query
Version:
TypeScript implementation of SQL builder
492 lines (375 loc) • 10.2 kB
text/mdx
import { Meta } from "@storybook/addon-docs";
import { QueryPreview } from "./QueryPreview";
<Meta title="1. Query" />
# Query Builder
The Query Builder is a powerful tool that allows developers to construct SQL queries without having to write raw SQL. By offering a fluent interface, it simplifies the process of creating complex queries and ensures that the syntax is correct, reducing the risk of errors.
## Basic Usage
The foundation of any query starts with the `Q.select` method. This method initializes the query building process:
<QueryPreview code={`Q.select().from('tableName')`} />
### Selecting Fields
Instead of selecting all fields, you can specify which columns you want to retrieve:
<QueryPreview code={`Q.select().from('table').addField('fieldName')`} />
If you need to rename a column in the result set, you can provide an alias:
<QueryPreview
code={`
Q.select().from('table').addField('fieldName', 'aliasName');
`}
/>
You can also add multiple fields at once:
<QueryPreview
code={`
Q.select()
.from('table')
.addFields([
{ name: 'fieldName1', alias: 'aliasName1' },
{ name: 'fieldName2', alias: 'aliasName2' },
]);
`}
/>
Or you can reset all specified fields:
<QueryPreview
code={`
Q.select()
.from('table')
.addField('fieldNameA')
.fields([{ name: 'fieldNameB' }]);
`}
/>
### Table Aliasing:
Sometimes, for the sake of clarity or to avoid column name conflicts, you might want to alias a table:
<QueryPreview
code={`
Q.select().from('table', 't');
`}
/>
### Union
<QueryPreview
code={`
Q.select()
.from('users')
.union(Q.select().from('users2'))
`}
/>
### Joining Tables
Join operations are essential in SQL for combining rows from two or more tables based on a related column. The Query Builder supports various types of joins: INNER, LEFT, RIGHT, and FULL joins.
### Basic Join
To perform a basic join (INNER JOIN by default):
<QueryPreview
code={`
Q.select()
.from('users')
.join(
Q.table('orders', 'o'),
Cond.columnEqual('users.id', 'o.user_id')
);
`}
/>
### Specific Join Types
You can specify the type of join you need:
**Inner Join**: Combines rows from different tables when the join condition is met.
<QueryPreview
code={`
Q.select()
.from('users')
.innerJoin(
Q.table('orders', 'o'),
Cond.columnEqual('users.id', 'o.user_id')
);
`}
/>
**Left Join**: Returns all rows from the left table, and the matched rows from the right table. If no match, NULLs are returned for columns of the right table.
<QueryPreview
code={`
Q.select()
.from('users')
.leftJoin(
Q.table('orders', 'o'),
Cond.columnEqual('users.id', 'o.user_id')
);
`}
/>
**Right Join**: Similar to the left join, but returns all rows from the right table and matched rows from the left table.
<QueryPreview
code={`
Q.select()
.from('users')
.rightJoin(
Q.table('orders', 'o'),
Cond.columnEqual('users.id', 'o.user_id')
);
`}
/>
**Full Join**: Combines the results of both left and right joins. It returns rows when there is a match in either left or right table.
<QueryPreview
code={`
Q.select()
.from('users')
.fullJoin(
Q.table('orders', 'o'),
Cond.columnEqual('users.id', 'o.user_id')
);
`}
/>
### Cross Join
A cross join returns the Cartesian product of the two tables, combining each row from the first table with every row from the second table:
<QueryPreview
code={`
Q.select()
.from('users')
.crossJoin(Q.table('orders', 'o'));
`}
/>
### Join Conditions
The join condition is specified using the `Conditions` object. This condition determines how the tables are joined.
For example, joining users and orders on user ID:
<QueryPreview
code={`
Cond.columnEqual('users.id', 'o.user_id');
`}
/>
### Multiple Joins
You can chain multiple joins to combine more than two tables:
<QueryPreview
code={`
Q.select()
.from('users')
.innerJoin(
Q.table('orders', 'o'),
Cond.columnEqual('users.id', 'o.user_id')
)
.leftJoin(
Q.table('products', 'p'),
Cond.columnEqual('o.product_id', 'p.id')
);
`}
/>
### Aliasing in Joins
When joining tables, especially with multiple joins, it's often useful to alias tables for clarity and to resolve column name conflicts:
<QueryPreview
code={`
Q.select()
.from('users', 'u')
.leftJoin(
Q.table('orders', 'o'),
Cond.columnEqual('u.id', 'o.user_id')
);
`}
/>
### Complex Join Conditions
Join conditions can be more complex, involving logical operators like AND, OR:
<QueryPreview
code={`
Q.select()
.from('users')
.innerJoin(Q.table('orders', 'o'), Cond.and([
Cond.columnEqual('users.id', 'o.user_id'),
Cond.greaterThan('orders.amount', 100),
]));
`}
/>
### Join with subquery
You can also join with a subquery as a table source. This is useful for aggregating or filtering data before joining:
<QueryPreview
code={`
Q.select()
.from('users', 'u')
.leftJoin(
Q.table(Q.select()
.from('orders')
.addField(Fn.sum('price','totalPrice'))
.limit(10)
, 'o'),
Cond.columnEqual('u.id', 'o.user_id')
);
`}
/>
### WHERE Conditions
Filtering the data you retrieve is a common operation. The `where` method allows you to specify conditions to limit the rows returned:
<QueryPreview
code={`
Q.select().from('table').where(Cond.equal('fieldName', 'foo')).where(Cond.lessThan('fieldName2', 123));
`}
/>
### Optional WHERE Conditions
You can also add where conditions optionaly by providing empty values. For example all of these conditions are not applied
<QueryPreview
code={`
Q.select()
.from('table')
.where(Cond.and([]))
.where(Cond.or([]))
.where(Cond.in('foo', 1>2 ? ['abc'] : []));
`}
/>
Also nested conditions are filtered
<QueryPreview
code={`
Q.select()
.from('table')
.where(Cond.and([
Cond.equal('foo','blah'),
Cond.or([
Cond.in('foo',[])
])
]));
`}
/>
### LIMIT and OFFSET
In scenarios where you don't want to retrieve all rows, you can limit the result set:
<QueryPreview
code={`
Q.select().from('table').limit(10);
`}
/>
If you want to skip a certain number of rows before retrieving the result set, use the offset method:
<QueryPreview
code={`
Q.select().from('table').offset(5);
`}
/>
### ORDER BY
Sorting your result set can be achieved using the `orderBy` method:
<QueryPreview
code={`
Q.select().from('table').orderBy('fieldName');
`}
/>
If you need the sorting to be in descending order:
<QueryPreview
code={`
Q.select().from('table').orderBy('fieldName').orderBy('fieldName2', 'DESC');
`}
/>
You can also reset order by configuration:
<QueryPreview
code={`
Q.select().from('table').orderBy('fieldName').removeOrderBy().orderBy('fieldName2', 'DESC');
`}
/>
### GROUP BY
When you want to group rows that have the same values in specified columns into summary rows:
<QueryPreview
code={`
Q.select().from('table').groupBy('fieldName').groupBy('fieldName2');
`}
/>
You can also reset group by configuration:
<QueryPreview
code={`
Q.select().from('table').groupBy('fieldName').removeGroupBy().groupBy('fieldName2','DESC');
`}
/>
### Complex Table Sources:
For more advanced queries, you might need to use subqueries as a table source:
<QueryPreview
code={`
Q.select().from(Q.select().from('table').where(Cond.equal('field', 'blah')),'subt');
`}
/>
## Mutations
You can also generate insert/update/delete statements
### Insert
<QueryPreview
code={`
Q.insert('table').values([{foo:'blah'}]);
`}
/>
### Insert select
<QueryPreview
code={`
Q.insert('table').select(Q.select().from('table2'));
`}
/>
You can also specify column names:
<QueryPreview
code={`
Q.insert('user_stats').select(Q.select().from('users').addField('region').addField(Fn.count('*')).groupBy('region'),['region','count']);
`}
/>
### Update
<QueryPreview
code={`
Q.update('table')
.set({
foo: 'blah',
total: Fn.divide(Fn.multiply('amount','price'),Q.raw(2))
}).where(Cond.equal('name','john'));
`}
/>
### Delete
<QueryPreview
code={`
Q.delete('table').where(Cond.equal('id',123));
`}
/>
## Examples
### Basic Select Query
A simple example to retrieve all columns from a table:
<QueryPreview
code={`
Q.select().from('foo');
`}
/>
### Query with Multiple Conditions
A more complex example showcasing multiple conditions:
<QueryPreview
code={`
Q.select()
.from('users')
.addField('id')
.addField('name')
.where(
Cond.and([
Cond.equal('age', 25),
Cond.or([
Cond.greaterThan('salary', 50000),
Cond.like('position', 'manager%'),
]),
])
)
.orderBy('salary', 'DESC')
.limit(10)
.offset(5);
`}
/>
### Query with Nested Conditions
Queries can also have nested conditions for more intricate filtering:
<QueryPreview
code={`
Q.select()
.from('products')
.where(
Cond.and([
Cond.between('price', [10, 50]),
Cond.or([
Cond.like('name', '%apple%'),
Cond.notLike('description', '%refurbished%'),
]),
])
);
`}
/>
## Immutability
One of the key features of the Query Builder is its immutability. This ensures that once a query is constructed, it cannot be changed, preventing unintended side-effects:
```tsx
const originalQuery = Q.select().from("table").orderBy("bar", "DESC");
const newQuery = originalQuery.orderBy("foo");
```
This design ensures that queries can be safely reused and extended without affecting the original query.
## Advanced Conditions:
Advanced conditions provide more flexibility in filtering data:
- `Cond.in(...)`: Checks if a column's value is within a set of values.
- `Cond.null(...)`: Checks if a column's value is NULL.
- `Cond.notNull(...)`: Checks if a column's value is NOT NULL.
- `Cond.columnEqual(...)`: Compares two columns for equality.
## Cloning:
If you need to create a copy of a query to make variations without affecting the original, you can use the `clone` method:
```tsx
originalQuery.clone();
```
## Serialization:
For scenarios where you might need to store the query structure or send it across a network, the Query Builder provides serialization methods:
```tsx
const serialized = query.serialize();
SelectQuery.deserialize(serialized);
```