Skip to content

Commit

Permalink
Support select queries without from clause (#605)
Browse files Browse the repository at this point in the history
* support select queries without from clause

* rename select --> selectNoFrom
  • Loading branch information
koskimas authored Jul 23, 2023
1 parent a998125 commit d8aae75
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 25 deletions.
38 changes: 36 additions & 2 deletions src/expression/expression-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ import { JSONPathBuilder } from '../query-builder/json-path-builder.js'
import { OperandExpression } from '../parser/expression-parser.js'
import { BinaryOperationNode } from '../operation-node/binary-operation-node.js'
import { AndNode } from '../operation-node/and-node.js'
import {
SelectArg,
SelectExpression,
Selection,
parseSelectArg,
} from '../parser/select-parser.js'

export interface ExpressionBuilder<DB, TB extends keyof DB> {
/**
Expand Down Expand Up @@ -250,6 +256,19 @@ export interface ExpressionBuilder<DB, TB extends keyof DB> {
from: TE
): SelectQueryBuilder<From<DB, TE>, FromTables<DB, TB, TE>, {}>

/**
* Creates a `select` expression without a `from` clause.
*
* If you want to create a `select from` query, use the `selectFrom` method instead.
* This one can be used to create a plain `select` statement without a `from` clause.
*
* This method accepts the same inputs as {@link SelectQueryBuilder.select}. See its
* documentation for examples.
*/
selectNoFrom<SE extends SelectExpression<DB, TB>>(
selection: SelectArg<DB, TB, SE>
): SelectQueryBuilder<DB, TB, Selection<DB, TB, SE>>

/**
* Creates a `case` statement/operator.
*
Expand Down Expand Up @@ -852,8 +871,23 @@ export function createExpressionBuilder<DB, TB extends keyof DB>(
selectFrom(table: TableExpressionOrList<DB, TB>): any {
return createSelectQueryBuilder({
queryId: createQueryId(),
executor: executor,
queryNode: SelectQueryNode.create(parseTableExpressionOrList(table)),
executor,
queryNode: SelectQueryNode.createFrom(
parseTableExpressionOrList(table)
),
})
},

selectNoFrom<SE extends SelectExpression<DB, TB>>(
selection: SelectArg<DB, TB, SE>
): SelectQueryBuilder<DB, TB, Selection<DB, TB, SE>> {
return createSelectQueryBuilder({
queryId: createQueryId(),
executor,
queryNode: SelectQueryNode.cloneWithSelections(
SelectQueryNode.create(),
parseSelectArg(selection)
),
})
},

Expand Down
11 changes: 9 additions & 2 deletions src/operation-node/select-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { SetOperationNode } from './set-operation-node.js'

export interface SelectQueryNode extends OperationNode {
readonly kind: 'SelectQueryNode'
readonly from: FromNode
readonly from?: FromNode
readonly selections?: ReadonlyArray<SelectionNode>
readonly distinctOn?: ReadonlyArray<OperationNode>
readonly joins?: ReadonlyArray<JoinNode>
Expand All @@ -43,7 +43,14 @@ export const SelectQueryNode = freeze({
return node.kind === 'SelectQueryNode'
},

create(
create(withNode?: WithNode): SelectQueryNode {
return freeze({
kind: 'SelectQueryNode',
...(withNode && { with: withNode }),
})
},

createFrom(
fromItems: ReadonlyArray<OperationNode>,
withNode?: WithNode
): SelectQueryNode {
Expand Down
2 changes: 1 addition & 1 deletion src/parser/parse-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function createSelectQueryBuilder(): SelectQueryBuilder<any, any, any> {
return newSelectQueryBuilder({
queryId: createQueryId(),
executor: NOOP_QUERY_EXECUTOR,
queryNode: SelectQueryNode.create(parseTableExpressionOrList([])),
queryNode: SelectQueryNode.createFrom(parseTableExpressionOrList([])),
})
}

Expand Down
14 changes: 4 additions & 10 deletions src/plugin/camel-case/camel-case-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { QueryResult } from '../../driver/database-connection.js'
import { RootOperationNode } from '../../query-compiler/query-compiler.js'
import {
isArrayBufferOrView,
isBuffer,
isDate,
isObject,
isPlainObject,
} from '../../util/object-utils.js'
import { isPlainObject } from '../../util/object-utils.js'
import { UnknownRow } from '../../util/type-utils.js'
import {
KyselyPlugin,
Expand Down Expand Up @@ -97,9 +91,9 @@ export interface CamelCasePluginOptions {
* ```
*
* As you can see from the example, __everything__ needs to be defined
* in camelCase in the typescript code: the table names, the columns,
* schemas, __everything__. When using the `CamelCasePlugin` Kysely
* works as if the database was defined in camelCase.
* in camelCase in the typescript code: table names, columns, schemas,
* __everything__. When using the `CamelCasePlugin` Kysely works as if
* the database was defined in camelCase.
*
* There are various options you can give to the plugin to modify
* the way identifiers are converted. See {@link CamelCasePluginOptions}.
Expand Down
19 changes: 11 additions & 8 deletions src/query-compiler/default-query-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,24 +151,27 @@ export class DefaultQueryCompiler
this.append(' ')
}

this.append('select ')
this.append('select')

if (node.distinctOn) {
this.compileDistinctOn(node.distinctOn)
this.append(' ')
this.compileDistinctOn(node.distinctOn)
}

if (node.frontModifiers && node.frontModifiers.length > 0) {
this.compileList(node.frontModifiers, ' ')
if (node.frontModifiers?.length) {
this.append(' ')
this.compileList(node.frontModifiers, ' ')
}

if (node.selections) {
this.compileList(node.selections)
this.append(' ')
this.compileList(node.selections)
}

this.visitNode(node.from)
if (node.from) {
this.append(' ')
this.visitNode(node.from)
}

if (node.joins) {
this.append(' ')
Expand Down Expand Up @@ -210,7 +213,7 @@ export class DefaultQueryCompiler
this.visitNode(node.offset)
}

if (node.endModifiers && node.endModifiers.length > 0) {
if (node.endModifiers?.length) {
this.append(' ')
this.compileList(node.endModifiers, ' ')
}
Expand Down Expand Up @@ -944,7 +947,7 @@ export class DefaultQueryCompiler
if (!node.materialized) {
this.append('not ')
}

this.append('materialized ')
}

Expand Down
68 changes: 67 additions & 1 deletion src/query-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ import { DeleteResult } from './query-builder/delete-result.js'
import { UpdateResult } from './query-builder/update-result.js'
import { KyselyPlugin } from './plugin/kysely-plugin.js'
import { CTEBuilderCallback } from './query-builder/cte-builder.js'
import {
SelectArg,
SelectExpression,
Selection,
parseSelectArg,
} from './parser/select-parser.js'

export class QueryCreator<DB> {
readonly #props: QueryCreatorProps
Expand Down Expand Up @@ -182,13 +188,73 @@ export class QueryCreator<DB> {
return createSelectQueryBuilder({
queryId: createQueryId(),
executor: this.#props.executor,
queryNode: SelectQueryNode.create(
queryNode: SelectQueryNode.createFrom(
parseTableExpressionOrList(from),
this.#props.withNode
),
})
}

/**
* Creates a `select` query builder without a `from` clause.
*
* If you want to create a `select from` query, use the `selectFrom` method instead.
* This one can be used to create a plain `select` statement without a `from` clause.
*
* This method accepts the same inputs as {@link SelectQueryBuilder.select}. See its
* documentation for more examples.
*
* ### Examples
*
* ```ts
* const result = db.select((eb) => [
* eb.selectFrom('person')
* .select('id')
* .where('first_name', '=', 'Jennifer')
* .limit(1)
* .as('jennifer_id'),
*
* eb.selectFrom('pet')
* .select('id')
* .where('name', '=', 'Doggo')
* .limit(1)
* .as('doggo_id')
* ])
* .executeTakeFirstOrThrow()
*
* console.log(result.jennifer_id)
* console.log(result.doggo_id)
* ```
*
* The generated SQL (PostgreSQL):
*
* ```sql
* select (
* select "id"
* from "person"
* where "first_name" = $1
* limit $2
* ) as "jennifer_id", (
* select "id"
* from "pet"
* where "name" = $3
* limit $4
* ) as "doggo_id"
* ```
*/
selectNoFrom<SE extends SelectExpression<DB, never>>(
selection: SelectArg<DB, never, SE>
): SelectQueryBuilder<DB, never, Selection<DB, never, SE>> {
return createSelectQueryBuilder({
queryId: createQueryId(),
executor: this.#props.executor,
queryNode: SelectQueryNode.cloneWithSelections(
SelectQueryNode.create(this.#props.withNode),
parseSelectArg(selection as any)
),
})
}

/**
* Creates an insert query.
*
Expand Down
37 changes: 37 additions & 0 deletions test/node/src/select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,43 @@ for (const dialect of DIALECTS) {
}
}

it('should create a select statement without a `from` clause', async () => {
const query = ctx.db.selectNoFrom((eb) => [
eb.selectNoFrom(eb.lit(1).as('one')).as('one'),
eb
.selectFrom('person')
.select('first_name')
.orderBy('first_name')
.limit(1)
.as('person_first_name'),
])

testSql(query, dialect, {
postgres: {
sql: `select (select 1 as "one") as "one", (select "first_name" from "person" order by "first_name" limit $1) as "person_first_name"`,
parameters: [1],
},
mysql: {
sql: 'select (select 1 as `one`) as `one`, (select `first_name` from `person` order by `first_name` limit ?) as `person_first_name`',
parameters: [1],
},
sqlite: {
sql: 'select (select 1 as "one") as "one", (select "first_name" from "person" order by "first_name" limit ?) as "person_first_name"',
parameters: [1],
},
})

const result = await query.execute()
expect(result).to.have.length(1)

if (dialect === 'mysql') {
// For some weird reason, MySQL returns `one` as a string.
expect(result[0]).to.eql({ one: '1', person_first_name: 'Arnold' })
} else {
expect(result[0]).to.eql({ one: 1, person_first_name: 'Arnold' })
}
})

it.skip('perf', async () => {
const ids = Array.from({ length: 100 }).map(() =>
Math.round(Math.random() * 1000)
Expand Down
45 changes: 44 additions & 1 deletion test/typings/test-d/expression.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ function testExpression(db: Kysely<Database>) {
)
}

function testExpressionBuilder(eb: ExpressionBuilder<Database, 'person'>) {
async function testExpressionBuilder(
eb: ExpressionBuilder<Database, 'person'>
) {
// Binary expression
expectAssignable<Expression<number>>(eb('age', '+', 1))

Expand Down Expand Up @@ -143,3 +145,44 @@ function testExpressionBuilder(eb: ExpressionBuilder<Database, 'person'>) {
expectError(eb.betweenSymmetric('age', 'wrong type', 2))
expectError(eb.betweenSymmetric('age', 1, 'wrong type'))
}

async function testExpressionBuilderSelect(
db: Kysely<Database>,
eb: ExpressionBuilder<Database, 'person'>
) {
expectAssignable<Expression<{ first_name: string }>>(
eb.selectNoFrom(eb.val('Jennifer').as('first_name'))
)

expectAssignable<Expression<{ first_name: string }>>(
eb.selectNoFrom((eb) => eb.val('Jennifer').as('first_name'))
)

expectAssignable<Expression<{ first_name: string; last_name: string }>>(
eb.selectNoFrom([
eb.val('Jennifer').as('first_name'),
eb(eb.val('Anis'), '||', eb.val('ton')).as('last_name'),
])
)

expectAssignable<
Expression<{ first_name: string; last_name: string | null }>
>(
eb.selectNoFrom((eb) => [
eb.val('Jennifer').as('first_name'),
eb.selectFrom('person').select('last_name').limit(1).as('last_name'),
])
)

const r1 = await db
.selectFrom('person as p')
.select((eb) => [eb.selectNoFrom('p.age').as('age')])
.executeTakeFirstOrThrow()
expectType<{ age: number | null }>(r1)

expectError(
db
.selectFrom('person')
.select((eb) => [eb.selectNoFrom('pet.name').as('name')])
)
}
31 changes: 31 additions & 0 deletions test/typings/test-d/select-no-from.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Kysely, SqlBool, sql } from '..'
import { Database } from '../shared'
import { expectType } from 'tsd'

async function testSelectNoFrom(db: Kysely<Database>) {
const r1 = await db
.selectNoFrom(sql<'bar'>`select 'bar'`.as('foo'))
.executeTakeFirstOrThrow()
expectType<{ foo: 'bar' }>(r1)

const r2 = await db
.selectNoFrom((eb) => eb(eb.val(1), '=', 1).as('very_useful'))
.executeTakeFirstOrThrow()
expectType<{ very_useful: SqlBool }>(r2)

const r3 = await db
.selectNoFrom([
sql<'bar'>`select 'bar'`.as('foo'),
db.selectFrom('pet').select('id').limit(1).as('pet_id'),
])
.executeTakeFirstOrThrow()
expectType<{ foo: 'bar'; pet_id: string | null }>(r3)

const r4 = await db
.selectNoFrom((eb) => [
eb(eb.val(1), '=', 1).as('very_useful'),
eb.selectFrom('pet').select('id').limit(1).as('pet_id'),
])
.executeTakeFirstOrThrow()
expectType<{ very_useful: SqlBool; pet_id: string | null }>(r4)
}

1 comment on commit d8aae75

@vercel
Copy link

@vercel vercel bot commented on d8aae75 Jul 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

kysely – ./

kysely-kysely-team.vercel.app
www.kysely.dev
kysely.dev
kysely-git-master-kysely-team.vercel.app

Please sign in to comment.