Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion buildScripts/docker/docker-compose-sqlserver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ services:
sqlserver:
container_name: SQLServer
restart: always
image: mcr.microsoft.com/mssql/server:2025-RC1-ubuntu-24.04
image: mcr.microsoft.com/mssql/server:2025-CU8-ubuntu-24.04
platform: linux/amd64
ports:
- "3005:1433"
Expand Down
76 changes: 76 additions & 0 deletions documentation-website/Writerside/topics/DSL-Querying-data.topic
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,82 @@

</chapter>
</chapter>
<chapter id="tableless-select">
<title>Tableless <code>SELECT</code> queries</title>
<p>
Use the top-level <code>select()</code> function to query any number of expressions without specifying a table (without a <code>FROM</code> clause specifically). A
tableless query starts with one implicit row, so a projection returns one row unless a
<code>.where { ... }</code> condition filters it out. Exposed renders the database's native source-less
<code>SELECT</code> form, including a dual table on databases that require one.
</p>
<code-block lang="kotlin"><![CDATA[
val numberOne = intLiteral(1)
val query: Query = select(numberOne, CurrentTimestamp)
val one: Int = query.single()[numberOne]

val query2 = select(listOf(intLiteral(1), stringLiteral("ready")))

val noRows = select(intLiteral(1)).where { Op.FALSE }
]]></code-block>
<p>On H2, <code>query</code> generates the following SQL:</p>
<code-block lang="sql">
SELECT 1, CURRENT_TIMESTAMP
</code-block>
<p>
For one expression, <code>selectValue()</code> executes immediately in the current transaction and returns
the typed value. Its R2DBC variant is a suspending function. This avoids retaining an expression only to
use it as a result-row key.
</p>
<code-block lang="kotlin"><![CDATA[
val one: Int = selectValue(intLiteral(1))
val serverTime = selectValue(CurrentTimestamp)
]]></code-block>
<p>
For convenience, <code>selectValue()</code> accepts supported Kotlin scalar values directly and wraps
them as SQL literals internally. For example, <code>selectValue(1)</code> is equivalent to
<code>selectValue(intLiteral(1))</code>. Date and time types from optional modules continue to use their
module-specific literal functions.
</p>
<code-block lang="kotlin"><![CDATA[
val one: Int = selectValue(1) // Executes SELECT 1 and returns Int
val ready = selectValue("ready") // Executes SELECT 'ready' and returns String
val serverTime = selectValue(CurrentTimestamp)
]]></code-block>
<p>
It is suitable for cases where you do not need explicit literal values and is equivalent to:
</p>
<code-block lang="kotlin"><![CDATA[
val one = literal(1)
val query = select(one)
val result: Int = query.single()[one]

val immediate: Int = selectValue(1) // Equivalent SQL literal, returned immediately
]]></code-block>
<p>
Tableless queries remain ordinary <code>Query</code> objects, so they can be filtered, combined with set
operations, aliased as derived tables, or passed to insert-from-select and other existing APIs. The
function does not infer a <code>FROM</code> clause from projected expressions: <code>select(Users.name)</code>
means the literal SQL shape <code>SELECT users.name</code>, not a query over <code>Users</code>.
</p>
<chapter id="tableless-select-limitations">
<title>Limitations</title>
<p>
A tableless <code>SELECT</code> have some database limitations:
</p>
<list>
<li>
SQL Server and Oracle versions before 23ai cannot project an <code>EXISTS</code> predicate directly.
</li>
<li>
Sequence, function, lateral-query, and merge support remains database-specific.
</li>
</list>
<p>
Exposed sends the SQL shape represented by the DSL; it does not rewrite expressions to make them
portable across databases.
</p>
</chapter>
</chapter>
<chapter id="conditional-where">
<title>Conditional <code>WHERE</code></title>
<p>
Expand Down
1 change: 1 addition & 0 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1587,6 +1587,7 @@ public final class org/jetbrains/exposed/v1/core/LiteralOpKt {
public static final fun doubleLiteral (D)Lorg/jetbrains/exposed/v1/core/LiteralOp;
public static final fun floatLiteral (F)Lorg/jetbrains/exposed/v1/core/LiteralOp;
public static final fun intLiteral (I)Lorg/jetbrains/exposed/v1/core/LiteralOp;
public static final fun literal (Ljava/lang/Object;)Lorg/jetbrains/exposed/v1/core/LiteralOp;
public static final fun longLiteral (J)Lorg/jetbrains/exposed/v1/core/LiteralOp;
public static final fun shortLiteral (S)Lorg/jetbrains/exposed/v1/core/LiteralOp;
public static final fun stringLiteral (Ljava/lang/String;)Lorg/jetbrains/exposed/v1/core/LiteralOp;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.jetbrains.exposed.v1.core

import org.jetbrains.exposed.v1.core.statements.api.ExposedBlob
import org.jetbrains.exposed.v1.core.vendors.PostgreSQLDialect
import org.jetbrains.exposed.v1.core.vendors.currentDialect

internal class BinaryLiteralColumnType : BasicBinaryColumnType() {
override fun nonNullValueToString(value: ByteArray): String {
val literal = BlobColumnType().nonNullValueToString(ExposedBlob(value))
return if (currentDialect is PostgreSQLDialect) "$literal::bytea" else literal
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.jetbrains.exposed.v1.core

import org.jetbrains.exposed.v1.core.java.UUIDColumnType
import java.math.BigDecimal
import java.util.UUID
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

/**
* Represents the specified [value] as an SQL literal, using the specified [columnType] to convert the value.
Expand Down Expand Up @@ -95,8 +99,54 @@ fun vectorLiteral(value: IntArray): LiteralOp<IntArray> = LiteralOp(
/** Returns the specified [value] as a literal of type [T]. */
@Suppress("UNCHECKED_CAST", "ComplexMethod")
fun <T, S : T?> ExpressionWithColumnType<S>.asLiteral(value: T): LiteralOp<T> = when {
value is ByteArray && columnType is BasicBinaryColumnType -> stringLiteral(value.toString(Charsets.UTF_8))
value is ByteArray && columnType is BasicBinaryColumnType -> literal(value)
columnType is ColumnWithTransform<*, *> -> (columnType as ColumnWithTransform<Any, Any>)
.let { LiteralOp(it.originalColumnType, it.unwrapRecursive(value)) }
else -> LiteralOp(columnType as IColumnType<T & Any>, value)
} as LiteralOp<T>

/**
* Returns the specified non-null scalar [value] as an SQL literal.
*
* Supported values are booleans, signed and unsigned integer and floating-point numbers, [BigDecimal], strings,
* characters, byte arrays, [Uuid], and [UUID]. Values from optional modules, such as date and time types, should use
* their module's typed literal function instead.
*
* This is a convenience function to replace direct usage of `*type*Literal` methods.
*
* For example:
* ```kotlin
* val status = literal("ready")
* ```
*
* @throws IllegalArgumentException If [value] has no automatically resolved literal type.
*/
@OptIn(ExperimentalUuidApi::class)
fun <T : Any> literal(value: T): LiteralOp<T> {
val result: LiteralOp<out Any> = when (value) {
is Boolean -> booleanLiteral(value)
is Byte -> byteLiteral(value)
is UByte -> ubyteLiteral(value)
is Short -> shortLiteral(value)
is UShort -> ushortLiteral(value)
is Int -> intLiteral(value)
is UInt -> uintLiteral(value)
is Long -> longLiteral(value)
is ULong -> ulongLiteral(value)
is Float -> floatLiteral(value)
is Double -> doubleLiteral(value)
is BigDecimal -> decimalLiteral(value)
is String -> stringLiteral(value)
is Char -> LiteralOp(CharacterColumnType(), value)
is ByteArray -> LiteralOp(BinaryLiteralColumnType(), value)
is Uuid -> LiteralOp(UuidColumnType(), value)
is UUID -> LiteralOp(UUIDColumnType(), value)
else -> throw IllegalArgumentException(
"Cannot create an SQL literal for ${value::class.qualifiedName}. " +
"Use an explicit Expression or LiteralOp with an IColumnType."
)
}

@Suppress("UNCHECKED_CAST")
return result as LiteralOp<T>
}
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ interface ISqlExpressionBuilder {
)
@Suppress("UNCHECKED_CAST", "ComplexMethod")
fun <T, S : T?> ExpressionWithColumnType<S>.asLiteral(value: T): LiteralOp<T> = when {
value is ByteArray && columnType is BasicBinaryColumnType -> stringLiteral(value.toString(Charsets.UTF_8))
value is ByteArray && columnType is BasicBinaryColumnType -> literal(value)
columnType is ColumnWithTransform<*, *> -> (columnType as ColumnWithTransform<Any, Any>)
.let { LiteralOp(it.originalColumnType, it.unwrapRecursive(value)) }
else -> LiteralOp(columnType as IColumnType<T & Any>, value)
Expand Down
4 changes: 4 additions & 0 deletions exposed-jdbc/api/exposed-jdbc.api
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,13 @@ public final class org/jetbrains/exposed/v1/jdbc/QueriesKt {
public static final fun replace (Lorg/jetbrains/exposed/v1/core/Table;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/v1/core/statements/ReplaceStatement;
public static final fun replace (Lorg/jetbrains/exposed/v1/core/Table;Lorg/jetbrains/exposed/v1/core/AbstractQuery;Ljava/util/List;)Ljava/lang/Integer;
public static synthetic fun replace$default (Lorg/jetbrains/exposed/v1/core/Table;Lorg/jetbrains/exposed/v1/core/AbstractQuery;Ljava/util/List;ILjava/lang/Object;)Ljava/lang/Integer;
public static final fun select (Ljava/util/List;)Lorg/jetbrains/exposed/v1/jdbc/Query;
public static final fun select (Lorg/jetbrains/exposed/v1/core/ColumnSet;Ljava/util/List;)Lorg/jetbrains/exposed/v1/jdbc/Query;
public static final fun select (Lorg/jetbrains/exposed/v1/core/ColumnSet;Lorg/jetbrains/exposed/v1/core/Expression;[Lorg/jetbrains/exposed/v1/core/Expression;)Lorg/jetbrains/exposed/v1/jdbc/Query;
public static final fun select (Lorg/jetbrains/exposed/v1/core/Expression;[Lorg/jetbrains/exposed/v1/core/Expression;)Lorg/jetbrains/exposed/v1/jdbc/Query;
public static final fun selectAll (Lorg/jetbrains/exposed/v1/core/FieldSet;)Lorg/jetbrains/exposed/v1/jdbc/Query;
public static final fun selectValue (Ljava/lang/Object;)Ljava/lang/Object;
public static final fun selectValue (Lorg/jetbrains/exposed/v1/core/Expression;)Ljava/lang/Object;
public static final fun update (Lorg/jetbrains/exposed/v1/core/Join;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)I
public static final fun update (Lorg/jetbrains/exposed/v1/core/Join;Lkotlin/jvm/functions/Function0;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)I
public static final fun update (Lorg/jetbrains/exposed/v1/core/Table;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;)I
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,73 @@ import kotlin.sequences.Sequence
*/
fun FieldSet.selectAll(): Query = Query(this, null)

/**
* Creates a `SELECT` [Query] without a `FROM` clause.
*
* Some queries without a `FROM` clause are valid, such as `select(intLiteral(1))`. Others are not. For example,
* Others are not. For example,
* `select(MyTable.id)` is rejected by the database during execution because `MyTable` is not a query source.
*
* For instance `SELECT 1` can be written as follows:
* ```kotlin
* val one = intLiteral(1)
* val result = select(one).single()[one]
* ```
*/
@LowPriorityInOverloadResolution
fun select(expression: Expression<*>, vararg expressions: Expression<*>): Query =
Table.Dual.select(listOf(expression) + expressions)

/**
* Creates a `SELECT` [Query] without a `FROM` clause.
*
* Some queries without a `FROM` clause are valid, such as `select(listOf(intLiteral(1)))`. Others are not. For
* example, `select(listOf(MyTable.id))` is rejected by the
* database during execution because `MyTable` is not a query source.
*
* ```kotlin
* val one = intLiteral(1)
* val status = stringLiteral("ready")
* val result = select(listOf(one, status)).single()
* ```
*
* @throws IllegalArgumentException If [expressions] is empty.
*/
@LowPriorityInOverloadResolution
fun select(expressions: List<Expression<*>>): Query {
require(expressions.isNotEmpty()) { "Can't prepare SELECT statement without columns or expressions to retrieve" }
return Table.Dual.select(expressions)
}

/**
* Executes a `SELECT` of [expression] without a `FROM` clause and returns its single value. This function
* executes immediately in the current transaction.
* Some queries without a `FROM` clause are valid, such as `selectValue(intLiteral(1))`. Others are not. For example,
* `selectValue(MyTable.id)` is rejected by the database during execution because `MyTable` is not a query source.
*
* For example:
* ```kotlin
* val result: Int = selectValue(intLiteral(1))
* ```
*/
fun <T> selectValue(expression: Expression<T>): T = select(expression).single()[expression]

/**
* Executes a `SELECT` of [value] as an SQL literal without a `FROM` clause and returns its single value.
*
* Some queries without a `FROM` clause are valid, such as `selectValue(1)`. Others are not. The database rejects the
* query during execution if the literal is not supported.
*
* For example:
* ```kotlin
* val result: Int = selectValue(1)
* ```
*
* @throws IllegalArgumentException If [value] is not supported by [literal].
*/
@LowPriorityInOverloadResolution
fun <T : Any> selectValue(value: T): T = selectValue(literal(value))

/**
* Creates a `SELECT` [Query] by selecting either a single [column], or a subset of [columns], from this [ColumnSet].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import org.jetbrains.exposed.v1.r2dbc.transactions.suspendTransaction
import org.junit.jupiter.api.Assumptions
import org.junit.jupiter.api.Test
import java.util.*
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.expect

Expand Down Expand Up @@ -1294,45 +1296,60 @@ class DDLTests : R2dbcDatabaseTestsBase() {
@Test
fun testTableModifiersAndStorageParametersSQL() {
// Test TableEngine enum values
kotlin.test.assertEquals("InnoDB", Table.TableEngine.INNODB.engineName)
kotlin.test.assertEquals("MyISAM", Table.TableEngine.MYISAM.engineName)
kotlin.test.assertEquals("MEMORY", Table.TableEngine.MEMORY.engineName)
kotlin.test.assertEquals("ARCHIVE", Table.TableEngine.ARCHIVE.engineName)
kotlin.test.assertEquals("CSV", Table.TableEngine.CSV.engineName)
assertEquals("InnoDB", Table.TableEngine.INNODB.engineName)
assertEquals("MyISAM", Table.TableEngine.MYISAM.engineName)
assertEquals("MEMORY", Table.TableEngine.MEMORY.engineName)
assertEquals("ARCHIVE", Table.TableEngine.ARCHIVE.engineName)
assertEquals("CSV", Table.TableEngine.CSV.engineName)

// Test Table.EngineOption SQL generation
kotlin.test.assertEquals("ENGINE=InnoDB", Table.EngineOption(Table.TableEngine.INNODB).toSQL())
kotlin.test.assertEquals("ENGINE=MEMORY", Table.EngineOption(Table.TableEngine.MEMORY).toSQL())
kotlin.test.assertEquals("ENGINE=MyISAM", Table.EngineOption(Table.TableEngine.MYISAM).toSQL())
assertEquals("ENGINE=InnoDB", Table.EngineOption(Table.TableEngine.INNODB).toSQL())
assertEquals("ENGINE=MEMORY", Table.EngineOption(Table.TableEngine.MEMORY).toSQL())
assertEquals("ENGINE=MyISAM", Table.EngineOption(Table.TableEngine.MYISAM).toSQL())

// Test Table.CharsetOption SQL generation
kotlin.test.assertEquals("DEFAULT CHARSET=utf8mb4", Table.CharsetOption("utf8mb4").toSQL())
kotlin.test.assertEquals(
assertEquals("DEFAULT CHARSET=utf8mb4", Table.CharsetOption("utf8mb4").toSQL())
assertEquals(
"DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
Table.CharsetOption("utf8mb4", "utf8mb4_unicode_ci").toSQL()
)

// Test Table.RawTableOption SQL generation
kotlin.test.assertEquals("ROW_FORMAT=COMPRESSED", Table.RawTableOption("ROW_FORMAT=COMPRESSED").toSQL())
assertEquals("ROW_FORMAT=COMPRESSED", Table.RawTableOption("ROW_FORMAT=COMPRESSED").toSQL())

// Test Table.FillFactorParameter SQL generation and validation
kotlin.test.assertEquals("fillfactor=70", Table.FillFactorParameter(70).toSQL())
kotlin.test.assertEquals("fillfactor=10", Table.FillFactorParameter(10).toSQL())
kotlin.test.assertEquals("fillfactor=100", Table.FillFactorParameter(100).toSQL())
assertEquals("fillfactor=70", Table.FillFactorParameter(70).toSQL())
assertEquals("fillfactor=10", Table.FillFactorParameter(10).toSQL())
assertEquals("fillfactor=100", Table.FillFactorParameter(100).toSQL())
assertFailsWith<IllegalArgumentException> { Table.FillFactorParameter(9) }
assertFailsWith<IllegalArgumentException> { Table.FillFactorParameter(101) }

// Test AutovacuumEnabledParameter SQL generation
kotlin.test.assertEquals("autovacuum_enabled=true", Table.AutovacuumEnabledParameter(true).toSQL())
kotlin.test.assertEquals("autovacuum_enabled=false", Table.AutovacuumEnabledParameter(false).toSQL())
assertEquals("autovacuum_enabled=true", Table.AutovacuumEnabledParameter(true).toSQL())
assertEquals("autovacuum_enabled=false", Table.AutovacuumEnabledParameter(false).toSQL())

// Test ToastTupleTargetParameter SQL generation and validation
kotlin.test.assertEquals("toast_tuple_target=8160", Table.ToastTupleTargetParameter(8160).toSQL())
kotlin.test.assertEquals("toast_tuple_target=1", Table.ToastTupleTargetParameter(1).toSQL())
assertEquals("toast_tuple_target=8160", Table.ToastTupleTargetParameter(8160).toSQL())
assertEquals("toast_tuple_target=1", Table.ToastTupleTargetParameter(1).toSQL())
assertFailsWith<IllegalArgumentException> { Table.ToastTupleTargetParameter(0) }
assertFailsWith<IllegalArgumentException> { Table.ToastTupleTargetParameter(-1) }

// Test RawTableStorageParameter SQL generation
kotlin.test.assertEquals("parallel_workers=4", Table.RawTableStorageParameter("parallel_workers=4").toSQL())
assertEquals("parallel_workers=4", Table.RawTableStorageParameter("parallel_workers=4").toSQL())
}

@Test
fun testBinaryDefaultPreservesNonUtf8Bytes() {
val expected = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47)
val table = object : Table("binary_default_test") {
val payload = binary("payload", 16).default(expected)
}

withTables(excludeSettings = TestDB.ALL_ORACLE_LIKE, table) {
table.insert {}

val actual = table.selectAll().single()[table.payload]
assertContentEquals(expected, actual)
}
}
}

This file was deleted.

Loading
Loading