From 828b1e7fc78ed93875acf5741cef177939356ed3 Mon Sep 17 00:00:00 2001 From: Miha_x64 Date: Sun, 24 May 2020 19:51:31 +0300 Subject: [PATCH] added SQL type overrides, #32 added Ilk as a supertype of all DataTypes, added DataType.NotNull as a supertype for Simple, Collect, and Partial --- .../aquadc/persistence/android/pref/types.kt | 60 ++-- .../extended/either/eitherTypes.kt | 36 +-- .../aquadc/persistence/extended/graphics.kt | 2 +- .../aquadc/persistence/extended/partial.kt | 4 +- .../extended/primitive collections.kt | 12 +- .../net/aquadc/persistence/extended/uuid.kt | 6 +- .../aquadc/persistence/stream/read-write.kt | 64 ++-- .../net/aquadc/persistence/struct/schema.kt | 2 +- .../net/aquadc/persistence/tokens/emit.kt | 12 +- .../net/aquadc/persistence/tokens/read.kt | 24 +- .../net/aquadc/persistence/type/type-enum.kt | 54 ++-- .../net/aquadc/persistence/type/type-glue.kt | 2 +- .../aquadc/persistence/type/types-basic.kt | 26 +- .../net/aquadc/persistence/type/types-okio.kt | 4 +- .../net/aquadc/persistence/type/types.kt | 282 +++++++++++------- .../kotlin/net/aquadc/persistence/util.kt | 8 +- .../properties/persistence/type-enum.kt | 6 +- .../net/aquadc/persistence/sql/ColMeta.kt | 164 ++++++---- .../net/aquadc/persistence/sql/Table.kt | 256 +++++++++++----- .../persistence/sql/blocking/Blocking.kt | 25 +- .../aquadc/persistence/sql/blocking/Eager.kt | 13 +- .../persistence/sql/blocking/JdbcSession.kt | 105 ++++--- .../aquadc/persistence/sql/blocking/Lazy.kt | 36 +-- .../sql/blocking/LowLevelSession.kt | 21 +- .../persistence/sql/blocking/SqliteSession.kt | 108 ++++--- .../net/aquadc/persistence/sql/delegates.kt | 13 +- .../persistence/sql/dialect/BaseDialect.kt | 47 +-- .../persistence/sql/dialect/postgres.kt | 20 +- .../aquadc/persistence/sql/dialect/sqlite.kt | 14 +- .../net/aquadc/persistence/sql/lenses.kt | 41 +-- .../kotlin/net/aquadc/persistence/sql/sql.kt | 3 +- .../net/aquadc/persistence/sql/template.kt | 106 +++---- .../kotlin/net/aquadc/persistence/sql/util.kt | 86 +++--- .../aquadc/persistence/sql/EmbedUtilsTest.kt | 2 +- .../aquadc/persistence/sql/RelationSchemas.kt | 12 +- .../aquadc/persistence/sql/TemplatesTest.kt | 22 +- .../net/aquadc/persistence/sql/postgres.kt | 138 +++++++++ 37 files changed, 1104 insertions(+), 732 deletions(-) diff --git a/android-bindings/src/main/kotlin/net/aquadc/persistence/android/pref/types.kt b/android-bindings/src/main/kotlin/net/aquadc/persistence/android/pref/types.kt index 38e2a04f..0b67f07d 100644 --- a/android-bindings/src/main/kotlin/net/aquadc/persistence/android/pref/types.kt +++ b/android-bindings/src/main/kotlin/net/aquadc/persistence/android/pref/types.kt @@ -30,7 +30,7 @@ internal fun DataType.get(prefs: SharedPreferences, name: String, default * we store 'null' ourselves. If a field has String type, 'null' is stored as Boolean 'false'. * Otherwise 'null' is stored as a String "null". */ -@JvmSynthetic internal val storedAsString = DataType.Simple.Kind.Str + DataType.Simple.Kind.Blob +@JvmSynthetic internal val storedAsString = DataType.NotNull.Simple.Kind.Str + DataType.NotNull.Simple.Kind.Blob @Suppress("UNCHECKED_CAST") private fun DataType.get(prefs: SharedPreferences, key: String): T { @@ -45,35 +45,35 @@ private fun DataType.get(prefs: SharedPreferences, key: String): T { val act = actualType when (act) { is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.Simple<*> -> if (value == (if (act.kind in storedAsString) false else "null")) return null as T - is DataType.Collect<*, *, *>, is DataType.Partial<*, *> -> if (value == false) return null as T + is DataType.NotNull.Simple<*> -> if (value == (if (act.kind in storedAsString) false else "null")) return null as T + is DataType.NotNull.Collect<*, *, *>, is DataType.NotNull.Partial<*, *> -> if (value == false) return null as T } act as DataType } else this return when (type) { is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.Simple -> type.load( + is DataType.NotNull.Simple -> type.load( if (type.hasStringRepresentation) value as CharSequence else when (type.kind) { - DataType.Simple.Kind.Bool -> value as Boolean - DataType.Simple.Kind.I32 -> value as Int - DataType.Simple.Kind.I64 -> value as Long - DataType.Simple.Kind.F32 -> value as Float - DataType.Simple.Kind.F64 -> JavaLangDouble.longBitsToDouble(value as Long) - DataType.Simple.Kind.Str -> value as String - DataType.Simple.Kind.Blob -> Base64.decode(value as String, Base64.DEFAULT) + DataType.NotNull.Simple.Kind.Bool -> value as Boolean + DataType.NotNull.Simple.Kind.I32 -> value as Int + DataType.NotNull.Simple.Kind.I64 -> value as Long + DataType.NotNull.Simple.Kind.F32 -> value as Float + DataType.NotNull.Simple.Kind.F64 -> JavaLangDouble.longBitsToDouble(value as Long) + DataType.NotNull.Simple.Kind.Str -> value as String + DataType.NotNull.Simple.Kind.Blob -> Base64.decode(value as String, Base64.DEFAULT) else -> throw AssertionError() } ) - is DataType.Collect -> type.elementType.let { elementType -> - if (elementType is DataType.Simple<*> && - (elementType.hasStringRepresentation || elementType.kind == DataType.Simple.Kind.Str)) // TODO should store everything in strings + is DataType.NotNull.Collect -> type.elementType.let { elementType -> + if (elementType is DataType.NotNull.Simple<*> && + (elementType.hasStringRepresentation || elementType.kind == DataType.NotNull.Simple.Kind.Str)) // TODO should store everything in strings type.load((value as Set).map(elementType::load)) // todo zero-copy else /* here we have a Collection, including potentially a collection of collections, structs, etc */ serialized(type).load(Base64.decode(value as String, Base64.DEFAULT)) } - is DataType.Partial<*, *> -> serialized(type).load(Base64.decode(value as String, Base64.DEFAULT)) + is DataType.NotNull.Partial<*, *> -> serialized(type).load(Base64.decode(value as String, Base64.DEFAULT)) } } @@ -83,11 +83,11 @@ internal fun DataType.put(editor: SharedPreferences.Editor, key: String, if (value == null) when (act) { is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.Simple<*> -> + is DataType.NotNull.Simple<*> -> if (act.kind in storedAsString) editor.putBoolean(key, false) else editor.putString(key, "null") - is DataType.Collect<*, *, *> -> + is DataType.NotNull.Collect<*, *, *> -> editor.putBoolean(key, false) - is DataType.Partial<*, *> -> + is DataType.NotNull.Partial<*, *> -> editor.putBoolean(key, false) }.also { return } @@ -96,25 +96,25 @@ internal fun DataType.put(editor: SharedPreferences.Editor, key: String, when (type) { is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.Simple -> + is DataType.NotNull.Simple -> if (type.hasStringRepresentation) editor.putString(key, type.storeAsString(value).toString()) else type.store(value).let { v -> when (type.kind) { - DataType.Simple.Kind.Bool -> editor.putBoolean(key, v as Boolean) - DataType.Simple.Kind.I32 -> editor.putInt(key, v as Int) - DataType.Simple.Kind.I64 -> editor.putLong(key, v as Long) - DataType.Simple.Kind.F32 -> editor.putFloat(key, v as Float) - DataType.Simple.Kind.F64 -> editor.putLong(key, java.lang.Double.doubleToLongBits(v as Double)) - DataType.Simple.Kind.Str -> editor.putString(key, v as String) - DataType.Simple.Kind.Blob -> editor.putString(key, Base64.encodeToString(v as ByteArray, Base64.DEFAULT)) + DataType.NotNull.Simple.Kind.Bool -> editor.putBoolean(key, v as Boolean) + DataType.NotNull.Simple.Kind.I32 -> editor.putInt(key, v as Int) + DataType.NotNull.Simple.Kind.I64 -> editor.putLong(key, v as Long) + DataType.NotNull.Simple.Kind.F32 -> editor.putFloat(key, v as Float) + DataType.NotNull.Simple.Kind.F64 -> editor.putLong(key, java.lang.Double.doubleToLongBits(v as Double)) + DataType.NotNull.Simple.Kind.Str -> editor.putString(key, v as String) + DataType.NotNull.Simple.Kind.Blob -> editor.putString(key, Base64.encodeToString(v as ByteArray, Base64.DEFAULT)) else -> throw AssertionError() } } - is DataType.Collect -> type.elementType.let { elementType -> - if (elementType is DataType.Simple && (elementType.hasStringRepresentation || elementType.kind == DataType.Simple.Kind.Str)) + is DataType.NotNull.Collect -> type.elementType.let { elementType -> + if (elementType is DataType.NotNull.Simple && (elementType.hasStringRepresentation || elementType.kind == DataType.NotNull.Simple.Kind.Str)) editor.putStringSet( key, type.store(value) .fatMapTo, T, String>(HashSet()) { v -> - (elementType as DataType.Simple) + (elementType as DataType.NotNull.Simple) .let { if (it.hasStringRepresentation) it.storeAsString(v) else it.store(v) as CharSequence } .toString() } @@ -122,7 +122,7 @@ internal fun DataType.put(editor: SharedPreferences.Editor, key: String, else editor.putString(key, Base64.encodeToString(serialized(type).store(value) as ByteArray, Base64.DEFAULT)) } - is DataType.Partial<*, *> -> + is DataType.NotNull.Partial<*, *> -> editor.putString(key, Base64.encodeToString(serialized(type).store(value) as ByteArray, Base64.DEFAULT)) } } diff --git a/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/either/eitherTypes.kt b/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/either/eitherTypes.kt index a0066e05..95a2d29e 100644 --- a/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/either/eitherTypes.kt +++ b/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/either/eitherTypes.kt @@ -19,7 +19,7 @@ import net.aquadc.persistence.type.DataType private class EitherType>( schema: SCH -) : DataType.Partial, SCH>(schema) { +) : DataType.NotNull.Partial, SCH>(schema) { override fun load(fields: FieldSet>, values: Any?): Either8 = when (schema.single(fields).ordinal.toInt()) { @@ -46,12 +46,12 @@ private class EitherType>( fun , B, DB : DataType> either( firstName: String, firstType: DA, secondName: String, secondType: DB -): DataType.Partial, Tuple> = +): DataType.NotNull.Partial, Tuple> = EitherType(Tuple(firstName, firstType, secondName, secondType)) @JvmSynthetic // useless for Java operator fun , B, DB : DataType> - DA.plus(second: DB): DataType.Partial, Tuple> = + DA.plus(second: DB): DataType.NotNull.Partial, Tuple> = either("first", this, "second", second) @@ -59,14 +59,14 @@ fun , B, DB : DataType, C, DC : DataType> either3( firstName: String, firstType: DA, secondName: String, secondType: DB, thirdName: String, thirdType: DC -): DataType.Partial, Tuple3> = +): DataType.NotNull.Partial, Tuple3> = EitherType(Tuple3( firstName, firstType, secondName, secondType, thirdName, thirdType )) @JvmSynthetic // useless for Java operator fun , B, DB : DataType, C, DC : DataType> - DataType.Partial, Tuple>.plus(third: DC): DataType.Partial, Tuple3> = + DataType.NotNull.Partial, Tuple>.plus(third: DC): DataType.NotNull.Partial, Tuple3> = either3("first", schema.run { First.type }, "second", schema.run { Second.type }, "third", third) @@ -75,14 +75,14 @@ fun , B, DB : DataType, C, DC : DataType, D, DD : Data secondName: String, secondType: DB, thirdName: String, thirdType: DC, fourthName: String, fourthType: DD -): DataType.Partial, Tuple4> = +): DataType.NotNull.Partial, Tuple4> = EitherType(Tuple4( firstName, firstType, secondName, secondType, thirdName, thirdType, fourthName, fourthType )) @JvmSynthetic // useless for Java @JvmName("e3plus") operator fun , B, DB : DataType, C, DC : DataType, D, DD : DataType> - DataType.Partial, Tuple3>.plus(fourth: DD): DataType.Partial, Tuple4> = + DataType.NotNull.Partial, Tuple3>.plus(fourth: DD): DataType.NotNull.Partial, Tuple4> = either4("first", schema.run { First.type }, "second", schema.run { Second.type }, "third", schema.run { Third.type }, "fourth", fourth) @@ -92,7 +92,7 @@ fun , B, DB : DataType, C, DC : DataType, D, DD : Data thirdName: String, thirdType: DC, fourthName: String, fourthType: DD, fifthName: String, fifthType: DE -): DataType.Partial, Tuple5> = +): DataType.NotNull.Partial, Tuple5> = EitherType(Tuple5( firstName, firstType, secondName, secondType, thirdName, thirdType, fourthName, fourthType, fifthName, fifthType @@ -100,7 +100,7 @@ fun , B, DB : DataType, C, DC : DataType, D, DD : Data @JvmSynthetic // useless for Java @JvmName("e4plus") operator fun , B, DB : DataType, C, DC : DataType, D, DD : DataType, E, DE : DataType> - DataType.Partial, Tuple4>.plus(fifth: DE): DataType.Partial, Tuple5> = + DataType.NotNull.Partial, Tuple4>.plus(fifth: DE): DataType.NotNull.Partial, Tuple5> = either5("first", schema.run { First.type }, "second", schema.run { Second.type }, "third", schema.run { Third.type }, "fourth", schema.run { Fourth.type }, "fifth", fifth) @@ -111,7 +111,7 @@ fun , B, DB : DataType, C, DC : DataType, D, DD : Data fourthName: String, fourthType: DD, fifthName: String, fifthType: DE, sixthName: String, sixthType: DF -): DataType.Partial, Tuple6> = +): DataType.NotNull.Partial, Tuple6> = EitherType(Tuple6( firstName, firstType, secondName, secondType, thirdName, thirdType, fourthName, fourthType, fifthName, fifthType, sixthName, sixthType @@ -119,7 +119,7 @@ fun , B, DB : DataType, C, DC : DataType, D, DD : Data @JvmSynthetic // useless for Java @JvmName("e5plus") operator fun , B, DB : DataType, C, DC : DataType, D, DD : DataType, E, DE : DataType, F, DF : DataType> - DataType.Partial, Tuple5>.plus(sixth: DF): DataType.Partial, Tuple6> = + DataType.NotNull.Partial, Tuple5>.plus(sixth: DF): DataType.NotNull.Partial, Tuple6> = either6("first", schema.run { First.type }, "second", schema.run { Second.type }, "third", schema.run { Third.type }, "fourth", schema.run { Fourth.type }, "fifth", schema.run { Fifth.type }, "sixth", sixth) @@ -131,7 +131,7 @@ fun , B, DB : DataType, C, DC : DataType, D, DD : Data fifthName: String, fifthType: DE, sixthName: String, sixthType: DF, seventhName: String, seventhType: DG -): DataType.Partial, Tuple7> = +): DataType.NotNull.Partial, Tuple7> = EitherType(Tuple7( firstName, firstType, secondName, secondType, thirdName, thirdType, fourthName, fourthType, fifthName, fifthType, sixthName, sixthType, seventhName, seventhType @@ -139,7 +139,7 @@ fun , B, DB : DataType, C, DC : DataType, D, DD : Data @JvmSynthetic // useless for Java @JvmName("e6plus") operator fun , B, DB : DataType, C, DC : DataType, D, DD : DataType, E, DE : DataType, F, DF : DataType, G, DG : DataType> - DataType.Partial, Tuple6>.plus(seventh: DG): DataType.Partial, Tuple7> = + DataType.NotNull.Partial, Tuple6>.plus(seventh: DG): DataType.NotNull.Partial, Tuple7> = either7("first", schema.run { First.type }, "second", schema.run { Second.type }, "third", schema.run { Third.type }, "fourth", schema.run { Fourth.type }, "fifth", schema.run { Fifth.type }, "sixth", schema.run { Sixth.type }, "seventh", seventh) @@ -152,7 +152,7 @@ fun , B, DB : DataType, C, DC : DataType, D, DD : Data sixthName: String, sixthType: DF, seventhName: String, seventhType: DG, eighthName: String, eighthType: DH -): DataType.Partial, Tuple8> = +): DataType.NotNull.Partial, Tuple8> = EitherType(Tuple8( firstName, firstType, secondName, secondType, thirdName, thirdType, fourthName, fourthType, fifthName, fifthType, sixthName, sixthType, seventhName, seventhType, eighthName, eighthType @@ -160,7 +160,7 @@ fun , B, DB : DataType, C, DC : DataType, D, DD : Data @JvmSynthetic // useless for Java @JvmName("e7plus") operator fun , B, DB : DataType, C, DC : DataType, D, DD : DataType, E, DE : DataType, F, DF : DataType, G, DG : DataType, H, DH : DataType> - DataType.Partial, Tuple7>.plus(eighth: DH): DataType.Partial, Tuple8> = + DataType.NotNull.Partial, Tuple7>.plus(eighth: DH): DataType.NotNull.Partial, Tuple8> = either8("first", schema.run { First.type }, "second", schema.run { Second.type }, "third", schema.run { Third.type }, "fourth", schema.run { Fourth.type }, "fifth", schema.run { Fifth.type }, "sixth", schema.run { Sixth.type }, "seventh", schema.run { Seventh.type }, "eighth", eighth) @@ -170,7 +170,7 @@ fun , B, DB : DataType, C, DC : DataType, D, DD : Data */ @JvmSynthetic // useless for Java @JvmName("e8plus") @Deprecated("Either9+ are not implemented", level = DeprecationLevel.ERROR) operator fun , B, DB : DataType, C, DC : DataType, D, DD : DataType, E, DE : DataType, F, DF : DataType, G, DG : DataType, H, DH : DataType, I, DI : DataType> - DataType.Partial, Tuple8>.plus(ninth: DI): Nothing = + DataType.NotNull.Partial, Tuple8>.plus(ninth: DI): Nothing = throw UnsupportedOperationException() /** @@ -179,6 +179,6 @@ fun , B, DB : DataType, C, DC : DataType, D, DD : Data */ @JvmSynthetic // useless for Java @JvmName("plusE") @Deprecated("right operand is not expected to be Either", level = DeprecationLevel.ERROR) operator fun - DataType.Partial, out Tuple8<*, *, *, *, *, *, *, *, *, *, *, *, *, *, *, *>> - .plus(other: DataType.Partial, out Tuple8<*, *, *, *, *, *, *, *, *, *, *, *, *, *, *, *>>): Nothing = + DataType.NotNull.Partial, out Tuple8<*, *, *, *, *, *, *, *, *, *, *, *, *, *, *, *>> + .plus(other: DataType.NotNull.Partial, out Tuple8<*, *, *, *, *, *, *, *, *, *, *, *, *, *, *, *>>): Nothing = throw UnsupportedOperationException() diff --git a/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/graphics.kt b/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/graphics.kt index 94af15ba..06f585c2 100644 --- a/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/graphics.kt +++ b/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/graphics.kt @@ -9,7 +9,7 @@ import net.aquadc.persistence.type.SimpleValue * Represents AARRGGBB colour as an [Int]. * For text formats, uses #AARRGGBB; when alpha==FF, uses #RRGGBB. */ -val colour: DataType.Simple = object : StringableSimpleType(Kind.I32) { +val colour: DataType.NotNull.Simple = object : StringableSimpleType(Kind.I32) { override fun load(value: SimpleValue): Int = if (value is CharSequence) parse(value) else value as Int override fun store(value: Int): SimpleValue = value diff --git a/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/partial.kt b/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/partial.kt index 72da8f7f..c3040717 100644 --- a/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/partial.kt +++ b/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/partial.kt @@ -25,8 +25,8 @@ import kotlin.contracts.contract private val EmptyArray = emptyArray() -fun > partial(schema: SCH): DataType.Partial, SCH> = - object : DataType.Partial, SCH>(schema) { +fun > partial(schema: SCH): DataType.NotNull.Partial, SCH> = + object : DataType.NotNull.Partial, SCH>(schema) { override fun load(fields: FieldSet>, values: Any?): PartialStruct = PartialStructSnapshot(schema, fields, when (fields.size) { diff --git a/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/primitive collections.kt b/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/primitive collections.kt index 91d3a237..5b7d92fd 100644 --- a/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/primitive collections.kt +++ b/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/primitive collections.kt @@ -3,12 +3,10 @@ package net.aquadc.persistence.extended import net.aquadc.persistence.type.AnyCollection import net.aquadc.persistence.type.DataType -import net.aquadc.persistence.type.i8 import net.aquadc.persistence.type.f64 import net.aquadc.persistence.type.f32 import net.aquadc.persistence.type.i32 import net.aquadc.persistence.type.i64 -import net.aquadc.persistence.type.i16 /** @@ -27,28 +25,28 @@ val shortCollection: Nothing get() = throw AssertionError() * Stores [IntArray] instances as collections of [Int]s. */ @JvmField -val intCollection: DataType.Collect> = ArrayNoOp(i32) +val intCollection: DataType.NotNull.Collect> = ArrayNoOp(i32) /** * Stores [LongArray] instances as collections of [Long]s. */ @JvmField -val longCollection: DataType.Collect> = ArrayNoOp(i64) +val longCollection: DataType.NotNull.Collect> = ArrayNoOp(i64) /** * Stores [FloatArray] instances as collections of [Float]s. */ @JvmField -val floatCollection: DataType.Collect> = ArrayNoOp(f32) +val floatCollection: DataType.NotNull.Collect> = ArrayNoOp(f32) /** * Stores [DoubleArray] instances as collections of [Double]s. */ @JvmField -val doubleCollection: DataType.Collect> = ArrayNoOp(f64) +val doubleCollection: DataType.NotNull.Collect> = ArrayNoOp(f64) -private class ArrayNoOp(type: Simple) : DataType.Collect>(type) { +private class ArrayNoOp(type: Simple) : DataType.NotNull.Collect>(type) { override fun load(value: AnyCollection): C { val kind = elementType.kind diff --git a/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/uuid.kt b/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/uuid.kt index 89c93d81..6ac05e4d 100644 --- a/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/uuid.kt +++ b/extended-persistence/src/main/kotlin/net/aquadc/persistence/extended/uuid.kt @@ -3,16 +3,17 @@ package net.aquadc.persistence.extended import net.aquadc.persistence.type.DataType import net.aquadc.persistence.type.SimpleValue -import java.util.* +import java.util.UUID -val uuid: DataType.Simple = object : StringableSimpleType(Kind.Blob) { +val uuid: DataType.NotNull.Simple = object : StringableSimpleType(Kind.Blob) { override fun load(value: SimpleValue): UUID = if (value is CharSequence) UUID.fromString(value.toString()) else fromBytes(value as ByteArray) override fun store(value: UUID): SimpleValue = toBytes(value.mostSignificantBits, value.leastSignificantBits) override fun storeAsString(value: UUID): CharSequence = value.toString() + // copy-paste from https://stackoverflow.com/a/27610608/3050249 private fun fromBytes(b: ByteArray): UUID = UUID( b2l(b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]), b2l(b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]) ) @@ -25,7 +26,6 @@ val uuid: DataType.Simple = object : StringableSimpleType(Kind.Blob) (b2.toLong() and 0xff shl 16) or (b1.toLong() and 0xff shl 8) or (b0.toLong() and 0xff) - private fun toBytes(hi: Long, lo: Long): ByteArray = byteArrayOf( (hi shr 56).toByte(), (hi shr 48).toByte(), diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/stream/read-write.kt b/persistence/src/main/kotlin/net/aquadc/persistence/stream/read-write.kt index e998f33c..00f7ee57 100644 --- a/persistence/src/main/kotlin/net/aquadc/persistence/stream/read-write.kt +++ b/persistence/src/main/kotlin/net/aquadc/persistence/stream/read-write.kt @@ -39,20 +39,20 @@ fun DataType.write(writer: BetterDataOutput, output: D, value: T) { when (type) { is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.Simple -> + is DataType.NotNull.Simple -> output.simple(value, nullable, type, writer) - is DataType.Collect -> - output.collection(value, nullable, type as DataType.Collect>, writer) - is DataType.Partial<*, *> -> - output.partial(value, nullable, type as DataType.Partial, writer) + is DataType.NotNull.Collect -> + output.collection(value, nullable, type as DataType.NotNull.Collect>, writer) + is DataType.NotNull.Partial<*, *> -> + output.partial(value, nullable, type as DataType.NotNull.Partial, writer) } } -private fun D.simple(arg: T, nullable: Boolean, type: DataType.Simple, output: BetterDataOutput) { +private fun D.simple(arg: T, nullable: Boolean, type: DataType.NotNull.Simple, output: BetterDataOutput) { val arg: SimpleValue? = if (nullable && arg === null) null else type.store(arg) // these values can be put into stream along with nullability info when (type.kind) { - DataType.Simple.Kind.Bool -> + DataType.NotNull.Simple.Kind.Bool -> return output.writeByte(this, when (arg as Boolean?) { true -> 1 @@ -60,8 +60,8 @@ private fun D.simple(arg: T, nullable: Boolean, type: DataType.Simple, null -> -1 }.toByte() ) - DataType.Simple.Kind.Str -> return output.writeString(this, arg as String?) - DataType.Simple.Kind.Blob -> return output.writeBytes(this, arg as ByteArray?) + DataType.NotNull.Simple.Kind.Str -> return output.writeString(this, arg as String?) + DataType.NotNull.Simple.Kind.Blob -> return output.writeBytes(this, arg as ByteArray?) else -> { /* continue */ } } @@ -73,15 +73,15 @@ private fun D.simple(arg: T, nullable: Boolean, type: DataType.Simple, } when (type.kind) { - DataType.Simple.Kind.I32 -> output.writeInt(this, arg as Int) - DataType.Simple.Kind.I64 -> output.writeLong(this, arg as Long) - DataType.Simple.Kind.F32 -> output.writeInt(this, java.lang.Float.floatToIntBits(arg as Float)) - DataType.Simple.Kind.F64 -> output.writeLong(this, java.lang.Double.doubleToLongBits(arg as Double)) - DataType.Simple.Kind.Bool, DataType.Simple.Kind.Str, DataType.Simple.Kind.Blob -> throw AssertionError() + DataType.NotNull.Simple.Kind.I32 -> output.writeInt(this, arg as Int) + DataType.NotNull.Simple.Kind.I64 -> output.writeLong(this, arg as Long) + DataType.NotNull.Simple.Kind.F32 -> output.writeInt(this, java.lang.Float.floatToIntBits(arg as Float)) + DataType.NotNull.Simple.Kind.F64 -> output.writeLong(this, java.lang.Double.doubleToLongBits(arg as Double)) + DataType.NotNull.Simple.Kind.Bool, DataType.NotNull.Simple.Kind.Str, DataType.NotNull.Simple.Kind.Blob -> throw AssertionError() }//.also { ] } -private fun D.collection(arg: T, nullable: Boolean, type: DataType.Collect>, output: BetterDataOutput) { +private fun D.collection(arg: T, nullable: Boolean, type: DataType.NotNull.Collect>, output: BetterDataOutput) { val arg: AnyCollection? = if (nullable && arg === null) null else type.store(arg) if (arg === null) { output.writeInt(this, -1) @@ -94,7 +94,7 @@ private fun D.collection(arg: T, nullable: Boolean, type: DataType.Col } } -private fun , D, T> D.partial(arg: T, nullable: Boolean, type: DataType.Partial, output: BetterDataOutput) { +private fun , D, T> D.partial(arg: T, nullable: Boolean, type: DataType.NotNull.Partial, output: BetterDataOutput) { if (nullable && arg === null) { output.writeByte(this, (-1).toByte()) } else { @@ -131,22 +131,22 @@ fun BetterDataInput.read(input: D, type: DataType): T { return when (actual) { is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.Simple -> + is DataType.NotNull.Simple -> input.simple(nullable, actual, this) - is DataType.Collect -> - input.collection(nullable, actual as DataType.Collect>, this) - is DataType.Partial<*, *> -> - input.partial(nullable, actual as DataType.Partial, this) + is DataType.NotNull.Collect -> + input.collection(nullable, actual as DataType.NotNull.Collect>, this) + is DataType.NotNull.Partial<*, *> -> + input.partial(nullable, actual as DataType.NotNull.Partial, this) } } private val boolDictionary = arrayOf(null, false, true) -private fun D.simple(nullable: Boolean, type: DataType.Simple, input: BetterDataInput): T { +private fun D.simple(nullable: Boolean, type: DataType.NotNull.Simple, input: BetterDataInput): T { @Suppress("IMPLICIT_CAST_TO_ANY") val value = when (type.kind) { - DataType.Simple.Kind.Bool -> boolDictionary[input.readByte(this).toInt() + 1] - DataType.Simple.Kind.Str -> input.readString(this) - DataType.Simple.Kind.Blob -> input.readBytes(this) + DataType.NotNull.Simple.Kind.Bool -> boolDictionary[input.readByte(this).toInt() + 1] + DataType.NotNull.Simple.Kind.Str -> input.readString(this) + DataType.NotNull.Simple.Kind.Blob -> input.readBytes(this) else -> Unit // continue } if (value != Unit) { @@ -160,16 +160,16 @@ private fun D.simple(nullable: Boolean, type: DataType.Simple, input: } return type.load(when (type.kind) { - DataType.Simple.Kind.I32 -> input.readInt(this) - DataType.Simple.Kind.I64 -> input.readLong(this) - DataType.Simple.Kind.F32 -> java.lang.Float.intBitsToFloat(input.readInt(this)) - DataType.Simple.Kind.F64 -> java.lang.Double.longBitsToDouble(input.readLong(this)) - DataType.Simple.Kind.Bool, DataType.Simple.Kind.Str, DataType.Simple.Kind.Blob -> throw AssertionError() + DataType.NotNull.Simple.Kind.I32 -> input.readInt(this) + DataType.NotNull.Simple.Kind.I64 -> input.readLong(this) + DataType.NotNull.Simple.Kind.F32 -> java.lang.Float.intBitsToFloat(input.readInt(this)) + DataType.NotNull.Simple.Kind.F64 -> java.lang.Double.longBitsToDouble(input.readLong(this)) + DataType.NotNull.Simple.Kind.Bool, DataType.NotNull.Simple.Kind.Str, DataType.NotNull.Simple.Kind.Blob -> throw AssertionError() else -> throw AssertionError() }) } -private fun D.collection(nullable: Boolean, type: DataType.Collect>, input: BetterDataInput): T { +private fun D.collection(nullable: Boolean, type: DataType.NotNull.Collect>, input: BetterDataInput): T { return when (val count = input.readInt(this)) { -1 -> check(nullable).let { null as T } 0 -> type.load(emptyList()) @@ -180,7 +180,7 @@ private fun D.collection(nullable: Boolean, type: DataType.Collect>() -private fun > D.partial(nullable: Boolean, type: DataType.Partial, input: BetterDataInput): T = +private fun > D.partial(nullable: Boolean, type: DataType.NotNull.Partial, input: BetterDataInput): T = input.readByte(this).toInt().let { size -> if (size == -1) { check(nullable) diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/struct/schema.kt b/persistence/src/main/kotlin/net/aquadc/persistence/struct/schema.kt index 9e3e9660..8f9a4f50 100644 --- a/persistence/src/main/kotlin/net/aquadc/persistence/struct/schema.kt +++ b/persistence/src/main/kotlin/net/aquadc/persistence/struct/schema.kt @@ -14,7 +14,7 @@ import net.aquadc.persistence.type.DataType * @see Struct * @see FieldDef */ -abstract class Schema> : DataType.Partial, SELF>() { +abstract class Schema> : DataType.NotNull.Partial, SELF>() { /** * A temporary list of [FieldDef]s used while [Schema] is getting constructed. diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/tokens/emit.kt b/persistence/src/main/kotlin/net/aquadc/persistence/tokens/emit.kt index e02e1cae..9f21f752 100644 --- a/persistence/src/main/kotlin/net/aquadc/persistence/tokens/emit.kt +++ b/persistence/src/main/kotlin/net/aquadc/persistence/tokens/emit.kt @@ -29,27 +29,27 @@ private suspend fun TokenStreamScope.yield(type: DataType, value: T) { when (type) { is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.Simple -> { + is DataType.NotNull.Simple -> { val token = if (type.hasStringRepresentation) Token.Str else kindToToken[type.kind]!! offer(token).let { coerceTo -> // internal API, he-he if (coerceTo != false) { - val stored = (type as DataType.Simple) + val stored = (type as DataType.NotNull.Simple) .let { if (it.hasStringRepresentation) it.storeAsString(value) else it.store(value) } yield((coerceTo as Token?).coerce(stored)) } } } - is DataType.Collect<*, *, *> -> { + is DataType.NotNull.Collect<*, *, *> -> { yieldSequence { val elT = type.elementType - (type as DataType.Collect).store(value).fatAsList().forEach { + (type as DataType.NotNull.Collect).store(value).fatAsList().forEach { yield(elT as DataType, it) } } } - is DataType.Partial<*, *> -> { + is DataType.NotNull.Partial<*, *> -> { yieldDictionary { - type as DataType.Partial> + type as DataType.NotNull.Partial> val fields = type.fields(value) val values = type.store(value) val schema: Schema> = type.schema as Schema> diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/tokens/read.kt b/persistence/src/main/kotlin/net/aquadc/persistence/tokens/read.kt index 932e6ff5..66f5961f 100644 --- a/persistence/src/main/kotlin/net/aquadc/persistence/tokens/read.kt +++ b/persistence/src/main/kotlin/net/aquadc/persistence/tokens/read.kt @@ -31,21 +31,21 @@ fun TokenStream.readAs(type: DataType): T { return when (type) { is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.Simple -> { + is DataType.NotNull.Simple -> { val token = if (type.hasStringRepresentation) Token.Str else kindToToken[type.kind]!! // !! exhaustive mapping type.load(poll(token)!!) as T // !! we never pass Token.Null so source must not return `null`s } - is DataType.Collect<*, *, *> -> { + is DataType.NotNull.Collect<*, *, *> -> { type.load(readListOf(type.elementType)) as T } - is DataType.Partial<*, *> -> { + is DataType.NotNull.Partial<*, *> -> { poll(Token.BeginDictionary) val sch = type.schema val struct = readPartial( - type as DataType.Partial, fieldValues, - { nextField(sch) as FieldDef? }, { readAs(it) } + type as DataType.NotNull.Partial, fieldValues, + { nextField(sch) as FieldDef? }, { readAs(it) } ) poll(Token.EndDictionary) struct as T @@ -197,12 +197,12 @@ internal class TokensIterator, T>( } @JvmSynthetic internal val kindToToken = enumMapOf( - DataType.Simple.Kind.Bool, Token.Bool, - DataType.Simple.Kind.I32, Token.I32, - DataType.Simple.Kind.I64, Token.I64, - DataType.Simple.Kind.F32, Token.F32, - DataType.Simple.Kind.F64, Token.F64, - DataType.Simple.Kind.Str, Token.Str + DataType.NotNull.Simple.Kind.Bool, Token.Bool, + DataType.NotNull.Simple.Kind.I32, Token.I32, + DataType.NotNull.Simple.Kind.I64, Token.I64, + DataType.NotNull.Simple.Kind.F32, Token.F32, + DataType.NotNull.Simple.Kind.F64, Token.F64, + DataType.NotNull.Simple.Kind.Str, Token.Str ).also { - it[DataType.Simple.Kind.Blob] = Token.Blob // enumMapOf has max. 8 key-value pairs + it[DataType.NotNull.Simple.Kind.Blob] = Token.Blob // enumMapOf has max. 8 key-value pairs } diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/type/type-enum.kt b/persistence/src/main/kotlin/net/aquadc/persistence/type/type-enum.kt index 20dae03d..f98b4094 100644 --- a/persistence/src/main/kotlin/net/aquadc/persistence/type/type-enum.kt +++ b/persistence/src/main/kotlin/net/aquadc/persistence/type/type-enum.kt @@ -19,11 +19,11 @@ import kotlin.collections.HashSet */ @JvmName("enumeration") @Suppress("UNCHECKED_CAST") // NoConstant is intentionally erased inline fun enum( - values: Array, - encodeAs: DataType.Simple, - noinline encode: (E) -> U, - noinline fallback: (U) -> E = NoConstant(E::class.java) as (Any?) -> Nothing -): DataType.Simple = + values: Array, + encodeAs: DataType.NotNull.Simple, + noinline encode: (E) -> U, + noinline fallback: (U) -> E = NoConstant(E::class.java) as (Any?) -> Nothing +): DataType.NotNull.Simple = enumInternal(values, encodeAs, encode, fallback) /** @@ -35,12 +35,12 @@ inline fun enum( */ @Suppress("UNCHECKED_CAST") @PublishedApi internal fun enumInternal( - values: Array, - encodeAs: DataType.Simple, - encode: (E) -> U, - fallback: (U) -> E -): DataType.Simple = - object : DataType.Simple(encodeAs.kind) { + values: Array, + encodeAs: DataType.NotNull.Simple, + encode: (E) -> U, + fallback: (U) -> E +): DataType.NotNull.Simple = + object : DataType.NotNull.Simple(encodeAs.kind) { private val lookup = values.associateByTo(newMap(values.size), encode).also { check(it.size == values.size) { @@ -55,7 +55,7 @@ inline fun enum( override fun store(value: Any?): SimpleValue = encodeAs.store(encode.invoke(value as E)) - } as DataType.Simple + } as DataType.NotNull.Simple // EnumSet @@ -68,10 +68,10 @@ inline fun enum( * @param ordinal a getter for `values.indexOf(value)` */ inline fun enumSet( - values: Array, - encodeAs: DataType.Simple, - noinline ordinal: (E) -> Int -): DataType.Simple> = + values: Array, + encodeAs: DataType.NotNull.Simple, + noinline ordinal: (E) -> Int +): DataType.NotNull.Simple> = enumSetInternal(E::class.java, values, encodeAs, ordinal) /** @@ -79,20 +79,20 @@ inline fun enumSet( * Finds an array of values automatically. */ inline fun > enumSet( - encodeAs: DataType.Simple, - noinline ordinal: (E) -> Int -): DataType.Simple> = + encodeAs: DataType.NotNull.Simple, + noinline ordinal: (E) -> Int +): DataType.NotNull.Simple> = enumSetInternal(E::class.java, enumValues(), encodeAs, ordinal) @Suppress("UNCHECKED_CAST") @PublishedApi internal fun enumSetInternal( - type: Class, - values: Array, - encodeAs: DataType.Simple, - ordinal: (E) -> Int -): DataType.Simple> = - object : DataType.Simple(encodeAs.kind) { + type: Class, + values: Array, + encodeAs: DataType.NotNull.Simple, + ordinal: (E) -> Int +): DataType.NotNull.Simple> = + object : DataType.NotNull.Simple(encodeAs.kind) { init { if (values.size > 64) throw UnsupportedOperationException("Enums with >64 values (JumboEnumSets) are not supported.") @@ -117,7 +117,7 @@ inline fun > enumSet( override fun store(value: Any?): SimpleValue = encodeAs.store((value as Set).fold(0L) { acc, e -> acc or (1L shl ordinal(e)) }) - } as DataType.Simple> + } as DataType.NotNull.Simple> /** * Creates a [Set]<[Enum]> type implementation for storing enum set as a collection of values. @@ -125,7 +125,7 @@ inline fun > enumSet( */ inline fun > enumSet( encodeAs: DE -): DataType.Collect, E, DE> = +): DataType.NotNull.Collect, E, DE> = setInternal(encodeAs, E::class.java.takeIf { it.isEnum }) diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/type/type-glue.kt b/persistence/src/main/kotlin/net/aquadc/persistence/type/type-glue.kt index 3f5be19e..8a48f4c1 100644 --- a/persistence/src/main/kotlin/net/aquadc/persistence/type/type-glue.kt +++ b/persistence/src/main/kotlin/net/aquadc/persistence/type/type-glue.kt @@ -13,7 +13,7 @@ import java.io.DataOutputStream /** * Represents [T] as a [ByteArray] (blob). */ -fun serialized(type: DataType): DataType.Simple = object : DataType.Simple(Kind.Blob) { +fun serialized(type: DataType): DataType.NotNull.Simple = object : DataType.NotNull.Simple(Kind.Blob) { override fun load(value: SimpleValue): T = DataStreams.read(DataInputStream(ByteArrayInputStream(value as ByteArray)), type) diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/type/types-basic.kt b/persistence/src/main/kotlin/net/aquadc/persistence/type/types-basic.kt index 85036ecb..33cc221c 100644 --- a/persistence/src/main/kotlin/net/aquadc/persistence/type/types-basic.kt +++ b/persistence/src/main/kotlin/net/aquadc/persistence/type/types-basic.kt @@ -6,7 +6,7 @@ import net.aquadc.persistence.fatTo import java.util.EnumSet -private class SimpleNoOp(kind: Kind) : DataType.Simple(kind) { +private class SimpleNoOp(kind: Kind) : DataType.NotNull.Simple(kind) { @Suppress("UNCHECKED_CAST") override fun load(value: SimpleValue): T { @@ -39,7 +39,7 @@ private class SimpleNoOp(kind: Kind) : DataType.Simple(kind) { /** * Describes [Boolean] instances. */ -@JvmField val bool: DataType.Simple = SimpleNoOp(DataType.Simple.Kind.Bool) +@JvmField val bool: DataType.NotNull.Simple = SimpleNoOp(DataType.NotNull.Simple.Kind.Bool) /** * Describes [Byte] instances. @@ -60,31 +60,31 @@ val short: Nothing get() = throw AssertionError() /** * Describes [Int] instances. */ -@JvmField val i32: DataType.Simple = SimpleNoOp(DataType.Simple.Kind.I32) +@JvmField val i32: DataType.NotNull.Simple = SimpleNoOp(DataType.NotNull.Simple.Kind.I32) @JvmField @Deprecated("renamed", ReplaceWith("i32"), level = DeprecationLevel.ERROR) val int = i32 /** * Describes [Long] instances. */ -@JvmField val i64: DataType.Simple = SimpleNoOp(DataType.Simple.Kind.I64) +@JvmField val i64: DataType.NotNull.Simple = SimpleNoOp(DataType.NotNull.Simple.Kind.I64) @JvmField @Deprecated("renamed", ReplaceWith("i64"), level = DeprecationLevel.ERROR) val long = i64 /** * Describes [Float] instances. */ -@JvmField val f32: DataType.Simple = SimpleNoOp(DataType.Simple.Kind.F32) +@JvmField val f32: DataType.NotNull.Simple = SimpleNoOp(DataType.NotNull.Simple.Kind.F32) @JvmField @Deprecated("renamed", ReplaceWith("f32"), level = DeprecationLevel.ERROR) val float = f32 /** * Describes [Double] instances. */ -@JvmField val f64: DataType.Simple = SimpleNoOp(DataType.Simple.Kind.F64) +@JvmField val f64: DataType.NotNull.Simple = SimpleNoOp(DataType.NotNull.Simple.Kind.F64) @JvmField @Deprecated("renamed", ReplaceWith("f64"), level = DeprecationLevel.ERROR) val double = f64 /** * Describes [String] instances. */ -@JvmField val string: DataType.Simple = SimpleNoOp(DataType.Simple.Kind.Str) +@JvmField val string: DataType.NotNull.Simple = SimpleNoOp(DataType.NotNull.Simple.Kind.Str) /** * Describes [ByteArray] instances. @@ -96,7 +96,7 @@ val short: Nothing get() = throw AssertionError() "Consider using immutable ByteString instead.", ReplaceWith("byteString") ) -@JvmField val byteArray: DataType.Simple = SimpleNoOp(DataType.Simple.Kind.Blob) +@JvmField val byteArray: DataType.NotNull.Simple = SimpleNoOp(DataType.NotNull.Simple.Kind.Blob) /** * Describes `T?` instances. @@ -106,7 +106,7 @@ inline fun > nullable(type: DT): DataType.Nullable, E, DE : DataType>(elementType: DE) - : DataType.Collect(elementType) { + : DataType.NotNull.Collect(elementType) { override fun store(value: C): AnyCollection = value @@ -117,7 +117,7 @@ internal abstract class CollectBase, E, DE : DataType>(elem * Represents a [Collection] of [E]. * Despite it is represented as a [List], duplicates handling depends on the underlying storage. */ -fun > collection(elementType: DE): DataType.Collect, E, DE> = +fun > collection(elementType: DE): DataType.NotNull.Collect, E, DE> = object : CollectBase, E, DE>(elementType) { override fun load(value: AnyCollection): List = value.fatAsList() as List // almost always zero copy @@ -126,7 +126,7 @@ fun > collection(elementType: DE): DataType.Collect, /** * Represents a [Set] of [E]. */ -fun > set(elementType: DE): DataType.Collect, E, DE> = +fun > set(elementType: DE): DataType.NotNull.Collect, E, DE> = setInternal(elementType, null) @PublishedApi internal fun > setInternal(elementType: DE, enumType: Class?): CollectBase, E, DE> { @@ -152,7 +152,7 @@ fun > set(elementType: DE): DataType.Collect, E, DE> fun list(@Suppress("UNUSED_PARAMETER") elementType: DataType): Nothing = throw UnsupportedOperationException() -@JvmField val nothing: DataType.Simple = object : DataType.Simple(Kind.I32) { +@JvmField val nothing: DataType.NotNull.Simple = object : DataType.NotNull.Simple(Kind.I32) { override fun load(value: SimpleValue): Nothing = throw UnsupportedOperationException() @@ -177,4 +177,4 @@ typealias SimpleValue = Any typealias AnyCollection = Any // @see fatMap, fatMapTo, fatAsList, don't forget to update them -typealias SimpleNullable = DataType.Nullable> +typealias SimpleNullable = DataType.Nullable> diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/type/types-okio.kt b/persistence/src/main/kotlin/net/aquadc/persistence/type/types-okio.kt index 8b0c8111..af73d690 100644 --- a/persistence/src/main/kotlin/net/aquadc/persistence/type/types-okio.kt +++ b/persistence/src/main/kotlin/net/aquadc/persistence/type/types-okio.kt @@ -6,7 +6,7 @@ import okio.ByteString @PublishedApi internal class ByteStr( private val blob: Simple -) : DataType.Simple(Kind.Blob) { +) : DataType.NotNull.Simple(Kind.Blob) { override fun load(value: SimpleValue): Any? = blob.load(value).let { ByteString.of(it, 0, it.size) } @@ -29,5 +29,5 @@ import okio.ByteString "NOTHING_TO_INLINE", // single constructor call is a great candidate for inlining "UNCHECKED_CAST" // ByteStr is erased to avoid bridge methods ) -inline fun byteString(blobType: DataType.Simple): DataType = +inline fun byteString(blobType: DataType.NotNull.Simple): DataType = ByteStr(blobType) as DataType diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/type/types.kt b/persistence/src/main/kotlin/net/aquadc/persistence/type/types.kt index ea591ab8..98932a83 100644 --- a/persistence/src/main/kotlin/net/aquadc/persistence/type/types.kt +++ b/persistence/src/main/kotlin/net/aquadc/persistence/type/types.kt @@ -4,6 +4,16 @@ import net.aquadc.persistence.struct.FieldDef import net.aquadc.persistence.struct.FieldSet import net.aquadc.persistence.struct.Schema +/** + * The root of all types. + * “Ilk” is just another synonym for “type” or “kind”. + * + * Used by :sql to use native types like `uuid`, `point` etc. + */ +interface Ilk> { + val type: DT + val custom: CustomType? +} /** * Describes type of stored values and underlying serialization techniques. @@ -23,12 +33,17 @@ import net.aquadc.persistence.struct.Schema * and, depending on underlying serialization machinery, * s1.fields[n] has either same name or same ordinal as s2.fields[n].) */ +// Unfortunately, I can't implement Ilk by DataType: it would be impossible +// to narrow down DT parameter value in subclasses due to https://youtrack.jetbrains.com/issue/KT-13380. +// I could write @Suppress("INCONSISTENT_TYPE_PARAMETER_VALUES") but this would be required +// on every subclass including user-side `Schema`s which is horrible and inappropriate. sealed class DataType { + // “accidental” override for Ilk.custom + val custom: CustomType? get() = null + /** - * Adds nullability to runtime representation of [actualType]. - * Wraps a non-nullable type, - * adds possibility of having `null` value in both runtime and stored representations. + * Adds nullability to both runtime and stored representation of [actualType]. * * The main reason why you can't have custom nullability is that * lens concatenation `toPartial / toSomething` or `toPartial / toSomethingNullable` @@ -42,137 +57,171 @@ sealed class DataType { * Wrapped non-nullable type. */ @JvmField val actualType: DT - ) : DataType() { + ) : DataType(), Ilk> { init { if (actualType is Nullable<*, *>) throw ClassCastException() // unchecked cast?.. } + override val type: Nullable get() = this } + // Unfortunately, I can't insert a supertype seamlessly. + // Nesting required: https://youtrack.jetbrains.com/issue/KT-13495 + // There's also no local type aliases. + @Deprecated("moved", ReplaceWith("DataType.NotNull.Simple(kind)")) + abstract class Simple(kind: NotNull.Simple.Kind) : NotNull.Simple(kind) { + @Deprecated("moved", ReplaceWith("DataType.NotNull.Simple.Kind")) + object Kind { + inline val Bool get() = NotNull.Simple.Kind.Bool + inline val I32 get() = NotNull.Simple.Kind.I32 + inline val I64 get() = NotNull.Simple.Kind.I64 + inline val F32 get() = NotNull.Simple.Kind.F32 + inline val F64 get() = NotNull.Simple.Kind.F64 + inline val Str get() = NotNull.Simple.Kind.Str + inline val Blob get() = NotNull.Simple.Kind.Blob + } + } + @Deprecated("moved", ReplaceWith("DataType.NotNull.Collect(elementType)")) + abstract class Collect>(elementType: DE) : NotNull.Collect(elementType) + @Deprecated("moved", ReplaceWith("DataType.NotNull.Partial(schema)")) + abstract class Partial>(schema: SCH) : NotNull.Partial(schema) + /** - * A simple, non-composite (and thus easily composable) type. + * Common supertype for all types which cannot be stored as `null`. */ - abstract class Simple( + sealed class NotNull : DataType() { + + /** + * A simple, non-composite (and thus easily composable) type. + */ + abstract class Simple( /** * Specifies exact type of stored values. */ @JvmField val kind: Kind - ) : DataType() { - - enum class Kind { - Bool, - @Deprecated("does not look very useful", level = DeprecationLevel.ERROR) I8, - @Deprecated("does not look very useful", level = DeprecationLevel.ERROR) I16, - I32, I64, // TODO: U32, U64, BigInt, BigFloat - F32, F64, - Str, Blob, - } + ) : NotNull(), Ilk> { - /** - * If true, string-based serialization formats will call - * [storeAsString] instead of [store], and will pass [CharSequence] to [load]. - * This is useful to override appearance of numbers or blobs in JSON, XML, etc. - */ - open val hasStringRepresentation: Boolean get() = false + enum class Kind { + Bool, + @Deprecated("does not look very useful", level = DeprecationLevel.ERROR) I8, + @Deprecated("does not look very useful", level = DeprecationLevel.ERROR) I16, + I32, I64, // TODO: U32, U64, BigInt, BigFloat + F32, F64, + Str, Blob, + } - /** - * Converts a simple persistable value into its in-memory representation. - * @return in-memory representation of [value] - */ - abstract fun load(value: SimpleValue): T - - /** - * Converts in-memory value into its simple persistable representation. - * @return persistable representation of [value] - */ - abstract fun store(value: T): SimpleValue + /** + * If true, string-based serialization formats will call + * [storeAsString] instead of [store], and will pass [CharSequence] to [load]. + * This is useful to override appearance of numbers or blobs in JSON, XML, etc. + */ + open val hasStringRepresentation: Boolean get() = false - /** - * If [hasStringRepresentation], string-based serialization formats - * will call this method instead of [store]. - */ - open fun storeAsString(value: T): CharSequence = - throw UnsupportedOperationException() + /** + * Converts a simple persistable value into its in-memory representation. + * @return in-memory representation of [value] + */ + abstract fun load(value: SimpleValue): T - } + /** + * Converts in-memory value into its simple persistable representation. + * @return persistable representation of [value] + */ + abstract fun store(value: T): SimpleValue - /** - * A collection of elements of [elementType]. - * In-memory type [C] is typically a collection, but it is not required. - * May have [List] or [Set] semantics, depending on implementations - * of both this data type and the underlying storage. - * - * Collection DataType handles only converting from/to a specified collection type, - * leaving values untouched. - */ - abstract class Collect>( /** - * [DataType] of all the elements in such collections. + * If [hasStringRepresentation], string-based serialization formats + * will call this method instead of [store]. */ - @JvmField val elementType: DE - ) : DataType() { + open fun storeAsString(value: T): CharSequence = + throw UnsupportedOperationException() - /** - * Converts a persistable collection value into its in-memory representation. - * Elements of input collection are already in their in-memory representation - * @return in-memory representation of [value] - */ - abstract fun load(value: AnyCollection): C + /*open val sqlType: CharSequence? get() = null + open fun storeSqlObject(value: T): Any? = throw UnsupportedOperationException()*/ + + override val type: Simple get() = this + } /** - * Converts in-memory value into a persistable collection. - * Values of output collection must be in their in-memory representation, - * it's caller's responsibility to convert them to persistable representation. - * @return persistable representation of [value], a collection of in-memory representations + * A collection of elements of [elementType]. + * In-memory type [C] is typically a collection, but it is not required. + * May have [List] or [Set] semantics, depending on implementations + * of both this data type and the underlying storage. + * + * Collection DataType handles only converting from/to a specified collection type, + * leaving values untouched. */ - abstract fun store(value: C): AnyCollection + abstract class Collect>( + /** + * [DataType] of all the elements in such collections. + */ + @JvmField val elementType: DE + ) : NotNull(), Ilk> { - } + /** + * Converts a persistable collection value into its in-memory representation. + * Elements of input collection are already in their in-memory representation + * @return in-memory representation of [value] + */ + abstract fun load(value: AnyCollection): C - /** - * Represents a set of optional key-value mappings, according to [schema]. - * [Schema] itself represents a special case of [Partial], where all mappings are required. - */ - abstract class Partial> : DataType { + /** + * Converts in-memory value into a persistable collection. + * Values of output collection must be in their in-memory representation, + * it's caller's responsibility to convert them to persistable representation. + * @return persistable representation of [value], a collection of in-memory representations + */ + abstract fun store(value: C): AnyCollection - @JvmField val schema: SCH - - constructor(schema: SCH) { - this.schema = schema - } - - internal constructor() { // for Schema itself - this.schema = this as SCH + override val type: Collect get() = this } - - /** - * Converts a persistable value into its in-memory representation. - * @param fields a set of fields provided within [values] array - * @param values values in their in-memory representation according to [fields] size - * 0 -> ignored - * 1 -> the value for the only field - * else -> 'packed' layout, no gaps between values - * @return in-memory representation of [fields] and their [values] - * @see net.aquadc.persistence.struct.indexOf - * @see net.aquadc.persistence.fill - */ - abstract fun load(fields: FieldSet>, values: Any?): T /** - * Returns a set of fields which have values. - * Required to parse data returned by [store] function. + * Represents a set of optional key-value mappings, according to [schema]. + * [Schema] itself represents a special case of [Partial], where all mappings are required. */ - abstract fun fields(value: T): FieldSet> + abstract class Partial> : NotNull, Ilk> { - /** - * Converts in-memory value into its persistable representation. - * @param value an input value to read from - * @return all values, using the same layouts as in [load], in their unchanged, in-memory representation - * @see fields to know how to interpret the return value - */ - abstract fun store(value: T): Any? + @JvmField val schema: SCH + constructor(schema: SCH) { + this.schema = schema + } + + internal constructor() { // for Schema itself + this.schema = this as SCH + } + + /** + * Converts a persistable value into its in-memory representation. + * @param fields a set of fields provided within [values] array + * @param values values in their in-memory representation according to [fields] size + * 0 -> ignored + * 1 -> the value for the only field + * else -> 'packed' layout, no gaps between values + * @return in-memory representation of [fields] and their [values] + * @see net.aquadc.persistence.struct.indexOf + * @see net.aquadc.persistence.fill + */ + abstract fun load(fields: FieldSet>, values: Any?): T + + /** + * Returns a set of fields which have values. + * Required to parse data returned by [store] function. + */ + abstract fun fields(value: T): FieldSet> + + /** + * Converts in-memory value into its persistable representation. + * @param value an input value to read from + * @return all values, using the same layouts as in [load], in their unchanged, in-memory representation + * @see fields to know how to interpret the return value + */ + abstract fun store(value: T): Any? + + override val type: Partial get() = this + } } // these look useless but help using assertEquals() in tests: @@ -183,30 +232,43 @@ sealed class DataType { return when (this) { is Nullable<*, *> -> other is Nullable<*, *> && actualType as DataType<*> == other.actualType - is Simple -> other is Simple<*> && kind === other.kind - is Collect<*, *, *> -> other is Collect<*, *, *> && elementType == other.elementType + is NotNull.Simple -> other is NotNull.Simple<*> && kind === other.kind + is NotNull.Collect<*, *, *> -> other is NotNull.Collect<*, *, *> && elementType == other.elementType is Schema<*> -> this === other - is Partial<*, *> -> other is Partial<*, *> && schema == other.schema + is NotNull.Partial<*, *> -> other is NotNull.Partial<*, *> && schema == other.schema } } final override fun hashCode(): Int = when (this) { is Nullable<*, *> -> 13 * actualType.hashCode() - is Simple -> 31 * kind.hashCode() - is Collect<*, *, *> -> 63 * elementType.hashCode() + is NotNull.Simple -> 31 * kind.hashCode() + is NotNull.Collect<*, *, *> -> 63 * elementType.hashCode() is Schema<*> -> System.identityHashCode(this) - is Partial<*, *> -> 127 * schema.hashCode() + is NotNull.Partial<*, *> -> 127 * schema.hashCode() } final override fun toString(): String = when (this) { is Nullable<*, *> -> "nullable($actualType)" - is Simple -> kind.toString() - is Collect<*, *, *> -> "collection($elementType)" + is NotNull.Simple -> kind.toString() + is NotNull.Collect<*, *, *> -> "collection($elementType)" is Schema<*> -> javaClass.simpleName - is Partial<*, *> -> "partial($schema)" + is NotNull.Partial<*, *> -> "partial($schema)" } // abstract class Dictionary internal constructor(keyType: DataType, valueType: DataType) : DataType(isNullable) TODO } + +/** + * A custom type. + * Used by :sql to take advantage of native types (e.g. Postgres `uuid`, `point` etc) directly. + * + * Very similar to `TwoWay` interface from :properties, + * but :persistence don't have common dependency with :properties. + */ +abstract class CustomType( + @JvmField val name: CharSequence +) : (T) -> Any? { + abstract fun back(p: Any?): T +} diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/util.kt b/persistence/src/main/kotlin/net/aquadc/persistence/util.kt index c42b6ce3..6cc3729b 100644 --- a/persistence/src/main/kotlin/net/aquadc/persistence/util.kt +++ b/persistence/src/main/kotlin/net/aquadc/persistence/util.kt @@ -1,6 +1,5 @@ package net.aquadc.persistence -import android.annotation.SuppressLint import androidx.annotation.RestrictTo import net.aquadc.persistence.struct.FieldDef import net.aquadc.persistence.struct.FieldSet @@ -18,7 +17,6 @@ import net.aquadc.persistence.struct.toString import net.aquadc.persistence.type.AnyCollection import net.aquadc.persistence.type.DataType import java.util.Arrays -import java.util.Collections import kotlin.concurrent.getOrSet @@ -206,9 +204,9 @@ fun > PartialStruct.fieldValues(): Any? { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) inline fun > readPartial( - type: DataType.Partial, fieldValues: ThreadLocal>, - maybeReadNextField: () -> FieldDef?, - readNextValue: (DataType<*>) -> Any? + type: DataType.NotNull.Partial, fieldValues: ThreadLocal>, + maybeReadNextField: () -> FieldDef?, + readNextValue: (DataType<*>) -> Any? ): T { var fields = emptyFieldSet>() var values: Any? = null diff --git a/properties/src/main/kotlin/net/aquadc/properties/persistence/type-enum.kt b/properties/src/main/kotlin/net/aquadc/properties/persistence/type-enum.kt index a047d70a..396d9d26 100644 --- a/properties/src/main/kotlin/net/aquadc/properties/persistence/type-enum.kt +++ b/properties/src/main/kotlin/net/aquadc/properties/persistence/type-enum.kt @@ -12,6 +12,6 @@ import net.aquadc.properties.function.Enumz */ @JvmName("enumeration") @Suppress("UNCHECKED_CAST") // NoConstant is intentionally erased inline fun > enum( - noinline fallback: (String) -> E = NoConstant(E::class.java) as (Any?) -> Nothing -): DataType.Simple = - enum(enumValues(), string, Enumz.Name, fallback) + noinline fallback: (String) -> E = NoConstant(E::class.java) as (Any?) -> Nothing +): DataType.NotNull.Simple = + enum(enumValues(), string, Enumz.Name, fallback) diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/ColMeta.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/ColMeta.kt index 1ba612b5..65de7c0f 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/ColMeta.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/ColMeta.kt @@ -1,10 +1,15 @@ package net.aquadc.persistence.sql +import net.aquadc.persistence.sql.blocking.SqliteSession import net.aquadc.persistence.struct.Lens import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.StoredLens import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.Ilk +import net.aquadc.persistence.type.string +import java.sql.PreparedStatement +import java.sql.ResultSet @Deprecated("renamed") @Suppress("UNUSED_TYPEALIAS_PARAMETER") @@ -26,9 +31,9 @@ sealed class ColMeta>( */ @Deprecated("renamed", ReplaceWith("embed(naming, path, fieldSetColName)", "net.aquadc.persistence.sql.ColMeta.Companion.*")) @JvmName("embeddedNullable") - inline fun , ET : Any, EDT : DataType.Nullable>> - Embedded(naming: NamingConvention, path: StoredLens, fieldSetColName: String): ColMeta = - Embed(naming, path, fieldSetColName) + inline fun , ET : Any, EDT : DataType.Nullable>> + Embedded(naming: NamingConvention, path: StoredLens, fieldSetColName: CharSequence): ColMeta = + Rel.Embed(naming, path, fieldSetColName) /** * @param naming which will concat names * @param path to a value stored as a [DataType.Partial] @@ -36,9 +41,9 @@ sealed class ColMeta>( */ @Deprecated("renamed", ReplaceWith("embed(naming, path, fieldSetColName)", "net.aquadc.persistence.sql.ColMeta.Companion.*")) @JvmName("embeddedPartial") - inline fun , ET, EDT : DataType.Partial> - Embedded(naming: NamingConvention, path: StoredLens, fieldSetColName: String): ColMeta = - Embed(naming, path, fieldSetColName) + inline fun , ET, EDT : DataType.NotNull.Partial> + Embedded(naming: NamingConvention, path: StoredLens, fieldSetColName: CharSequence): ColMeta = + Rel.Embed(naming, path, fieldSetColName) /** * @param naming which will concat names * @param path to a value stored as a struct with [Schema] @@ -46,8 +51,8 @@ sealed class ColMeta>( @Deprecated("renamed", ReplaceWith("embed(naming, path)", "net.aquadc.persistence.sql.ColMeta.Companion.*")) @JvmName("embeddedStruct") inline fun , ES : Schema> - Embedded(naming: NamingConvention, path: StoredLens, ES>): ColMeta = - Embed(naming, path, null) + Embedded(naming: NamingConvention, path: StoredLens, ES>): ColMeta = + Rel.Embed(naming, path, null) /** @@ -56,9 +61,9 @@ sealed class ColMeta>( * @param fieldSetColName a name of a column which will internally be used to remember which fields are set */ @JvmName("embedNullable") - inline fun , ET : Any, EDT : DataType.Nullable>> - embed(naming: NamingConvention, path: StoredLens, fieldSetColName: String): ColMeta = - Embed(naming, path, fieldSetColName) + inline fun , ET : Any, EDT : DataType.Nullable>> + embed(naming: NamingConvention, path: StoredLens, fieldSetColName: CharSequence): ColMeta = + Rel.Embed(naming, path, fieldSetColName) /** * @param naming which will concat names @@ -66,9 +71,9 @@ sealed class ColMeta>( * @param fieldSetColName a name of a column which will internally be used to remember which fields are set */ @JvmName("embedPartial") - inline fun , ET, EDT : DataType.Partial> - embed(naming: NamingConvention, path: StoredLens, fieldSetColName: String): ColMeta = - Embed(naming, path, fieldSetColName) + inline fun , ET, EDT : DataType.NotNull.Partial> + embed(naming: NamingConvention, path: StoredLens, fieldSetColName: CharSequence): ColMeta = + Rel.Embed(naming, path, fieldSetColName) /** * @param naming which will concat names @@ -77,63 +82,104 @@ sealed class ColMeta>( @JvmName("embedStruct") inline fun , ES : Schema> embed(naming: NamingConvention, path: StoredLens, ES>): ColMeta = - Embed(naming, path, null) + Rel.Embed(naming, path, null) + + /** + * Override [DataType] in "CREATE TABLE" statement. + * For example, [string] will be represented as "text" by default, + * but you can write `type(SomeStringField, "varchar(256)")` in order to change it. + */ + inline fun , T> type(path: StoredLens>, typeName: CharSequence): ColMeta = + Type(path, typeName, null) + + /** + * Override [DataType] behaviour. This will + * * alter "CREATE TABLE" statement same way as [type] does, + * * bypass [DataType], and bind [store]d parameters directly, e.g. with [PreparedStatement.setObject], + * * and read [load]ed parameters directly, e.g. using [ResultSet.getObject]. + * [SqliteSession] ignores type overrides and takes into account only [typeName]. + */ + inline fun , T> S.nativeType( + path: StoredLens>, custom: Ilk + ): ColMeta = + Type(path, null, custom) + + /** + * Override [DataType] behaviour. This will + * * alter "CREATE TABLE" statement same way as [type] does, + * * bypass [DataType], and bind parameters directly, e.g. with [PreparedStatement.setObject], + * * and read parameters directly, e.g. using [ResultSet.getObject]. + * [SqliteSession] ignores type overrides and takes into account only [typeName]. + */ + @Suppress("UNCHECKED_CAST") + inline fun , T> S.nativeType( + path: StoredLens>, typeName: CharSequence + ): ColMeta = + Type(path, null, NativeType(typeName, path.type)) } - /** - * Embed a (Partial)[Struct] of type [ET] into current table. - * @param S outer [Schema] - */ - @PublishedApi internal class Embed> constructor( - val naming: NamingConvention, - path: StoredLens, - val fieldSetColName: String? + @PublishedApi internal class Type, T> constructor( + path: StoredLens, + @JvmField val typeName: CharSequence?, + @JvmField val override: Ilk? ) : ColMeta(path) - /** - * Reference a single entity by its primary key. - * @param S outer schema - * @param FS foreign schema - */ - @Deprecated("Not implemented yet.", level = DeprecationLevel.ERROR) // todo - class ToOne, ID : IdBound, FS : Schema, FID : IdBound>( + @PublishedApi internal sealed class Rel>(path: StoredLens) : ColMeta(path) { + + /** + * Embed a (Partial)[Struct] into current table. + * @param S outer [Schema] + */ + @PublishedApi internal class Embed> constructor( + val naming: NamingConvention, + path: StoredLens, + val fieldSetColName: CharSequence? + ) : Rel(path) + + /** + * Reference a single entity by its primary key. + * @param S outer schema + * @param FS foreign schema + */ + @Deprecated("Not implemented yet.", level = DeprecationLevel.ERROR) // todo + class ToOne, FS : Schema, FID : IdBound>( path: StoredLens?, *>, foreignTable: Table - ) : ColMeta(path) { - init { - checkToOne(TODO(), path, foreignTable) + ) : Rel(path) { + init { + checkToOne(TODO(), path, foreignTable) + } } - } - /** - * There are some entities which reference this one by our primary key. - * @param S outer schema - * @param R outer record - * @param FS foreign schema - * @param FR foreign record - */ - @Deprecated("Not implemented yet.", level = DeprecationLevel.ERROR) - class ToMany, ID : IdBound, FS : Schema, FID : IdBound, C : Collection>> private constructor( + /** + * There are some entities which reference this one by our primary key. + * @param S outer schema + * @param FS foreign schema + */ + @Deprecated("Not implemented yet.", level = DeprecationLevel.ERROR) + class ToMany, ID : IdBound, FS : Schema, FID : IdBound, C : Collection>> private constructor( ourTable: Table, path: StoredLens, foreignTable: Table, joinColumn: StoredLens - ) : ColMeta(path) { - init { - checkToMany(ourTable.schema, path, foreignTable) - checkToOne(foreignTable.schema, joinColumn, ourTable) // ToMany is actually many ToOnes - } - - companion object { - operator fun , ID : IdBound, FS : Schema, FID : IdBound, C : Collection>> Table.invoke( + ) : Rel(path) { + init { + checkToMany(ourTable.schema, path, foreignTable) + checkToOne(foreignTable.schema, joinColumn, ourTable) // ToMany is actually many ToOnes + } + + companion object { + operator fun , ID : IdBound, FS : Schema, FID : IdBound, C : Collection>> Table.invoke( path: Lens, Record, C, *>, foreignTable: Table, joinColumn: Lens, Record, *, *> - ): Nothing = TODO() // ToMany = ToMany(this, path, foreignTable, joinColumn) + ): Nothing = TODO() // ToMany = ToMany(this, path, foreignTable, joinColumn) + } } - } - @Deprecated("Not implemented yet.", level = DeprecationLevel.ERROR) - class ManyToMany, ID : IdBound, FS : Schema, C : Collection>>( + @Deprecated("Not implemented yet.", level = DeprecationLevel.ERROR) + class ManyToMany, FS : Schema, C : Collection>>( path: StoredLens, foreignTable: Table, joinTable: JoinTable - ) : ColMeta(path) { - init { - checkToMany(TODO(), path, foreignTable) + ) : Rel(path) { + init { + checkToMany(TODO(), path, foreignTable) + } } + } // for tests @@ -168,7 +214,7 @@ typealias JoinTable = Nothing internal fun , FS : Schema> checkToMany( schema: S, path: StoredLens, foreignTable: Table) { val type = path.type(schema) - check(type is DataType.Collect<*, *, *>) { + check(type is DataType.NotNull.Collect<*, *, *>) { "only fields of Collection types can be used with to-many relations" } val elType = type.elementType diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/Table.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/Table.kt index fe486be5..7b6f0181 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/Table.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/Table.kt @@ -15,11 +15,19 @@ import net.aquadc.persistence.struct.StoredNamedLens import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.struct.forEachIndexed import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.Ilk import net.aquadc.persistence.type.nothing import net.aquadc.properties.internal.ManagedProperty import net.aquadc.properties.internal.Unset +/** + * Since a [Table] could have type overrides, + * column type name is either a normal [DataType], + * or a [Pair] of type and overriding [CharSequence]. + */ +typealias SqlTypeName = Any /*= DataType | CharSequence */ + /** * Represents a table, i. e. defines structs which can be persisted in a database. * @param SCH self, i. e. this table @@ -27,19 +35,18 @@ import net.aquadc.properties.internal.Unset */ open class Table, ID : IdBound> private constructor( - val schema: SCH, - val name: String, - val idColName: CharSequence, - val idColType: DataType.Simple, - val pkField: ImmutableField>? // todo: consistent names, ID || PK + val schema: SCH, + val name: String, + val idColName: CharSequence, + idColType: DataType.NotNull.Simple, + val pkField: ImmutableField>? // todo: consistent names, ID || PK // TODO: [unique] indices https://github.com/greenrobot/greenDAO/blob/72cad8c9d5bf25d6ed3bdad493cee0aee5af8a70/greendao-api/src/main/java/org/greenrobot/greendao/annotation/Index.java -// TODO: auto increment ) { - constructor(schema: SCH, name: String, idColName: String, idColType: DataType.Simple) : + constructor(schema: SCH, name: String, idColName: String, idColType: DataType.NotNull.Simple) : this(schema, name, idColName, idColType, null) - constructor(schema: SCH, name: String, idCol: ImmutableField>) : + constructor(schema: SCH, name: String, idCol: ImmutableField>) : this(schema, name, schema.run { idCol.name }, schema.run { idCol.type }, idCol) /** @@ -55,40 +62,73 @@ private constructor( * Relations could form dependency circles, that's why we don't require them in constructor. * This method could mention other tables but must not touch their columns. */ - protected open fun meta(): Array> = relations() + protected open fun SCH.meta(): Array> = relations() @JvmSynthetic @JvmField internal var _delegates: Map, SqlPropertyDelegate>? = null @JvmSynthetic @JvmField internal var _recipe: Array? = null - @JvmSynthetic @JvmField internal var _managedColumns: Array>? = null +// @JvmSynthetic @JvmField internal var _managedColumns: Array>? = null @JvmSynthetic @JvmField internal var _managedColNames: Array? = null - @JvmSynthetic @JvmField internal var _managedColTypes: Array>? = null + @JvmSynthetic @JvmField internal var _managedColTypes: Array>? = null + @JvmSynthetic @JvmField internal var _managedColTypeNames: Array? = null + @JvmSynthetic @JvmField internal var _idColType: Ilk> = idColType // can be changed (overridden) during init + @JvmSynthetic @JvmField internal var _idColTypeName: SqlTypeName = idColType // can be overridden during init private val _columns: Lazy>> = lazy { - val meta = meta().let { meta -> - meta.associateByTo(newMap, ColMeta>(meta.size), ColMeta::path) + var relations: MutableMap, ColMeta.Rel>? = null + var types: MutableMap, ColMeta.Type>? = null + schema.meta().forEach { + when (it) { + is ColMeta.Type -> check( + (types ?: newMap, ColMeta.Type>().also { types = it }) + .put(it.path, it) == null) + is ColMeta.Rel -> check( + (relations ?: newMap, ColMeta.Rel>().also { relations = it }) + .put(it.path, it) == null) + }!! } val columns = CheckNamesList>(schema.fields.size) + val columnTypes = ArrayList>(schema.fields.size) + val columnTypeNames = ArrayList(schema.fields.size) if (pkField == null) { - columns.add(PkLens(this), idColName) + val pkLens = PkLens(this, _idColType as DataType.NotNull.Simple) + columns.add(pkLens, idColName) + types?.remove(pkLens)?.let(::overrideIdType) + columnTypeNames.add(_idColTypeName) + columnTypes.add(_idColType) } val delegates = newMap, SqlPropertyDelegate>() val recipe = ArrayList() val ss = Nesting.StructStart(false, null, null, schema) recipe.add(ss) - embed(meta, schema, null, null, columns, delegates, recipe) + embed(relations, types, schema, null, null, columns, columnTypes, columnTypeNames, delegates, recipe) ss.colCount = columns.size recipe.add(Nesting.StructEnd) this._recipe = recipe.array() - if (meta.isNotEmpty()) throw RuntimeException("cannot consume meta: ${meta.values}") + relations?.takeIf { it.isNotEmpty() }?.let { throw RuntimeException("Cannot consume relations: ${it.values}") } + + this._delegates = delegates//todo:.optimizeReadOnlyMap() - this._delegates = delegates val colsArray = columns.array() - _managedColumns = if (pkField == null) columns.subList(1, columns.size).array() else colsArray - _managedColNames = _managedColumns!!.mapIndexedToArray { _, it -> it.name(schema) } - _managedColTypes = _managedColumns!!.mapIndexedToArray { _, it -> it.type(schema) } + val skipPkCol = pkField == null + val managedColumns = if (skipPkCol) columns.subList(1, columns.size).array() else colsArray + val manColTs = if (skipPkCol) columnTypes.subList(1, columns.size) else columnTypes + _managedColTypes = manColTs.array() + val manColTNs = if (skipPkCol) columnTypeNames.subList(1, columns.size) else columnTypeNames + _managedColTypeNames = if (manColTs == manColTNs) _managedColTypes else manColTNs.array() + _managedColNames = managedColumns.mapIndexedToArray { _, it -> it.name(schema) } + + types?.remove(columns[0]) // we've unconditionally peeked it earlier and conditionally polled within embed() + types?.takeIf { it.isNotEmpty() }?.let { throw RuntimeException("Cannot consume type overrides: " + + it.values.map { t -> t.javaClass.simpleName + '(' + t.path + ')' }) } + colsArray } + @JvmSynthetic internal fun overrideIdType(type: ColMeta.Type) { + (type.typeName ?: type.override?.custom?.name ?: type.override?.type)?.let { _idColTypeName = it } + type.override?.let { _idColType = it as Ilk> } + } + internal class CheckNamesList>(initialCapacity: Int) : ArrayList(initialCapacity) { private val names = newSet(initialCapacity) fun add(element: E, itsName: CharSequence): Boolean { @@ -100,9 +140,13 @@ private constructor( // some bad code with raw types here @Suppress("UPPER_BOUND_VIOLATED") @JvmSynthetic internal fun embed( - metas: MutableMap, ColMeta>, schema: Schema<*>, + rels: MutableMap, ColMeta.Rel>?, + types: MutableMap, ColMeta.Type>?, + schema: Schema<*>, naming: NamingConvention?, prefix: StoredNamedLens?, outColumns: CheckNamesList>, + outColumnTypes: ArrayList>, + outColumnTypeNames: ArrayList, outDelegates: MutableMap, SqlPropertyDelegate>?, outRecipe: ArrayList ) { @@ -115,51 +159,81 @@ private constructor( else /* implies naming != null */ naming!!.concatErased(this.schema, schema, prefix, field) as StoredNamedLens> val type = (field as FieldDef, Any?, DataType>).type(schema) - val relType = when (type) { - is DataType.Partial<*, *> -> type - is DataType.Nullable<*, *> -> type.actualType as? DataType.Partial<*, *> - // ignore collections of (partial) structs, the can be stored only within 'real' relations while we support only Embedded ones at the moment + val tOverr = types?.remove(path) + if (tOverr != null) { + addColumn(outColumns, outColumnTypes, outColumnTypeNames, path, tOverr) + if (outColumns.size == 1) overrideIdType(tOverr) // this is primary key + } else when (type) { + is DataType.NotNull.Partial<*, *> -> type + is DataType.Nullable<*, *> -> type.actualType as? DataType.NotNull.Partial<*, *> + // ignore collections of (partial) structs, they can be stored only within 'real' relations while we support only Embedded ones at the moment else -> null + }?.let { relType -> + // got a struct type, a relation must be declared + addRelationalCols( + rels, path, relType, outColumns, schema, types, + outColumnTypes, outColumnTypeNames, outRecipe, field, type, outDelegates + ) + } ?: run { + addColumn(outColumns, outColumnTypes, outColumnTypeNames, path, null) } + } + } - if (relType != null) { - // got a struct type, a relation must be declared - val meta = metas.remove(path) - ?: throw NoSuchElementException("${this@Table} requires a Relation to be declared for path $path storing values of type $relType") - - when (meta) { - is ColMeta.Embed<*> -> { - val start = outColumns.size - val fieldSetCol = meta.fieldSetColName?.let { fieldSetColName -> - (meta.naming.concatErased(this.schema, schema, path, FieldSetLens>(fieldSetColName)) as StoredNamedLens) - .also { outColumns.add(it, it.name(this.schema)) } - } - - val relSchema = relType.schema - val recipeStart = outRecipe.size - val ss = Nesting.StructStart(fieldSetCol != null, field, type, relType) - outRecipe.add(ss) - /*val nestedLenses =*/ embed(metas, relSchema, meta.naming, path, outColumns, null, outRecipe) - ss.colCount = outColumns.size - start - outRecipe.add(Nesting.StructEnd) - - check(outDelegates?.put(path, Embedded( - this.schema, - outColumns.subList(start, outColumns.size), - // ArrayList$SubList checks for modifications and cannot be passed as is - outRecipe.subList(recipeStart, outRecipe.size).array(), - start - )) === null) + @Suppress("UPPER_BOUND_VIOLATED") + private fun addRelationalCols(rels: MutableMap, ColMeta.Rel>?, path: StoredNamedLens, relType: DataType.NotNull.Partial<*, *>, outColumns: CheckNamesList>, schema: Schema<*>, types: MutableMap, ColMeta.Type>?, outColumnTypes: ArrayList>, outColumnTypeNames: ArrayList, outRecipe: ArrayList, field: FieldDef, out Any?, out DataType<*>>, type: DataType, outDelegates: MutableMap, SqlPropertyDelegate>?) { + val rel = rels?.remove(path) + ?: throw NoSuchElementException("${this@Table} requires a Relation to be declared for path $path storing values of type $relType") + + when (rel) { + is ColMeta.Rel.Embed<*> -> { + val start = outColumns.size + val fieldSetCol = rel.fieldSetColName?.let { fieldSetColName -> + (rel.naming.concatErased(this.schema, schema, path, FieldSetLens>(fieldSetColName)) + as StoredNamedLens).also { path -> + val tOverr = types?.remove(path) + addColumn(outColumns, outColumnTypes, outColumnTypeNames, path, tOverr) } - else -> TODO() -// is Relation.ToOne<*, *, *, *, *> -> -// is Relation.ToMany<*, *, *, *, *, *, *> -> -// is Relation.ManyToMany<*, *, *, *, *> -> - }.also { } - } else { - outColumns.add(path, path.name(this.schema)) + } + + val relSchema = relType.schema + val recipeStart = outRecipe.size + val ss = Nesting.StructStart(fieldSetCol != null, field, type, relType) + outRecipe.add(ss) + embed( + rels, types, relSchema, rel.naming, path, + outColumns, outColumnTypes, outColumnTypeNames, null, outRecipe + ) + ss.colCount = outColumns.size - start + outRecipe.add(Nesting.StructEnd) + + val subColumns = outColumns.subList(start, outColumns.size) + check(outDelegates?.put(path, Embedded( + // ArrayList$SubList checks for modifications and cannot be passed as is + outRecipe.subList(recipeStart, outRecipe.size).array(), + start, + Array(subColumns.size) { i -> subColumns[i].name(this.schema) }, + outColumnTypes.subList(start, outColumns.size).array() + )) === null) } - } + else -> TODO() +// is Relation.ToOne<*, *, *, *, *> -> +// is Relation.ToMany<*, *, *, *, *, *, *> -> +// is Relation.ManyToMany<*, *, *, *, *> -> + }.also { } + } + + private fun addColumn( + outColumns: CheckNamesList>, + outColumnTypes: ArrayList>, + outColumnTypeNames: ArrayList, + path: StoredNamedLens, + tOverr: ColMeta.Type? + ) { + val t = path.type(this.schema) as Ilk<*, *> + outColumns.add(path, path.name(schema)) + outColumnTypes.add(tOverr?.override ?: t) + outColumnTypeNames.add(tOverr?.typeName ?: tOverr?.override?.custom?.name ?: tOverr?.override?.type ?: t) } internal sealed class Nesting { @@ -167,7 +241,7 @@ private constructor( @JvmField val hasFieldSet: Boolean, @JvmField val myField: FieldDef<*, *, *>?, @JvmField val type: DataType<*>?, - @JvmField val unwrappedType: DataType.Partial<*, *> + @JvmField val unwrappedType: DataType.NotNull.Partial<*, *> ) : Nesting() { @JvmField var colCount: Int = 0 } @@ -178,26 +252,41 @@ private constructor( val columns: Array> get() = _columns.value - val pkColumn: NamedLens, Record, ID, out DataType.Simple> - get() = _columns.let { + val idColType: Ilk> + get() = columns.let { _idColType } // not ?: 'cause it can be assigned but require override + + val idColTypeName: SqlTypeName //= DataType.Simple | CharSequence + get() = columns.let { _idColTypeName } // not ?: 'cause it can be assigned but require override + + val pkColumn: NamedLens, Record, ID, out DataType.NotNull.Simple> + get() = pkField ?: _columns.let { if (it.isInitialized()) - it.value[0] as NamedLens, Record, ID, out DataType.Simple> - else - TODO("allow getting PK within meta() without reentrancy") + it.value[0] as NamedLens, Record, ID, out DataType.NotNull.Simple> + else PkLens(this, _idColType.type as DataType.NotNull.Simple) as NamedLens, Record, ID, out DataType.NotNull.Simple> } internal val recipe: Array get() = _recipe ?: _columns.value.let { _ /* unwrap lazy */ -> _recipe!! } - val managedColumns: Array> - get() = _managedColumns ?: _columns.value.let { _ /* unwrap lazy */ -> _managedColumns!! } + fun indexOfManaged(column: StoredLens): Int { + val idxOfCol = columns.indexOf(column) + // _managedColumns = if (pkField == null) columns.subList(1, columns.size).array() else colsArray + // [unmanaged PK (0, -1 for us), 1 (0 for us), 2 (1 for us), …] + return idxOfCol - (if (pkField == null && idxOfCol >= 0) 1 else 0) + } + + /*val managedColumns: Array> + get() = _managedColumns ?: _columns.value.let { _ /* unwrap lazy */ -> _managedColumns!! }*/ val managedColNames: Array get() = _managedColNames ?: _columns.value.let { _managedColNames!! } - val managedColTypes: Array> + val managedColTypes: Array> get() = _managedColTypes ?: _columns.value.let { _managedColTypes!! } + val managedColTypeNames: Array + get() = _managedColTypeNames ?: _columns.value.let { _managedColTypeNames!! } + private var _columnsByName: Map>? = null @Deprecated("names are now `CharSequence`s with undefined hashCode()/equals()") @@ -223,7 +312,14 @@ private constructor( return delegates[lens] ?: simpleDelegate as SqlPropertyDelegate } internal fun columnByLens(lens: StoredLens): StoredNamedLens? = - (columnIndices as Map, Int>)[lens]?.let { columns[it] as StoredNamedLens } + colIndexByLens(lens)?.let { columns[it] as StoredNamedLens } + + fun > typeOf(col: StoredLens): Ilk = ( + if (col == pkColumn) idColType else managedColTypes[colIndexByLens(col)!! - columns.size + managedColNames.size] + ) as Ilk + + private fun colIndexByLens(lens: StoredLens) = + (columnIndices as Map, Int>)[lens] @JvmSynthetic internal fun commitValues(record: Record, mutFieldValues: Array) { schema.forEachIndexed(schema.mutableFieldSet) { i, field -> @@ -249,7 +345,7 @@ private constructor( .append(", name=").append(name) .append(", ") - // don't trigger initialization, it may be broken + // don't trigger initialization, it could be broken if (_columns.isInitialized()) append(columns.size).append(" columns") else append("uninitialized") @@ -270,11 +366,11 @@ private constructor( typealias SimpleTable = Table @Suppress("NOTHING_TO_INLINE") // pass-through -inline fun , ID : IdBound> tableOf(schema: SCH, name: String, idColName: String, idColType: DataType.Simple): SimpleTable = +inline fun , ID : IdBound> tableOf(schema: SCH, name: String, idColName: String, idColType: DataType.NotNull.Simple): Table = Table(schema, name, idColName, idColType) @Suppress("NOTHING_TO_INLINE") // pass-through -inline fun , ID : IdBound> tableOf(schema: SCH, name: String, idCol: ImmutableField>): SimpleTable = +inline fun , ID : IdBound> tableOf(schema: SCH, name: String, idCol: ImmutableField>): Table = Table(schema, name, idCol) // just extend Table, @@ -282,19 +378,19 @@ inline fun , ID : IdBound> tableOf(schema: SCH, name: String, // (he-he, modern Java allows writing `new Table<>() {}`): inline fun , ID : IdBound> tableOf( - schema: SCH, name: String, idColName: String, idColType: DataType.Simple, - crossinline meta: SCH.() -> Array> + schema: SCH, name: String, idColName: String, idColType: DataType.NotNull.Simple, + crossinline meta: SCH.() -> Array> ): Table = object : Table(schema, name, idColName, idColType) { - override fun meta(): Array> = meta.invoke(schema) + override fun SCH.meta(): Array> = meta.invoke(schema) } inline fun , ID : IdBound> tableOf( - schema: SCH, name: String, idCol: ImmutableField>, - crossinline meta: () -> Array> + schema: SCH, name: String, idCol: ImmutableField>, + crossinline meta: SCH.() -> Array> ): Table = object : Table(schema, name, idCol) { - override fun meta(): Array> = meta.invoke() + override fun SCH.meta(): Array> = meta.invoke(schema) } @Suppress("NOTHING_TO_INLINE") // just to be consistent with other functions diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Blocking.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Blocking.kt index f4b45b66..59ea499e 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Blocking.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Blocking.kt @@ -11,6 +11,7 @@ import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.struct.StructSnapshot import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.Ilk import net.aquadc.persistence.type.SimpleNullable import java.io.InputStream @@ -20,23 +21,23 @@ import java.io.InputStream interface Blocking { // Android SQLite API has special methods for single-cell selections fun cell( - query: String, - argumentTypes: Array>, arguments: Array, - type: DataType, orElse: () -> T + query: String, + argumentTypes: Array>>, arguments: Array, + type: Ilk, orElse: () -> T ): T fun select( - query: String, argumentTypes: Array>, arguments: Array, expectedCols: Int + query: String, argumentTypes: Array>>, arguments: Array, expectedCols: Int ): CUR fun sizeHint(cursor: CUR): Int fun next(cursor: CUR): Boolean - fun cellByName(cursor: CUR, name: CharSequence, type: DataType): T - fun cellAt(cursor: CUR, col: Int, type: DataType): T + fun cellByName(cursor: CUR, name: CharSequence, type: Ilk): T + fun cellAt(cursor: CUR, col: Int, type: Ilk): T - fun rowByName(cursor: CUR, columnNames: Array, columnTypes: Array>): Array - fun rowByPosition(cursor: CUR, offset: Int, types: Array>): Array + fun rowByName(cursor: CUR, columnNames: Array, columnTypes: Array>): Array + fun rowByPosition(cursor: CUR, offset: Int, types: Array>): Array /** * Closes the given cursor. @@ -50,7 +51,7 @@ interface Blocking { object Eagerly { inline fun cell( - returnType: DataType.Simple, noinline orElse: () -> R = throwNse + returnType: DataType.NotNull.Simple, noinline orElse: () -> R = throwNse ): Fetch, R> = FetchCellEagerly(returnType, orElse) @@ -59,7 +60,7 @@ object Eagerly { ): Fetch, R?> = FetchCellEagerly(returnType, orElse) - inline fun col(elementType: DataType.Simple): Fetch, List> = + inline fun col(elementType: DataType.NotNull.Simple): Fetch, List> = FetchColEagerly(elementType) inline fun col(elementType: SimpleNullable): Fetch, List> = @@ -78,7 +79,7 @@ object Eagerly { object Lazily { inline fun cell( - returnType: DataType.Simple, noinline orElse: () -> R = throwNse + returnType: DataType.NotNull.Simple, noinline orElse: () -> R = throwNse ): Fetch, Lazy> = FetchCellLazily(returnType, orElse) @@ -88,7 +89,7 @@ object Lazily { FetchCellLazily(returnType, orElse) inline fun col( - elementType: DataType.Simple + elementType: DataType.NotNull.Simple ): Fetch, CloseableIterator> = FetchColLazily(elementType) diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Eager.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Eager.kt index 7e048c7f..6ce16171 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Eager.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Eager.kt @@ -7,22 +7,23 @@ import net.aquadc.persistence.sql.mapRow import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.StructSnapshot import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.Ilk @PublishedApi internal class FetchCellEagerly( - private val rt: DataType, + private val rt: Ilk, private val orElse: () -> R ) : Fetch, R> { override fun fetch( - from: Blocking, query: String, argumentTypes: Array>, arguments: Array + from: Blocking, query: String, argumentTypes: Array>, arguments: Array ): R = from.cell(query, argumentTypes, arguments, rt, orElse) } @PublishedApi internal class FetchColEagerly( - private val rt: DataType + private val rt: Ilk ) : Fetch, List> { override fun fetch( - from: Blocking, query: String, argumentTypes: Array>, arguments: Array + from: Blocking, query: String, argumentTypes: Array>, arguments: Array ): List { val cur = from.select(query, argumentTypes, arguments, 1) try { @@ -47,7 +48,7 @@ import net.aquadc.persistence.type.DataType private val orElse: () -> StructSnapshot ) : Fetch, StructSnapshot> { override fun fetch( - from: Blocking, query: String, argumentTypes: Array>, arguments: Array + from: Blocking, query: String, argumentTypes: Array>, arguments: Array ): StructSnapshot { val managedColNames = table.managedColNames val managedColTypes = table.managedColTypes @@ -68,7 +69,7 @@ import net.aquadc.persistence.type.DataType private val bindBy: BindBy ) : Fetch, List>> { override fun fetch( - from: Blocking, query: String, argumentTypes: Array>, arguments: Array + from: Blocking, query: String, argumentTypes: Array>, arguments: Array ): List> { val colNames = table.managedColNames val colTypes = table.managedColTypes diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt index 2294c728..08cb8705 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt @@ -23,6 +23,7 @@ import net.aquadc.persistence.sql.noOrder import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.Ilk import net.aquadc.persistence.type.i64 import org.intellij.lang.annotations.Language import java.sql.Connection @@ -162,7 +163,7 @@ class JdbcSession( } override fun , ID : IdBound, T> fetchSingle( - table: Table, colName: CharSequence, colType: DataType, id: ID + table: Table, colName: CharSequence, colType: Ilk, id: ID ): T = select(table /* fixme allocation */, arrayOf(colName), pkCond(table, id), noOrder()) .fetchSingle(colType) @@ -180,14 +181,14 @@ class JdbcSession( select(table, null, condition, noOrder()).fetchSingle(i64) override fun , ID : IdBound> fetch( - table: Table, columnNames: Array, columnTypes: Array>, id: ID + table: Table, columnNames: Array, columnTypes: Array>, id: ID ): Array = select(table, columnNames, pkCond(table, id), noOrder()).fetchColumns(columnTypes) override val transaction: RealTransaction? get() = this@JdbcSession.transaction - private fun ResultSet.fetchAllRows(type: DataType): List { + private fun ResultSet.fetchAllRows(type: Ilk): List { // TODO pre-size collection && try not to box primitives val values = ArrayList() while (next()) @@ -196,7 +197,7 @@ class JdbcSession( return values } - private fun ResultSet.fetchSingle(type: DataType): T = + private fun ResultSet.fetchSingle(type: Ilk): T = try { check(next()) type.get(this, 0) @@ -204,7 +205,7 @@ class JdbcSession( close() } - private fun ResultSet.fetchColumns(types: Array>): Array = + private fun ResultSet.fetchColumns(types: Array>): Array = try { check(next()) types.mapIndexedToArray { index, type -> @@ -214,54 +215,62 @@ class JdbcSession( close() } - private fun DataType.bind(statement: PreparedStatement, index: Int, value: T) { + private fun Ilk.bind(statement: PreparedStatement, index: Int, value: T) { val i = 1 + index - flattened { isNullable, simple -> - if (value == null) { - check(isNullable) - statement.setNull(i, Types.NULL) - } else { - val v = simple.store(value) - when (simple.kind) { - DataType.Simple.Kind.Bool -> statement.setBoolean(i, v as Boolean) - DataType.Simple.Kind.I32 -> statement.setInt(i, v as Int) - DataType.Simple.Kind.I64 -> statement.setLong(i, v as Long) - DataType.Simple.Kind.F32 -> statement.setFloat(i, v as Float) - DataType.Simple.Kind.F64 -> statement.setDouble(i, v as Double) - DataType.Simple.Kind.Str -> statement.setString(i, v as String) - // not sure whether setBlob should be used: - DataType.Simple.Kind.Blob -> statement.setObject(i, v as ByteArray) - }//.also { } + val custom = this.custom + if (custom == null) { + (type as DataType).flattened { isNullable, simple -> + if (value == null) { + check(isNullable) + statement.setNull(i, Types.NULL) + } else { + val v = simple.store(value) + when (simple.kind) { + DataType.NotNull.Simple.Kind.Bool -> statement.setBoolean(i, v as Boolean) + DataType.NotNull.Simple.Kind.I32 -> statement.setInt(i, v as Int) + DataType.NotNull.Simple.Kind.I64 -> statement.setLong(i, v as Long) + DataType.NotNull.Simple.Kind.F32 -> statement.setFloat(i, v as Float) + DataType.NotNull.Simple.Kind.F64 -> statement.setDouble(i, v as Double) + DataType.NotNull.Simple.Kind.Str -> statement.setString(i, v as String) + // not sure whether setBlob should be used: + DataType.NotNull.Simple.Kind.Blob -> statement.setObject(i, v as ByteArray) + }//.also { } + } } + } else { + statement.setObject(i, custom.invoke(value), Types.OTHER) } } @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST") - private /*wannabe inline*/ fun DataType.get(resultSet: ResultSet, index: Int): T { + private /*wannabe inline*/ fun Ilk.get(resultSet: ResultSet, index: Int): T { return get1indexed(resultSet, 1 + index) } - private fun DataType.get1indexed(resultSet: ResultSet, i: Int): T { - return flattened { isNullable, simple -> - val v = when (simple.kind) { - DataType.Simple.Kind.Bool -> resultSet.getBoolean(i) - DataType.Simple.Kind.I32 -> resultSet.getInt(i) - DataType.Simple.Kind.I64 -> resultSet.getLong(i) - DataType.Simple.Kind.F32 -> resultSet.getFloat(i) - DataType.Simple.Kind.F64 -> resultSet.getDouble(i) - DataType.Simple.Kind.Str -> resultSet.getString(i) - DataType.Simple.Kind.Blob -> resultSet.getBytes(i) - else -> throw AssertionError() + private fun Ilk.get1indexed(resultSet: ResultSet, i: Int): T = custom.let { custom -> + if (custom == null) { + (type as DataType).flattened { isNullable, simple -> + val v = when (simple.kind) { + DataType.Simple.Kind.Bool -> resultSet.getBoolean(i) + DataType.Simple.Kind.I32 -> resultSet.getInt(i) + DataType.Simple.Kind.I64 -> resultSet.getLong(i) + DataType.Simple.Kind.F32 -> resultSet.getFloat(i) + DataType.Simple.Kind.F64 -> resultSet.getDouble(i) + DataType.Simple.Kind.Str -> resultSet.getString(i) + DataType.Simple.Kind.Blob -> resultSet.getBytes(i) + else -> throw AssertionError() + } + // must check, will get zeroes otherwise + if (resultSet.wasNull()) check(isNullable).let { null as T } + else simple.load(v) } - - // must check, will get zeroes otherwise - if (resultSet.wasNull()) check(isNullable).let { null as T } - else simple.load(v) + } else { + custom.back(resultSet.getObject(i)) } } override fun cell( - query: String, argumentTypes: Array>, arguments: Array, type: DataType, orElse: () -> T + query: String, argumentTypes: Array>>, arguments: Array, type: Ilk, orElse: () -> T ): T { val rs = select(query, argumentTypes, arguments, 1) try { @@ -275,13 +284,13 @@ class JdbcSession( } override fun select( - query: String, argumentTypes: Array>, arguments: Array, expectedCols: Int + query: String, argumentTypes: Array>>, arguments: Array, expectedCols: Int ): ResultSet = selectStatements .getOrSet(::HashMap) .getOrPut(query) { connection.prepareStatement(query) } .also { stmt -> for (idx in argumentTypes.indices) { - (argumentTypes[idx] as DataType).bind(stmt, idx, arguments[idx]) + (argumentTypes[idx] as Ilk).bind(stmt, idx, arguments[idx]) } } .executeQuery() @@ -297,13 +306,13 @@ class JdbcSession( override fun sizeHint(cursor: ResultSet): Int = -1 override fun next(cursor: ResultSet): Boolean = cursor.next() - override fun cellByName(cursor: ResultSet, name: CharSequence, type: DataType): T = + override fun cellByName(cursor: ResultSet, name: CharSequence, type: Ilk): T = type.get1indexed(cursor, cursor.findColumn(name.toString())) - override fun cellAt(cursor: ResultSet, col: Int, type: DataType): T = + override fun cellAt(cursor: ResultSet, col: Int, type: Ilk): T = type.get(cursor, col) - override fun rowByName(cursor: ResultSet, columnNames: Array, columnTypes: Array>): Array = + override fun rowByName(cursor: ResultSet, columnNames: Array, columnTypes: Array>): Array = Array(columnNames.size) { idx -> cellByName(cursor, columnNames[idx], columnTypes[idx]) } - override fun rowByPosition(cursor: ResultSet, offset: Int, types: Array>): Array = + override fun rowByPosition(cursor: ResultSet, offset: Int, types: Array>): Array = Array(types.size) { idx -> types[idx].get(cursor, offset + idx) } override fun close(cursor: ResultSet) = @@ -342,9 +351,9 @@ class JdbcSession( } override fun rawQuery( - @Language("SQL") query: String, - argumentTypes: Array>, - fetch: Fetch, R> + @Language("SQL") query: String, + argumentTypes: Array>, + fetch: Fetch, R> ): VarFunc = BlockingQuery(lowLevel, query, argumentTypes, fetch) diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Lazy.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Lazy.kt index 81fcca43..5cec0585 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Lazy.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Lazy.kt @@ -6,24 +6,24 @@ import net.aquadc.persistence.IteratorAndTransientStruct import net.aquadc.persistence.NullSchema import net.aquadc.persistence.sql.BindBy import net.aquadc.persistence.sql.Fetch -import net.aquadc.persistence.sql.Record import net.aquadc.persistence.sql.Table import net.aquadc.persistence.struct.FieldDef import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.struct.StructSnapshot import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.Ilk import java.io.FilterInputStream import java.io.InputStream import java.sql.ResultSet import java.sql.SQLFeatureNotSupportedException @PublishedApi internal class FetchCellLazily( - private val rt: DataType, - private val orElse: () -> R + private val rt: Ilk, + private val orElse: () -> R ) : Fetch, Lazy> { override fun fetch( - from: Blocking, query: String, argumentTypes: Array>, arguments: Array + from: Blocking, query: String, argumentTypes: Array>, arguments: Array ): Lazy { val rt = rt; val orElse = orElse // don't capture `this` return lazy { from.cell(query, argumentTypes, arguments, rt, orElse) } @@ -31,10 +31,10 @@ import java.sql.SQLFeatureNotSupportedException } @PublishedApi internal class FetchColLazily( - private val rt: DataType + private val rt: Ilk ) : Fetch, CloseableIterator> { override fun fetch( - from: Blocking, query: String, argumentTypes: Array>, arguments: Array + from: Blocking, query: String, argumentTypes: Array>, arguments: Array ): CloseableIterator { val rt = rt // don't capture `this` return object : CurIterator(from, query, argumentTypes, arguments, null, BindBy.Name/*whatever*/, NullSchema) { @@ -51,7 +51,7 @@ import java.sql.SQLFeatureNotSupportedException private var fallback: Struct? = null override fun fetch( - from: Blocking, query: String, argumentTypes: Array>, arguments: Array + from: Blocking, query: String, argumentTypes: Array>, arguments: Array ): CloseableStruct { val lazy = CurIterator>(from, query, argumentTypes, arguments, table, bindBy, table.schema) @@ -69,7 +69,7 @@ import java.sql.SQLFeatureNotSupportedException private val transient: Boolean ) : Fetch, CloseableIterator>> { override fun fetch( - from: Blocking, query: String, argumentTypes: Array>, arguments: Array + from: Blocking, query: String, argumentTypes: Array>, arguments: Array ): CloseableIterator> { val transient = transient // don't capture this return object : CurIterator>( @@ -84,7 +84,7 @@ import java.sql.SQLFeatureNotSupportedException @PublishedApi internal object InputStreamFromResultSet : Fetch, InputStream> { override fun fetch( - from: Blocking, query: String, argumentTypes: Array>, arguments: Array + from: Blocking, query: String, argumentTypes: Array>, arguments: Array ): InputStream = from.select(query, argumentTypes, arguments, 1).let { rs -> check(rs.next()) { rs.close(); "ResultSet is empty." } @@ -105,20 +105,20 @@ import java.sql.SQLFeatureNotSupportedException @Deprecated("moved") typealias CloseableStruct = CloseableStruct private open class CurIterator, R>( - protected val from: Blocking, - private val query: String, - private val argumentTypes: Array>, - private val arguments: Array, - - private val table: Table?, - private val bindBy: BindBy, - schema: SCH + protected val from: Blocking, + private val query: String, + private val argumentTypes: Array>, + private val arguments: Array, + + private val table: Table?, + private val bindBy: BindBy, + schema: SCH ) : IteratorAndTransientStruct(schema) { private var _cur: CUR? = null private val cur get() = _cur ?: run { check(state == 0) { "Iterator is closed." } - from.select(query, argumentTypes, arguments, table?.managedColumns?.size ?: 1).also { _cur = it } + from.select(query, argumentTypes, arguments, table?.managedColNames?.size ?: 1).also { _cur = it } } var state = 0 diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/LowLevelSession.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/LowLevelSession.kt index 0b0ef03f..3ae3277d 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/LowLevelSession.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/LowLevelSession.kt @@ -17,21 +17,26 @@ import net.aquadc.persistence.struct.Lens import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.Ilk import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.getOrSet internal class BlockingQuery( - private val session: Blocking, - private val query: String, - private val argumentTypes: Array>, - private val fetch: Fetch, R> + private val session: Blocking, + private val query: String, + private val argumentTypes: Array>, + private val fetch: Fetch, R> ) : VarFuncImpl(), VarFunc { override fun invokeUnchecked(vararg arg: Any): R = fetch.fetch(session, query, argumentTypes, arg) + // for debugging + override fun toString(): String = + fetch.javaClass.simpleName + '(' + query + ')' + } internal abstract class LowLevelSession : Blocking { @@ -39,8 +44,8 @@ internal abstract class LowLevelSession : Blocking { /** [columnNames] : [values] is a map */ abstract fun , ID : IdBound> update( - table: Table, id: ID, - columnNames: Any/*=[arrayOf]CharSequence*/, columnTypes: Any/*=[arrayOf]DataType*/, values: Any?/*=[arrayOf]Any?*/ + table: Table, id: ID, + columnNames: Any/*=[arrayOf]CharSequence*/, columnTypes: Any/*=[arrayOf]Ilk*/, values: Any?/*=[arrayOf]Any?*/ ) abstract fun , ID : IdBound> delete(table: Table, primaryKey: ID) @@ -50,7 +55,7 @@ internal abstract class LowLevelSession : Blocking { abstract fun onTransactionEnd(successful: Boolean) abstract fun , ID : IdBound, T> fetchSingle( - table: Table, colName: CharSequence, colType: DataType, id: ID + table: Table, colName: CharSequence, colType: Ilk, id: ID ): T abstract fun , ID : IdBound> fetchPrimaryKeys( @@ -62,7 +67,7 @@ internal abstract class LowLevelSession : Blocking { ): Long abstract fun , ID : IdBound> fetch( - table: Table, columnNames: Array, columnTypes: Array>, id: ID + table: Table, columnNames: Array, columnTypes: Array>, id: ID ): Array abstract val transaction: RealTransaction? diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/SqliteSession.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/SqliteSession.kt index 84082857..525544dd 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/SqliteSession.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/SqliteSession.kt @@ -34,6 +34,7 @@ import net.aquadc.persistence.sql.noOrder import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.Ilk import net.aquadc.persistence.type.i64 import org.intellij.lang.annotations.Language import java.util.concurrent.locks.ReentrantReadWriteLock @@ -68,19 +69,19 @@ class SqliteSession( val statement = dao.insertStatement ?: connection.compileStatement(SqliteDialect.insert(table)).also { dao.insertStatement = it } bindInsertionParams(table, data) { type, idx, value -> - type.bind(statement, idx, value) + (type.type as DataType).bind(statement, idx, value) } val id = statement.executeInsert() check(id != -1L) - return table.idColType.let { + return table.idColType.type.let { it.load(when (it.kind) { - DataType.Simple.Kind.Bool -> throw IllegalArgumentException() // O RLY?! Boolean primary key?.. - DataType.Simple.Kind.I32 -> id.chkIn(Int.MIN_VALUE, Int.MAX_VALUE, Int::class.java).toInt() - DataType.Simple.Kind.I64 -> id - DataType.Simple.Kind.F32 -> throw IllegalArgumentException() // O RLY?! Floating primary key?.. - DataType.Simple.Kind.F64 -> throw IllegalArgumentException() - DataType.Simple.Kind.Str -> id.toString() - DataType.Simple.Kind.Blob -> throw IllegalArgumentException() // Possible but unclear what do you want + DataType.NotNull.Simple.Kind.Bool -> throw IllegalArgumentException() // O RLY?! Boolean primary key?.. + DataType.NotNull.Simple.Kind.I32 -> id.chkIn(Int.MIN_VALUE, Int.MAX_VALUE, Int::class.java).toInt() + DataType.NotNull.Simple.Kind.I64 -> id + DataType.NotNull.Simple.Kind.F32 -> throw IllegalArgumentException() // O RLY?! Floating primary key?.. + DataType.NotNull.Simple.Kind.F64 -> throw IllegalArgumentException() + DataType.NotNull.Simple.Kind.Str -> id.toString() + DataType.NotNull.Simple.Kind.Blob -> throw IllegalArgumentException() // Possible but unclear what do you want else -> throw AssertionError() }) } @@ -103,16 +104,16 @@ class SqliteSession( override fun , ID : IdBound> update(table: Table, id: ID, columnNames: Any, columnTypes: Any, values: Any?) { val statement = updateStatementWLocked(table, columnNames) val colCount = bindValues(columnTypes, values) { type, idx, value -> - type.bind(statement, idx, value) + (type.type as DataType).bind(statement, idx, value) } - table.idColType.bind(statement, colCount, id) + table.idColType.type.bind(statement, colCount, id) check(statement.executeUpdateDelete() == 1) } override fun , ID : IdBound> delete(table: Table, primaryKey: ID) { val dao = getDao(table) val statement = dao.deleteStatement ?: connection.compileStatement(SqliteDialect.deleteRecordQuery(table)).also { dao.deleteStatement = it } - table.idColType.bind(statement, 0, primaryKey) + table.idColType.type.bind(statement, 0, primaryKey) check(statement.executeUpdateDelete() == 1) } @@ -166,17 +167,17 @@ class SqliteSession( // a workaround for binding BLOBs, as suggested in https://stackoverflow.com/a/23159664/3050249 private inner class CurFac>( - private val condition: WhereCondition?, - private val table: Table?, - private val argumentTypes: Array>?, - private val arguments: Array? + private val condition: WhereCondition?, + private val table: Table?, + private val argumentTypes: Array>>?, + private val arguments: Array? ) : SQLiteDatabase.CursorFactory { override fun newCursor(db: SQLiteDatabase?, masterQuery: SQLiteCursorDriver?, editTable: String?, query: SQLiteQuery): Cursor { when { condition != null -> bindQueryParams(condition, table!!) { type, idx, value -> - type.bind(query, idx, value) + (type.type as DataType).bind(query, idx, value) } argumentTypes != null -> arguments!!.let { args -> argumentTypes.forEachIndexed { idx, type -> @@ -192,20 +193,20 @@ class SqliteSession( } override fun , ID : IdBound, T> fetchSingle( - table: Table, colName: CharSequence, colType: DataType, id: ID + table: Table, colName: CharSequence, colType: Ilk, id: ID ): T = - select(table, arrayOf(colName) /* fixme allocation */, pkCond(table, id), noOrder()) - .fetchSingle(colType) + select(table, arrayOf(colName) /* fixme allocation */, pkCond(table, id), noOrder()) + .fetchSingle(colType.type as DataType) override fun , ID : IdBound> fetchPrimaryKeys( table: Table, condition: WhereCondition, order: Array> ): Array = - select(table, arrayOf(table.pkColumn.name(table.schema)) /* fixme allocation */, condition, order) - .fetchAllRows(table.idColType) - .array() as Array + select(table, arrayOf(table.pkColumn.name(table.schema)) /* fixme allocation */, condition, order) + .fetchAllRows(table.idColType.type) + .array() as Array override fun , ID : IdBound> fetch( - table: Table, columnNames: Array, columnTypes: Array>, id: ID + table: Table, columnNames: Array, columnTypes: Array>, id: ID ): Array = select(table, columnNames, pkCond(table, id), noOrder()).fetchColumns(columnTypes) @@ -237,11 +238,11 @@ class SqliteSession( close() } - private fun Cursor.fetchColumns(columnTypes: Array>): Array = + private fun Cursor.fetchColumns(columnTypes: Array>): Array = try { check(moveToFirst()) columnTypes.mapIndexedToArray { index, type -> - type.get(this, index) + type.type.get(this, index) } } finally { close() @@ -256,13 +257,13 @@ class SqliteSession( } else { val v = simple.store(value) when (simple.kind) { - DataType.Simple.Kind.Bool -> statement.bindLong(i, if (v as Boolean) 1 else 0) - DataType.Simple.Kind.I32, - DataType.Simple.Kind.I64 -> statement.bindLong(i, (v as Number).toLong()) - DataType.Simple.Kind.F32, - DataType.Simple.Kind.F64 -> statement.bindDouble(i, (v as Number).toDouble()) - DataType.Simple.Kind.Str -> statement.bindString(i, v as String) - DataType.Simple.Kind.Blob -> statement.bindBlob(i, v as ByteArray) + DataType.NotNull.Simple.Kind.Bool -> statement.bindLong(i, if (v as Boolean) 1 else 0) + DataType.NotNull.Simple.Kind.I32, + DataType.NotNull.Simple.Kind.I64 -> statement.bindLong(i, (v as Number).toLong()) + DataType.NotNull.Simple.Kind.F32, + DataType.NotNull.Simple.Kind.F64 -> statement.bindDouble(i, (v as Number).toDouble()) + DataType.NotNull.Simple.Kind.Str -> statement.bindString(i, v as String) + DataType.NotNull.Simple.Kind.Blob -> statement.bindBlob(i, v as ByteArray) }//.also { } } } @@ -273,31 +274,31 @@ class SqliteSession( if (cursor.isNull(index)) check(isNullable).let { null as T } else simple.load(when (simple.kind) { - DataType.Simple.Kind.Bool -> cursor.getInt(index) == 1 - DataType.Simple.Kind.I32 -> cursor.getInt(index) - DataType.Simple.Kind.I64 -> cursor.getLong(index) - DataType.Simple.Kind.F32 -> cursor.getFloat(index) - DataType.Simple.Kind.F64 -> cursor.getDouble(index) - DataType.Simple.Kind.Str -> cursor.getString(index) - DataType.Simple.Kind.Blob -> cursor.getBlob(index) + DataType.NotNull.Simple.Kind.Bool -> cursor.getInt(index) == 1 + DataType.NotNull.Simple.Kind.I32 -> cursor.getInt(index) + DataType.NotNull.Simple.Kind.I64 -> cursor.getLong(index) + DataType.NotNull.Simple.Kind.F32 -> cursor.getFloat(index) + DataType.NotNull.Simple.Kind.F64 -> cursor.getDouble(index) + DataType.NotNull.Simple.Kind.Str -> cursor.getString(index) + DataType.NotNull.Simple.Kind.Blob -> cursor.getBlob(index) else -> throw AssertionError() }) } override fun cell( - query: String, argumentTypes: Array>, arguments: Array, type: DataType, orElse: () -> T + query: String, argumentTypes: Array>>, arguments: Array, type: Ilk, orElse: () -> T ): T { val cur = select(query, argumentTypes, arguments, 1) try { if (!cur.moveToFirst()) return orElse() - val value = type.get(cur, 0) + val value = (type.type as DataType).get(cur, 0) check(!cur.moveToNext()) return value } finally { cur.close() } } - override fun select(query: String, argumentTypes: Array>, arguments: Array, expectedCols: Int): Cursor = + override fun select(query: String, argumentTypes: Array>>, arguments: Array, expectedCols: Int): Cursor = connection.rawQueryWithFactory( CurFac(null, null, argumentTypes, arguments), query, @@ -312,23 +313,20 @@ class SqliteSession( override fun sizeHint(cursor: Cursor): Int = cursor.count override fun next(cursor: Cursor): Boolean = cursor.moveToNext() - private fun cellByName(cursor: Cursor, guess: Int, name: CharSequence, type: DataType): T = - type.get( - cursor, - cursor.getColIdx(guess, name) - ) - override fun cellByName(cursor: Cursor, name: CharSequence, type: DataType): T = + private fun cellByName(cursor: Cursor, guess: Int, name: CharSequence, type: Ilk): T = + (type.type as DataType).get(cursor, cursor.getColIdx(guess, name)) + override fun cellByName(cursor: Cursor, name: CharSequence, type: Ilk): T = cellByName(cursor, Integer.MAX_VALUE /* don't even try to guess */, name, type) - override fun cellAt(cursor: Cursor, col: Int, type: DataType): T = - type.get(cursor, col) + override fun cellAt(cursor: Cursor, col: Int, type: Ilk): T = + (type.type as DataType).get(cursor, col) - override fun rowByName(cursor: Cursor, columnNames: Array, columnTypes: Array>): Array = + override fun rowByName(cursor: Cursor, columnNames: Array, columnTypes: Array>): Array = Array(columnNames.size) { idx -> cellByName(cursor, idx, columnNames[idx], columnTypes[idx]) } - override fun rowByPosition(cursor: Cursor, offset: Int, types: Array>): Array = + override fun rowByPosition(cursor: Cursor, offset: Int, types: Array>): Array = Array(types.size) { idx -> - types[idx].get(cursor, offset + idx) + types[idx].type.get(cursor, offset + idx) } // TODO: could subclass SQLiteCursor and attach IntArray instead of looking this up every time @@ -375,7 +373,7 @@ class SqliteSession( } } - override fun rawQuery(@Language("SQL") query: String, argumentTypes: Array>, fetch: Fetch, R>): VarFunc = + override fun rawQuery(@Language("SQL") query: String, argumentTypes: Array>, fetch: Fetch, R>): VarFunc = BlockingQuery(lowLevel, query, argumentTypes, fetch) } diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/delegates.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/delegates.kt index e8d234f6..ffa56a68 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/delegates.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/delegates.kt @@ -6,6 +6,7 @@ import net.aquadc.persistence.struct.FieldDef import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.StoredNamedLens import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.Ilk /** * Responsible for fetching and updating data. @@ -34,7 +35,7 @@ internal class Simple, ID : IdBound> : SqlPropertyDelegate fetch( lowSession: LowLevelSession<*, *>, table: Table, field: FieldDef, id: ID ): T = table.schema.let { sch -> // the following cast seems to be unnecessary with new inference - lowSession.fetchSingle(table, sch.nameOf(field), sch.typeOf(field as FieldDef>), id) + lowSession.fetchSingle(table, sch.nameOf(field), table.typeOf(field as FieldDef>), id) } override fun get( @@ -47,18 +48,16 @@ internal class Simple, ID : IdBound> : SqlPropertyDelegate, table: Table, field: FieldDef, id: ID, previous: T, update: T ): Unit = table.schema.let { sch -> - lowSession.update(table, id, field.name(sch), field.type(sch), update) + lowSession.update(table, id, field.name(sch), table.typeOf(field), update) } } internal class Embedded, ID : IdBound>( - private val schema: SCH, - tmpColumns: List>, private val recipe: Array, // contains a single start-end pair with (flattened) nesting inside - private val myOffset: Int + private val myOffset: Int, + private val columnNames: Array, + private val columnTypes: Array> ) : SqlPropertyDelegate { - private val columnNames = Array(tmpColumns.size) { i -> tmpColumns[i].name(schema) } - private val columnTypes = Array(tmpColumns.size) { i -> tmpColumns[i].type(schema) } override fun fetch( lowSession: LowLevelSession<*, *>, table: Table, field: FieldDef, id: ID diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/BaseDialect.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/BaseDialect.kt index 19b98749..5b694aeb 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/BaseDialect.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/BaseDialect.kt @@ -17,7 +17,7 @@ import java.io.DataOutputStream @RestrictTo(RestrictTo.Scope.LIBRARY) /*internal*/ open class BaseDialect( - private val types: InlineEnumMap, + private val types: InlineEnumMap, private val truncate: String ) : Dialect { @@ -109,10 +109,14 @@ import java.io.DataOutputStream val managedPk = table.pkField != null val sb = StringBuilder("CREATE").append(' ') .appendIf(temporary, "TEMP ").append("TABLE").append(' ').appendName(table.name).append(" (") - .appendName(table.idColName).append(' ').appendPkType(table.idColType, managedPk).append(" PRIMARY KEY") + .appendName(table.idColName).append(' ').let { + val t = table.idColTypeName + if (t is DataType<*>) it.appendPkType(t as DataType.NotNull.Simple<*>, managedPk) + else it.append(t as CharSequence) + }.append(" PRIMARY KEY") val colNames = table.managedColNames - val colTypes = table.managedColTypes + val colTypes = table.managedColTypeNames val startIndex = if (managedPk) 1 else 0 val endExclusive = colNames.size @@ -121,12 +125,17 @@ import java.io.DataOutputStream // skip for (i in startIndex until endExclusive) { - sb.appendName(colNames[i]).append(' ').appendNameOf(colTypes[i]) + sb.appendName(colNames[i]).append(' ') + .let { + val t = colTypes[i] + if (t is DataType<*>) it.appendNameOf(t) + else it.append(t as CharSequence) + } /* this is useless since we can store only a full struct with all fields filled: if (hasDefault) sb.appendDefault(...) */ - .append(", ") + .append(", ") } } sb.setLength(sb.length - 2) // trim last comma; schema.fields must not be empty @@ -134,7 +143,7 @@ import java.io.DataOutputStream } private inline fun StringBuilder.appendIf(cond: Boolean, what: String): StringBuilder = if (cond) append(what) else this - protected open fun StringBuilder.appendPkType(type: DataType.Simple<*>, managed: Boolean): StringBuilder = + protected open fun StringBuilder.appendPkType(type: DataType.NotNull.Simple<*>, managed: Boolean): StringBuilder = appendNameOf(type) // used by SQLite, overridden for Postgres private fun StringBuilder.appendDefault(type: DataType, default: T) { @@ -146,22 +155,22 @@ import java.io.DataOutputStream when (type) { is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.Simple -> type.store(default).let { v -> + is DataType.NotNull.Simple -> type.store(default).let { v -> when (type.kind) { - DataType.Simple.Kind.Bool -> append(if (v as Boolean) '1' else '0') - DataType.Simple.Kind.I32, - DataType.Simple.Kind.I64, - DataType.Simple.Kind.F32, - DataType.Simple.Kind.F64 -> append('\'').append(v.toString()).append('\'') - DataType.Simple.Kind.Str -> append('\'').append(v as String).append('\'') - DataType.Simple.Kind.Blob -> append("x'").append(TODO("append HEX") as String).append('\'') + DataType.NotNull.Simple.Kind.Bool -> append(if (v as Boolean) '1' else '0') + DataType.NotNull.Simple.Kind.I32, + DataType.NotNull.Simple.Kind.I64, + DataType.NotNull.Simple.Kind.F32, + DataType.NotNull.Simple.Kind.F64 -> append('\'').append(v.toString()).append('\'') + DataType.NotNull.Simple.Kind.Str -> append('\'').append(v as String).append('\'') + DataType.NotNull.Simple.Kind.Blob -> append("x'").append(TODO("append HEX") as String).append('\'') }//.also { } Unit } - is DataType.Collect<*, *, *> -> append("x'").append( + is DataType.NotNull.Collect<*, *, *> -> append("x'").append( TODO("append HEX" + ByteArrayOutputStream().also { type.write(DataStreams, DataOutputStream(it), default) }.toByteArray()) as String ).append('\'') - is DataType.Partial<*, *> -> throw UnsupportedOperationException() + is DataType.NotNull.Partial<*, *> -> throw UnsupportedOperationException() } } @@ -169,9 +178,9 @@ import java.io.DataOutputStream val act = if (dataType is DataType.Nullable<*, *>) dataType.actualType else dataType when (act) { is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.Simple<*> -> append(types[act.kind]!!) - is DataType.Collect<*, *, *> -> append("BLOB") - is DataType.Partial<*, *> -> throw UnsupportedOperationException() // column can't be of Partial type at this point + is DataType.NotNull.Simple<*> -> append(types[act.kind]!!) + is DataType.NotNull.Collect<*, *, *> -> append(types[DataType.NotNull.Simple.Kind.Blob]!!) + is DataType.NotNull.Partial<*, *> -> throw UnsupportedOperationException() // column can't be of Partial type at this point } if (dataType === act) { append(" NOT NULL") diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/postgres.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/postgres.kt index 8dbfc6fa..8f60e32a 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/postgres.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/postgres.kt @@ -12,22 +12,22 @@ import net.aquadc.persistence.type.DataType @JvmField val PostgresDialect: Dialect = object : BaseDialect( enumMapOf( - DataType.Simple.Kind.Bool, "bool", - DataType.Simple.Kind.I32, "int", - DataType.Simple.Kind.I64, "int8", - DataType.Simple.Kind.F32, "real", - DataType.Simple.Kind.F64, "float8", - DataType.Simple.Kind.Str, "text", - DataType.Simple.Kind.Blob, "bytea" + DataType.NotNull.Simple.Kind.Bool, "bool", + DataType.NotNull.Simple.Kind.I32, "int", + DataType.NotNull.Simple.Kind.I64, "int8", + DataType.NotNull.Simple.Kind.F32, "real", + DataType.NotNull.Simple.Kind.F64, "float8", + DataType.NotNull.Simple.Kind.Str, "text", + DataType.NotNull.Simple.Kind.Blob, "bytea" ), truncate = "TRUNCATE TABLE" ) { - override fun StringBuilder.appendPkType(type: DataType.Simple<*>, managed: Boolean): StringBuilder = + override fun StringBuilder.appendPkType(type: DataType.NotNull.Simple<*>, managed: Boolean): StringBuilder = if (managed) appendNameOf(type) else { // If PK column is 'managed', we just take `structToInsert[pkField]`. // Otherwise, its our responsibility to make PK auto-generated - if (type.kind == DataType.Simple.Kind.I32) append("serial") - else if (type.kind == DataType.Simple.Kind.I64) append("serial8") + if (type.kind == DataType.NotNull.Simple.Kind.I32) append("serial NOT NULL") + else if (type.kind == DataType.NotNull.Simple.Kind.I64) append("serial8 NOT NULL") else throw UnsupportedOperationException() // wat? Boolean, float, double, string, byte[] primary key? O_o } } diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/sqlite.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/sqlite.kt index efa97d1f..5fddf059 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/sqlite.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/sqlite.kt @@ -12,13 +12,13 @@ import net.aquadc.persistence.type.DataType // wannabe `@JvmField val` but this breaks compilation. Dafuq? object SqliteDialect : BaseDialect( enumMapOf( - DataType.Simple.Kind.Bool, "INTEGER", - DataType.Simple.Kind.I32, "INTEGER", - DataType.Simple.Kind.I64, "INTEGER", - DataType.Simple.Kind.F32, "REAL", - DataType.Simple.Kind.F64, "REAL", - DataType.Simple.Kind.Str, "TEXT", - DataType.Simple.Kind.Blob, "BLOB" + DataType.NotNull.Simple.Kind.Bool, "INTEGER", + DataType.NotNull.Simple.Kind.I32, "INTEGER", + DataType.NotNull.Simple.Kind.I64, "INTEGER", + DataType.NotNull.Simple.Kind.F32, "REAL", + DataType.NotNull.Simple.Kind.F64, "REAL", + DataType.NotNull.Simple.Kind.Str, "TEXT", + DataType.NotNull.Simple.Kind.Blob, "BLOB" ), truncate = "DELETE FROM" ) diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/lenses.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/lenses.kt index 2a40e15c..565ecfba 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/lenses.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/lenses.kt @@ -17,13 +17,13 @@ import net.aquadc.persistence.type.nullable * String concatenation factory to build names for nested lenses. */ interface NamingConvention { - fun concatNames(outer: CharSequence, nested: CharSequence): String + fun concatNames(outer: CharSequence, nested: CharSequence): CharSequence } @JvmName("structLens") inline operator fun < TS : Schema, - US : Schema, UD : DataType.Partial, US>, + US : Schema, UD : DataType.NotNull.Partial, US>, V, VD : DataType > Lens, Struct, Struct, UD>.div( @@ -33,7 +33,7 @@ interface NamingConvention { @JvmName("partialToNonNullableLens") inline operator fun < TS : Schema, - US : Schema, UD : DataType.Partial, US>, + US : Schema, UD : DataType.NotNull.Partial, US>, V : Any, VD : DataType > Lens, Struct, PartialStruct, UD>.div( @@ -43,7 +43,7 @@ interface NamingConvention { @JvmName("partialToNullableLens") inline operator fun < TS : Schema, - US : Schema, UD : DataType.Partial, US>, + US : Schema, UD : DataType.NotNull.Partial, US>, V : Any, VD : DataType.Nullable > Lens, Struct, PartialStruct, UD>.div( @@ -53,7 +53,7 @@ interface NamingConvention { @JvmName("nullablePartialToNonNullableLens") inline operator fun < TS : Schema, - US : Schema, UR : PartialStruct, UD : DataType.Nullable>, + US : Schema, UR : PartialStruct, UD : DataType.Nullable>, V : Any, VD : DataType > Lens, Struct, UR?, UD>.div( @@ -63,7 +63,7 @@ interface NamingConvention { @JvmName("nullablePartialToNullableLens") inline operator fun < TS : Schema, - US : Schema, UR : PartialStruct, UD : DataType.Nullable>, + US : Schema, UR : PartialStruct, UD : DataType.Nullable>, V : Any, VD : DataType.Nullable > Lens, Struct, UR?, UD>.div( @@ -88,7 +88,7 @@ interface NamingConvention { US : Schema, V : Any, VD : DataType > - StoredLens>.rem( + StoredLens>.rem( nested: StoredLens ): StoredLens> = Telescope(null, nullable(nested.type), this, nested) @@ -98,7 +98,7 @@ interface NamingConvention { US : Schema, V : Any, VD : DataType.Nullable > - StoredLens>.rem( + StoredLens>.rem( nested: StoredLens ): StoredLens = Telescope(null, nested.type, this, nested) @@ -108,7 +108,7 @@ interface NamingConvention { US : Schema, V : Any, VD : DataType > - StoredLens>>.rem( + StoredLens>>.rem( nested: StoredLens ): StoredLens> = Telescope(null, nullable(nested.type), this, nested) @@ -118,7 +118,7 @@ interface NamingConvention { US : Schema, V : Any, VD : DataType.Nullable > - StoredLens>>.rem( + StoredLens>>.rem( nested: StoredLens ): StoredLens = Telescope(null, nested.type, this, nested) @@ -179,21 +179,22 @@ private class ConcatConvention(private val delimiter: Char) : NamingConvention { @PublishedApi internal class Telescope, TR : PartialStruct, S : Struct, US : Schema, T, U, UD : DataType> @PublishedApi internal constructor( - name: String?, + name: CharSequence?, exactType: UD, private val outer: StoredLens, private val nested: StoredLens ) : BaseLens(name, exactType) { @PublishedApi internal constructor( - name: String?, + name: CharSequence?, exactType: UD, outer: Lens, nested: Lens, Struct, out U, *> ) : this(name, exactType, outer as StoredLens, nested as StoredLens) - @PublishedApi - internal constructor(name: String?, outer: Lens, nested: Lens, Struct, out U, *>) : this( + @PublishedApi internal constructor( + name: CharSequence?, outer: Lens, nested: Lens, Struct, out U, *> + ) : this( name, Unit.run { if (outer.type is Schema<*> || nested.type is DataType.Nullable<*, *>) nested.type @@ -256,9 +257,9 @@ Telescope, TR : PartialStruct, S : Struct, US : Schema, ID : IdBound> constructor( - private val table: Table -) : BaseLens, Record, ID, DataType.Simple>( - table.idColName, table.idColType + private val table: Table, type: DataType.NotNull.Simple +) : BaseLens, Record, ID, DataType.NotNull.Simple>( + table.idColName, type ) { override fun hasValue(struct: Record): Boolean = @@ -270,7 +271,7 @@ internal class PkLens, ID : IdBound> constructor( override fun hashCode(): Int = 64 // FieldDef.hashCode() is in [0; 63], be different! - override fun equals(other: Any?): Boolean = // for tests + override fun equals(other: Any?): Boolean = other is PkLens<*, *> && other.table === table override fun toString(): String = @@ -284,8 +285,8 @@ internal class PkLens, ID : IdBound> constructor( } internal class FieldSetLens>( - name: String -) : BaseLens, Struct, Long, DataType.Simple>(name, i64) { + name: CharSequence +) : BaseLens, Struct, Long, DataType.NotNull.Simple>(name, i64) { override fun hasValue(struct: PartialStruct): Boolean = true diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/sql.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/sql.kt index d947e23a..e562b755 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/sql.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/sql.kt @@ -5,7 +5,6 @@ ] package net.aquadc.persistence.sql -import net.aquadc.persistence.struct.FieldDef import net.aquadc.persistence.struct.FieldSet import net.aquadc.persistence.struct.MutableField import net.aquadc.persistence.struct.PartialStruct @@ -55,7 +54,7 @@ interface Session { */ fun beginTransaction(): Transaction - fun rawQuery(@Language("SQL") query: String, argumentTypes: Array>, fetch: Fetch): VarFunc + fun rawQuery(@Language("SQL") query: String, argumentTypes: Array>, fetch: Fetch): VarFunc // ^^^^^^^^^^^^^^^^ add Database Navigator to IntelliJ for SQL highlighting in String literals } diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/template.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/template.kt index edb14cc5..f0d4cd3c 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/template.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/template.kt @@ -19,7 +19,7 @@ interface VarFunc { } interface Fetch { - fun fetch(from: SRC, query: String, argumentTypes: Array>, arguments: Array): R + fun fetch(from: SRC, query: String, argumentTypes: Array>, arguments: Array): R } enum class BindBy { @@ -35,85 +35,85 @@ inline fun Session.query( rawQuery(query, emptyArray(), fetch) as VarFuncImpl inline fun Session.query( - @Language("SQL") query: String, - type: DataType.Simple, - fetch: Fetch + @Language("SQL") query: String, + type: DataType.NotNull.Simple, + fetch: Fetch ): (T) -> R = rawQuery(query, arrayOf(type), fetch) as VarFuncImpl inline fun Session.query( - @Language("SQL") query: String, - type1: DataType.Simple, - type2: DataType.Simple, - fetch: Fetch + @Language("SQL") query: String, + type1: DataType.NotNull.Simple, + type2: DataType.NotNull.Simple, + fetch: Fetch ): (T1, T2) -> R = rawQuery(query, arrayOf(type1, type2), fetch) as VarFuncImpl inline fun Session.query( - @Language("SQL") query: String, - type1: DataType.Simple, - type2: DataType.Simple, - type3: DataType.Simple, - fetch: Fetch + @Language("SQL") query: String, + type1: DataType.NotNull.Simple, + type2: DataType.NotNull.Simple, + type3: DataType.NotNull.Simple, + fetch: Fetch ): (T1, T2, T3) -> R = rawQuery(query, arrayOf(type1, type2, type3), fetch) as VarFuncImpl inline fun Session.query( - @Language("SQL") query: String, - type1: DataType.Simple, - type2: DataType.Simple, - type3: DataType.Simple, - type4: DataType.Simple, - fetch: Fetch + @Language("SQL") query: String, + type1: DataType.NotNull.Simple, + type2: DataType.NotNull.Simple, + type3: DataType.NotNull.Simple, + type4: DataType.NotNull.Simple, + fetch: Fetch ): (T1, T2, T3, T4) -> R = rawQuery(query, arrayOf(type1, type2, type3, type4), fetch) as VarFuncImpl inline fun Session.query( - @Language("SQL") query: String, - type1: DataType.Simple, - type2: DataType.Simple, - type3: DataType.Simple, - type4: DataType.Simple, - type5: DataType.Simple, - fetch: Fetch + @Language("SQL") query: String, + type1: DataType.NotNull.Simple, + type2: DataType.NotNull.Simple, + type3: DataType.NotNull.Simple, + type4: DataType.NotNull.Simple, + type5: DataType.NotNull.Simple, + fetch: Fetch ): (T1, T2, T3, T4, T5) -> R = rawQuery(query, arrayOf(type1, type2, type3, type4, type5), fetch) as VarFuncImpl inline fun Session.query( - @Language("SQL") query: String, - type1: DataType.Simple, - type2: DataType.Simple, - type3: DataType.Simple, - type4: DataType.Simple, - type5: DataType.Simple, - type6: DataType.Simple, - fetch: Fetch + @Language("SQL") query: String, + type1: DataType.NotNull.Simple, + type2: DataType.NotNull.Simple, + type3: DataType.NotNull.Simple, + type4: DataType.NotNull.Simple, + type5: DataType.NotNull.Simple, + type6: DataType.NotNull.Simple, + fetch: Fetch ): (T1, T2, T3, T4, T5) -> R = rawQuery(query, arrayOf(type1, type2, type3, type4, type5, type6), fetch) as VarFuncImpl inline fun Session.query( - @Language("SQL") query: String, - type1: DataType.Simple, - type2: DataType.Simple, - type3: DataType.Simple, - type4: DataType.Simple, - type5: DataType.Simple, - type6: DataType.Simple, - type7: DataType.Simple, - fetch: Fetch + @Language("SQL") query: String, + type1: DataType.NotNull.Simple, + type2: DataType.NotNull.Simple, + type3: DataType.NotNull.Simple, + type4: DataType.NotNull.Simple, + type5: DataType.NotNull.Simple, + type6: DataType.NotNull.Simple, + type7: DataType.NotNull.Simple, + fetch: Fetch ): (T1, T2, T3, T4, T5, T7) -> R = rawQuery(query, arrayOf(type1, type2, type3, type4, type5, type6, type7), fetch) as VarFuncImpl inline fun Session.query( - @Language("SQL") query: String, - type1: DataType.Simple, - type2: DataType.Simple, - type3: DataType.Simple, - type4: DataType.Simple, - type5: DataType.Simple, - type6: DataType.Simple, - type7: DataType.Simple, - type8: DataType.Simple, - fetch: Fetch + @Language("SQL") query: String, + type1: DataType.NotNull.Simple, + type2: DataType.NotNull.Simple, + type3: DataType.NotNull.Simple, + type4: DataType.NotNull.Simple, + type5: DataType.NotNull.Simple, + type6: DataType.NotNull.Simple, + type7: DataType.NotNull.Simple, + type8: DataType.NotNull.Simple, + fetch: Fetch ): (T1, T2, T3, T4, T5, T7, T8) -> R = rawQuery(query, arrayOf(type1, type2, type3, type4, type5, type6, type7, type8), fetch) as VarFuncImpl diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/util.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/util.kt index 06c31af7..33c1ec4d 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/util.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/util.kt @@ -13,7 +13,9 @@ import net.aquadc.persistence.struct.StructSnapshot import net.aquadc.persistence.struct.contains as originalContains import net.aquadc.persistence.struct.indexOf import net.aquadc.persistence.struct.size +import net.aquadc.persistence.type.CustomType import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.Ilk import net.aquadc.persistence.type.serialized import java.lang.ref.WeakReference import java.util.concurrent.ConcurrentMap @@ -39,23 +41,23 @@ internal inline fun UpdatesMap() = newMap< > >() -internal inline fun DataType.flattened(func: (isNullable: Boolean, simple: DataType.Simple) -> R): R = +internal inline fun DataType.flattened(func: (isNullable: Boolean, simple: DataType.NotNull.Simple) -> R): R = when (this) { is DataType.Nullable<*, *> -> { when (val actualType = actualType as DataType) { is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.Simple -> func(true, actualType) - is DataType.Collect<*, *, *>, - is DataType.Partial<*, *> -> func(true, serialized(actualType)) + is DataType.NotNull.Simple -> func(true, actualType) + is DataType.NotNull.Collect<*, *, *>, + is DataType.NotNull.Partial<*, *> -> func(true, serialized(actualType)) } } - is DataType.Simple -> func(false, this) - is DataType.Collect<*, *, *>, - is DataType.Partial<*, *> -> func(false, serialized(this)) + is DataType.NotNull.Simple -> func(false, this) + is DataType.NotNull.Collect<*, *, *>, + is DataType.NotNull.Partial<*, *> -> func(false, serialized(this)) } internal inline fun > bindQueryParams( - condition: WhereCondition, table: Table, bind: (DataType, idx: Int, value: Any?) -> Unit + condition: WhereCondition, table: Table, bind: (Ilk, idx: Int, value: Any?) -> Unit ) { val size = condition.size if (size > 0) { @@ -67,37 +69,34 @@ internal inline fun > bindQueryParams( for (i in 0 until size) { val colIndex = indices[argCols[i]]!! val column = cols[colIndex] - check(argCols[i]!!.type(table.schema) == column.type(table.schema)) - // I hope `argCols[i].type` is the same as `column.type`, but let's overcare :) - - bind(column.type(table.schema) as DataType, i, argValues[i]) + bind(table.typeOf(column) as Ilk, i, argValues[i]) // erase its type and assume that caller is clever enough } } } internal inline fun > bindInsertionParams( - table: Table, - data: Struct, - bind: (DataType, idx: Int, value: Any?) -> Unit + table: Table, + data: Struct, + bind: (Ilk, idx: Int, value: Any?) -> Unit ) { - val columns = table.managedColumns + val columns = table.managedColTypes arrayOfNulls(columns.size).also { flatten(table.recipe, it, data, 0, 0) }.forEachIndexed { idx, value -> - bind(columns[idx].type(table.schema) as DataType, idx, value) + bind(columns[idx] as Ilk, idx, value) } } internal inline fun bindValues( - columnTypes: Any, values: Any?, bind: (DataType, idx: Int, value: Any?) -> Unit + columnTypes: Any, values: Any?, bind: (Ilk, idx: Int, value: Any?) -> Unit ): Int = if (columnTypes is Array<*>) { - columnTypes as Array> + columnTypes as Array> values as Array<*>? columnTypes.forEachIndexed { i, type -> - bind(type as DataType, i, values?.get(i)) + bind(type as Ilk, i, values?.get(i)) } columnTypes.size } else { - bind(columnTypes as DataType, 0, values) + bind(columnTypes as Ilk, 0, values) 1 } @@ -197,7 +196,7 @@ internal fun inflate( } // yay! commit & push - val t = start.unwrappedType as DataType.Partial + val t = start.unwrappedType as DataType.NotNull.Partial fieldSet as FieldSet>? mutColumnValues[_dstPos] = if (fieldSet == null) null @@ -227,7 +226,7 @@ internal fun flatten( if (start.hasFieldSet && type is DataType.Nullable<*, *> && value === null) return // fieldSet is null, all fields are nulls, nothing to do here ------------------------------------------- - val erased = start.unwrappedType as DataType.Partial + val erased = start.unwrappedType as DataType.NotNull.Partial val fieldSet = if (start.hasFieldSet) (erased.fields(value) as FieldSet, FieldDef, *, *>>).also { @@ -307,32 +306,34 @@ private inline operator fun FieldSet, *>?.contains(field: FieldDef<*, this != null && this.originalContains>(field as FieldDef, *, *>) internal fun Blocking.row( - cursor: CUR, offset: Int, columnNames: Array, columnTypes: Array>, bindBy: BindBy + cursor: CUR, offset: Int, columnNames: Array, columnTypes: Array>, bindBy: BindBy ): Array = when (bindBy) { BindBy.Name -> rowByName(cursor, columnNames, columnTypes) BindBy.Position -> rowByPosition(cursor, offset, columnTypes) } -internal fun , CUR, R> Blocking.cell( - cursor: CUR, table: Table, column: StoredNamedLens>, bindBy: BindBy -): R = when (bindBy) { - BindBy.Name -> cellByName(cursor, column.name(table.schema), column.type(table.schema)) - BindBy.Position -> cellAt(cursor, table.managedColumns.forceIndexOf(column), column.type(table.schema)) +internal fun , CUR, R> Blocking.cell( // todo inline me + cursor: CUR, table: Table, column: StoredNamedLens>, bindBy: BindBy +): R { + val type = column.type(table.schema) + return when (bindBy) { + BindBy.Name -> cellByName(cursor, column.name(table.schema), type as Ilk) + // oh... every DataType case implements Ilk, so we can cast ^^^^^^^^^^^^^^^^^ + BindBy.Position -> cellAt(cursor, forceIndexOfManaged(table, column), type as Ilk) + } } -private fun Array.forceIndexOf(element: Any): Int { - // note: ^^^^^^^ unlike indexOf, there's no special case for null, just 'cause we don't need it - for (index in indices) - if (element == this[index]) - return index - throw NoSuchElementException(element.toString() + " !in " + contentToString()) -} +private fun > forceIndexOfManaged(table: Table, column: StoredNamedLens>): Int = + table.indexOfManaged(column).let { idx -> + if (idx >= 0) idx + else throw NoSuchElementException(table.run { column.name } + " !in " + table.managedColNames.contentToString()) + } internal fun > Blocking.mapRow( bindBy: BindBy, cur: CUR, colNames: Array, - colTypes: Array>, + colTypes: Array>, recipe: Array ): StructSnapshot { val firstValues = row(cur, 0, colNames, colTypes, bindBy) @@ -342,3 +343,14 @@ internal fun > Blocking.mapRow( } @PublishedApi @JvmField internal val throwNse = { throw NoSuchElementException() } + +@PublishedApi internal open class NativeType>( + name: CharSequence, + override val type: DT +) : CustomType(name), Ilk { + override fun invoke(p1: T): Any? = p1 + @Suppress("UNCHECKED_CAST") + override fun back(p: Any?): T = p as T + + override val custom: CustomType? get() = this +} diff --git a/sql/src/test/kotlin/net/aquadc/persistence/sql/EmbedUtilsTest.kt b/sql/src/test/kotlin/net/aquadc/persistence/sql/EmbedUtilsTest.kt index c2ffa3fc..93a5f21e 100644 --- a/sql/src/test/kotlin/net/aquadc/persistence/sql/EmbedUtilsTest.kt +++ b/sql/src/test/kotlin/net/aquadc/persistence/sql/EmbedUtilsTest.kt @@ -226,7 +226,7 @@ class EmbedUtilsTest { ) private fun > assertWorks(table: SimpleTable, value: Struct, flatExpect: Array) { - val dest = arrayOfNulls(table.managedColumns.size) + val dest = arrayOfNulls(table.managedColNames.size) flatten(table.recipe, dest, value, 0, 0) assertArrayEquals(flatExpect, dest) diff --git a/sql/src/test/kotlin/net/aquadc/persistence/sql/RelationSchemas.kt b/sql/src/test/kotlin/net/aquadc/persistence/sql/RelationSchemas.kt index 48ac130e..a4711c09 100644 --- a/sql/src/test/kotlin/net/aquadc/persistence/sql/RelationSchemas.kt +++ b/sql/src/test/kotlin/net/aquadc/persistence/sql/RelationSchemas.kt @@ -12,7 +12,7 @@ import net.aquadc.persistence.type.string import org.junit.Assert.assertEquals import org.junit.Test -typealias ShallowSchema = Tuple, Long, DataType.Simple> +typealias ShallowSchema = Tuple, Long, DataType.NotNull.Simple> val shallowSchema = Tuple("a", string, "b", i64) val dupeEmbed = Tuple("a_b", string, "a", shallowSchema) @@ -30,7 +30,7 @@ class RelationSchemas { @Test fun `no rels`() { val table = tableOf(shallowSchema, "zzz", "_id", i64) assertEquals( - arrayOf(PkLens(table), shallowSchema.First, shallowSchema.Second), + arrayOf(PkLens(table, i64), shallowSchema.First, shallowSchema.Second), table.columns ) } @@ -77,7 +77,7 @@ class RelationSchemas { assertEquals(arrayOf("_id", "a", "b_a", "b_b"), table.columns.namesIn(embedSchema)) assertEquals( arrayOf( - PkLens(table), + PkLens(table, i64), embedSchema.First, Telescope("", embedSchema.Second, shallowSchema.First), Telescope("", embedSchema.Second, shallowSchema.Second) @@ -95,7 +95,7 @@ class RelationSchemas { assertEquals(arrayOf("_id", "a", "b_fieldsSet", "b_a", "b_b"), table.columns.namesIn(embedPartial)) assertEquals( arrayOf( - PkLens(table), + PkLens(table, i64), embedPartial.First, embedPartial.Second / FieldSetLens("fieldsSet"), Telescope("b_a", embedPartial.Second, shallowSchema.First), @@ -114,7 +114,7 @@ class RelationSchemas { assertEquals(arrayOf("_id", "a", "b_nullability", "b_a", "b_b"), table.columns.namesIn(embedNullable)) assertEquals( arrayOf( - PkLens(table), + PkLens(table, i64), embedNullable.First, embedNullable.Second / FieldSetLens("nullability"), Telescope("b_a", embedNullable.Second, shallowSchema.First), @@ -136,7 +136,7 @@ class RelationSchemas { ) assertEquals( arrayOf( - PkLens(table), + PkLens(table, i64), embedNullablePartial.First, embedNullablePartial.Second / FieldSetLens("fieldSetAndNullability"), Telescope("b_a", embedNullablePartial.Second, shallowSchema.First), diff --git a/sql/src/test/kotlin/net/aquadc/persistence/sql/TemplatesTest.kt b/sql/src/test/kotlin/net/aquadc/persistence/sql/TemplatesTest.kt index 57a17e67..6fb7a503 100644 --- a/sql/src/test/kotlin/net/aquadc/persistence/sql/TemplatesTest.kt +++ b/sql/src/test/kotlin/net/aquadc/persistence/sql/TemplatesTest.kt @@ -68,7 +68,7 @@ abstract class TemplatesTest { Eagerly.run { val sumAndMul = session.query( "SELECT ? + ?, ? * ?", i32, i32, i32, i32, - struct, Int, DataType.Simple>>(projection(i32 * i32), BindBy.Position) + struct, Int, DataType.NotNull.Simple>>(projection(i32 * i32), BindBy.Position) ) val (f, s) = sumAndMul(80, 4, 6, 8) assertEquals(Pair(84, 48), Pair(f, s)) @@ -76,7 +76,7 @@ abstract class TemplatesTest { Lazily.run { val sumAndMul = session.query( "SELECT ? + ?, ? * ?", i32, i32, i32, i32, - struct, Int, DataType.Simple>>(projection(i32 * i32), BindBy.Position) + struct, Int, DataType.NotNull.Simple>>(projection(i32 * i32), BindBy.Position) ) sumAndMul(80, 4, 6, 8).use { val (f, s) = it @@ -91,12 +91,12 @@ abstract class TemplatesTest { Eagerly.run { try { session.query( "SELECT 0, 0 LIMIT 0", - struct, Int, DataType.Simple>>(projection(intPair), BindBy.Position) + struct, Int, DataType.NotNull.Simple>>(projection(intPair), BindBy.Position) )(); fail() } catch (expected: NoSuchElementException) {} val (f, s) = session.query( "SELECT 0, 0 LIMIT 0", - struct, Int, DataType.Simple>>(projection(intPair), BindBy.Position) { intPair(1, 2) } + struct, Int, DataType.NotNull.Simple>>(projection(intPair), BindBy.Position) { intPair(1, 2) } )() assertEquals(1, f) assertEquals(2, s) @@ -104,12 +104,12 @@ abstract class TemplatesTest { Lazily.run { try { session.query( "SELECT 0, 0 LIMIT 0", - struct, Int, DataType.Simple>>(projection(intPair), BindBy.Position) + struct, Int, DataType.NotNull.Simple>>(projection(intPair), BindBy.Position) )(); fail() } catch (expected: NoSuchElementException) {} session.query( "SELECT 0, 0 LIMIT 0", - struct, Int, DataType.Simple>>(projection(intPair), BindBy.Position) { intPair(1, 2) } + struct, Int, DataType.NotNull.Simple>>(projection(intPair), BindBy.Position) { intPair(1, 2) } )().use { val (f, s) = it assertEquals(1, f) @@ -146,14 +146,14 @@ abstract class TemplatesTest { Eagerly.run { val userContact = session.query( USER_BY_NAME, string, - struct, String, DataType.Simple>>, Tuple, String, DataType.Simple>, Struct, Long, DataType.Simple>>, Tuple, Long, DataType.Simple>>>(joined, BindBy.Name) + struct, String, DataType.NotNull.Simple>>, Tuple, String, DataType.NotNull.Simple>, Struct, Long, DataType.NotNull.Simple>>, Tuple, Long, DataType.NotNull.Simple>>>(joined, BindBy.Name) ) val contact = userContact("John") assertEquals(expectedJohn, contact) val userContacts = session.query( USERS_BY_NAME_AND_EMAIL_START, string, string, - structs, String, DataType.Simple>>, Tuple, String, DataType.Simple>, Struct, Long, DataType.Simple>>, Tuple, Long, DataType.Simple>>>(joined, BindBy.Name) + structs, String, DataType.NotNull.Simple>>, Tuple, String, DataType.NotNull.Simple>, Struct, Long, DataType.NotNull.Simple>>, Tuple, Long, DataType.NotNull.Simple>>>(joined, BindBy.Name) ) val contacts = userContacts("John", "john") assertEquals(listOf(expectedJohn), contacts) @@ -161,7 +161,7 @@ abstract class TemplatesTest { Lazily.run { val userContact = session.query( USER_BY_NAME, string, - struct, String, DataType.Simple>>, Tuple, String, DataType.Simple>, Struct, Long, DataType.Simple>>, Tuple, Long, DataType.Simple>>>(joined, BindBy.Name) + struct, String, DataType.NotNull.Simple>>, Tuple, String, DataType.NotNull.Simple>, Struct, Long, DataType.NotNull.Simple>>, Tuple, Long, DataType.NotNull.Simple>>>(joined, BindBy.Name) ) userContact("John").use { he -> assertEquals(expectedJohn, he) @@ -169,7 +169,7 @@ abstract class TemplatesTest { val userContacts = session.query( USERS_BY_NAME_AND_EMAIL_START, string, string, - structs, String, DataType.Simple>>, Tuple, String, DataType.Simple>, Struct, Long, DataType.Simple>>, Tuple, Long, DataType.Simple>>>(joined, BindBy.Name) + structs, String, DataType.NotNull.Simple>>, Tuple, String, DataType.NotNull.Simple>, Struct, Long, DataType.NotNull.Simple>>, Tuple, Long, DataType.NotNull.Simple>>>(joined, BindBy.Name) ) userContacts("John", "john").use { iter -> assertEquals(expectedJohn, iter.next()) @@ -187,7 +187,7 @@ abstract class TemplatesTest { val transientUserContacts = session.query( USERS_BY_NAME_AND_EMAIL_START, string, string, - structs, String, DataType.Simple>>, Tuple, String, DataType.Simple>, Struct, Long, DataType.Simple>>, Tuple, Long, DataType.Simple>>>(joined, BindBy.Name) + structs, String, DataType.NotNull.Simple>>, Tuple, String, DataType.NotNull.Simple>, Struct, Long, DataType.NotNull.Simple>>, Tuple, Long, DataType.NotNull.Simple>>>(joined, BindBy.Name) ) transientUserContacts("John", "john").use { iter -> // don't collect TemporaryStructs! assertEquals(expectedJohn, iter.next()) diff --git a/sql/src/test/kotlin/net/aquadc/persistence/sql/postgres.kt b/sql/src/test/kotlin/net/aquadc/persistence/sql/postgres.kt index e400d832..4433651a 100644 --- a/sql/src/test/kotlin/net/aquadc/persistence/sql/postgres.kt +++ b/sql/src/test/kotlin/net/aquadc/persistence/sql/postgres.kt @@ -3,10 +3,29 @@ package net.aquadc.persistence.sql import net.aquadc.persistence.extended.either.EitherLeft import net.aquadc.persistence.extended.either.EitherRight import net.aquadc.persistence.extended.either.fold +import net.aquadc.persistence.extended.uuid +import net.aquadc.persistence.sql.ColMeta.Companion.embed +import net.aquadc.persistence.sql.ColMeta.Companion.nativeType +import net.aquadc.persistence.sql.ColMeta.Companion.type import net.aquadc.persistence.sql.blocking.Blocking import net.aquadc.persistence.sql.dialect.postgres.PostgresDialect +import net.aquadc.persistence.struct.Schema +import net.aquadc.persistence.struct.Struct +import net.aquadc.persistence.struct.invoke +import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.collection +import net.aquadc.persistence.type.i32 +import net.aquadc.persistence.type.i64 +import net.aquadc.persistence.type.serialized +import net.aquadc.persistence.type.string +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotSame import org.junit.AssumptionViolatedException +import org.junit.Test +import org.postgresql.jdbc.PgConnection +import org.postgresql.util.PGobject import org.postgresql.util.PSQLException +import java.util.* private val _db = try { @@ -33,4 +52,123 @@ class QueryBuilderPostgres : QueryBuilderTests() { } class TemplatesPostgres : TemplatesTest() { override val session: Session> get() = db + + object Yoozer : Schema() { + val Id = "id" let uuid + val Name = "name" let string + val Extras = "extras" let SomeSchema + val Numbers = "numbers" let collection(i32) + } + private fun PGobject(type: String, value: String) = PGobject().also { + it.type = type + it.value = value + } + private val sampleYoozer = Yoozer { + it[Id] = UUID.randomUUID() + it[Name] = "Some name" + it[Extras] = SomeSchema { + it[A] = "qwe" + it[B] = 123 + it[C] = 10_987_654_321L + } + it[Numbers] = intArrayOf(1, 2, 3).asList() + } + + @Test fun `just a table`() { + val Yuzerz = tableOf(Yoozer, "yoozerz1", "_id", i64) { arrayOf(embed(SnakeCase, Extras)) } + val schema = PostgresDialect.createTable(Yuzerz) + assertEquals( + """CREATE TABLE "yoozerz1" + ("_id" serial8 NOT NULL PRIMARY KEY, + "id" bytea NOT NULL, + "name" text NOT NULL, + "extras_a" text NOT NULL, + "extras_b" int NOT NULL, + "extras_c" int8 NOT NULL, + "numbers" bytea NOT NULL);""".replace(Regex("\n\\s+"), " "), + schema + ) + assertInserts(schema, Yuzerz) + } + + @Test fun `custom table`() { + val Yuzerz = object : Table(Yoozer, "yoozerz2", "_id", i64) { + override fun Yoozer.meta(): Array> = arrayOf( + type(pkColumn, "serial NOT NULL"), // this.pkColumn would be inaccessible within lambda + nativeType(Extras, serialized(SomeSchema)) + ) + } + val schema = PostgresDialect.createTable(Yuzerz) + assertEquals( + """CREATE TABLE "yoozerz2" + ("_id" serial NOT NULL PRIMARY KEY, + "id" bytea NOT NULL, + "name" text NOT NULL, + "extras" bytea NOT NULL, + "numbers" bytea NOT NULL);""".replace(Regex("\n\\s+"), " "), + schema + ) + assertInserts(schema, Yuzerz) + } + + @Test fun `very custom table`() { + val stmt = db.connection.createStatement() + stmt.execute("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";") + stmt.close() + + val someJsonb = object : NativeType, SomeSchema>("jsonb NOT NULL", SomeSchema) { + override fun invoke(p1: Struct): Any? = + PGobject("jsonb", """["${p1[SomeSchema.A]}", ${p1[SomeSchema.B]}, ${p1[SomeSchema.C]}]""") + override fun back(p: Any?): Struct = + (p as PGobject).value.trim('[', ']').split(", ").let { tokens -> + SomeSchema { + it[A] = tokens[0].trim('"') + it[B] = tokens[1].toInt() + it[C] = tokens[2].toLong() + } + } + } + val intArray = object : NativeType, DataType.NotNull.Collect, Int, DataType.NotNull.Simple>>("int ARRAY NOT NULL", collection(i32)) { + override fun invoke(p1: List): Any? = + db.connection.unwrap(PgConnection::class.java).createArrayOf("int", p1.toIntArray()) + override fun back(p: Any?): List = + ((p as java.sql.Array).array as Array).asList() + } + val Yoozerz = tableOf(Yoozer, "yoozerz3", Yoozer.Id) { arrayOf( + nativeType(Id, "uuid NOT NULL DEFAULT uuid_generate_v4()"), + type(Name, "varchar(128) NOT NULL"), + nativeType(Extras, someJsonb), + nativeType(Numbers, intArray) + ) } + + val schema = PostgresDialect.createTable(Yoozerz) + assertEquals( + """CREATE TABLE "yoozerz3" + ("id" uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY, + "name" varchar(128) NOT NULL, + "extras" jsonb NOT NULL, + "numbers" int ARRAY NOT NULL);""".replace(Regex("\n\\s+"), " "), + schema + ) + assertInserts(schema, Yoozerz) + } + private fun assertInserts(create: String, table: Table) { + db.withTransaction { + try { + db.connection.createStatement().run { + execute(create) + close() + } + + val rec = insert(table, sampleYoozer) + assertNotSame(sampleYoozer, rec) + assertEquals(sampleYoozer, rec) + } finally { + db.connection.createStatement().run { + execute("DROP TABLE " + table.name) + close() + } + } + } + } }