@an5/adapters 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -2
- package/dist/an5Adapter.d.ts +37 -0
- package/dist/an5Adapter.js +138 -35
- package/dist/base/types.d.ts +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -1
- package/dist/sqlite/browserEngine.d.ts +13 -0
- package/dist/sqlite/browserEngine.js +13 -0
- package/dist/sqlite/engine.d.ts +5 -0
- package/dist/sqlite/engine.js +10 -0
- package/dist/sqlite/index.d.ts +1 -0
- package/dist/sqlite/index.js +1 -0
- package/dist/sqlite/vector.d.ts +155 -0
- package/dist/sqlite/vector.js +436 -0
- package/dotnet/An5Adapter.cs +85 -27
- package/dotnet/Base/Core.cs +14 -0
- package/dotnet/Mssql/MssqlEngine.cs +44 -1
- package/dotnet/Postgres/PostgresEngine.cs +2 -0
- package/dotnet/Sqlite/SqliteEngine.cs +82 -2
- package/dotnet/Sqlite/SqliteVectors.cs +401 -0
- package/golang/an5_adapter.go +84 -32
- package/golang/base/vectors.go +263 -0
- package/golang/sqlite/vector.go +248 -0
- package/golang/sqlite_vector_test.go +288 -0
- package/golang/table_client.go +67 -3
- package/java/pom.xml +177 -0
- package/java/src/main/java/an5/adapters/An5Adapter.java +374 -0
- package/java/src/main/java/an5/adapters/An5Aggregate.java +116 -0
- package/java/src/main/java/an5/adapters/An5GroupBy.java +97 -0
- package/java/src/main/java/an5/adapters/An5Query.java +100 -0
- package/java/src/main/java/an5/adapters/An5TableClient.java +1031 -0
- package/java/src/main/java/an5/adapters/An5ViewClient.java +107 -0
- package/java/src/main/java/an5/adapters/base/Dialect.java +103 -0
- package/java/src/main/java/an5/adapters/base/FieldMeta.java +42 -0
- package/java/src/main/java/an5/adapters/base/JdbcUrls.java +201 -0
- package/java/src/main/java/an5/adapters/base/Metadata.java +195 -0
- package/java/src/main/java/an5/adapters/base/RelationDef.java +26 -0
- package/java/src/main/java/an5/adapters/base/SqlBuilder.java +422 -0
- package/java/src/main/java/an5/adapters/base/SqliteVectors.java +375 -0
- package/java/src/main/java/an5/adapters/base/Vectors.java +123 -0
- package/java/src/test/java/An5AdaptersSmoke.java +733 -0
- package/java/src/test/java/Json.java +174 -0
- package/java/src/test/java/QuerySemantics.java +111 -0
- package/kotlin/build.gradle.kts +145 -0
- package/kotlin/settings.gradle.kts +10 -0
- package/kotlin/src/main/kotlin/an5/adapters/Aggregate.kt +89 -0
- package/kotlin/src/main/kotlin/an5/adapters/An5.kt +91 -0
- package/kotlin/src/main/kotlin/an5/adapters/Query.kt +130 -0
- package/kotlin/src/main/kotlin/an5/adapters/Rows.kt +166 -0
- package/kotlin/src/main/kotlin/an5/adapters/TableClient.kt +169 -0
- package/kotlin/src/main/kotlin/an5/adapters/Types.kt +77 -0
- package/kotlin/src/test/kotlin/an5/adapters/An5Smoke.kt +329 -0
- package/native/sqlite/CMakeLists.txt +12 -0
- package/native/sqlite/README.md +117 -0
- package/native/sqlite/an5_vector.c +209 -0
- package/package.json +33 -7
- package/python/an5_adapter.py +37 -6
- package/python/base/__init__.py +15 -0
- package/python/base/vectors.py +434 -0
- package/python/sqlite/__init__.py +2 -2
- package/python/sqlite/provider.py +21 -1
- package/python/table_client.py +129 -59
- package/rust/src/adapter.rs +342 -24
- package/rust/src/base/mod.rs +7 -0
- package/rust/src/base/vectors.rs +293 -0
- package/rust/src/lib.rs +3 -3
- package/scripts/sqlite-native-build.js +32 -0
- package/swift/Package.swift +22 -0
- package/swift/Sources/An5Adapters/An5Adapter.swift +160 -0
- package/swift/Sources/An5Adapters/Metadata.swift +183 -0
- package/swift/Sources/An5Adapters/Query.swift +105 -0
- package/swift/Sources/An5Adapters/Row+Accessors.swift +129 -0
- package/swift/Sources/An5Adapters/SQLBuilder.swift +359 -0
- package/swift/Sources/An5Adapters/SQLiteDriver.swift +301 -0
- package/swift/Sources/An5Adapters/SqliteVectors.swift +243 -0
- package/swift/Sources/An5Adapters/TableClient.swift +671 -0
- package/swift/Sources/An5Adapters/Types.swift +127 -0
- package/swift/Sources/An5Adapters/Vectors.swift +86 -0
- package/swift/Sources/An5Adapters/ViewClient.swift +50 -0
- package/swift/Sources/CSQLite/module.modulemap +8 -0
- package/swift/Sources/CSQLite/shim.h +9 -0
- package/swift/Tests/An5AdaptersTests/QuerySemanticsTests.swift +51 -0
- package/swift/Tests/An5AdaptersTests/RuntimeSmokeTests.swift +488 -0
- package/typescript/src/an5Adapter.ts +146 -29
- package/typescript/src/base/types.ts +15 -0
- package/typescript/src/index.ts +13 -0
- package/typescript/src/sqlite/browserEngine.ts +24 -0
- package/typescript/src/sqlite/engine.ts +13 -0
- package/typescript/src/sqlite/index.ts +1 -1
- package/typescript/src/sqlite/vector.ts +541 -0
- package/golang/sqlite/sqlite.go +0 -14
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
package an5.adapters
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds a query.
|
|
5
|
+
*
|
|
6
|
+
* Every field has a default, so [query] with an empty block means the same thing an
|
|
7
|
+
* omitted filter means in the other adapters: no filter, no order, no limit.
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* val adults = db.table("User").findMany {
|
|
11
|
+
* where("age" gte 18)
|
|
12
|
+
* orderBy("name")
|
|
13
|
+
* take(10)
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
class QueryBuilder {
|
|
18
|
+
private val clauses = mutableListOf<Where>()
|
|
19
|
+
private val order = mutableListOf<Map<String, String>>()
|
|
20
|
+
private var offset = 0
|
|
21
|
+
private var limit: Int? = null
|
|
22
|
+
private var columns: List<String>? = null
|
|
23
|
+
private val relations = mutableMapOf<String, Any?>()
|
|
24
|
+
|
|
25
|
+
/** Restricts the rows. Repeated calls are combined with `AND`. */
|
|
26
|
+
fun where(clause: Where) {
|
|
27
|
+
clauses += clause
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fun orderBy(column: String, direction: Sort = Sort.ASC) {
|
|
31
|
+
order += mapOf(column to direction.sql)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Orders by several columns at once, each with its own direction. */
|
|
35
|
+
fun orderBy(columns: List<Pair<String, Sort>>) {
|
|
36
|
+
order.addAll(columns.map { (column, direction) -> mapOf(column to direction.sql) })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Skips this many rows. Negative values are treated as zero. */
|
|
40
|
+
fun skip(count: Int) {
|
|
41
|
+
offset = count.coerceAtLeast(0)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Caps the number of rows; `null` means no limit. */
|
|
45
|
+
fun take(count: Int?) {
|
|
46
|
+
limit = count
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Restricts the returned columns; relations and `_count` are eager-loaded instead. */
|
|
50
|
+
fun select(vararg columns: String) {
|
|
51
|
+
this.columns = columns.toList()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Eager-loads relations. Repeated calls merge. */
|
|
55
|
+
fun include(relations: Include) {
|
|
56
|
+
this.relations += relations
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
internal fun build(): an5.adapters.An5Query {
|
|
60
|
+
val query = an5.adapters.An5Query()
|
|
61
|
+
if (clauses.isNotEmpty()) {
|
|
62
|
+
query.where(if (clauses.size == 1) clauses[0].toJavaWhere() else andOf(*clauses.toTypedArray()).toJavaWhere())
|
|
63
|
+
}
|
|
64
|
+
if (order.isNotEmpty()) {
|
|
65
|
+
query.orderBy(order.toList())
|
|
66
|
+
}
|
|
67
|
+
query.skip(offset)
|
|
68
|
+
query.take(limit)
|
|
69
|
+
columns?.let { query.select(*it.toTypedArray()) }
|
|
70
|
+
if (relations.isNotEmpty()) {
|
|
71
|
+
query.include(relations.toJavaMap())
|
|
72
|
+
}
|
|
73
|
+
return query
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Builds a query with [block], ready to hand to a table client. */
|
|
78
|
+
fun query(block: QueryBuilder.() -> Unit): an5.adapters.An5Query = QueryBuilder().apply(block).build()
|
|
79
|
+
|
|
80
|
+
// ─── Filter building ────────────────────────────────────────────────────────────────
|
|
81
|
+
// Named after the operators they produce rather than Kotlin's own vocabulary, so a filter
|
|
82
|
+
// reads the same in Kotlin as it does in every other AN5 client.
|
|
83
|
+
|
|
84
|
+
/** `"age" gte 18` — greater than or equal. */
|
|
85
|
+
infix fun String.gte(value: Any?): Where = mapOf(this to mapOf("gte" to value))
|
|
86
|
+
|
|
87
|
+
/** `"age" lte 18` — less than or equal. */
|
|
88
|
+
infix fun String.lte(value: Any?): Where = mapOf(this to mapOf("lte" to value))
|
|
89
|
+
|
|
90
|
+
/** `"age" gt 18` — greater than. */
|
|
91
|
+
infix fun String.gt(value: Any?): Where = mapOf(this to mapOf("gt" to value))
|
|
92
|
+
|
|
93
|
+
/** `"age" lt 18` — less than. */
|
|
94
|
+
infix fun String.lt(value: Any?): Where = mapOf(this to mapOf("lt" to value))
|
|
95
|
+
|
|
96
|
+
/** `"name" eq "Ada"` — equality. */
|
|
97
|
+
infix fun String.eq(value: Any?): Where = mapOf(this to mapOf("equals" to value))
|
|
98
|
+
|
|
99
|
+
/** `"name" neq "Ada"` — inequality. */
|
|
100
|
+
infix fun String.neq(value: Any?): Where = mapOf(this to mapOf("not" to value))
|
|
101
|
+
|
|
102
|
+
/** `"name" contains "da"` — a substring match; case sensitivity follows the dialect. */
|
|
103
|
+
infix fun String.contains(value: String): Where = mapOf(this to mapOf("contains" to value))
|
|
104
|
+
|
|
105
|
+
/** `"name" startsWith "Ad"`. */
|
|
106
|
+
infix fun String.startsWith(value: String): Where = mapOf(this to mapOf("startsWith" to value))
|
|
107
|
+
|
|
108
|
+
/** `"name" endsWith "ce"`. */
|
|
109
|
+
infix fun String.endsWith(value: String): Where = mapOf(this to mapOf("endsWith" to value))
|
|
110
|
+
|
|
111
|
+
/** `"id".isNull()` — the column is `NULL`. */
|
|
112
|
+
fun String.isNull(): Where = mapOf(this to null)
|
|
113
|
+
|
|
114
|
+
/** `"id".isNotNull()` — the column has a value. */
|
|
115
|
+
fun String.isNotNull(): Where = mapOf(this to mapOf("not" to null))
|
|
116
|
+
|
|
117
|
+
/** `"id" inList listOf(1, 2)` — an empty list matches nothing. */
|
|
118
|
+
infix fun <T> String.inList(values: List<T>): Where = mapOf(this to mapOf("in" to values))
|
|
119
|
+
|
|
120
|
+
/** `"id" notInList listOf(1, 2)` — an empty list matches everything. */
|
|
121
|
+
infix fun <T> String.notInList(values: List<T>): Where = mapOf(this to mapOf("notIn" to values))
|
|
122
|
+
|
|
123
|
+
/** Any of [clauses]; an empty list matches nothing. */
|
|
124
|
+
fun orOf(vararg clauses: Where): Where = mapOf("OR" to clauses.toList())
|
|
125
|
+
|
|
126
|
+
/** All of [clauses]; an empty list matches everything. */
|
|
127
|
+
fun andOf(vararg clauses: Where): Where = mapOf("AND" to clauses.toList())
|
|
128
|
+
|
|
129
|
+
/** None of [clauses]; an empty list matches everything. */
|
|
130
|
+
fun notOf(vararg clauses: Where): Where = mapOf("NOT" to clauses.toList())
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package an5.adapters
|
|
2
|
+
|
|
3
|
+
import java.math.BigDecimal
|
|
4
|
+
import java.sql.Timestamp
|
|
5
|
+
import java.time.Instant
|
|
6
|
+
import java.time.LocalDate
|
|
7
|
+
import java.time.LocalDateTime
|
|
8
|
+
import java.time.OffsetDateTime
|
|
9
|
+
import java.time.ZoneOffset
|
|
10
|
+
import java.util.UUID
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Typed reads off a row.
|
|
14
|
+
*
|
|
15
|
+
* A column comes back from JDBC as whatever the driver decided: an `INT` may arrive as
|
|
16
|
+
* `Integer`, `Long` or `BigDecimal` depending on the driver and the declared width, and a
|
|
17
|
+
* `NUMERIC` column that fits in a `Long` still arrives as `BigDecimal`. These accessors
|
|
18
|
+
* take any of them, so generated client code never has to cast — and never silently
|
|
19
|
+
* truncates a decimal on the way.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** The column's value as a [String], or `null` when it is `NULL`. */
|
|
23
|
+
fun Row.string(column: String): String? = this[column]?.toString()
|
|
24
|
+
|
|
25
|
+
/** The column's value as an [Int], or [fallback] when it is `NULL`. */
|
|
26
|
+
fun Row.int(column: String, fallback: Int = 0): Int = number(column)?.toInt() ?: fallback
|
|
27
|
+
|
|
28
|
+
/** The column's value as a [Long], or [fallback] when it is `NULL`. */
|
|
29
|
+
fun Row.long(column: String, fallback: Long = 0L): Long = number(column)?.toLong() ?: fallback
|
|
30
|
+
|
|
31
|
+
/** The column's value as a [Double], or [fallback] when it is `NULL`. */
|
|
32
|
+
fun Row.double(column: String, fallback: Double = 0.0): Double = number(column)?.toDouble() ?: fallback
|
|
33
|
+
|
|
34
|
+
/** The column's value as a [Boolean]; `NULL` reads as [fallback]. */
|
|
35
|
+
fun Row.bool(column: String, fallback: Boolean = false): Boolean =
|
|
36
|
+
when (val value = this[column]) {
|
|
37
|
+
null -> fallback
|
|
38
|
+
is Boolean -> value
|
|
39
|
+
is Number -> value.toInt() != 0
|
|
40
|
+
else -> value.toString().toBooleanStrictOrNull() ?: fallback
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The column's value as a [ByteArray], or `null` when it is `NULL` or not binary. */
|
|
44
|
+
fun Row.bytes(column: String): ByteArray? = this[column] as? ByteArray
|
|
45
|
+
|
|
46
|
+
/** The column's value boxed, or [fallback] when it is `NULL`. */
|
|
47
|
+
@Suppress("UNCHECKED_CAST")
|
|
48
|
+
fun <T : Any> Row.valueOr(column: String, fallback: T): T = (this[column] as? T) ?: fallback
|
|
49
|
+
|
|
50
|
+
/** The column's value, or `null` when it is `NULL`. */
|
|
51
|
+
@Suppress("UNCHECKED_CAST")
|
|
52
|
+
fun <T> Row.value(column: String): T? = this[column] as? T
|
|
53
|
+
|
|
54
|
+
/** The eager-loaded relation rows under [relation], or an empty list. */
|
|
55
|
+
@Suppress("UNCHECKED_CAST")
|
|
56
|
+
fun Row.related(relation: String): List<Row> = (this[relation] as? List<Row>) ?: emptyList()
|
|
57
|
+
|
|
58
|
+
/** The eager-loaded single relation under [relation], or `null`. */
|
|
59
|
+
@Suppress("UNCHECKED_CAST")
|
|
60
|
+
fun Row.relatedOne(relation: String): Row? = this[relation] as? Row
|
|
61
|
+
|
|
62
|
+
/** The `_count` totals for a query that asked for them. */
|
|
63
|
+
fun Row.relationCount(relation: String): Int =
|
|
64
|
+
((this["_count"] as? Map<*, *>)?.get(relation) as? Number)?.toInt() ?: 0
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Copies the row into a data class.
|
|
68
|
+
*
|
|
69
|
+
* ```
|
|
70
|
+
* data class User(val id: String, val name: String?)
|
|
71
|
+
* fun Row.toUser() = User(string("id")!!, string("name"))
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
inline fun <T> Row.decode(transform: Row.() -> T): T = transform()
|
|
75
|
+
|
|
76
|
+
private fun Row.number(column: String): Number? = when (val value = this[column]) {
|
|
77
|
+
null -> null
|
|
78
|
+
is Number -> value
|
|
79
|
+
is Boolean -> if (value) 1 else 0
|
|
80
|
+
else -> value.toString().trim().toDoubleOrNull()
|
|
81
|
+
}
|
|
82
|
+
// ─── Nullable reads ────────────────────────────────────────────────────────────────
|
|
83
|
+
//
|
|
84
|
+
// The `*OrNull` family, for a generated model whose column is optional: a nullable column
|
|
85
|
+
// that read as `0` or `""` would write the default back on the next update instead of
|
|
86
|
+
// leaving it alone.
|
|
87
|
+
|
|
88
|
+
/** The column's value as a [String], or `null` when it is `NULL` or missing. */
|
|
89
|
+
fun Row.stringOrNull(column: String): String? = this[column]?.toString()
|
|
90
|
+
|
|
91
|
+
/** The column's value as an [Int], or `null` when it is `NULL`, missing or not numeric. */
|
|
92
|
+
fun Row.intOrNull(column: String): Int? = asNumber(column)?.toInt()
|
|
93
|
+
|
|
94
|
+
/** The column's value as a [Long], or `null` when it is `NULL`, missing or not numeric. */
|
|
95
|
+
fun Row.longOrNull(column: String): Long? = asNumber(column)?.toLong()
|
|
96
|
+
|
|
97
|
+
/** The column's value as a [Double], or `null` when it is `NULL`, missing or not numeric. */
|
|
98
|
+
fun Row.doubleOrNull(column: String): Double? = asNumber(column)?.toDouble()
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The column's value as a [BigDecimal], or `null`.
|
|
102
|
+
*
|
|
103
|
+
* Goes through the string form rather than the `Double` constructor: a `DECIMAL` column read
|
|
104
|
+
* as a `Double` would already have lost the digits that do not fit in a binary fraction.
|
|
105
|
+
*/
|
|
106
|
+
fun Row.decimalOrNull(column: String): BigDecimal? = when (val value = this[column]) {
|
|
107
|
+
null -> null
|
|
108
|
+
is BigDecimal -> value
|
|
109
|
+
is java.math.BigInteger -> BigDecimal(value)
|
|
110
|
+
is Number -> BigDecimal.valueOf(value.toLong())
|
|
111
|
+
else -> value.toString().trim().toBigDecimalOrNull()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The column's value as a [Boolean], or `null` when it is `NULL` or not recognisable. */
|
|
115
|
+
fun Row.boolOrNull(column: String): Boolean? = when (val value = this[column]) {
|
|
116
|
+
null -> null
|
|
117
|
+
is Boolean -> value
|
|
118
|
+
is Number -> value.toInt() != 0
|
|
119
|
+
else -> value.toString().trim().toBooleanStrictOrNull()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The column's value as a [LocalDateTime], or `null`. */
|
|
123
|
+
fun Row.localDateTimeOrNull(column: String): LocalDateTime? = when (val value = this[column]) {
|
|
124
|
+
null -> null
|
|
125
|
+
is LocalDateTime -> value
|
|
126
|
+
is Timestamp -> value.toLocalDateTime()
|
|
127
|
+
is java.sql.Date -> value.toLocalDate().atStartOfDay()
|
|
128
|
+
is OffsetDateTime -> value.toLocalDateTime()
|
|
129
|
+
is Instant -> LocalDateTime.ofInstant(value, ZoneOffset.UTC)
|
|
130
|
+
is Number -> LocalDateTime.ofInstant(Instant.ofEpochMilli(value.toLong()), ZoneOffset.UTC)
|
|
131
|
+
else -> runCatching { LocalDateTime.parse(value.toString().trim().replace(' ', 'T')) }.getOrNull()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The column's value as a [LocalDate], or `null`. */
|
|
135
|
+
fun Row.localDateOrNull(column: String): LocalDate? = when (val value = this[column]) {
|
|
136
|
+
null -> null
|
|
137
|
+
is LocalDate -> value
|
|
138
|
+
is java.sql.Date -> value.toLocalDate()
|
|
139
|
+
else -> localDateTimeOrNull(column)?.toLocalDate()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The column's value as a [UUID], or `null`. */
|
|
143
|
+
fun Row.uuidOrNull(column: String): UUID? = stringOrNull(column)?.let { text ->
|
|
144
|
+
runCatching { UUID.fromString(text) }.getOrNull()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The column's value as a [ByteArray], or `null`. */
|
|
148
|
+
fun Row.bytesOrNull(column: String): ByteArray? = this[column] as? ByteArray
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The column's value as a vector, or `null` for `NULL`.
|
|
152
|
+
*
|
|
153
|
+
* A `VECTOR(n)` column holds float32 bytes, but a column written before that encoding held
|
|
154
|
+
* JSON text, so both are accepted; so is an array a driver has already decoded. Decoding is
|
|
155
|
+
* the same code the ranking query uses, which is what keeps a column readable as the vector
|
|
156
|
+
* it is stored as.
|
|
157
|
+
*/
|
|
158
|
+
fun Row.vectorOrNull(column: String): DoubleArray? =
|
|
159
|
+
an5.adapters.base.SqliteVectors.decodeVector(this[column], 0)
|
|
160
|
+
|
|
161
|
+
private fun Row.asNumber(column: String): Number? = when (val value = this[column]) {
|
|
162
|
+
null -> null
|
|
163
|
+
is Number -> value
|
|
164
|
+
is Boolean -> if (value) 1 else 0
|
|
165
|
+
else -> value.toString().trim().toDoubleOrNull()
|
|
166
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
package an5.adapters
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CRUD and queries for one table.
|
|
5
|
+
*
|
|
6
|
+
* A wrapper over the JVM table client: the schema knowledge and the SQL live there, and
|
|
7
|
+
* this adds the Kotlin call shapes — a [query] block for filters, named arguments for
|
|
8
|
+
* writes — without reimplementing anything.
|
|
9
|
+
*/
|
|
10
|
+
class TableClient internal constructor(
|
|
11
|
+
adapter: an5.adapters.An5Adapter,
|
|
12
|
+
model: String,
|
|
13
|
+
) {
|
|
14
|
+
private val delegate: an5.adapters.An5TableClient = adapter.table(model)
|
|
15
|
+
|
|
16
|
+
/** The model this client reads and writes. */
|
|
17
|
+
val model: String get() = delegate.modelName()
|
|
18
|
+
|
|
19
|
+
// ─── Reads ────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Every row, unfiltered. */
|
|
22
|
+
fun findMany(): List<Row> = delegate.findMany()
|
|
23
|
+
|
|
24
|
+
/** The rows matching the query the block builds. */
|
|
25
|
+
fun findMany(block: QueryBuilder.() -> Unit): List<Row> = delegate.findMany(query(block))
|
|
26
|
+
|
|
27
|
+
/** The rows matching [where], with the optional refinements [block] adds. */
|
|
28
|
+
fun findMany(where: Where, block: QueryBuilder.() -> Unit = {}): List<Row> =
|
|
29
|
+
delegate.findMany(QueryBuilder().apply { this.where(where); block() }.build())
|
|
30
|
+
|
|
31
|
+
/** The first matching row, or `null`. Always limited to one row server-side. */
|
|
32
|
+
fun findFirst(block: QueryBuilder.() -> Unit): Row? =
|
|
33
|
+
delegate.findFirst(QueryBuilder().apply(block).build())
|
|
34
|
+
|
|
35
|
+
fun findFirst(where: Where): Row? = delegate.findFirst(where.toJavaFilter())
|
|
36
|
+
|
|
37
|
+
/** The row [where] identifies, or `null`. */
|
|
38
|
+
fun findUnique(where: Where): Row? = delegate.findUnique(where.toJavaFilter())
|
|
39
|
+
|
|
40
|
+
/** How many rows match. */
|
|
41
|
+
fun count(where: Where? = null): Long = delegate.count(where?.toJavaWhere())
|
|
42
|
+
|
|
43
|
+
// ─── Writes ───────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Inserts a row and returns it, re-read so server-side defaults come back with it.
|
|
47
|
+
*
|
|
48
|
+
* A model with an `@id` column and no value for it gets a generated identifier, which
|
|
49
|
+
* is what makes a client-created row addressable without a database-side default.
|
|
50
|
+
*/
|
|
51
|
+
fun create(data: Data): Row = delegate.create(data.toJavaData())
|
|
52
|
+
|
|
53
|
+
fun create(data: Data, block: QueryBuilder.() -> Unit): Row =
|
|
54
|
+
delegate.create(data.toJavaData(), QueryBuilder().apply(block).build())
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Inserts many rows, one statement each.
|
|
58
|
+
*
|
|
59
|
+
* Row-at-a-time on purpose: a batch would share one parameter list, so a single bad
|
|
60
|
+
* row would lose every good row with it.
|
|
61
|
+
*/
|
|
62
|
+
fun createMany(rows: List<Data>, skipDuplicates: Boolean = false): Int =
|
|
63
|
+
affected(delegate.createMany(rows.map { it.toJavaData() }, skipDuplicates))
|
|
64
|
+
|
|
65
|
+
/** Updates the rows matching [where] and returns the first of them re-read. */
|
|
66
|
+
fun update(where: Where, data: Data): Row? = delegate.update(where.toJavaFilter(), data.toJavaData())
|
|
67
|
+
|
|
68
|
+
/** Updates every matching row and gives back how many changed. */
|
|
69
|
+
fun updateMany(where: Where?, data: Data): Int =
|
|
70
|
+
affected(delegate.updateMany(where?.toJavaWhere(), data.toJavaData()))
|
|
71
|
+
|
|
72
|
+
/** Deletes the matching rows and returns the one that was there first. */
|
|
73
|
+
fun delete(where: Where): Row? = delegate.delete(where.toJavaFilter())
|
|
74
|
+
|
|
75
|
+
/** Deletes every matching row, or the whole table when [where] is `null`. */
|
|
76
|
+
fun deleteMany(where: Where? = null): Int = affected(delegate.deleteMany(where?.toJavaWhere()))
|
|
77
|
+
|
|
78
|
+
/** Updates the matching row when it exists, creates it otherwise. */
|
|
79
|
+
fun upsert(where: Where, create: Data, update: Data): Row =
|
|
80
|
+
delegate.upsert(where.toJavaFilter(), create.toJavaData(), update.toJavaData())
|
|
81
|
+
|
|
82
|
+
// ─── Aggregation ──────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* One row of aggregate values, keyed `_count`, `_sum_<field>` and so on.
|
|
86
|
+
*
|
|
87
|
+
* ```
|
|
88
|
+
* db.table("Order").aggregate { count(); sum("total"); avg("total") }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
fun aggregate(block: AggregateBuilder.() -> Unit): Row =
|
|
92
|
+
delegate.aggregate(AggregateBuilder().apply(block).build())
|
|
93
|
+
|
|
94
|
+
/** One row per group, each carrying the group's `_count` and aggregates. */
|
|
95
|
+
fun groupBy(vararg by: String, block: AggregateBuilder.() -> Unit = {}): List<Row> =
|
|
96
|
+
delegate.groupBy(AggregateBuilder().apply(block).groupedBy(*by).build())
|
|
97
|
+
|
|
98
|
+
// ─── Vector search ────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The rows nearest [vector], by the database's own vector functions where it has
|
|
102
|
+
* them and in memory where it does not.
|
|
103
|
+
*/
|
|
104
|
+
@JvmOverloads
|
|
105
|
+
fun vectorSearch(
|
|
106
|
+
vector: DoubleArray,
|
|
107
|
+
take: Int = 10,
|
|
108
|
+
where: Where? = null,
|
|
109
|
+
vectorField: String = "embedding",
|
|
110
|
+
metric: DistanceMetric = DistanceMetric.COSINE,
|
|
111
|
+
): List<Row> = delegate.vectorSearch(vector, take, where?.toJavaWhere(), vectorField, metric.sql)
|
|
112
|
+
|
|
113
|
+
override fun toString(): String = "TableClient($model)"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A wrapper over the JVM view client: reads work, writes are refused. */
|
|
117
|
+
class ViewClient internal constructor(
|
|
118
|
+
adapter: an5.adapters.An5Adapter,
|
|
119
|
+
viewName: String,
|
|
120
|
+
) {
|
|
121
|
+
private val delegate: an5.adapters.An5ViewClient = adapter.view(viewName)
|
|
122
|
+
|
|
123
|
+
/** The view this client reads. */
|
|
124
|
+
val name: String get() = delegate.viewName()
|
|
125
|
+
|
|
126
|
+
fun findMany(): List<Row> = delegate.findMany()
|
|
127
|
+
|
|
128
|
+
fun findMany(block: QueryBuilder.() -> Unit): List<Row> = delegate.findMany(query(block))
|
|
129
|
+
|
|
130
|
+
fun findFirst(where: Where): Row? = delegate.findFirst(where.toJavaFilter())
|
|
131
|
+
|
|
132
|
+
fun findUnique(where: Where): Row? = delegate.findUnique(where.toJavaFilter())
|
|
133
|
+
|
|
134
|
+
fun count(where: Where? = null): Long = delegate.count(where?.toJavaWhere())
|
|
135
|
+
|
|
136
|
+
fun aggregate(block: AggregateBuilder.() -> Unit): Row =
|
|
137
|
+
delegate.aggregate(AggregateBuilder().apply(block).build())
|
|
138
|
+
|
|
139
|
+
fun groupBy(vararg by: String, block: AggregateBuilder.() -> Unit = {}): List<Row> =
|
|
140
|
+
delegate.groupBy(AggregateBuilder().apply(block).groupedBy(*by).build())
|
|
141
|
+
|
|
142
|
+
@JvmOverloads
|
|
143
|
+
fun vectorSearch(
|
|
144
|
+
vector: DoubleArray,
|
|
145
|
+
take: Int = 10,
|
|
146
|
+
where: Where? = null,
|
|
147
|
+
vectorField: String = "embedding",
|
|
148
|
+
metric: DistanceMetric = DistanceMetric.COSINE,
|
|
149
|
+
): List<Row> = delegate.vectorSearch(vector, take, where?.toJavaWhere(), vectorField, metric.sql)
|
|
150
|
+
|
|
151
|
+
fun create(data: Data): Row = delegate.create(data.toJavaData())
|
|
152
|
+
|
|
153
|
+
fun update(where: Where, data: Data): Row? = delegate.update(where.toJavaFilter(), data.toJavaData())
|
|
154
|
+
|
|
155
|
+
fun updateMany(where: Where?, data: Data): Int =
|
|
156
|
+
affected(delegate.updateMany(where?.toJavaWhere(), data.toJavaData()))
|
|
157
|
+
|
|
158
|
+
fun delete(where: Where): Row? = delegate.delete(where.toJavaFilter())
|
|
159
|
+
|
|
160
|
+
fun deleteMany(where: Where? = null): Int = affected(delegate.deleteMany(where?.toJavaWhere()))
|
|
161
|
+
|
|
162
|
+
fun upsert(where: Where, create: Data, update: Data): Row =
|
|
163
|
+
delegate.upsert(where.toJavaFilter(), create.toJavaData(), update.toJavaData())
|
|
164
|
+
|
|
165
|
+
override fun toString(): String = "ViewClient($name)"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The affected row count a write returned. */
|
|
169
|
+
private fun affected(result: Map<String, Any?>): Int = (result["count"] as? Number)?.toInt() ?: 0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
package an5.adapters
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The database dialects AN5 speaks.
|
|
5
|
+
*
|
|
6
|
+
* A typealias rather than a second enum: the JVM adapter already decides quoting,
|
|
7
|
+
* pagination and the `WITH (NOLOCK)` hint, and a copy of the enum here would be a second
|
|
8
|
+
* source of truth that could drift from the one that produces the SQL.
|
|
9
|
+
*/
|
|
10
|
+
typealias Dialect = an5.adapters.base.Dialect
|
|
11
|
+
|
|
12
|
+
/** One row, keyed by column label, in the order the query selected the columns. */
|
|
13
|
+
typealias Row = Map<String, Any?>
|
|
14
|
+
|
|
15
|
+
/** A filter tree: scalar values, operator maps, and nested `AND`/`OR`/`NOT` groups. */
|
|
16
|
+
typealias Where = Map<String, Any?>
|
|
17
|
+
|
|
18
|
+
/** A write payload: column values, plus any relation writes the metadata declares. */
|
|
19
|
+
typealias Data = Map<String, Any?>
|
|
20
|
+
|
|
21
|
+
/** An eager-load description: relation name to `true`, or to a nested query description. */
|
|
22
|
+
typealias Include = Map<String, Any?>
|
|
23
|
+
|
|
24
|
+
/** Sort direction, with the SQL each one writes. */
|
|
25
|
+
enum class Sort(val sql: String) {
|
|
26
|
+
ASC("asc"),
|
|
27
|
+
DESC("desc"),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The distance metrics a vector search understands.
|
|
32
|
+
*
|
|
33
|
+
* Each metric is turned into a distance before sorting, so ascending order always means
|
|
34
|
+
* "closer" — cosine similarity and dot product would otherwise have to be sorted
|
|
35
|
+
* descending, which is the kind of detail no caller should have to remember.
|
|
36
|
+
*/
|
|
37
|
+
enum class DistanceMetric(val sql: String) {
|
|
38
|
+
COSINE("cosine"),
|
|
39
|
+
EUCLIDEAN("euclidean"),
|
|
40
|
+
DOT("dot"),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Rebuilds a Kotlin value as a plain Java one.
|
|
45
|
+
*
|
|
46
|
+
* Kotlin's `Map` and `List` interfaces are read-only, so they are not assignable to the
|
|
47
|
+
* `java.util.Map` parameters the JVM adapter declares. Every value that crosses that
|
|
48
|
+
* boundary goes through here, which is also what lets the public API use `mapOf` — the
|
|
49
|
+
* literal everyone writes — instead of `mutableMapOf`.
|
|
50
|
+
*/
|
|
51
|
+
internal fun Any?.toJava(): Any? = when (this) {
|
|
52
|
+
is Map<*, *> -> toJavaMap()
|
|
53
|
+
is Iterable<*> -> map { it.toJava() }
|
|
54
|
+
is Array<*> -> map { it.toJava() }
|
|
55
|
+
is IntArray -> map { it }
|
|
56
|
+
is DoubleArray -> map { it }
|
|
57
|
+
is LongArray -> map { it }
|
|
58
|
+
else -> this
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The map as the adapter takes it: mutable, with string keys and values converted. */
|
|
62
|
+
internal fun Map<*, *>.toJavaMap(): MutableMap<String, Any?> {
|
|
63
|
+
val target = LinkedHashMap<String, Any?>(size)
|
|
64
|
+
for ((key, value) in this) {
|
|
65
|
+
target[key.toString()] = value.toJava()
|
|
66
|
+
}
|
|
67
|
+
return target
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The filter tree as the adapter takes it, or `null` when there is none. */
|
|
71
|
+
internal fun Where?.toJavaWhere(): MutableMap<String, Any?>? = this?.toJavaMap()
|
|
72
|
+
|
|
73
|
+
/** The filter tree as the adapter takes it; for parameters that are not optional. */
|
|
74
|
+
internal fun Where.toJavaFilter(): MutableMap<String, Any?> = toJavaMap()
|
|
75
|
+
|
|
76
|
+
/** The payload as the adapter takes it. */
|
|
77
|
+
internal fun Data.toJavaData(): MutableMap<String, Any?> = toJavaMap()
|