Skip to content
Snippets Groups Projects
Commit dd3aaefa authored by Florian Spreckelsen's avatar Florian Spreckelsen
Browse files

ENH: Add minimal functionality

parent 128e5866
No related branches found
No related tags found
1 merge request!4ENH: Add minimal functionality
......@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Basic functionality to establish connection to a CaosDB server and
retrieve its version (using the Extern C interface of caosdb-cpplib)
* Support for Windows
* Support for config files
* Minimal error handling
### Changed
......
# Active Contributors
* Alexander Kreft
* Florian Spreckelsen <f.spreckelsen@indiscale.com>
# This file is machine-generated - editing it directly is not advised
[[Libdl]]
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
[[Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
name = "CaosDB"
uuid = "091fdcf5-a163-4d8f-97f4-9adce40cd04e"
authors = ["florian <f.spreckelsen@inidscale.com>"]
version = "0.0.1"
version = "0.0.2"
[deps]
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
......@@ -13,7 +13,9 @@ client library for CaosDB and a part of the CaosDB project.
## Setup
Please read the [README\_SETUP.md](README_SETUP.md) for instructions
on how to develop, build, and use this code.
on how to develop, build, and use this code. caosdb-julialib is
developped and tested using Julia 1.6, we cannot guarantee its
compatibility with versions <= 1.5.
## Further reading
......
......@@ -3,6 +3,7 @@
#
# Copyright (C) 2021 Indiscale GmbH <info@indiscale.com>
# Copyright (C) 2021 Florian Spreckelsen <f.spreckelsen@indiscale.com>
# Copyright (C) 2021 Alexander Kreft
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
......@@ -23,10 +24,75 @@
module CaosDB
using Libdl
"""
Chose the name of the library according to the OS you're running.
"""
library_name = (@static Sys.iswindows() ? "ccaosdb" : "libccaosdb")
if isempty(find_library(library_name))
@error "Could not find $library_name"
end
module Exceptions
export evaluate_return_code, CaosDBException, GenericCaosDBException, CaosDBMessage
using Logging
using CaosDB
"""
The parent type of all CaosDB errors that can also be used for testing.
"""
abstract type CaosDBException <: Exception end
"""
A generic exception that will be raised in case of non-zero return
values of the calls to libccaosdb. May carry a message string and a
code.
"""
struct GenericCaosDBException <: CaosDBException
msg::String
code::Cint
end
Base.showerror(io::IO, e::CaosDBException) =
print(io, "CaosDBException: ", e.msg, " (Status code ", e.code, ")")
"""
Struct containing Messages and codes for status codes<0 that do not
correspond to errors or success.
"""
struct CaosDBMessage
msg::String
code::Cint
end
Base.show(io::IO, m::CaosDBMessage) = print(io, m.msg, " (Status code ", m.code, ")")
"""
function evaluate_return_code(code::Cint)
Evaluate the return code of a libccaosdb ccall and raise a
`GenericCaosDBException` in case of a non-zero return code.
"""
function evaluate_return_code(code::Cint)
if code != 0
msg = ccall(
(:caosdb_get_status_description, CaosDB.library_name),
Cstring,
(Cint,),
code,
)
if code > 0
throw(GenericCaosDBException(unsafe_string(msg), code))
else
@info CaosDBMessage(unsafe_string(msg), code)
end
end
end
end # Exceptions
module Info
......@@ -90,13 +156,13 @@ authenticator object from a configuration.
mutable struct _Authenticator
wrapped_authenticator::Ptr{Cvoid}
function _Authenticator()
function _Authenticator(managed_by_julia::Bool = false)
auth = new()
# force this to point to C_NULL after initialization
auth.wrapped_authenticator = C_NULL
if managed_by_julia
# Only append a finalizer for this if the object is
# actually managed by Julia and not created and destroyed
# internally by libcaosdb.
function f(t)
if t.wrapped_authenticator != C_NULL
# Only if pointer was filled after real initialization
ccall(
(:caosdb_authentication_delete_authenticator, CaosDB.library_name),
Cint,
......@@ -104,9 +170,10 @@ mutable struct _Authenticator
Ref{_Authenticator}(t),
)
end
end
finalizer(f, auth)
end
return auth
end
end
"""
......@@ -124,7 +191,7 @@ function create_plain_password_authenticator(
password::AbstractString,
)
auth = Ref{_Authenticator}(_Authenticator())
auth = Ref{_Authenticator}(_Authenticator(true))
err_code = ccall(
(:caosdb_authentication_create_plain_password_authenticator, CaosDB.library_name),
......@@ -135,11 +202,7 @@ function create_plain_password_authenticator(
password,
)
if err_code != 0
@error "Creating authenticator failed with code $err_code"
end
CaosDB.Exceptions.evaluate_return_code(err_code)
return auth
......@@ -147,365 +210,7 @@ end
end # Authentication
module Connection
using ..CaosDB
export connect
"""
Struct containing the actual connection to a CaosDB server. Meant for
internal use; call a `CaosDB.Connection.create_<connection>` function
to create an connection object from a configuration.
"""
mutable struct _Connection
wrapped_connection::Ptr{Cvoid}
function _Connection()
conn = new()
conn.wrapped_connection = C_NULL
function f(t)
if t.wrapped_connection != C_NULL
ccall(
(:caosdb_connection_delete_connection, CaosDB.library_name),
Cint,
(Ref{_Connection},),
Ref{_Connection}(t),
)
end
end
finalizer(f, conn)
end
end
"""
Struct containing a pointer to the wrapped cpp class providing the
certificate provider. Meant for internal use; call a
`CaosDB.Connection.create_<certificate_provider>` function to create
an certificate-provider object from a configuration.
"""
mutable struct _CertificateProvider
wrapped_certificate_provider::Ptr{Cvoid}
function _CertificateProvider()
prov = new()
prov.wrapped_certificate_provider = C_NULL
function f(t)
if t.wrapped_certificate_provider != C_NULL
ccall(
(:caosdb_connection_delete_certificate_provider, CaosDB.library_name),
Cint,
(Ref{_CertificateProvider},),
Ref{_CertificateProvider}(t),
)
end
end
finalizer(f, prov)
end
end
"""
Struct containing a pointer to the wrapped cpp class for storing the
connection configuration. Meant for internal use; call a
`CaosDB.Connection.create_<configuration>` function to create
an connection-configuration object from a configuration.
"""
mutable struct _Configuration
wrapped_connection_configuration::Ptr{Cvoid}
function _Configuration()
config = new()
config.wrapped_connection_configuration = C_NULL
function f(t)
if t.wrapped_connection_configuration != C_NULL
ccall(
(
:caosdb_connection_delete_connection_configuration,
CaosDB.library_name,
),
Cint,
(Ref{_Configuration},),
Ref{_Configuration}(t),
)
end
end
finalizer(f, config)
end
end
"""
create_pem_file_certificate_provider(path::AbstractString)
Return a `_CertificateProvider` for the pem certificate located at
`path`.
"""
function create_pem_file_certificate_provider(path::AbstractString)
cert_provider = Ref{_CertificateProvider}(_CertificateProvider())
err_code = ccall(
(:caosdb_connection_create_pem_file_certificate_provider, CaosDB.library_name),
Cint,
(Ref{_CertificateProvider}, Cstring),
cert_provider,
path,
)
if err_code != 0
@error "PEM certificate creation returned code $err_code."
end
return cert_provider
end
"""
create_tls_connection_configuration(
host::AbstractString,
port::Cint,
authenticator::Ref{CaosDB.Authentication._Authenticator},
provider::Ref{_CertificateProvider}
)
Return a TLS connection configuration with authentication.
"""
function create_tls_connection_configuration(
host::AbstractString,
port::Cint,
authenticator::Ref{CaosDB.Authentication._Authenticator},
provider::Ref{_CertificateProvider},
)
config = Ref{_Configuration}(_Configuration())
err_code = ccall(
(:caosdb_connection_create_tls_connection_configuration, CaosDB.library_name),
Cint,
(
Ref{_Configuration},
Cstring,
Cint,
Ref{CaosDB.Authentication._Authenticator},
Ref{_CertificateProvider},
),
config,
host,
port,
authenticator,
provider,
)
if err_code != 0
@error "TLS-configuration creation returned code $err_code."
end
return config
end
function create_insecure_connection_configuration(host::AbstractString, port::Cint)
config = Ref{_Configuration}(_Configuration())
err_code = ccall(
(:caosdb_connection_create_insecure_connection_configuration, CaosDB.library_name),
Cint,
(Ref{_Configuration}, Cstring, Cint),
config,
host,
port,
)
if err_code != 0
@error "Insecure configuration creation returned code $err_code."
end
return config
end
"""
create_connection(config::Ref{_Configuration})
Return a connection based on the given `config`.
"""
function create_connection(config::Ref{_Configuration})
connection = Ref{_Connection}(_Connection())
err_code = ccall(
(:caosdb_connection_create_connection, CaosDB.library_name),
Cint,
(Ref{_Connection}, Ref{_Configuration}),
connection,
config,
)
if err_code != 0
@error "Creating connection failed with code $err_code."
end
return connection
end
"""
get_version_info(con::Ref{_Connection})
Return the version of the CaosDB server that `con` is connected
to.
"""
function get_version_info(con::Ref{_Connection})
info = Ref{CaosDB.Info._VersionInfo}(CaosDB.Info._VersionInfo())
err_code = ccall(
(:caosdb_connection_get_version_info, CaosDB.library_name),
Cint,
(Ref{CaosDB.Info._VersionInfo}, Ref{_Connection}),
info,
con,
)
# TODO Real error-code handling
if err_code != 0
@error "Version info returned with code $err_code"
end
return info
end
"""
print_version_info(con::Ref{_Connection})
Retrieve the version info for the CaosDB server `con` is connected to,
and print the version in a nice message.
"""
function print_version_info(con::Ref{_Connection})
# Dereference to access the fields
info = get_version_info(con)[]
major = info.major
minor = info.minor
patch = info.patch
pre_release_str = unsafe_string(info.pre_release)
build_str = unsafe_string(info.build)
println(
"Connected to a CaosDB server with version $major.$minor.$patch-$pre_release_str-$build_str.",
)
end
"""
function connect([;
host::AbstractString="",
port_str::AbstractString="undefined",
cacert::AbstractString="",
username::AbstractString="",
password::AbstractString="undefined"]
)
Return a connection object created for the given `host`:`port` with an
SSL certificate located at `cacert` with the given credentials.
# Extended help
!!! info
Because of type-stability, and since an empty string may be a
valid password, the value of `password`, for which it is fetched
from an environmental variable, is "undefined". This means that if
you absolutely must use "undefined" as your password, you have to
specify it via the `CAOSDB_PASSWORD` variable.
# Arguments
- `host::AbstractString=""`: The hostname of the CaosDB server. If
none is provided, the `CAOSDB_SERVER_HOST` environmental variable is
used instead. If that's not defined, "localhost" is used.
- `port_str::AbstractString="undefined"`: The port of the CaosDB
server, given as string. If none is provided, the
`CAOSDB_SERVER_GRPC_PORT_HTTPS` environmental variable is used
instead. If that's not defined, "8443" is used. The default value is
"undefined" rather than an empty string because an empty string
could be a valid port, too, i.e. the CaosDB server is available at
`host` without a port.
- `cacert::AbstractString=""`: The path to the SSL certificate of the
CaosDB server. If none is provided, the `CAOSDB_SERVER_CERT`
environmental variable is used instead.
- `username::AbstractString=""`: The username with which to log in
into the CaosDB server. If none is provided, the `CAOSDB_USER`
environmental variable is used instead. If that's not defined,
"admin" is used.
- `password::AbstractString="undefined"`: The password with which to
log in into the CaosDB server. If none is provided, the
`CAOSDB_PASSWORD` environmental variable is used instead. If that's
not defined, "caosdb" is used. The default value is "undefined"
rather than an empty string to allow an empty password.
"""
function connect(;
host::AbstractString = "",
port_str::AbstractString = "undefined",
cacert::AbstractString = "",
username::AbstractString = "",
password::AbstractString = "undefined",
)
if host == ""
host = CaosDB.Utility.get_env_var("CAOSDB_SERVER_HOST", "localhost")
end
if port_str == "undefined"
port_str = CaosDB.Utility.get_env_var("CAOSDB_SERVER_GRPC_PORT_HTTPS", "8443")
end
port = parse(Cint, port_str)
if cacert == ""
cacert = CaosDB.Utility.get_env_var("CAOSDB_SERVER_CERT")
end
if username == ""
username = CaosDB.Utility.get_env_var("CAOSDB_USER", "admin")
end
if password == "undefined"
password = CaosDB.Utility.get_env_var("CAOSDB_PASSWORD", "caosdb")
end
provider = create_pem_file_certificate_provider(cacert)
authenticator =
CaosDB.Authentication.create_plain_password_authenticator(username, password)
config = create_tls_connection_configuration(host, port, authenticator, provider)
connection = create_connection(config)
print_version_info(connection)
return connection
end
end # Connection
include("Connection.jl")
module Entity end
......
# ** header v3.0
# This file is a part of the CaosDB Project.
#
# Copyright (C) 2021 Indiscale GmbH <info@indiscale.com>
# Copyright (C) 2021 Florian Spreckelsen <f.spreckelsen@indiscale.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
# ** end header
#
module Connection
using ..CaosDB
export connect, connect_manually
"""
Struct containing the actual connection to a CaosDB server. Meant for
internal use; call a `CaosDB.Connection.create_<connection>` function
to create an connection object from a configuration.
"""
mutable struct _Connection
wrapped_connection::Ptr{Cvoid}
function _Connection(managed_by_julia::Bool = false)
conn = new()
if managed_by_julia
# Only append a finalizer for this if the object is
# actually managed by Julia and not created and destroyed
# internally by libcaosdb.
function f(t)
ccall(
(:caosdb_connection_delete_connection, CaosDB.library_name),
Cint,
(Ref{_Connection},),
Ref{_Connection}(t),
)
end
finalizer(f, conn)
end
return conn
end
end
"""
Struct containing a pointer to the wrapped cpp class providing the
certificate provider. Meant for internal use; call a
`CaosDB.Connection.create_<certificate_provider>` function to create
an certificate-provider object from a configuration.
"""
mutable struct _CertificateProvider
wrapped_certificate_provider::Ptr{Cvoid}
function _CertificateProvider(managed_by_julia::Bool = false)
prov = new()
if managed_by_julia
# Only append a finalizer for this if the object is
# actually managed by Julia and not created and destroyed
# internally by libcaosdb.
function f(t)
ccall(
(:caosdb_connection_delete_certificate_provider, CaosDB.library_name),
Cint,
(Ref{_CertificateProvider},),
Ref{_CertificateProvider}(t),
)
end
finalizer(f, prov)
end
return prov
end
end
"""
Struct containing a pointer to the wrapped cpp class for storing the
connection configuration. Meant for internal use; call a
`CaosDB.Connection.create_<configuration>` function to create
an connection-configuration object from a configuration.
"""
mutable struct _Configuration
wrapped_connection_configuration::Ptr{Cvoid}
function _Configuration(managed_by_julia::Bool = false)
config = new()
if managed_by_julia
# Only append a finalizer for this if the object is
# actually managed by Julia and not created and destroyed
# internally by libcaosdb.
function f(t)
ccall(
(
:caosdb_connection_delete_connection_configuration,
CaosDB.library_name,
),
Cint,
(Ref{_Configuration},),
Ref{_Configuration}(t),
)
end
finalizer(f, config)
end
return config
end
end
"""
create_pem_file_certificate_provider(path::AbstractString)
Return a `_CertificateProvider` for the pem certificate located at
`path`.
"""
function create_pem_file_certificate_provider(path::AbstractString)
cert_provider = Ref{_CertificateProvider}(_CertificateProvider(true))
err_code = ccall(
(:caosdb_connection_create_pem_file_certificate_provider, CaosDB.library_name),
Cint,
(Ref{_CertificateProvider}, Cstring),
cert_provider,
path,
)
CaosDB.Exceptions.evaluate_return_code(err_code)
return cert_provider
end
"""
create_tls_connection_configuration(
host::AbstractString,
port::Cint,
authenticator::Ref{CaosDB.Authentication._Authenticator},
provider::Ref{_CertificateProvider}
)
Return a TLS connection configuration with authentication.
"""
function create_tls_connection_configuration(
host::AbstractString,
port::Cint,
authenticator::Ref{CaosDB.Authentication._Authenticator},
provider::Ref{_CertificateProvider},
)
config = Ref{_Configuration}(_Configuration(true))
err_code = ccall(
(:caosdb_connection_create_tls_connection_configuration, CaosDB.library_name),
Cint,
(
Ref{_Configuration},
Cstring,
Cint,
Ref{CaosDB.Authentication._Authenticator},
Ref{_CertificateProvider},
),
config,
host,
port,
authenticator,
provider,
)
CaosDB.Exceptions.evaluate_return_code(err_code)
return config
end
function create_insecure_connection_configuration(host::AbstractString, port::Cint)
config = Ref{_Configuration}(_Configuration(true))
err_code = ccall(
(:caosdb_connection_create_insecure_connection_configuration, CaosDB.library_name),
Cint,
(Ref{_Configuration}, Cstring, Cint),
config,
host,
port,
)
CaosDB.Exceptions.evaluate_return_code(err_code)
return config
end
"""
create_connection(config::Ref{_Configuration})
Return a connection based on the given `config`.
"""
function create_connection(config::Ref{_Configuration})
connection = Ref{_Connection}(_Connection(true))
err_code = ccall(
(:caosdb_connection_create_connection, CaosDB.library_name),
Cint,
(Ref{_Connection}, Ref{_Configuration}),
connection,
config,
)
CaosDB.Exceptions.evaluate_return_code(err_code)
return connection
end
function get_connection(name::AbstractString = "default")
connection = Ref{_Connection}(_Connection())
if name == "default"
err_code = ccall(
(
:caosdb_connection_connection_manager_get_default_connection,
CaosDB.library_name,
),
Cint,
(Ref{_Connection},),
connection,
)
else
err_code = ccall(
(:caosdb_connection_connection_manager_get_connection, CaosDB.library_name),
Cint,
(Ref{_Connection}, Cstring),
connection,
name,
)
end
CaosDB.Exceptions.evaluate_return_code(err_code)
return connection
end
"""
get_version_info(con::Ref{_Connection})
Return the version of the CaosDB server that `con` is connected
to.
"""
function get_version_info(con::Ref{_Connection})
info = Ref{CaosDB.Info._VersionInfo}(CaosDB.Info._VersionInfo())
err_code = ccall(
(:caosdb_connection_get_version_info, CaosDB.library_name),
Cint,
(Ref{CaosDB.Info._VersionInfo}, Ref{_Connection}),
info,
con,
)
CaosDB.Exceptions.evaluate_return_code(err_code)
return info
end
"""
print_version_info(con::Ref{_Connection})
Retrieve the version info for the CaosDB server `con` is connected to,
and print the version in a nice message.
"""
function print_version_info(con::Ref{_Connection})
# Dereference to access the fields
info = get_version_info(con)[]
major = info.major
minor = info.minor
patch = info.patch
pre_release_str = unsafe_string(info.pre_release)
build_str = unsafe_string(info.build)
println(
"Connected to a CaosDB server with version $major.$minor.$patch-$pre_release_str-$build_str.",
)
end
"""
function connect_manually([;
host::AbstractString="",
port_str::AbstractString="undefined",
cacert::AbstractString="",
username::AbstractString="",
password::AbstractString="undefined"]
)
Return a connection object created for the given `host`:`port` with an
SSL certificate located at `cacert` with the given credentials.
# Extended help
!!! info
Because of type-stability, and since an empty string may be a
valid password, the value of `password`, for which it is fetched
from an environmental variable, is "undefined". This means that if
you absolutely must use "undefined" as your password, you have to
specify it via the `CAOSDB_PASSWORD` variable.
# Arguments
- `host::AbstractString=""`: The hostname of the CaosDB server. If
none is provided, the `CAOSDB_SERVER_HOST` environmental variable is
used instead. If that's not defined, "localhost" is used.
- `port_str::AbstractString="undefined"`: The port of the CaosDB
server, given as string. If none is provided, the
`CAOSDB_SERVER_GRPC_PORT_HTTPS` environmental variable is used
instead. If that's not defined, "8443" is used. The default value is
"undefined" rather than an empty string because an empty string
could be a valid port, too, i.e. the CaosDB server is available at
`host` without a port.
- `cacert::AbstractString=""`: The path to the SSL certificate of the
CaosDB server. If none is provided, the `CAOSDB_SERVER_CERT`
environmental variable is used instead.
- `username::AbstractString=""`: The username with which to log in
into the CaosDB server. If none is provided, the `CAOSDB_USER`
environmental variable is used instead. If that's not defined,
"admin" is used.
- `password::AbstractString="undefined"`: The password with which to
log in into the CaosDB server. If none is provided, the
`CAOSDB_PASSWORD` environmental variable is used instead. If that's
not defined, "caosdb" is used. The default value is "undefined"
rather than an empty string to allow an empty password.
"""
function connect_manually(;
host::AbstractString = "",
port_str::AbstractString = "undefined",
cacert::AbstractString = "",
username::AbstractString = "",
password::AbstractString = "undefined",
)
if host == ""
host = CaosDB.Utility.get_env_var("CAOSDB_SERVER_HOST", "localhost")
end
if port_str == "undefined"
port_str = CaosDB.Utility.get_env_var("CAOSDB_SERVER_GRPC_PORT_HTTPS", "8443")
end
port = parse(Cint, port_str)
if cacert == ""
cacert = CaosDB.Utility.get_env_var("CAOSDB_SERVER_CERT")
end
if username == ""
username = CaosDB.Utility.get_env_var("CAOSDB_USER", "admin")
end
if password == "undefined"
password = CaosDB.Utility.get_env_var("CAOSDB_PASSWORD", "caosdb")
end
provider = create_pem_file_certificate_provider(cacert)
authenticator =
CaosDB.Authentication.create_plain_password_authenticator(username, password)
config = create_tls_connection_configuration(host, port, authenticator, provider)
connection = create_connection(config)
print_version_info(connection)
return connection
end
"""
connect(name::AbstractString="default")
Create a connection with name `name` from your configuration file,
print the version of the server the connection is established to, and
return the connection object.
# Arguments
- name::AbstractString="default": The name of the configuration
defined in your config json that will be used to connect to the
CaosDB server defined therein. Default value is "default".
"""
function connect(name::AbstractString = "default")
conn = get_connection(name)
print_version_info(conn)
return conn
end
end # Connection
......@@ -23,9 +23,45 @@
using Test
using CaosDB
@testset "CaosDBUnitTests" begin
@testset "TestUtility" begin
if haskey(ENV, "SHELL")
shell_var = ENV["SHELL"]
else
shell_var = "default"
end
@test CaosDB.Utility.get_env_var("SHELL", "default") == shell_var
end
@testset "TestExceptions" begin
# In case of success, nothing is done
@test CaosDB.Exceptions.evaluate_return_code(Cint(0)) == nothing
# CaosDBExceptions are thrown for return codes > 0
@test_throws CaosDB.Exceptions.CaosDBException CaosDB.Exceptions.evaluate_return_code(
Cint(14),
)
@test_throws CaosDB.Exceptions.GenericCaosDBException CaosDB.Exceptions.evaluate_return_code(
Cint(14),
)
try
CaosDB.Exceptions.evaluate_return_code(Cint(14))
# fail if this doesn't throw an error
@test false
catch e
@test isa(e, CaosDB.Exceptions.GenericCaosDBException)
@test e.code == 14
end
# Return codes < 0 correspond to unfinished transactions and
# we expect messages to be returned.
@test CaosDB.Exceptions.evaluate_return_code(Cint(-1)) == nothing
@test_logs (
:info,
CaosDB.Exceptions.CaosDBMessage(
"The transaction is currently being executed.",
Cint(-1),
),
) CaosDB.Exceptions.evaluate_return_code(Cint(-1))
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment