Compare commits
No commits in common. "49fc7002333b5db79a4764286d637bcb9ef77aff" and "bb2119b1d139badaf4f9f79e32aa652d557e410f" have entirely different histories.
49fc700233
...
bb2119b1d1
@ -20,7 +20,7 @@
|
|||||||
set -e
|
set -e
|
||||||
echo "publishing all artifacts"
|
echo "publishing all artifacts"
|
||||||
echo
|
echo
|
||||||
./gradlew publishToMavenLocal site:jsBrowserDistribution publish buildInstallablePlugin :lyng:linkReleaseExecutableLinuxX64 :lyng:installJvmDist --parallel --no-configuration-cache
|
./gradlew publishToMavenLocal site:jsBrowserDistribution publish buildInstallablePlugin :lyng:linkReleaseExecutableLinuxX64 :lyng:installJvmDist --parallel #--no-configuration-cache
|
||||||
|
|
||||||
#echo
|
#echo
|
||||||
#echo "Creating plugin"
|
#echo "Creating plugin"
|
||||||
|
|||||||
@ -205,8 +205,7 @@ assertThrows(RollbackException) {
|
|||||||
- `columns` — positional `SqlColumn` metadata, available before iteration.
|
- `columns` — positional `SqlColumn` metadata, available before iteration.
|
||||||
- `size()` — result row count.
|
- `size()` — result row count.
|
||||||
- `isEmpty()` — fast emptiness check where possible.
|
- `isEmpty()` — fast emptiness check where possible.
|
||||||
- `iterator()` — normal row iteration while the transaction is active.
|
- `iterator()` / `toList()` — normal row iteration.
|
||||||
- `toList()` — materialize detached `SqlRow` snapshots that may be used after the transaction ends.
|
|
||||||
|
|
||||||
##### `SqlRow`
|
##### `SqlRow`
|
||||||
|
|
||||||
@ -371,24 +370,14 @@ PostgreSQL-specific notes:
|
|||||||
|
|
||||||
#### Lifetime rules
|
#### Lifetime rules
|
||||||
|
|
||||||
`ResultSet` is valid only while its owning transaction is active.
|
Result sets and rows are valid only while their owning transaction is active.
|
||||||
|
|
||||||
`SqlRow` values are detached snapshots once materialized, so this pattern is valid:
|
|
||||||
|
|
||||||
```lyng
|
|
||||||
val rows = db.transaction { tx ->
|
|
||||||
tx.select("select name from person order by id").toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals("Ada", rows[0]["name"])
|
|
||||||
```
|
|
||||||
|
|
||||||
This means:
|
This means:
|
||||||
|
|
||||||
- do not keep `ResultSet` objects after the transaction block returns
|
- do not keep `ResultSet` or `SqlRow` objects after the transaction block returns
|
||||||
- materialize rows with `toList()` inside the transaction when they must outlive it
|
- copy the values you need into ordinary Lyng objects inside the transaction
|
||||||
|
|
||||||
The same rule applies to generated keys from `ExecutionResult.getGeneratedKeys()`: the `ResultSet` is transaction-scoped, but rows returned by `toList()` are detached.
|
The same lifetime rule applies to generated keys returned by `ExecutionResult.getGeneratedKeys()`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,7 @@ db.transaction { tx ->
|
|||||||
println(" #" + row["ID"] + " " + row["title"] + " done=" + row["done"] + " due=" + row["due_date"])
|
println(" #" + row["ID"] + " " + row["title"] + " done=" + row["done"] + " due=" + row["due_date"])
|
||||||
}
|
}
|
||||||
|
|
||||||
// toList() materializes detached rows that stay usable after transaction close.
|
// If values need to survive after the transaction closes, copy them now.
|
||||||
val snapshot = tx.select("select title, due_date from task order by id").toList()
|
val snapshot = tx.select("select title, due_date from task order by id").toList()
|
||||||
assertEquals("Write a SQLite example", snapshot[0]["title"])
|
assertEquals("Write a SQLite example", snapshot[0]["title"])
|
||||||
assertEquals(Date(2026, 4, 16), snapshot[1]["due_date"])
|
assertEquals(Date(2026, 4, 16), snapshot[1]["due_date"])
|
||||||
|
|||||||
@ -151,13 +151,8 @@ kotlin {
|
|||||||
|
|
||||||
tasks.named<KotlinNativeTest>("linuxX64Test") {
|
tasks.named<KotlinNativeTest>("linuxX64Test") {
|
||||||
dependsOn(tasks.named("linkDebugExecutableLinuxX64"))
|
dependsOn(tasks.named("linkDebugExecutableLinuxX64"))
|
||||||
dependsOn(tasks.named("linkReleaseExecutableLinuxX64"))
|
|
||||||
environment(
|
environment(
|
||||||
"LYNG_CLI_NATIVE_BIN",
|
"LYNG_CLI_NATIVE_BIN",
|
||||||
layout.buildDirectory.file("bin/linuxX64/debugExecutable/lyng.kexe").get().asFile.absolutePath
|
layout.buildDirectory.file("bin/linuxX64/debugExecutable/lyng.kexe").get().asFile.absolutePath
|
||||||
)
|
)
|
||||||
environment(
|
|
||||||
"LYNG_CLI_NATIVE_RELEASE_BIN",
|
|
||||||
layout.buildDirectory.file("bin/linuxX64/releaseExecutable/lyng.kexe").get().asFile.absolutePath
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,9 +119,8 @@ internal class CliExecutionRuntime(
|
|||||||
facade.call(handler)
|
facade.call(handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
session.cancel()
|
session.cancelAndJoin()
|
||||||
shutdownSystemNetEngine()
|
shutdownSystemNetEngine()
|
||||||
session.join()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdownBlocking() {
|
fun shutdownBlocking() {
|
||||||
|
|||||||
@ -1,132 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.lyng_cli
|
|
||||||
|
|
||||||
import kotlinx.cinterop.*
|
|
||||||
import okio.FileSystem
|
|
||||||
import okio.Path
|
|
||||||
import okio.Path.Companion.toPath
|
|
||||||
import platform.posix.O_CREAT
|
|
||||||
import platform.posix.O_TRUNC
|
|
||||||
import platform.posix.O_WRONLY
|
|
||||||
import platform.posix.SIGKILL
|
|
||||||
import platform.posix.SIGSEGV
|
|
||||||
import platform.posix._exit
|
|
||||||
import platform.posix.close
|
|
||||||
import platform.posix.dup2
|
|
||||||
import platform.posix.execvp
|
|
||||||
import platform.posix.fork
|
|
||||||
import platform.posix.getenv
|
|
||||||
import platform.posix.getpid
|
|
||||||
import platform.posix.kill
|
|
||||||
import platform.posix.open
|
|
||||||
import platform.posix.usleep
|
|
||||||
import platform.posix.waitpid
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
|
||||||
class CliWebSocketNativeRegressionTest {
|
|
||||||
@Test
|
|
||||||
fun releaseCliDoesNotSegfaultOnConcurrentWebSocketClients() {
|
|
||||||
val executable = getenv("LYNG_CLI_NATIVE_RELEASE_BIN")?.toKString()
|
|
||||||
?: error("LYNG_CLI_NATIVE_RELEASE_BIN is not set")
|
|
||||||
val fs = FileSystem.SYSTEM
|
|
||||||
val repoRoot = ascend(executable.toPath(), 6)
|
|
||||||
val scriptPath = repoRoot / "bugs" / "ws-segfault.lyng"
|
|
||||||
check(fs.exists(scriptPath)) { "bug repro script not found at $scriptPath" }
|
|
||||||
|
|
||||||
val tempDir = "/tmp/lyng_ws_native_${getpid()}_${kotlin.random.Random.nextInt()}".toPath()
|
|
||||||
val stdoutPath = tempDir / "stdout.txt"
|
|
||||||
val stderrPath = tempDir / "stderr.txt"
|
|
||||||
|
|
||||||
fs.createDirectories(tempDir)
|
|
||||||
try {
|
|
||||||
val pid = launchCli(executable, scriptPath, stdoutPath, stderrPath)
|
|
||||||
usleep(5_000_000u)
|
|
||||||
|
|
||||||
if (kill(pid, 0) == 0) {
|
|
||||||
kill(pid, SIGKILL)
|
|
||||||
}
|
|
||||||
|
|
||||||
val status = waitForPid(pid)
|
|
||||||
val termSignal = status and 0x7f
|
|
||||||
val stdout = readUtf8IfExists(fs, stdoutPath)
|
|
||||||
val stderr = readUtf8IfExists(fs, stderrPath)
|
|
||||||
val allOutput = "$stdout\n$stderr"
|
|
||||||
|
|
||||||
assertFalse(termSignal == SIGSEGV, "native CLI crashed with SIGSEGV. Output:\n$allOutput")
|
|
||||||
assertTrue(
|
|
||||||
stdout.lineSequence().count { it == "test send to ws://127.0.0.1:9998... OK" } == 2,
|
|
||||||
"expected both websocket clients to finish. Output:\n$allOutput"
|
|
||||||
)
|
|
||||||
assertFalse(allOutput.contains("Segmentation fault"), "process output reported a segmentation fault:\n$allOutput")
|
|
||||||
} finally {
|
|
||||||
fs.deleteRecursively(tempDir, mustExist = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ascend(path: Path, levels: Int): Path {
|
|
||||||
var current = path
|
|
||||||
repeat(levels) {
|
|
||||||
current = current.parent ?: error("cannot ascend $levels levels from $path")
|
|
||||||
}
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun readUtf8IfExists(fs: FileSystem, path: Path): String {
|
|
||||||
return if (fs.exists(path)) fs.read(path) { readUtf8() } else ""
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun waitForPid(pid: Int): Int = memScoped {
|
|
||||||
val status = alloc<IntVar>()
|
|
||||||
val waited = waitpid(pid, status.ptr, 0)
|
|
||||||
check(waited == pid) { "waitpid failed for $pid" }
|
|
||||||
status.value
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchCli(
|
|
||||||
executable: String,
|
|
||||||
scriptPath: Path,
|
|
||||||
stdoutPath: Path,
|
|
||||||
stderrPath: Path,
|
|
||||||
): Int = memScoped {
|
|
||||||
val pid = fork()
|
|
||||||
check(pid >= 0) { "fork failed" }
|
|
||||||
if (pid == 0) {
|
|
||||||
val stdoutFd = open(stdoutPath.toString(), O_WRONLY or O_CREAT or O_TRUNC, 0x1A4)
|
|
||||||
val stderrFd = open(stderrPath.toString(), O_WRONLY or O_CREAT or O_TRUNC, 0x1A4)
|
|
||||||
if (stdoutFd < 0 || stderrFd < 0) {
|
|
||||||
_exit(2)
|
|
||||||
}
|
|
||||||
dup2(stdoutFd, 1)
|
|
||||||
dup2(stderrFd, 2)
|
|
||||||
close(stdoutFd)
|
|
||||||
close(stderrFd)
|
|
||||||
|
|
||||||
val argv = allocArray<CPointerVar<ByteVar>>(3)
|
|
||||||
argv[0] = executable.cstr.ptr
|
|
||||||
argv[1] = scriptPath.toString().cstr.ptr
|
|
||||||
argv[2] = null
|
|
||||||
execvp(executable, argv)
|
|
||||||
_exit(127)
|
|
||||||
}
|
|
||||||
pid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -21,6 +21,7 @@ import net.sergeych.lyng.Arguments
|
|||||||
import net.sergeych.lyng.ModuleScope
|
import net.sergeych.lyng.ModuleScope
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.ScopeFacade
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.asFacade
|
||||||
import net.sergeych.lyng.obj.Obj
|
import net.sergeych.lyng.obj.Obj
|
||||||
import net.sergeych.lyng.obj.ObjBool
|
import net.sergeych.lyng.obj.ObjBool
|
||||||
import net.sergeych.lyng.obj.ObjClass
|
import net.sergeych.lyng.obj.ObjClass
|
||||||
@ -233,10 +234,12 @@ internal class SqlRuntimeTypes private constructor(
|
|||||||
|
|
||||||
rowClass.addProperty("size", getter = {
|
rowClass.addProperty("size", getter = {
|
||||||
val self = thisAs<SqlRowObj>()
|
val self = thisAs<SqlRowObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
ObjInt.of(self.values.size.toLong())
|
ObjInt.of(self.values.size.toLong())
|
||||||
})
|
})
|
||||||
rowClass.addProperty("values", getter = {
|
rowClass.addProperty("values", getter = {
|
||||||
val self = thisAs<SqlRowObj>()
|
val self = thisAs<SqlRowObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
ObjImmutableList(self.values)
|
ObjImmutableList(self.values)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -299,13 +302,14 @@ internal class SqlResultSetObj(
|
|||||||
data: SqlResultSetData,
|
data: SqlResultSetData,
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
val columns: List<Obj> = data.columns.map { SqlColumnObj(types, it) }
|
val columns: List<Obj> = data.columns.map { SqlColumnObj(types, it) }
|
||||||
val rows: List<Obj> = buildRows(types, data)
|
val rows: List<Obj> = buildRows(types, lifetime, data)
|
||||||
|
|
||||||
override val objClass: ObjClass
|
override val objClass: ObjClass
|
||||||
get() = types.resultSetClass
|
get() = types.resultSetClass
|
||||||
|
|
||||||
private fun buildRows(
|
private fun buildRows(
|
||||||
types: SqlRuntimeTypes,
|
types: SqlRuntimeTypes,
|
||||||
|
lifetime: SqlTransactionLifetime,
|
||||||
data: SqlResultSetData,
|
data: SqlResultSetData,
|
||||||
): List<Obj> {
|
): List<Obj> {
|
||||||
val indexByName = linkedMapOf<String, MutableList<Int>>()
|
val indexByName = linkedMapOf<String, MutableList<Int>>()
|
||||||
@ -313,13 +317,14 @@ internal class SqlResultSetObj(
|
|||||||
indexByName.getOrPut(column.name.lowercase()) { mutableListOf() }.add(index)
|
indexByName.getOrPut(column.name.lowercase()) { mutableListOf() }.add(index)
|
||||||
}
|
}
|
||||||
return data.rows.map { rowValues ->
|
return data.rows.map { rowValues ->
|
||||||
SqlRowObj(types, rowValues, indexByName)
|
SqlRowObj(types, lifetime, rowValues, indexByName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class SqlRowObj(
|
internal class SqlRowObj(
|
||||||
val types: SqlRuntimeTypes,
|
val types: SqlRuntimeTypes,
|
||||||
|
val lifetime: SqlTransactionLifetime,
|
||||||
val values: List<Obj>,
|
val values: List<Obj>,
|
||||||
private val indexByName: Map<String, List<Int>>,
|
private val indexByName: Map<String, List<Int>>,
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
@ -327,6 +332,7 @@ internal class SqlRowObj(
|
|||||||
get() = types.rowClass
|
get() = types.rowClass
|
||||||
|
|
||||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||||
|
lifetime.ensureActive(scope.asFacade())
|
||||||
return when (index) {
|
return when (index) {
|
||||||
is ObjInt -> {
|
is ObjInt -> {
|
||||||
val idx = index.value.toInt()
|
val idx = index.value.toInt()
|
||||||
|
|||||||
@ -17,46 +17,35 @@ import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
|||||||
|
|
||||||
internal fun createKtorWsEngine(
|
internal fun createKtorWsEngine(
|
||||||
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
||||||
shareClient: Boolean = true,
|
): LyngWsEngine = KtorLyngWsEngine(engineFactory)
|
||||||
): LyngWsEngine = KtorLyngWsEngine(engineFactory, shareClient)
|
|
||||||
|
|
||||||
private class KtorLyngWsEngine(
|
private class KtorLyngWsEngine(
|
||||||
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
||||||
private val shareClient: Boolean,
|
|
||||||
) : LyngWsEngine {
|
) : LyngWsEngine {
|
||||||
private val clientFactory: () -> HttpClient = {
|
private val clientResult = runCatching {
|
||||||
HttpClient(engineFactory) {
|
HttpClient(engineFactory) {
|
||||||
install(WebSockets)
|
install(WebSockets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val sharedClientResult = if (shareClient) runCatching { clientFactory() } else null
|
|
||||||
private val supportProbe = if (!shareClient) runCatching { clientFactory().close() } else null
|
|
||||||
|
|
||||||
override val isSupported: Boolean
|
override val isSupported: Boolean
|
||||||
get() = sharedClientResult?.isSuccess ?: supportProbe?.isSuccess ?: true
|
get() = clientResult.isSuccess
|
||||||
|
|
||||||
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
||||||
val client = (sharedClientResult?.getOrElse {
|
val client = clientResult.getOrElse {
|
||||||
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
||||||
} ?: runCatching { clientFactory() }.getOrElse {
|
}
|
||||||
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
|
||||||
})
|
|
||||||
val session = client.webSocketSession {
|
val session = client.webSocketSession {
|
||||||
url(url)
|
url(url)
|
||||||
headers.forEach { (name, value) -> header(name, value) }
|
headers.forEach { (name, value) -> header(name, value) }
|
||||||
}
|
}
|
||||||
return KtorLyngWsSession(
|
return KtorLyngWsSession(url, session)
|
||||||
targetUrl = url,
|
|
||||||
session = session,
|
|
||||||
ownedClient = client.takeUnless { shareClient },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class KtorLyngWsSession(
|
private class KtorLyngWsSession(
|
||||||
private val targetUrl: String,
|
private val targetUrl: String,
|
||||||
private val session: DefaultWebSocketSession,
|
private val session: DefaultWebSocketSession,
|
||||||
private val ownedClient: HttpClient? = null,
|
|
||||||
) : LyngWsSession {
|
) : LyngWsSession {
|
||||||
private var closed = false
|
private var closed = false
|
||||||
|
|
||||||
@ -66,22 +55,12 @@ private class KtorLyngWsSession(
|
|||||||
|
|
||||||
override suspend fun sendText(text: String) {
|
override suspend fun sendText(text: String) {
|
||||||
ensureOpen()
|
ensureOpen()
|
||||||
try {
|
session.send(text)
|
||||||
session.send(text)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
release()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendBytes(data: ByteArray) {
|
override suspend fun sendBytes(data: ByteArray) {
|
||||||
ensureOpen()
|
ensureOpen()
|
||||||
try {
|
session.send(data)
|
||||||
session.send(data)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
release()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun receive(): LyngWsMessage? {
|
override suspend fun receive(): LyngWsMessage? {
|
||||||
@ -89,17 +68,14 @@ private class KtorLyngWsSession(
|
|||||||
val frame = try {
|
val frame = try {
|
||||||
session.incoming.receive()
|
session.incoming.receive()
|
||||||
} catch (_: ClosedReceiveChannelException) {
|
} catch (_: ClosedReceiveChannelException) {
|
||||||
release()
|
closed = true
|
||||||
return null
|
return null
|
||||||
} catch (e: Throwable) {
|
|
||||||
release()
|
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
return when (frame) {
|
return when (frame) {
|
||||||
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
|
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
|
||||||
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
|
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
|
||||||
is Frame.Close -> {
|
is Frame.Close -> {
|
||||||
release()
|
closed = true
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
else -> receive()
|
else -> receive()
|
||||||
@ -108,20 +84,11 @@ private class KtorLyngWsSession(
|
|||||||
|
|
||||||
override suspend fun close(code: Int, reason: String) {
|
override suspend fun close(code: Int, reason: String) {
|
||||||
if (closed) return
|
if (closed) return
|
||||||
try {
|
closed = true
|
||||||
session.close(CloseReason(code.toShort(), reason))
|
session.close(CloseReason(code.toShort(), reason))
|
||||||
} finally {
|
|
||||||
release()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureOpen() {
|
private fun ensureOpen() {
|
||||||
if (closed) throw IllegalStateException("websocket session is closed")
|
if (closed) throw IllegalStateException("websocket session is closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun release() {
|
|
||||||
if (closed) return
|
|
||||||
closed = true
|
|
||||||
ownedClient?.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,392 +0,0 @@
|
|||||||
package net.sergeych.lyngio.ws
|
|
||||||
|
|
||||||
import io.ktor.http.Url
|
|
||||||
import io.ktor.http.URLProtocol
|
|
||||||
import net.sergeych.lyngio.net.LyngNetEngine
|
|
||||||
import net.sergeych.lyngio.net.LyngTcpSocket
|
|
||||||
import net.sergeych.lyngio.net.getSystemNetEngine
|
|
||||||
import net.sergeych.mp_tools.encodeToBase64
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
internal fun createSocketWsEngine(
|
|
||||||
secureFallback: LyngWsEngine = UnsupportedLyngWsEngine,
|
|
||||||
): LyngWsEngine = SocketLyngWsEngine(getSystemNetEngine(), secureFallback)
|
|
||||||
|
|
||||||
private class SocketLyngWsEngine(
|
|
||||||
private val netEngine: LyngNetEngine,
|
|
||||||
private val secureFallback: LyngWsEngine,
|
|
||||||
) : LyngWsEngine {
|
|
||||||
override val isSupported: Boolean
|
|
||||||
get() = (netEngine.isSupported && netEngine.isTcpAvailable) || secureFallback.isSupported
|
|
||||||
|
|
||||||
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
|
||||||
val parsedUrl = Url(url)
|
|
||||||
return when (parsedUrl.protocol.name.lowercase()) {
|
|
||||||
URLProtocol.WS.name.lowercase() -> connectPlain(parsedUrl, headers)
|
|
||||||
URLProtocol.WSS.name.lowercase() -> secureFallback.connect(url, headers)
|
|
||||||
else -> throw UnsupportedOperationException("Unsupported websocket scheme: ${parsedUrl.protocol.name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun connectPlain(url: Url, headers: Map<String, String>): LyngWsSession {
|
|
||||||
if (!netEngine.isSupported || !netEngine.isTcpAvailable) {
|
|
||||||
throw UnsupportedOperationException("WebSocket client is not supported on this runtime")
|
|
||||||
}
|
|
||||||
val socket = netEngine.tcpConnect(url.host, url.port, timeoutMillis = null, noDelay = true)
|
|
||||||
try {
|
|
||||||
val key = randomBytes(16).encodeToBase64()
|
|
||||||
val requestPath = buildRequestPath(url)
|
|
||||||
val hostHeader = buildHostHeader(url)
|
|
||||||
val request = buildString {
|
|
||||||
append("GET ").append(requestPath).append(" HTTP/1.1\r\n")
|
|
||||||
append("Host: ").append(hostHeader).append("\r\n")
|
|
||||||
append("Upgrade: websocket\r\n")
|
|
||||||
append("Connection: Upgrade\r\n")
|
|
||||||
append("Sec-WebSocket-Key: ").append(key).append("\r\n")
|
|
||||||
append("Sec-WebSocket-Version: 13\r\n")
|
|
||||||
headers.forEach { (name, value) ->
|
|
||||||
append(name).append(": ").append(value).append("\r\n")
|
|
||||||
}
|
|
||||||
append("\r\n")
|
|
||||||
}
|
|
||||||
socket.writeUtf8(request)
|
|
||||||
socket.flush()
|
|
||||||
validateHandshake(socket, key)
|
|
||||||
return SocketLyngWsSession(url.toString(), socket)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
socket.close()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SocketLyngWsSession(
|
|
||||||
private val targetUrl: String,
|
|
||||||
private val socket: LyngTcpSocket,
|
|
||||||
) : LyngWsSession {
|
|
||||||
private var closed = false
|
|
||||||
private var closeSent = false
|
|
||||||
private var pending = ByteArray(0)
|
|
||||||
private var fragmentedOpcode: Int? = null
|
|
||||||
private var fragmentedPayload = ByteArray(0)
|
|
||||||
|
|
||||||
override fun isOpen(): Boolean = !closed && socket.isOpen()
|
|
||||||
|
|
||||||
override fun url(): String = targetUrl
|
|
||||||
|
|
||||||
override suspend fun sendText(text: String) {
|
|
||||||
ensureOpen()
|
|
||||||
sendFrame(OPCODE_TEXT, text.encodeToByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun sendBytes(data: ByteArray) {
|
|
||||||
ensureOpen()
|
|
||||||
sendFrame(OPCODE_BINARY, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun receive(): LyngWsMessage? {
|
|
||||||
if (closed) return null
|
|
||||||
while (true) {
|
|
||||||
val frame = readFrame() ?: run {
|
|
||||||
release()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
when (frame.opcode) {
|
|
||||||
OPCODE_CONTINUATION -> {
|
|
||||||
val opcode = fragmentedOpcode ?: throw IllegalStateException("unexpected websocket continuation frame")
|
|
||||||
fragmentedPayload += frame.payload
|
|
||||||
if (frame.fin) {
|
|
||||||
val payload = fragmentedPayload
|
|
||||||
fragmentedOpcode = null
|
|
||||||
fragmentedPayload = ByteArray(0)
|
|
||||||
return payload.toMessage(opcode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OPCODE_TEXT, OPCODE_BINARY -> {
|
|
||||||
if (frame.fin) return frame.payload.toMessage(frame.opcode)
|
|
||||||
fragmentedOpcode = frame.opcode
|
|
||||||
fragmentedPayload = frame.payload
|
|
||||||
}
|
|
||||||
OPCODE_CLOSE -> {
|
|
||||||
if (!closeSent) {
|
|
||||||
sendFrame(OPCODE_CLOSE, frame.payload)
|
|
||||||
closeSent = true
|
|
||||||
}
|
|
||||||
release()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
OPCODE_PING -> sendFrame(OPCODE_PONG, frame.payload)
|
|
||||||
OPCODE_PONG -> Unit
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun close(code: Int, reason: String) {
|
|
||||||
if (closed) return
|
|
||||||
val reasonBytes = reason.encodeToByteArray()
|
|
||||||
val payload = ByteArray(reasonBytes.size + 2)
|
|
||||||
payload[0] = (code shr 8).toByte()
|
|
||||||
payload[1] = code.toByte()
|
|
||||||
reasonBytes.copyInto(payload, destinationOffset = 2)
|
|
||||||
try {
|
|
||||||
if (!closeSent) {
|
|
||||||
sendFrame(OPCODE_CLOSE, payload)
|
|
||||||
closeSent = true
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendFrame(opcode: Int, payload: ByteArray) {
|
|
||||||
ensureOpen()
|
|
||||||
val header = buildFrameHeader(opcode, payload.size, masked = true)
|
|
||||||
val mask = randomBytes(4)
|
|
||||||
val maskedPayload = payload.copyOf()
|
|
||||||
maskedPayload.indices.forEach { index ->
|
|
||||||
maskedPayload[index] = (maskedPayload[index].toInt() xor mask[index % mask.size].toInt()).toByte()
|
|
||||||
}
|
|
||||||
socket.write(header + mask + maskedPayload)
|
|
||||||
socket.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun readFrame(): WsFrame? {
|
|
||||||
val head = readExact(2) ?: return null
|
|
||||||
val fin = (head[0].toInt() and 0x80) != 0
|
|
||||||
val opcode = head[0].toInt() and 0x0f
|
|
||||||
val masked = (head[1].toInt() and 0x80) != 0
|
|
||||||
val payloadLength = when (val lengthCode = head[1].toInt() and 0x7f) {
|
|
||||||
126 -> {
|
|
||||||
val extended = readExact(2) ?: return null
|
|
||||||
((extended[0].toInt() and 0xff) shl 8) or (extended[1].toInt() and 0xff)
|
|
||||||
}
|
|
||||||
127 -> {
|
|
||||||
val extended = readExact(8) ?: return null
|
|
||||||
var acc = 0L
|
|
||||||
extended.forEach { byte ->
|
|
||||||
acc = (acc shl 8) or (byte.toInt() and 0xff).toLong()
|
|
||||||
}
|
|
||||||
require(acc <= Int.MAX_VALUE.toLong()) { "websocket frame is too large" }
|
|
||||||
acc.toInt()
|
|
||||||
}
|
|
||||||
else -> lengthCode
|
|
||||||
}
|
|
||||||
val mask = if (masked) readExact(4) ?: return null else null
|
|
||||||
val payload = if (payloadLength > 0) readExact(payloadLength) ?: return null else ByteArray(0)
|
|
||||||
if (mask != null) {
|
|
||||||
payload.indices.forEach { index ->
|
|
||||||
payload[index] = (payload[index].toInt() xor mask[index % mask.size].toInt()).toByte()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return WsFrame(fin = fin, opcode = opcode, payload = payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun readExact(byteCount: Int): ByteArray? {
|
|
||||||
while (pending.size < byteCount) {
|
|
||||||
val chunk = socket.read(maxOf(4096, byteCount - pending.size)) ?: break
|
|
||||||
if (chunk.isEmpty()) break
|
|
||||||
pending += chunk
|
|
||||||
}
|
|
||||||
if (pending.size < byteCount) return null
|
|
||||||
val result = pending.copyOfRange(0, byteCount)
|
|
||||||
pending = pending.copyOfRange(byteCount, pending.size)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun release() {
|
|
||||||
if (closed) return
|
|
||||||
closed = true
|
|
||||||
socket.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureOpen() {
|
|
||||||
if (closed || !socket.isOpen()) throw IllegalStateException("websocket session is closed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class WsFrame(
|
|
||||||
val fin: Boolean,
|
|
||||||
val opcode: Int,
|
|
||||||
val payload: ByteArray,
|
|
||||||
)
|
|
||||||
|
|
||||||
private suspend fun validateHandshake(socket: LyngTcpSocket, key: String) {
|
|
||||||
val statusLine = socket.readLine() ?: error("websocket handshake failed: missing response status")
|
|
||||||
require(statusLine.startsWith("HTTP/1.1 101") || statusLine.startsWith("HTTP/1.0 101")) {
|
|
||||||
"websocket handshake failed: $statusLine"
|
|
||||||
}
|
|
||||||
val headers = linkedMapOf<String, String>()
|
|
||||||
while (true) {
|
|
||||||
val line = socket.readLine() ?: error("websocket handshake failed: unexpected EOF")
|
|
||||||
if (line.isBlank()) break
|
|
||||||
val colonAt = line.indexOf(':')
|
|
||||||
require(colonAt > 0) { "invalid websocket header: $line" }
|
|
||||||
val name = line.substring(0, colonAt).trim().lowercase()
|
|
||||||
val value = line.substring(colonAt + 1).trim()
|
|
||||||
headers[name] = value
|
|
||||||
}
|
|
||||||
require(headers["upgrade"]?.lowercase() == "websocket") { "websocket handshake failed: missing Upgrade header" }
|
|
||||||
require(headers["connection"]?.lowercase()?.contains("upgrade") == true) {
|
|
||||||
"websocket handshake failed: missing Connection header"
|
|
||||||
}
|
|
||||||
require(headers["sec-websocket-accept"] == websocketAcceptKey(key)) {
|
|
||||||
"websocket handshake failed: invalid Sec-WebSocket-Accept"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun websocketAcceptKey(key: String): String =
|
|
||||||
sha1((key + WS_GUID).encodeToByteArray()).encodeToBase64()
|
|
||||||
|
|
||||||
private fun buildRequestPath(url: Url): String {
|
|
||||||
val path = url.encodedPath.ifEmpty { "/" }
|
|
||||||
val query = url.encodedQuery
|
|
||||||
return if (query.isBlank()) path else "$path?$query"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildHostHeader(url: Url): String {
|
|
||||||
val host = if (':' in url.host && !url.host.startsWith("[")) "[${url.host}]" else url.host
|
|
||||||
return if (url.port == url.protocol.defaultPort) host else "$host:${url.port}"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildFrameHeader(opcode: Int, payloadSize: Int, masked: Boolean): ByteArray {
|
|
||||||
require(payloadSize >= 0) { "payload size must be non-negative" }
|
|
||||||
val firstByte = (0x80 or (opcode and 0x0f)).toByte()
|
|
||||||
val maskBit = if (masked) 0x80 else 0
|
|
||||||
return when {
|
|
||||||
payloadSize <= 125 -> byteArrayOf(firstByte, (maskBit or payloadSize).toByte())
|
|
||||||
payloadSize <= 0xffff -> byteArrayOf(
|
|
||||||
firstByte,
|
|
||||||
(maskBit or 126).toByte(),
|
|
||||||
((payloadSize ushr 8) and 0xff).toByte(),
|
|
||||||
(payloadSize and 0xff).toByte(),
|
|
||||||
)
|
|
||||||
else -> byteArrayOf(
|
|
||||||
firstByte,
|
|
||||||
(maskBit or 127).toByte(),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
((payloadSize ushr 24) and 0xff).toByte(),
|
|
||||||
((payloadSize ushr 16) and 0xff).toByte(),
|
|
||||||
((payloadSize ushr 8) and 0xff).toByte(),
|
|
||||||
(payloadSize and 0xff).toByte(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ByteArray.toMessage(opcode: Int): LyngWsMessage = when (opcode) {
|
|
||||||
OPCODE_TEXT -> LyngWsMessage(isText = true, text = decodeToString())
|
|
||||||
OPCODE_BINARY -> LyngWsMessage(isText = false, data = copyOf())
|
|
||||||
else -> throw IllegalStateException("unsupported websocket opcode: $opcode")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun randomBytes(size: Int): ByteArray = ByteArray(size).also(Random.Default::nextBytes)
|
|
||||||
|
|
||||||
private fun sha1(input: ByteArray): ByteArray {
|
|
||||||
var h0 = 0x67452301
|
|
||||||
var h1 = 0xEFCDAB89.toInt()
|
|
||||||
var h2 = 0x98BADCFE.toInt()
|
|
||||||
var h3 = 0x10325476
|
|
||||||
var h4 = 0xC3D2E1F0.toInt()
|
|
||||||
|
|
||||||
val msgLen = input.size
|
|
||||||
val bitLen = msgLen.toLong() * 8L
|
|
||||||
val totalLen = ((msgLen + 1 + 8 + 63) / 64) * 64
|
|
||||||
val padded = ByteArray(totalLen).also { buf ->
|
|
||||||
input.copyInto(buf)
|
|
||||||
buf[msgLen] = 0x80.toByte()
|
|
||||||
for (i in 0..7) {
|
|
||||||
buf[totalLen - 8 + i] = ((bitLen ushr (56 - i * 8)) and 0xff).toByte()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val words = IntArray(80)
|
|
||||||
var blockStart = 0
|
|
||||||
while (blockStart < padded.size) {
|
|
||||||
for (i in 0..15) {
|
|
||||||
val off = blockStart + i * 4
|
|
||||||
words[i] = ((padded[off].toInt() and 0xff) shl 24) or
|
|
||||||
((padded[off + 1].toInt() and 0xff) shl 16) or
|
|
||||||
((padded[off + 2].toInt() and 0xff) shl 8) or
|
|
||||||
(padded[off + 3].toInt() and 0xff)
|
|
||||||
}
|
|
||||||
for (i in 16..79) {
|
|
||||||
val mixed = words[i - 3] xor words[i - 8] xor words[i - 14] xor words[i - 16]
|
|
||||||
words[i] = (mixed shl 1) or (mixed ushr 31)
|
|
||||||
}
|
|
||||||
|
|
||||||
var a = h0
|
|
||||||
var b = h1
|
|
||||||
var c = h2
|
|
||||||
var d = h3
|
|
||||||
var e = h4
|
|
||||||
|
|
||||||
for (i in 0..19) {
|
|
||||||
val f = (b and c) or (b.inv() and d)
|
|
||||||
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x5A827999 + words[i]
|
|
||||||
e = d
|
|
||||||
d = c
|
|
||||||
c = (b shl 30) or (b ushr 2)
|
|
||||||
b = a
|
|
||||||
a = temp
|
|
||||||
}
|
|
||||||
for (i in 20..39) {
|
|
||||||
val f = b xor c xor d
|
|
||||||
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x6ED9EBA1 + words[i]
|
|
||||||
e = d
|
|
||||||
d = c
|
|
||||||
c = (b shl 30) or (b ushr 2)
|
|
||||||
b = a
|
|
||||||
a = temp
|
|
||||||
}
|
|
||||||
for (i in 40..59) {
|
|
||||||
val f = (b and c) or (b and d) or (c and d)
|
|
||||||
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0x8F1BBCDC.toInt() + words[i]
|
|
||||||
e = d
|
|
||||||
d = c
|
|
||||||
c = (b shl 30) or (b ushr 2)
|
|
||||||
b = a
|
|
||||||
a = temp
|
|
||||||
}
|
|
||||||
for (i in 60..79) {
|
|
||||||
val f = b xor c xor d
|
|
||||||
val temp = ((a shl 5) or (a ushr 27)) + f + e + 0xCA62C1D6.toInt() + words[i]
|
|
||||||
e = d
|
|
||||||
d = c
|
|
||||||
c = (b shl 30) or (b ushr 2)
|
|
||||||
b = a
|
|
||||||
a = temp
|
|
||||||
}
|
|
||||||
|
|
||||||
h0 += a
|
|
||||||
h1 += b
|
|
||||||
h2 += c
|
|
||||||
h3 += d
|
|
||||||
h4 += e
|
|
||||||
blockStart += 64
|
|
||||||
}
|
|
||||||
|
|
||||||
return ByteArray(20).also { out ->
|
|
||||||
fun putInt(offset: Int, value: Int) {
|
|
||||||
out[offset] = (value ushr 24).toByte()
|
|
||||||
out[offset + 1] = (value ushr 16).toByte()
|
|
||||||
out[offset + 2] = (value ushr 8).toByte()
|
|
||||||
out[offset + 3] = value.toByte()
|
|
||||||
}
|
|
||||||
putInt(0, h0)
|
|
||||||
putInt(4, h1)
|
|
||||||
putInt(8, h2)
|
|
||||||
putInt(12, h3)
|
|
||||||
putInt(16, h4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
||||||
private const val OPCODE_CONTINUATION = 0x0
|
|
||||||
private const val OPCODE_TEXT = 0x1
|
|
||||||
private const val OPCODE_BINARY = 0x2
|
|
||||||
private const val OPCODE_CLOSE = 0x8
|
|
||||||
private const val OPCODE_PING = 0x9
|
|
||||||
private const val OPCODE_PONG = 0xA
|
|
||||||
@ -112,34 +112,6 @@ class LyngJdbcModuleTest {
|
|||||||
assertEquals(3L, result.value)
|
assertEquals(3L, result.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testMaterializedRowsListCanBeReturnedFromTransaction() = runTest {
|
|
||||||
val scope = Script.newScope()
|
|
||||||
createJdbcModule(scope.importManager)
|
|
||||||
val jdbcModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.jdbc")
|
|
||||||
val db = jdbcModule.callFn("openH2", ObjString("mem:rows_return_${System.nanoTime()};DB_CLOSE_DELAY=-1"))
|
|
||||||
|
|
||||||
val rows = db.invokeInstanceMethod(
|
|
||||||
scope,
|
|
||||||
"transaction",
|
|
||||||
ObjExternCallable.fromBridge {
|
|
||||||
val tx = requiredArg<Obj>(0)
|
|
||||||
tx.invokeInstanceMethod(
|
|
||||||
requireScope(),
|
|
||||||
"execute",
|
|
||||||
ObjString("create table person(id bigint auto_increment primary key, name varchar(120) not null)")
|
|
||||||
)
|
|
||||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into person(name) values(?)"), ObjString("Ada"))
|
|
||||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into person(name) values(?)"), ObjString("Linus"))
|
|
||||||
tx.invokeInstanceMethod(requireScope(), "select", ObjString("select name from person order by id"))
|
|
||||||
.invokeInstanceMethod(requireScope(), "toList")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals("Ada", (rows.getAt(scope, ObjInt.Zero).getAt(scope, ObjString("name")) as ObjString).value)
|
|
||||||
assertEquals("Linus", (rows.getAt(scope, ObjInt.of(1)).getAt(scope, ObjString("name")) as ObjString).value)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun scalarSelect(scope: net.sergeych.lyng.Scope, db: Obj, sql: String): Long {
|
private suspend fun scalarSelect(scope: net.sergeych.lyng.Scope, db: Obj, sql: String): Long {
|
||||||
val result = db.invokeInstanceMethod(
|
val result = db.invokeInstanceMethod(
|
||||||
scope,
|
scope,
|
||||||
|
|||||||
@ -250,7 +250,7 @@ class LyngSqliteModuleTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMaterializedRowSurvivesAfterTransactionEnds() = runTest {
|
fun testRowFailsAfterTransactionEnds() = runTest {
|
||||||
val scope = Script.newScope()
|
val scope = Script.newScope()
|
||||||
val db = openMemoryDb(scope)
|
val db = openMemoryDb(scope)
|
||||||
var leakedRow: Obj = ObjNull
|
var leakedRow: Obj = ObjNull
|
||||||
@ -265,34 +265,12 @@ class LyngSqliteModuleTest {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val answer = leakedRow.getAt(scope, ObjString("answer")) as ObjInt
|
val error = assertFailsWith<ExecutionError> {
|
||||||
assertEquals(42L, answer.value)
|
leakedRow.getAt(scope, ObjString("answer"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||||
fun testMaterializedRowsListCanBeReturnedFromTransaction() = runTest {
|
assertTrue(error.errorMessage.contains("transaction is active"), error.errorMessage)
|
||||||
val scope = Script.newScope()
|
|
||||||
val db = openMemoryDb(scope)
|
|
||||||
|
|
||||||
val rows = db.invokeInstanceMethod(
|
|
||||||
scope,
|
|
||||||
"transaction",
|
|
||||||
ObjExternCallable.fromBridge {
|
|
||||||
val tx = requiredArg<Obj>(0)
|
|
||||||
tx.invokeInstanceMethod(
|
|
||||||
requireScope(),
|
|
||||||
"execute",
|
|
||||||
ObjString("create table items(id integer primary key autoincrement, name text not null)")
|
|
||||||
)
|
|
||||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into items(name) values(?)"), ObjString("alpha"))
|
|
||||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into items(name) values(?)"), ObjString("beta"))
|
|
||||||
tx.invokeInstanceMethod(requireScope(), "select", ObjString("select name from items order by id"))
|
|
||||||
.invokeInstanceMethod(requireScope(), "toList")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals("alpha", stringValue(scope, rows.getAt(scope, ObjInt.Zero).getAt(scope, ObjString("name"))))
|
|
||||||
assertEquals("beta", stringValue(scope, rows.getAt(scope, ObjInt.of(1)).getAt(scope, ObjString("name"))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -2,5 +2,4 @@ package net.sergeych.lyngio.ws
|
|||||||
|
|
||||||
import io.ktor.client.engine.curl.Curl
|
import io.ktor.client.engine.curl.Curl
|
||||||
|
|
||||||
actual fun getSystemWsEngine(): LyngWsEngine =
|
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(Curl)
|
||||||
createSocketWsEngine(secureFallback = createKtorWsEngine(Curl, shareClient = false))
|
|
||||||
|
|||||||
@ -186,7 +186,7 @@ class LyngSqliteModuleNativeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMaterializedRowSurvivesAfterTransactionEnds() = runTest {
|
fun testRowFailsAfterTransactionEnds() = runTest {
|
||||||
val scope = Script.newScope()
|
val scope = Script.newScope()
|
||||||
val db = openMemoryDb(scope)
|
val db = openMemoryDb(scope)
|
||||||
var leakedRow: Obj = ObjNull
|
var leakedRow: Obj = ObjNull
|
||||||
@ -201,34 +201,12 @@ class LyngSqliteModuleNativeTest {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val answer = leakedRow.getAt(scope, ObjString("answer")) as ObjInt
|
val error = assertFailsWith<ExecutionError> {
|
||||||
assertEquals(42L, answer.value)
|
leakedRow.getAt(scope, ObjString("answer"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||||
fun testMaterializedRowsListCanBeReturnedFromTransaction() = runTest {
|
assertTrue(error.errorMessage.contains("transaction is active"), error.errorMessage)
|
||||||
val scope = Script.newScope()
|
|
||||||
val db = openMemoryDb(scope)
|
|
||||||
|
|
||||||
val rows = db.invokeInstanceMethod(
|
|
||||||
scope,
|
|
||||||
"transaction",
|
|
||||||
ObjExternCallable.fromBridge {
|
|
||||||
val tx = requiredArg<Obj>(0)
|
|
||||||
tx.invokeInstanceMethod(
|
|
||||||
requireScope(),
|
|
||||||
"execute",
|
|
||||||
ObjString("create table items(id integer primary key autoincrement, name text not null)")
|
|
||||||
)
|
|
||||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into items(name) values(?)"), ObjString("alpha"))
|
|
||||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("insert into items(name) values(?)"), ObjString("beta"))
|
|
||||||
tx.invokeInstanceMethod(requireScope(), "select", ObjString("select name from items order by id"))
|
|
||||||
.invokeInstanceMethod(requireScope(), "toList")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals("alpha", stringValue(scope, rows.getAt(scope, ObjInt.Zero).getAt(scope, ObjString("name"))))
|
|
||||||
assertEquals("beta", stringValue(scope, rows.getAt(scope, ObjInt.of(1)).getAt(scope, ObjString("name"))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -44,11 +44,11 @@ extern class SqlRow {
|
|||||||
- iteration to the end or canceled iteration should close the underlying
|
- iteration to the end or canceled iteration should close the underlying
|
||||||
resources automatically
|
resources automatically
|
||||||
- using the result set after its transaction ends is invalid
|
- using the result set after its transaction ends is invalid
|
||||||
- rows obtained from the result set stay usable after the owning
|
- rows obtained from the result set are also invalid after the owning
|
||||||
transaction ends once they have been materialized
|
transaction ends, even if the implementation had already buffered them
|
||||||
|
|
||||||
Calling `toList()` while the transaction is active is the normal way to
|
If user code wants row data to survive independently, it should copy the
|
||||||
detach rows for use after the transaction block returns.
|
values it needs into ordinary Lyng objects while the transaction is active.
|
||||||
*/
|
*/
|
||||||
extern class ResultSet : Iterable<SqlRow> {
|
extern class ResultSet : Iterable<SqlRow> {
|
||||||
/*
|
/*
|
||||||
@ -84,8 +84,7 @@ extern class ExecutionResult {
|
|||||||
other result set.
|
other result set.
|
||||||
|
|
||||||
If the statement produced no generated values, the returned result set
|
If the statement produced no generated values, the returned result set
|
||||||
is empty. Call `toList()` if generated-key rows must outlive the
|
is empty.
|
||||||
transaction.
|
|
||||||
*/
|
*/
|
||||||
fun getGeneratedKeys(): ResultSet
|
fun getGeneratedKeys(): ResultSet
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10679,12 +10679,8 @@ class Compiler(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (getter != null || setter != null) {
|
if (getter != null || setter != null) {
|
||||||
val prop = if (actualExtern) {
|
val prop = ObjProperty(name, getter, setter)
|
||||||
ObjProperty(name, null, null)
|
val initStmt = if (!isAbstract) {
|
||||||
} else {
|
|
||||||
ObjProperty(name, getter, setter)
|
|
||||||
}
|
|
||||||
val initStmt = if (!isAbstract && !actualExtern) {
|
|
||||||
val initStatement = InstancePropertyInitStatement(
|
val initStatement = InstancePropertyInitStatement(
|
||||||
storageName = storageName,
|
storageName = storageName,
|
||||||
isMutable = isMutable,
|
isMutable = isMutable,
|
||||||
|
|||||||
@ -1,62 +0,0 @@
|
|||||||
package net.sergeych.lyng.obj
|
|
||||||
|
|
||||||
import net.sergeych.lyng.ScopeFacade
|
|
||||||
import net.sergeych.lyng.requiredArg
|
|
||||||
|
|
||||||
internal fun ObjClass.bindClassFn(name: String, code: suspend ScopeFacade.() -> Obj) {
|
|
||||||
val callable = ObjExternCallable.fromBridge { code() }
|
|
||||||
val memberRecord = members[name]
|
|
||||||
val classScopeRecord = classScope?.objects?.get(name)
|
|
||||||
if (memberRecord != null) {
|
|
||||||
val methodId = ensureMethodIdForBridge(name, memberRecord)
|
|
||||||
val newRecord = memberRecord.copy(
|
|
||||||
value = callable,
|
|
||||||
type = ObjRecord.Type.Fun,
|
|
||||||
methodId = methodId,
|
|
||||||
isAbstract = false,
|
|
||||||
)
|
|
||||||
replaceMemberForBridge(name, newRecord)
|
|
||||||
if (classScopeRecord != null) {
|
|
||||||
replaceClassScopeMemberForBridge(name, newRecord)
|
|
||||||
}
|
|
||||||
} else if (classScopeRecord != null) {
|
|
||||||
val methodId = ensureMethodIdForBridge(name, classScopeRecord)
|
|
||||||
replaceClassScopeMemberForBridge(
|
|
||||||
name,
|
|
||||||
classScopeRecord.copy(
|
|
||||||
value = callable,
|
|
||||||
type = ObjRecord.Type.Fun,
|
|
||||||
methodId = methodId,
|
|
||||||
isAbstract = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
addClassFn(name, code = code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun ObjClass.bindProperty(
|
|
||||||
name: String,
|
|
||||||
getter: (suspend ScopeFacade.() -> Obj)? = null,
|
|
||||||
setter: (suspend ScopeFacade.(Obj) -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
val g = getter?.let { ObjExternCallable.fromBridge { it() } }
|
|
||||||
val s = setter?.let { ObjExternCallable.fromBridge { it(requiredArg(0)); ObjVoid } }
|
|
||||||
val prop = ObjProperty(name, g, s)
|
|
||||||
val existing = members[name]
|
|
||||||
if (existing != null) {
|
|
||||||
val newRecord = existing.copy(
|
|
||||||
value = prop,
|
|
||||||
type = ObjRecord.Type.Property,
|
|
||||||
methodId = ensureMethodIdForBridge(name, existing),
|
|
||||||
fieldId = null,
|
|
||||||
isAbstract = false,
|
|
||||||
)
|
|
||||||
replaceMemberForBridge(name, newRecord)
|
|
||||||
if (classScope?.objects?.containsKey(name) == true) {
|
|
||||||
replaceClassScopeMemberForBridge(name, newRecord)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addProperty(name, getter = getter, setter = setter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -18,10 +18,13 @@
|
|||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
import net.sergeych.lyng.*
|
import net.sergeych.lyng.*
|
||||||
|
import net.sergeych.lyng.miniast.addPropertyDoc
|
||||||
|
import net.sergeych.lyng.miniast.type
|
||||||
import net.sergeych.lyng.requiredArg
|
import net.sergeych.lyng.requiredArg
|
||||||
|
|
||||||
object ObjComplexSupport {
|
object ObjComplexSupport {
|
||||||
private object BoundMarker
|
private object BoundMarker
|
||||||
|
private val complexTypeDecl = TypeDecl.Simple("lyng.complex.Complex", false)
|
||||||
|
|
||||||
suspend fun bindTo(module: ModuleScope) {
|
suspend fun bindTo(module: ModuleScope) {
|
||||||
val complexClass = module.requireClass("Complex")
|
val complexClass = module.requireClass("Complex")
|
||||||
@ -30,20 +33,36 @@ object ObjComplexSupport {
|
|||||||
|
|
||||||
val decimalModule = module.currentImportProvider.createModuleScope(module.pos, "lyng.decimal")
|
val decimalModule = module.currentImportProvider.createModuleScope(module.pos, "lyng.decimal")
|
||||||
val decimalClass = decimalModule.requireClass("Decimal")
|
val decimalClass = decimalModule.requireClass("Decimal")
|
||||||
decimalClass.bindProperty("re", getter = {
|
|
||||||
newComplex(
|
decimalClass.addPropertyDoc(
|
||||||
complexClass,
|
name = "re",
|
||||||
decimalToReal(thisObj),
|
doc = "Convert this Decimal to a Complex with zero imaginary part.",
|
||||||
0.0
|
type = type("lyng.complex.Complex"),
|
||||||
)
|
moduleName = "lyng.complex",
|
||||||
})
|
getter = {
|
||||||
decimalClass.bindProperty("i", getter = {
|
newComplex(
|
||||||
newComplex(
|
complexClass,
|
||||||
complexClass,
|
decimalToReal(thisObj),
|
||||||
0.0,
|
0.0
|
||||||
decimalToReal(thisObj)
|
)
|
||||||
)
|
}
|
||||||
})
|
)
|
||||||
|
decimalClass.members["re"] = decimalClass.members.getValue("re").copy(typeDecl = complexTypeDecl)
|
||||||
|
|
||||||
|
decimalClass.addPropertyDoc(
|
||||||
|
name = "i",
|
||||||
|
doc = "Convert this Decimal to a pure imaginary Complex after rounding to Real.",
|
||||||
|
type = type("lyng.complex.Complex"),
|
||||||
|
moduleName = "lyng.complex",
|
||||||
|
getter = {
|
||||||
|
newComplex(
|
||||||
|
complexClass,
|
||||||
|
0.0,
|
||||||
|
decimalToReal(thisObj)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
decimalClass.members["i"] = decimalClass.members.getValue("i").copy(typeDecl = complexTypeDecl)
|
||||||
|
|
||||||
OperatorInteropRegistry.register(
|
OperatorInteropRegistry.register(
|
||||||
leftClass = decimalClass,
|
leftClass = decimalClass,
|
||||||
|
|||||||
@ -106,15 +106,15 @@ object ObjDecimalSupport {
|
|||||||
decimalClass.addFn("toStringExpanded") {
|
decimalClass.addFn("toStringExpanded") {
|
||||||
ObjString(valueOf(thisObj).toStringExpanded())
|
ObjString(valueOf(thisObj).toStringExpanded())
|
||||||
}
|
}
|
||||||
decimalClass.bindClassFn("fromInt") {
|
decimalClass.addClassFn("fromInt") {
|
||||||
val value = requiredArg<ObjInt>(0).value
|
val value = requiredArg<ObjInt>(0).value
|
||||||
newInstance(decimalClass, IonBigDecimal.fromLong(value))
|
newInstance(decimalClass, IonBigDecimal.fromLong(value))
|
||||||
}
|
}
|
||||||
decimalClass.bindClassFn("fromReal") {
|
decimalClass.addClassFn("fromReal") {
|
||||||
val value = requiredArg<ObjReal>(0).value
|
val value = requiredArg<ObjReal>(0).value
|
||||||
newInstanceFromFiniteReal(decimalClass, value)
|
newInstanceFromFiniteReal(decimalClass, value)
|
||||||
}
|
}
|
||||||
decimalClass.bindClassFn("fromString") {
|
decimalClass.addClassFn("fromString") {
|
||||||
val value = requiredArg<ObjString>(0).value
|
val value = requiredArg<ObjString>(0).value
|
||||||
try {
|
try {
|
||||||
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
|
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
|
||||||
|
|||||||
@ -53,10 +53,10 @@ object ObjMatrixSupport {
|
|||||||
}
|
}
|
||||||
hooks += { _, instance -> instance.kotlinInstanceData = defaultVector }
|
hooks += { _, instance -> instance.kotlinInstanceData = defaultVector }
|
||||||
|
|
||||||
vectorClass.bindProperty("size", getter = {
|
vectorClass.addProperty("size", getter = {
|
||||||
ObjInt.of(vectorOf(thisObj).size.toLong())
|
ObjInt.of(vectorOf(thisObj).size.toLong())
|
||||||
})
|
})
|
||||||
vectorClass.bindProperty("length", getter = {
|
vectorClass.addProperty("length", getter = {
|
||||||
ObjInt.of(vectorOf(thisObj).size.toLong())
|
ObjInt.of(vectorOf(thisObj).size.toLong())
|
||||||
})
|
})
|
||||||
vectorClass.addFn("toList") {
|
vectorClass.addFn("toList") {
|
||||||
@ -99,10 +99,10 @@ object ObjMatrixSupport {
|
|||||||
ObjInt.of(vectorOf(thisObj).compareTo(coerceVectorArg(requireScope(), args.firstAndOnly())).toLong())
|
ObjInt.of(vectorOf(thisObj).compareTo(coerceVectorArg(requireScope(), args.firstAndOnly())).toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
vectorClass.bindClassFn("fromList") {
|
vectorClass.addClassFn("fromList") {
|
||||||
newVector(vectorClass, parseVector(requireScope(), requiredArg(0)))
|
newVector(vectorClass, parseVector(requireScope(), requiredArg(0)))
|
||||||
}
|
}
|
||||||
vectorClass.bindClassFn("zeros") {
|
vectorClass.addClassFn("zeros") {
|
||||||
val size = requiredArg<ObjInt>(0).value.toInt()
|
val size = requiredArg<ObjInt>(0).value.toInt()
|
||||||
if (size <= 0) requireScope().raiseIllegalArgument("vector size must be positive")
|
if (size <= 0) requireScope().raiseIllegalArgument("vector size must be positive")
|
||||||
newVector(vectorClass, VectorData(DoubleArray(size)))
|
newVector(vectorClass, VectorData(DoubleArray(size)))
|
||||||
@ -119,13 +119,13 @@ object ObjMatrixSupport {
|
|||||||
}
|
}
|
||||||
hooks += { _, instance -> instance.kotlinInstanceData = defaultMatrix }
|
hooks += { _, instance -> instance.kotlinInstanceData = defaultMatrix }
|
||||||
|
|
||||||
matrixClass.bindProperty("rows", getter = {
|
matrixClass.addProperty("rows", getter = {
|
||||||
ObjInt.of(matrixOf(thisObj).rows.toLong())
|
ObjInt.of(matrixOf(thisObj).rows.toLong())
|
||||||
})
|
})
|
||||||
matrixClass.bindProperty("cols", getter = {
|
matrixClass.addProperty("cols", getter = {
|
||||||
ObjInt.of(matrixOf(thisObj).cols.toLong())
|
ObjInt.of(matrixOf(thisObj).cols.toLong())
|
||||||
})
|
})
|
||||||
matrixClass.bindProperty("shape", getter = {
|
matrixClass.addProperty("shape", getter = {
|
||||||
ObjList(
|
ObjList(
|
||||||
mutableListOf(
|
mutableListOf(
|
||||||
ObjInt.of(matrixOf(thisObj).rows.toLong()),
|
ObjInt.of(matrixOf(thisObj).rows.toLong()),
|
||||||
@ -133,7 +133,7 @@ object ObjMatrixSupport {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
matrixClass.bindProperty("isSquare", getter = {
|
matrixClass.addProperty("isSquare", getter = {
|
||||||
matrixOf(thisObj).isSquare.toObj()
|
matrixOf(thisObj).isSquare.toObj()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -208,17 +208,17 @@ object ObjMatrixSupport {
|
|||||||
ObjInt.of(matrixOf(thisObj).compareTo(coerceMatrixArg(requireScope(), args.firstAndOnly())).toLong())
|
ObjInt.of(matrixOf(thisObj).compareTo(coerceMatrixArg(requireScope(), args.firstAndOnly())).toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
matrixClass.bindClassFn("fromRows") {
|
matrixClass.addClassFn("fromRows") {
|
||||||
newMatrix(matrixClass, parseRows(requireScope(), requiredArg(0)))
|
newMatrix(matrixClass, parseRows(requireScope(), requiredArg(0)))
|
||||||
}
|
}
|
||||||
matrixClass.bindClassFn("zeros") {
|
matrixClass.addClassFn("zeros") {
|
||||||
val rows = requiredArg<ObjInt>(0).value.toInt()
|
val rows = requiredArg<ObjInt>(0).value.toInt()
|
||||||
val cols = requiredArg<ObjInt>(1).value.toInt()
|
val cols = requiredArg<ObjInt>(1).value.toInt()
|
||||||
if (rows <= 0) requireScope().raiseIllegalArgument("matrix must have at least one row")
|
if (rows <= 0) requireScope().raiseIllegalArgument("matrix must have at least one row")
|
||||||
if (cols <= 0) requireScope().raiseIllegalArgument("matrix must have at least one column")
|
if (cols <= 0) requireScope().raiseIllegalArgument("matrix must have at least one column")
|
||||||
newMatrix(matrixClass, MatrixData(rows, cols, DoubleArray(rows * cols)))
|
newMatrix(matrixClass, MatrixData(rows, cols, DoubleArray(rows * cols)))
|
||||||
}
|
}
|
||||||
matrixClass.bindClassFn("identity") {
|
matrixClass.addClassFn("identity") {
|
||||||
val size = requiredArg<ObjInt>(0).value.toInt()
|
val size = requiredArg<ObjInt>(0).value.toInt()
|
||||||
if (size <= 0) requireScope().raiseIllegalArgument("identity matrix size must be positive")
|
if (size <= 0) requireScope().raiseIllegalArgument("identity matrix size must be positive")
|
||||||
val values = DoubleArray(size * size)
|
val values = DoubleArray(size * size)
|
||||||
|
|||||||
@ -201,7 +201,14 @@ extern class Decimal() {
|
|||||||
*
|
*
|
||||||
* Contexts are dynamic and block-local. After the block finishes, the previous context is restored.
|
* Contexts are dynamic and block-local. After the block finishes, the previous context is restored.
|
||||||
*/
|
*/
|
||||||
extern fun withDecimalContext<T>(context: Object, block: ()->T): T
|
extern fun withDecimalContext<T>(context: DecimalContext, block: ()->T): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience overload for changing only precision.
|
||||||
|
*
|
||||||
|
* Equivalent to `withDecimalContext(DecimalContext(precision, DecimalRounding.HalfEven), block)`.
|
||||||
|
*/
|
||||||
|
extern fun withDecimalContext<T>(precision: Int, block: ()->T): T
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience overload for changing precision and rounding explicitly.
|
* Convenience overload for changing precision and rounding explicitly.
|
||||||
|
|||||||
@ -205,8 +205,10 @@ class DecimalModuleTest {
|
|||||||
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||||
assertEquals("0.3333333333", withDecimalContext(10) { (1.d / 3.d).toStringExpanded() })
|
assertEquals("0.3333333333", withDecimalContext(10) { (1.d / 3.d).toStringExpanded() })
|
||||||
assertEquals("0.666667", withDecimalContext(6) { ("2".d / 3.d).toStringExpanded() })
|
assertEquals("0.666667", withDecimalContext(6) { ("2".d / 3.d).toStringExpanded() })
|
||||||
|
assertEquals("0.666667", withDecimalContext(DecimalContext(6)) { ("2".d / 3.d).toStringExpanded() })
|
||||||
assertEquals("0.12", withDecimalContext(2) { (1.d / 8.d).toStringExpanded() })
|
assertEquals("0.12", withDecimalContext(2) { (1.d / 8.d).toStringExpanded() })
|
||||||
assertEquals("0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() })
|
assertEquals("0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("0.13", withDecimalContext(DecimalContext(2, DecimalRounding.HalfAwayFromZero)) { (1.d / 8.d).toStringExpanded() })
|
||||||
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
|
|||||||
@ -23,7 +23,7 @@ actual object PerfDefaults {
|
|||||||
|
|
||||||
actual val ARG_BUILDER: Boolean = true
|
actual val ARG_BUILDER: Boolean = true
|
||||||
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
|
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
|
||||||
actual val SCOPE_POOL: Boolean = false
|
actual val SCOPE_POOL: Boolean = true
|
||||||
|
|
||||||
actual val FIELD_PIC: Boolean = true
|
actual val FIELD_PIC: Boolean = true
|
||||||
actual val METHOD_PIC: Boolean = true
|
actual val METHOD_PIC: Boolean = true
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package lyng.complex
|
package lyng.complex
|
||||||
|
|
||||||
import lyng.decimal
|
|
||||||
import lyng.operators
|
import lyng.operators
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -113,10 +112,8 @@ fun cis(angle: Real): Complex = Complex.fromPolar(1.0, angle)
|
|||||||
|
|
||||||
val Int.re: Complex get() = Complex.fromInt(this)
|
val Int.re: Complex get() = Complex.fromInt(this)
|
||||||
val Real.re: Complex get() = Complex.fromReal(this)
|
val Real.re: Complex get() = Complex.fromReal(this)
|
||||||
val Decimal.re: Complex get() = Complex(this.toReal(), 0.0)
|
|
||||||
val Int.i: Complex get() = Complex.imaginary(this + 0.0)
|
val Int.i: Complex get() = Complex.imaginary(this + 0.0)
|
||||||
val Real.i: Complex get() = Complex.imaginary(this)
|
val Real.i: Complex get() = Complex.imaginary(this)
|
||||||
val Decimal.i: Complex get() = Complex(0.0, this.toReal())
|
|
||||||
|
|
||||||
OperatorInterop.register(
|
OperatorInterop.register(
|
||||||
Int,
|
Int,
|
||||||
|
|||||||
@ -6,11 +6,9 @@ type MatrixScalar = Real | Int
|
|||||||
extern class Vector() {
|
extern class Vector() {
|
||||||
/** Number of elements. */
|
/** Number of elements. */
|
||||||
val size: Int
|
val size: Int
|
||||||
get() = 0
|
|
||||||
|
|
||||||
/** Alias to `size`. */
|
/** Alias to `size`. */
|
||||||
val length: Int
|
val length: Int
|
||||||
get() = 0
|
|
||||||
|
|
||||||
/** Convert to a plain list. */
|
/** Convert to a plain list. */
|
||||||
extern fun toList(): List<Real>
|
extern fun toList(): List<Real>
|
||||||
@ -63,19 +61,15 @@ extern class Vector() {
|
|||||||
extern class Matrix() {
|
extern class Matrix() {
|
||||||
/** Number of rows. */
|
/** Number of rows. */
|
||||||
val rows: Int
|
val rows: Int
|
||||||
get() = 0
|
|
||||||
|
|
||||||
/** Number of columns. */
|
/** Number of columns. */
|
||||||
val cols: Int
|
val cols: Int
|
||||||
get() = 0
|
|
||||||
|
|
||||||
/** Two-element shape `[rows, cols]`. */
|
/** Two-element shape `[rows, cols]`. */
|
||||||
val shape: List<Int>
|
val shape: List<Int>
|
||||||
get() = []
|
|
||||||
|
|
||||||
/** Whether `rows == cols`. */
|
/** Whether `rows == cols`. */
|
||||||
val isSquare: Bool
|
val isSquare: Bool
|
||||||
get() = false
|
|
||||||
|
|
||||||
/** Element-wise addition. Shapes must match. */
|
/** Element-wise addition. Shapes must match. */
|
||||||
extern fun plus(other: Matrix): Matrix
|
extern fun plus(other: Matrix): Matrix
|
||||||
|
|||||||
@ -61,10 +61,10 @@ Notes:
|
|||||||
rather than silently degrading to some other visible type.
|
rather than silently degrading to some other visible type.
|
||||||
- `ResultSet` should stay iterable, but also expose `isEmpty()` for cheap
|
- `ResultSet` should stay iterable, but also expose `isEmpty()` for cheap
|
||||||
emptiness checks where possible and `size()` as a separate operation.
|
emptiness checks where possible and `size()` as a separate operation.
|
||||||
- `ResultSet` is valid only while the owning transaction is active.
|
- `ResultSet` and all `SqlRow` instances obtained from it are valid only while
|
||||||
- Materialized `SqlRow` values should be detached snapshots, so
|
the owning transaction is active. After transaction end, any further row or
|
||||||
`transaction { tx.select(...).toList() }` is a valid pattern and the rows
|
result-set access should fail with `SqlUsageException`, even if the provider
|
||||||
remain usable after transaction end.
|
had buffered data internally.
|
||||||
- Portable SQL parameter values should match the row conversion set: `null`,
|
- Portable SQL parameter values should match the row conversion set: `null`,
|
||||||
`Bool`, `Int`, `Double`, `Decimal`, `String`, `Buffer`,
|
`Bool`, `Int`, `Double`, `Decimal`, `String`, `Buffer`,
|
||||||
`Date`, `DateTime`, and `Instant`.
|
`Date`, `DateTime`, and `Instant`.
|
||||||
|
|||||||
@ -42,11 +42,11 @@ class SqlRow(
|
|||||||
- iteration to the end or canceled iteration should close the underlying
|
- iteration to the end or canceled iteration should close the underlying
|
||||||
resources automatically
|
resources automatically
|
||||||
- using the result set after its transaction ends is invalid
|
- using the result set after its transaction ends is invalid
|
||||||
- rows obtained from the result set stay usable after the owning
|
- rows obtained from the result set are also invalid after the owning
|
||||||
transaction ends once they have been materialized
|
transaction ends, even if the implementation had already buffered them
|
||||||
|
|
||||||
Calling [toList] while the transaction is active is the normal way to
|
If user code wants row data to survive independently, it should copy the
|
||||||
detach rows for use after the transaction block returns.
|
values it needs into ordinary Lyng objects while the transaction is active.
|
||||||
*/
|
*/
|
||||||
interface ResultSet : Iterable<SqlRow> {
|
interface ResultSet : Iterable<SqlRow> {
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -96,8 +96,8 @@ row-id behavior are all connection-local.
|
|||||||
The provider may stream rows or buffer them, but it must preserve the core
|
The provider may stream rows or buffer them, but it must preserve the core
|
||||||
contract:
|
contract:
|
||||||
- result sets are valid only while the owning transaction is active
|
- result sets are valid only while the owning transaction is active
|
||||||
- rows obtained from a result set should stay usable after the owning
|
- rows obtained from a result set are also invalid after the owning
|
||||||
transaction ends once they were materialized, e.g. with `toList()`
|
transaction ends, even if they were already buffered
|
||||||
- iteration closes underlying resources when finished or canceled
|
- iteration closes underlying resources when finished or canceled
|
||||||
- `isEmpty()` should be cheap where possible
|
- `isEmpty()` should be cheap where possible
|
||||||
- `size()` may consume or buffer the full result
|
- `size()` may consume or buffer the full result
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user