diff --git a/src/expression/expression-builder.ts b/src/expression/expression-builder.ts index c541a8a1c..3e4baf037 100644 --- a/src/expression/expression-builder.ts +++ b/src/expression/expression-builder.ts @@ -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 { /** @@ -250,6 +256,19 @@ export interface ExpressionBuilder { from: TE ): SelectQueryBuilder, FromTables, {}> + /** + * 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>( + selection: SelectArg + ): SelectQueryBuilder> + /** * Creates a `case` statement/operator. * @@ -852,8 +871,23 @@ export function createExpressionBuilder( selectFrom(table: TableExpressionOrList): any { return createSelectQueryBuilder({ queryId: createQueryId(), - executor: executor, - queryNode: SelectQueryNode.create(parseTableExpressionOrList(table)), + executor, + queryNode: SelectQueryNode.createFrom( + parseTableExpressionOrList(table) + ), + }) + }, + + selectNoFrom>( + selection: SelectArg + ): SelectQueryBuilder> { + return createSelectQueryBuilder({ + queryId: createQueryId(), + executor, + queryNode: SelectQueryNode.cloneWithSelections( + SelectQueryNode.create(), + parseSelectArg(selection) + ), }) }, diff --git a/src/operation-node/select-query-node.ts b/src/operation-node/select-query-node.ts index e75b8fba5..e9c08806d 100644 --- a/src/operation-node/select-query-node.ts +++ b/src/operation-node/select-query-node.ts @@ -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 readonly distinctOn?: ReadonlyArray readonly joins?: ReadonlyArray @@ -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, withNode?: WithNode ): SelectQueryNode { diff --git a/src/parser/parse-utils.ts b/src/parser/parse-utils.ts index f440b99f3..a2e2d99bf 100644 --- a/src/parser/parse-utils.ts +++ b/src/parser/parse-utils.ts @@ -20,7 +20,7 @@ export function createSelectQueryBuilder(): SelectQueryBuilder { return newSelectQueryBuilder({ queryId: createQueryId(), executor: NOOP_QUERY_EXECUTOR, - queryNode: SelectQueryNode.create(parseTableExpressionOrList([])), + queryNode: SelectQueryNode.createFrom(parseTableExpressionOrList([])), }) } diff --git a/src/plugin/camel-case/camel-case-plugin.ts b/src/plugin/camel-case/camel-case-plugin.ts index f24792787..a32491d0c 100644 --- a/src/plugin/camel-case/camel-case-plugin.ts +++ b/src/plugin/camel-case/camel-case-plugin.ts @@ -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, @@ -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}. diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 42b9f0b2c..8f71256e5 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -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(' ') @@ -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, ' ') } @@ -944,7 +947,7 @@ export class DefaultQueryCompiler if (!node.materialized) { this.append('not ') } - + this.append('materialized ') } diff --git a/src/query-creator.ts b/src/query-creator.ts index c6b2346a3..123145a17 100644 --- a/src/query-creator.ts +++ b/src/query-creator.ts @@ -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 { readonly #props: QueryCreatorProps @@ -182,13 +188,73 @@ export class QueryCreator { 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>( + selection: SelectArg + ): SelectQueryBuilder> { + return createSelectQueryBuilder({ + queryId: createQueryId(), + executor: this.#props.executor, + queryNode: SelectQueryNode.cloneWithSelections( + SelectQueryNode.create(this.#props.withNode), + parseSelectArg(selection as any) + ), + }) + } + /** * Creates an insert query. * diff --git a/test/node/src/select.test.ts b/test/node/src/select.test.ts index 5f0b15ae1..3aba9e3ad 100644 --- a/test/node/src/select.test.ts +++ b/test/node/src/select.test.ts @@ -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) diff --git a/test/typings/test-d/expression.test-d.ts b/test/typings/test-d/expression.test-d.ts index 6af2e4854..28bc79ef9 100644 --- a/test/typings/test-d/expression.test-d.ts +++ b/test/typings/test-d/expression.test-d.ts @@ -25,7 +25,9 @@ function testExpression(db: Kysely) { ) } -function testExpressionBuilder(eb: ExpressionBuilder) { +async function testExpressionBuilder( + eb: ExpressionBuilder +) { // Binary expression expectAssignable>(eb('age', '+', 1)) @@ -143,3 +145,44 @@ function testExpressionBuilder(eb: ExpressionBuilder) { expectError(eb.betweenSymmetric('age', 'wrong type', 2)) expectError(eb.betweenSymmetric('age', 1, 'wrong type')) } + +async function testExpressionBuilderSelect( + db: Kysely, + eb: ExpressionBuilder +) { + expectAssignable>( + eb.selectNoFrom(eb.val('Jennifer').as('first_name')) + ) + + expectAssignable>( + eb.selectNoFrom((eb) => eb.val('Jennifer').as('first_name')) + ) + + expectAssignable>( + 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')]) + ) +} diff --git a/test/typings/test-d/select-no-from.test-d.ts b/test/typings/test-d/select-no-from.test-d.ts new file mode 100644 index 000000000..2d1a35e31 --- /dev/null +++ b/test/typings/test-d/select-no-from.test-d.ts @@ -0,0 +1,31 @@ +import { Kysely, SqlBool, sql } from '..' +import { Database } from '../shared' +import { expectType } from 'tsd' + +async function testSelectNoFrom(db: Kysely) { + 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) +}