diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 53bf319..fe63bb6 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 9e55231..4cc6529 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ - + diff --git a/build.gradle.kts b/build.gradle.kts index d8b7d94..31e69b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,9 @@ plugins { - kotlin("jvm") version "2.0.0-RC2" + kotlin("jvm") version "1.9.23" } group = "nl.astraeus" -version = "1.0-SNAPSHOT" +version = "1.0.0-SNAPSHOT" repositories { mavenCentral() @@ -16,6 +16,7 @@ dependencies { tasks.test { useJUnitPlatform() } + kotlin { jvmToolchain(17) -} \ No newline at end of file +} diff --git a/src/main/kotlin/nl/astraeus/persistence/Datastore.kt b/src/main/kotlin/nl/astraeus/persistence/Datastore.kt index d9c58f1..399baa7 100644 --- a/src/main/kotlin/nl/astraeus/persistence/Datastore.kt +++ b/src/main/kotlin/nl/astraeus/persistence/Datastore.kt @@ -6,6 +6,7 @@ import java.io.ObjectOutputStream import java.io.Serializable import java.text.DecimalFormat import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong import kotlin.reflect.KClass enum class ActionType { @@ -14,8 +15,8 @@ enum class ActionType { } class TypeData( - var nextId: Long = 1L, - val data: MutableMap = ConcurrentHashMap(), + var nextId: AtomicLong = AtomicLong(1L), + val data: MutableMap = ConcurrentHashMap(), ) : Serializable class Action( @@ -43,6 +44,23 @@ class Datastore( loadTransactions() } + fun getNextId(javaClass: Class): Long { + if (data[javaClass] == null) { + synchronized(this) { + if (data[javaClass] == null) { + data[javaClass] = TypeData() + } + } + } + + return data[javaClass]!!.nextId.getAndIncrement() + } + + fun setMaxId(javaClass: Class, id: Long) { + val nextId = data.getOrPut(javaClass) { TypeData() }.nextId + if (nextId.get() <= id) nextId.set(id + 1) + } + override fun toString(): String { return "Datastore(directory=${fileManager.directory}, classes=${data.keys.size}, indexes=${indexes.keys.size})" } @@ -95,14 +113,15 @@ class Datastore( when (action.type) { ActionType.STORE -> { - if (action.obj.id == 0L) { - action.obj.id = typeData.nextId++ - } typeData.data[action.obj.id] = action.obj + if (action.obj.id >= typeData.nextId.get()) { + typeData.nextId.set(action.obj.id + 1) + } + for (index in indexes[action.obj::class.java]?.values ?: listOf()) { index.remove(action.obj.id) - index.add(action.obj as Persistable) + index.add(action.obj) } } @@ -118,6 +137,15 @@ class Datastore( } } + + fun count(clazz: KClass): Int { + val typeData = data.getOrPut(clazz.java) { + TypeData() + } + + return typeData.data.size + } + fun find(clazz: KClass, id: Long): T? { val typeData = data.getOrPut(clazz.java) { TypeData() @@ -148,15 +176,18 @@ class Datastore( return indexes[kClass.java]?.get(indexName) } - fun storeActions(actions: MutableList) { + fun storeAndExecute(actions: MutableList) { if (actions.isNotEmpty()) { synchronized(this) { writeTransaction(actions) + execute(actions) } } } private fun readTransaction(ois: ObjectInputStream) { + val versionNumber = ois.readInt() + check (versionNumber == 1) { "Unsupported version number: $versionNumber" } val transactionNumber = ois.readLong() nextTransactionNumber = transactionNumber + 1 val actions = ois.readObject() as MutableList @@ -167,6 +198,8 @@ class Datastore( val number = transactionFormatter.format(nextTransactionNumber) val file = File(directory, "transaction-$number.trn") ObjectOutputStream(file.outputStream()).use { oos -> + // version number + oos.writeInt(1) oos.writeLong(nextTransactionNumber++) oos.writeObject(actions) } @@ -178,6 +211,8 @@ class Datastore( val number = transactionFormatter.format(nextTransactionNumber) val file = File(directory, "transaction-$number.snp") ObjectOutputStream(file.outputStream()).use { oos -> + // version number + oos.writeInt(1) oos.writeLong(nextTransactionNumber++) oos.writeObject(data) oos.writeInt(indexes.size) @@ -195,6 +230,8 @@ class Datastore( } private fun readSnapshot(ois: ObjectInputStream) { + val versionNumber = ois.readInt() + check (versionNumber == 1) { "Unsupported version number: $versionNumber" } nextTransactionNumber = ois.readLong() + 1 data.clear() data.putAll(ois.readObject() as MutableMap, TypeData>) diff --git a/src/main/kotlin/nl/astraeus/persistence/Indexing.kt b/src/main/kotlin/nl/astraeus/persistence/Indexing.kt index b501495..40f9117 100644 --- a/src/main/kotlin/nl/astraeus/persistence/Indexing.kt +++ b/src/main/kotlin/nl/astraeus/persistence/Indexing.kt @@ -5,14 +5,28 @@ import kotlin.reflect.KClass typealias PersistableIndex = Index +inline fun index( + name: String, + noinline value: (Persistable) -> Serializable? +): Index = Index( + T::class, + name, + value +) + class Index( - kcls: KClass, + val cls: Class, val name: String, val value: (Persistable) -> Serializable?, ) : Serializable { - val cls: Class = kcls.java val index = mutableMapOf>() + constructor( + cls: KClass, + name: String, + value: (Persistable) -> Serializable? + ) : this(cls.java, name, value) + fun add(obj: Persistable) { val key = value(obj) diff --git a/src/main/kotlin/nl/astraeus/persistence/Persistable.kt b/src/main/kotlin/nl/astraeus/persistence/Persistable.kt index 523f810..dcdb282 100644 --- a/src/main/kotlin/nl/astraeus/persistence/Persistable.kt +++ b/src/main/kotlin/nl/astraeus/persistence/Persistable.kt @@ -6,7 +6,7 @@ import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.Serializable -interface Persistable : Serializable, Cloneable { +interface Persistable : Serializable { var id: Long var version: Long @@ -23,3 +23,13 @@ interface Persistable : Serializable, Cloneable { } } } + +abstract class AbstractPersistable : Persistable { + override fun copy(): Persistable { + return super.copy() + } + + companion object { + private const val serialVersionUID: Long = 1L + } +} diff --git a/src/main/kotlin/nl/astraeus/persistence/Persistent.kt b/src/main/kotlin/nl/astraeus/persistence/Persistent.kt index eddf633..a0e59fd 100644 --- a/src/main/kotlin/nl/astraeus/persistence/Persistent.kt +++ b/src/main/kotlin/nl/astraeus/persistence/Persistent.kt @@ -14,6 +14,10 @@ class Persistent( ) { val datastore: Datastore = Datastore(directory, indexes) + fun query(block: Query.() -> T): T { + return block(Query(this)) + } + fun transaction(block: Transaction.() -> Unit) { var cleanup = false if (transactions.get() == null) { diff --git a/src/main/kotlin/nl/astraeus/persistence/Transaction.kt b/src/main/kotlin/nl/astraeus/persistence/Transaction.kt index 255db02..51e3099 100644 --- a/src/main/kotlin/nl/astraeus/persistence/Transaction.kt +++ b/src/main/kotlin/nl/astraeus/persistence/Transaction.kt @@ -3,12 +3,105 @@ package nl.astraeus.nl.astraeus.persistence import java.io.Serializable import kotlin.reflect.KClass -class Transaction( +inline fun Query.count(): Int = this.count(T::class) +inline fun Query.find(id: Long): T? = this.find(T::class, id) +inline fun Query.search(noinline search: (T) -> Boolean): List = + this.search(T::class, search) +inline fun Query.findByIndex( + indexName: String, + search: Serializable +): List = this.findByIndex(T::class, indexName, search) +inline fun Query.searchIndex( + indexName: String, + noinline search: (Serializable) -> Boolean, +): List = this.searchIndex(T::class, indexName, search) + +inline fun Transaction.count(): Int = this.count(T::class) +inline fun Transaction.find(id: Long): T? = this.find(T::class, id) +inline fun Transaction.search(noinline search: (T) -> Boolean): List = + this.search(T::class, search) +inline fun Transaction.findByIndex( + indexName: String, + search: Serializable +): List = this.findByIndex(T::class, indexName, search) +inline fun Transaction.searchIndex( + indexName: String, + noinline search: (Serializable) -> Boolean, +): List = this.searchIndex(T::class, indexName, search) + +open class Query( val persistent: Persistent, ) : Serializable { + + fun count(clazz: Class): Int = count(clazz.kotlin) + fun count(clazz: KClass): Int = persistent.datastore.count(clazz) + + fun find(clazz: Class, id: Long): T? { + return find(clazz.kotlin, id) + } + + open fun find(clazz: KClass, id: Long): T? = persistent.datastore.find(clazz, id) + + open fun search(clazz: KClass, search: (T) -> Boolean): List = persistent.datastore.search(clazz, search) + + fun findByIndex( + kcls: KClass, + indexName: String, + search: Serializable + ): List = findByIndex(kcls.java, indexName, search) + + open fun findByIndex( + cls: Class, + indexName: String, + search: Serializable + ): List { + val result = mutableListOf() + val index = persistent.datastore.findIndex(cls.kotlin, indexName) + ?: throw IllegalArgumentException("Index with name $indexName not found for class ${cls.simpleName}") + + index.find(search).forEach { id -> + result.add(id as T) + } + + return result + } + + fun searchIndex( + kcls: KClass, + indexName: String, + search: (Serializable) -> Boolean, + ): List = searchIndex(kcls.java, indexName, search) + + open fun searchIndex( + cls: Class, + indexName: String, + search: (Serializable) -> Boolean, + ): List { + val result = mutableListOf() + val index = persistent.datastore.findIndex(cls.kotlin, indexName) ?: throw IllegalArgumentException("Index not found") + + index.index.keys.forEach { key -> + if (search(key)) { + index.find(key).forEach { id -> + result.add(id as T) + } + } + } + + return result + } +} + +class Transaction( + persistent: Persistent, +) : Query(persistent), Serializable { private val actions = mutableListOf() fun store(obj: Persistable) { + if (obj.id == 0L) { + obj.id = persistent.datastore.getNextId(obj.javaClass) + } + actions.add(Action(ActionType.STORE, obj)) } @@ -16,23 +109,23 @@ class Transaction( actions.add(Action(ActionType.DELETE, obj)) } - fun find(clazz: KClass, id: Long): T? { - var result: T? = persistent.datastore.find(clazz, id) + fun commit() { + persistent.datastore.storeAndExecute(actions) + actions.clear() + } + + override fun find(clazz: KClass, id: Long): T? { + var result = super.find(clazz, id) for (action in actions) { if (action.obj::class == clazz && action.obj.id == id) { - result = when { - action.type == ActionType.DELETE -> { + result = when(action.type) { + ActionType.DELETE -> { null } - - action.type == ActionType.STORE -> { + ActionType.STORE -> { action.obj as? T } - - else -> { - result - } } } } @@ -40,10 +133,9 @@ class Transaction( return result } - fun search(clazz: KClass, search: (T) -> Boolean): List { - val fromDatastore: List = persistent.datastore.search(clazz, search) + override fun search(clazz: KClass, search: (T) -> Boolean): List { val result = mutableListOf() - result.addAll(fromDatastore) + result.addAll(super.search(clazz, search)) for (obj in result) { for (action in actions) { @@ -61,27 +153,13 @@ class Transaction( return result } - fun commit() { - persistent.datastore.storeActions(actions) - persistent.datastore.execute(actions) - actions.clear() - } - - fun findByIndex( - kClass: KClass, - indexName: String, - search: Serializable - ): List { + override fun findByIndex(cls: Class, indexName: String, search: Serializable): List { val result = mutableListOf() - val index = persistent.datastore.findIndex(kClass, indexName) - ?: throw IllegalArgumentException("Index with name $indexName not found for class ${kClass.simpleName}") - - index.find(search).forEach { id -> - result.add(id as T) - } + val index = persistent.datastore.findIndex(cls.kotlin, indexName) ?: throw IllegalArgumentException("Index not found") + result.addAll(super.findByIndex(cls, indexName, search)) for (action in actions) { - if (action.obj::class == kClass) { + if (action.obj::class == cls.kotlin) { if (action.type == ActionType.DELETE) { if (index.matches(action.obj, search)) { result.remove(action.obj as T) @@ -98,24 +176,13 @@ class Transaction( return result } - fun searchIndex( - kClass: KClass, - indexName: String, - search: (Serializable) -> Boolean, - ): List { + override fun searchIndex(cls: Class, indexName: String, search: (Serializable) -> Boolean): List { val result = mutableListOf() - val index = persistent.datastore.findIndex(kClass, indexName) ?: throw IllegalArgumentException("Index not found") - - index.index.keys.forEach { key -> - if (search(key)) { - index.find(key).forEach { id -> - result.add(id as T) - } - } - } + val index = persistent.datastore.findIndex(cls.kotlin, indexName) ?: throw IllegalArgumentException("Index not found") + result.addAll(super.searchIndex(cls, indexName, search)) for (action in actions) { - if (action.obj::class == kClass) { + if (action.obj::class == cls.kotlin) { val indexedValue = index.value(action.obj) if (indexedValue != null && index.matches(action.obj, indexedValue)) { if (action.type == ActionType.DELETE) { diff --git a/src/test/java/nl/astraeus/persistence/TestPersistenceJava.java b/src/test/java/nl/astraeus/persistence/TestPersistenceJava.java new file mode 100644 index 0000000..e6579de --- /dev/null +++ b/src/test/java/nl/astraeus/persistence/TestPersistenceJava.java @@ -0,0 +1,126 @@ +package nl.astraeus.persistence; + +import nl.astraeus.nl.astraeus.persistence.AbstractPersistable; +import nl.astraeus.nl.astraeus.persistence.Index; +import nl.astraeus.nl.astraeus.persistence.Persistable; +import nl.astraeus.nl.astraeus.persistence.Persistent; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.List; + +public class TestPersistenceJava { + + static class Person extends AbstractPersistable { + private static Long serialVersionUID = 1L; + + private long id = 0; + private long version = 0; + private String name; + private int age; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public long getId() { + return id; + } + + @Override + public void setId(long id) { + this.id = id; + } + + @Override + public long getVersion() { + return version; + } + + @Override + public void setVersion(long version) { + this.version = version; + } + } + + @Test + void testPersistence() { + System.out.println("TestPersistenceJava.testPersistence"); + + Persistent persistent = new Persistent( + new File("data", "java-test"), + new Index[] { + new Index<>( + Person.class, + "name", + (p) -> ((Person)p).getName() + ) + } + ); + + persistent.transaction((t) -> { + Person person = t.find(Person.class, 1L); + + if (person != null) { + System.out.println( + "Person: " + + person.getName() + " is " + + person.getAge() + " years old." + ); + } + + return null; + }); + + persistent.transaction((t) -> { + Person person = new Person("John Doe", 42); + + t.store(person); + + return null; + }); + + + persistent.transaction((t) -> { + List persons = t.findByIndex( + Person.class, + "name", + "John Doe" + ); + + for (Person person : persons) { + System.out.println( + "Person: " + + person.getName() + " is " + + person.getAge() + " years old." + ); + } + + return null; + }); + + persistent.snapshot(); + persistent.getDatastore().printStatus(); + persistent.removeOldFiles(); + } + +} diff --git a/src/test/kotlin/nl/astraeus/persistence/TestPersistence.kt b/src/test/kotlin/nl/astraeus/persistence/TestPersistence.kt index 7a30f46..e89f137 100644 --- a/src/test/kotlin/nl/astraeus/persistence/TestPersistence.kt +++ b/src/test/kotlin/nl/astraeus/persistence/TestPersistence.kt @@ -1,9 +1,13 @@ package nl.astraeus.persistence -import nl.astraeus.nl.astraeus.persistence.Index import nl.astraeus.nl.astraeus.persistence.Persistable import nl.astraeus.nl.astraeus.persistence.Persistent import nl.astraeus.nl.astraeus.persistence.Reference +import nl.astraeus.nl.astraeus.persistence.find +import nl.astraeus.nl.astraeus.persistence.findByIndex +import nl.astraeus.nl.astraeus.persistence.index +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull import java.io.File import kotlin.test.Test @@ -39,31 +43,31 @@ class TestPersistence { println("Test persistence") val pst = Persistent( - directory = File("data"), + directory = File("data", "test-persistence"), arrayOf( - Index(Person::class, "name") { p -> (p as? Person)?.name ?: "" }, - Index(Person::class, "age") { p -> (p as? Person)?.age ?: -1 }, - Index(Person::class, "ageGt20") { p -> ((p as? Person)?.age ?: 0) > 20 }, - Index(Person::class, "ageGt23") { p -> ((p as? Person)?.age ?: 0) > 23 }, - Index(Person::class, "ageOnlyGt20") { p -> + index("name") { p -> (p as? Person)?.name ?: "" }, + index("age") { p -> (p as? Person)?.age ?: -1 }, + index("ageGt20") { p -> ((p as? Person)?.age ?: 0) > 20 }, + index("ageGt23") { p -> ((p as? Person)?.age ?: 0) > 23 }, + index("ageOnlyGt20") { p -> if (((p as? Person)?.age ?: 0) > 20) { true } else { null } }, - Index(Company::class, "name") { p -> (p as? Company)?.name ?: "" }, + index("name") { p -> (p as? Company)?.name ?: "" }, ) ) pst.transaction { - val person = find(Person::class, 1L) ?: Person( + val person = find(1L) ?: Person( id = 1L, name = "John Doe", age = 25 ) - val company = find(Company::class, 1L) ?: Company( + val company = find(1L) ?: Company( id = 1L, name = "ACME" ) @@ -83,14 +87,19 @@ class TestPersistence { age = 18 )) - findByIndex(Person::class, "name", "John Doe").forEach { p -> + findByIndex("name", "John Doe").forEach { p -> println("Found person by name: ${p.name} - ${p.age}") } - findByIndex(Person::class, "age", 23).forEach { p -> + val persons: List = findByIndex("age", 23) + + persons.forEach { p -> println("Found person by age: ${p.name} - ${p.age}") } + val companies: List = findByIndex("name", "ACME") + assert(companies.isNotEmpty()) + findByIndex(Person::class, "ageGt20", true).forEach { p -> println("Found person by age > 20: ${p.name} - ${p.age}") } @@ -112,6 +121,12 @@ class TestPersistence { assert(c2 != null) } + pst.query { + val person = find(1L) + + assertNotNull(person) + } + pst.transaction { val person = find(Person::class, 1L) @@ -140,14 +155,19 @@ class TestPersistence { age = 23 ) ) - store( - Person( - id = 11L, - name = "John Clown", - age = 18 - ) + val person = Person( + id = 11L, + name = "John Clown", + age = 18 ) + store(person) + assertNotNull(find(Person::class, person.id)) + delete(person) + assertNull(find(Person::class, person.id)) + store(person) + assertNotNull(find(Person::class, person.id)) + searchIndex(Person::class, "name") { name -> (name as? String)?.startsWith("John") == true }.forEach { p -> diff --git a/src/test/kotlin/nl/astraeus/persistence/TestThreaded.kt b/src/test/kotlin/nl/astraeus/persistence/TestThreaded.kt new file mode 100644 index 0000000..2946ccf --- /dev/null +++ b/src/test/kotlin/nl/astraeus/persistence/TestThreaded.kt @@ -0,0 +1,129 @@ +package nl.astraeus.persistence + +import nl.astraeus.nl.astraeus.persistence.Persistable +import nl.astraeus.nl.astraeus.persistence.Persistent +import nl.astraeus.nl.astraeus.persistence.Reference +import nl.astraeus.nl.astraeus.persistence.count +import nl.astraeus.nl.astraeus.persistence.index +import nl.astraeus.nl.astraeus.persistence.searchIndex +import java.io.File +import kotlin.random.Random +import kotlin.test.Test + +class TestThreaded { + + class Company( + override var id: Long = 0, + override var version: Long = 0, + val name: String, + val adres: String = "Blaat" + ) : Persistable, Cloneable { + //var persons: MutableList by ListReference(Person::class.java) + + companion object { + private const val serialVersionUID: Long = 1L + } + } + + class Person( + override var id: Long = 0, + override var version: Long = 0, + val name: String, + val age: Int, + ) : Persistable, Cloneable { + var company: Company by Reference(Company::class.java) + + companion object { + private const val serialVersionUID: Long = 1L + } + } + + @Test + fun testThreaded() { + println("Test threaded") + + val pst = Persistent( + directory = File("data", "test-threaded"), + arrayOf( + index("name") { p -> (p as? Person)?.name ?: "" }, + index("age") { p -> (p as? Person)?.age ?: -1 }, + index("ageGt20") { p -> ((p as? Person)?.age ?: 0) > 20 }, + index("ageGt23") { p -> ((p as? Person)?.age ?: 0) > 23 }, + index("ageOnlyGt20") { p -> + if (((p as? Person)?.age ?: 0) > 20) { + true + } else { + null + } + }, + index("nameAndAge") { p -> + val person = p as? Person + + if (person == null) { + null + } else { + person.name to person.age + } + }, + index("name") { p -> (p as? Company)?.name ?: "" }, + ) + ) + + val companyNames = arrayOf("Company A", "Company B", "Company C", "Company D", "Company E") + val names = arrayOf("John Doe", "Jane Doe", "John Smith", "Jane Smith", "John Johnson", "Jane Johnson") + val random = Random(System.currentTimeMillis()) + + val empty = pst.query { + count() == 0 + } + + if (empty) { + val runnable = { + repeat(10) { + pst.transaction { + val company = Company( + id = 0L, + name = companyNames[random.nextInt(companyNames.size)] + ) + repeat(10) { + val person = Person( + id = 0L, + name = names[random.nextInt(names.size)], + age = random.nextInt(0, 100), + ) + person.company = company + + store(person) + } + } + } + } + + val threads = Array(25) { + Thread(runnable) + } + + for (thread in threads) { + thread.start() + } + + for (thread in threads) { + thread.join() + } + } + + pst.query { + searchIndex("nameAndAge") { nameAndAge -> + val (name, age) = nameAndAge as Pair + + name.contains("mit") && age > 80 + }.forEach { p -> + println("Found person by name and age: ${p.id}: ${p.name} - ${p.age}") + } + } + + pst.snapshot() + pst.datastore.printStatus() + pst.removeOldFiles() + } +}