@an5/adapters 0.2.10 → 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.
Files changed (109) hide show
  1. package/README.md +163 -2
  2. package/dist/an5Adapter.d.ts +37 -0
  3. package/dist/an5Adapter.js +190 -65
  4. package/dist/base/metadata.d.ts +2 -0
  5. package/dist/base/metadata.js +10 -0
  6. package/dist/base/sql.d.ts +2 -0
  7. package/dist/base/sql.js +24 -25
  8. package/dist/base/types.d.ts +14 -0
  9. package/dist/googlesheets/adapter.js +16 -8
  10. package/dist/googlesheets/config.d.ts +13 -0
  11. package/dist/googlesheets/config.js +5 -1
  12. package/dist/googlesheets/oauth.d.ts +3 -0
  13. package/dist/googlesheets/oauth.js +43 -0
  14. package/dist/googlesheets/parseConnectionString.js +12 -0
  15. package/dist/googlesheets/tableClient.js +5 -23
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +8 -1
  18. package/dist/sqlite/browserEngine.d.ts +13 -0
  19. package/dist/sqlite/browserEngine.js +13 -0
  20. package/dist/sqlite/engine.d.ts +5 -0
  21. package/dist/sqlite/engine.js +10 -0
  22. package/dist/sqlite/index.d.ts +1 -0
  23. package/dist/sqlite/index.js +1 -0
  24. package/dist/sqlite/vector.d.ts +155 -0
  25. package/dist/sqlite/vector.js +436 -0
  26. package/dotnet/An5Adapter.cs +85 -27
  27. package/dotnet/Base/Core.cs +14 -0
  28. package/dotnet/Mssql/MssqlEngine.cs +44 -1
  29. package/dotnet/Postgres/PostgresEngine.cs +2 -0
  30. package/dotnet/Sqlite/SqliteEngine.cs +82 -2
  31. package/dotnet/Sqlite/SqliteVectors.cs +401 -0
  32. package/golang/an5_adapter.go +84 -32
  33. package/golang/base/vectors.go +263 -0
  34. package/golang/sqlite/vector.go +248 -0
  35. package/golang/sqlite_vector_test.go +288 -0
  36. package/golang/table_client.go +67 -3
  37. package/java/pom.xml +177 -0
  38. package/java/src/main/java/an5/adapters/An5Adapter.java +374 -0
  39. package/java/src/main/java/an5/adapters/An5Aggregate.java +116 -0
  40. package/java/src/main/java/an5/adapters/An5GroupBy.java +97 -0
  41. package/java/src/main/java/an5/adapters/An5Query.java +100 -0
  42. package/java/src/main/java/an5/adapters/An5TableClient.java +1031 -0
  43. package/java/src/main/java/an5/adapters/An5ViewClient.java +107 -0
  44. package/java/src/main/java/an5/adapters/base/Dialect.java +103 -0
  45. package/java/src/main/java/an5/adapters/base/FieldMeta.java +42 -0
  46. package/java/src/main/java/an5/adapters/base/JdbcUrls.java +201 -0
  47. package/java/src/main/java/an5/adapters/base/Metadata.java +195 -0
  48. package/java/src/main/java/an5/adapters/base/RelationDef.java +26 -0
  49. package/java/src/main/java/an5/adapters/base/SqlBuilder.java +422 -0
  50. package/java/src/main/java/an5/adapters/base/SqliteVectors.java +375 -0
  51. package/java/src/main/java/an5/adapters/base/Vectors.java +123 -0
  52. package/java/src/test/java/An5AdaptersSmoke.java +733 -0
  53. package/java/src/test/java/Json.java +174 -0
  54. package/java/src/test/java/QuerySemantics.java +111 -0
  55. package/kotlin/build.gradle.kts +145 -0
  56. package/kotlin/settings.gradle.kts +10 -0
  57. package/kotlin/src/main/kotlin/an5/adapters/Aggregate.kt +89 -0
  58. package/kotlin/src/main/kotlin/an5/adapters/An5.kt +91 -0
  59. package/kotlin/src/main/kotlin/an5/adapters/Query.kt +130 -0
  60. package/kotlin/src/main/kotlin/an5/adapters/Rows.kt +166 -0
  61. package/kotlin/src/main/kotlin/an5/adapters/TableClient.kt +169 -0
  62. package/kotlin/src/main/kotlin/an5/adapters/Types.kt +77 -0
  63. package/kotlin/src/test/kotlin/an5/adapters/An5Smoke.kt +329 -0
  64. package/native/sqlite/CMakeLists.txt +12 -0
  65. package/native/sqlite/README.md +117 -0
  66. package/native/sqlite/an5_vector.c +209 -0
  67. package/package.json +40 -9
  68. package/python/an5_adapter.py +37 -6
  69. package/python/base/__init__.py +15 -0
  70. package/python/base/vectors.py +434 -0
  71. package/python/sqlite/__init__.py +2 -2
  72. package/python/sqlite/provider.py +21 -1
  73. package/python/table_client.py +129 -59
  74. package/rust/src/adapter.rs +342 -24
  75. package/rust/src/base/mod.rs +7 -0
  76. package/rust/src/base/vectors.rs +293 -0
  77. package/rust/src/lib.rs +3 -3
  78. package/scripts/sqlite-native-build.js +32 -0
  79. package/swift/Package.swift +22 -0
  80. package/swift/Sources/An5Adapters/An5Adapter.swift +160 -0
  81. package/swift/Sources/An5Adapters/Metadata.swift +183 -0
  82. package/swift/Sources/An5Adapters/Query.swift +105 -0
  83. package/swift/Sources/An5Adapters/Row+Accessors.swift +129 -0
  84. package/swift/Sources/An5Adapters/SQLBuilder.swift +359 -0
  85. package/swift/Sources/An5Adapters/SQLiteDriver.swift +301 -0
  86. package/swift/Sources/An5Adapters/SqliteVectors.swift +243 -0
  87. package/swift/Sources/An5Adapters/TableClient.swift +671 -0
  88. package/swift/Sources/An5Adapters/Types.swift +127 -0
  89. package/swift/Sources/An5Adapters/Vectors.swift +86 -0
  90. package/swift/Sources/An5Adapters/ViewClient.swift +50 -0
  91. package/swift/Sources/CSQLite/module.modulemap +8 -0
  92. package/swift/Sources/CSQLite/shim.h +9 -0
  93. package/swift/Tests/An5AdaptersTests/QuerySemanticsTests.swift +51 -0
  94. package/swift/Tests/An5AdaptersTests/RuntimeSmokeTests.swift +488 -0
  95. package/typescript/src/an5Adapter.ts +200 -58
  96. package/typescript/src/base/metadata.ts +8 -0
  97. package/typescript/src/base/sql.ts +23 -16
  98. package/typescript/src/base/types.ts +15 -0
  99. package/typescript/src/googlesheets/adapter.ts +17 -8
  100. package/typescript/src/googlesheets/config.ts +10 -1
  101. package/typescript/src/googlesheets/oauth.ts +31 -0
  102. package/typescript/src/googlesheets/parseConnectionString.ts +8 -0
  103. package/typescript/src/googlesheets/tableClient.ts +5 -19
  104. package/typescript/src/index.ts +13 -0
  105. package/typescript/src/sqlite/browserEngine.ts +24 -0
  106. package/typescript/src/sqlite/engine.ts +13 -0
  107. package/typescript/src/sqlite/index.ts +1 -1
  108. package/typescript/src/sqlite/vector.ts +541 -0
  109. package/golang/sqlite/sqlite.go +0 -14
@@ -0,0 +1,174 @@
1
+ import java.util.ArrayList;
2
+ import java.util.LinkedHashMap;
3
+ import java.util.List;
4
+ import java.util.Map;
5
+
6
+ /**
7
+ * A minimal JSON reader for the test suite.
8
+ *
9
+ * <p>The adapter itself has no JSON dependency — it never parses a document, it only ever
10
+ * reads and writes vector literals — so pulling one in for production code to satisfy a
11
+ * test would be the wrong trade. This reads exactly the subset the shared query-semantics
12
+ * fixture uses: objects, arrays, strings, numbers, booleans and null.
13
+ */
14
+ final class Json {
15
+
16
+ private final String source;
17
+ private int position;
18
+
19
+ private Json(String source) {
20
+ this.source = source;
21
+ }
22
+
23
+ static Object parse(String text) {
24
+ Json parser = new Json(text);
25
+ parser.skipWhitespace();
26
+ Object value = parser.readValue();
27
+ parser.skipWhitespace();
28
+ return value;
29
+ }
30
+
31
+ @SuppressWarnings("unchecked")
32
+ static Map<String, Object> asObject(Object value) {
33
+ return value instanceof Map ? (Map<String, Object>) value : new LinkedHashMap<String, Object>();
34
+ }
35
+
36
+ @SuppressWarnings("unchecked")
37
+ static List<Object> asList(Object value) {
38
+ return value instanceof List ? (List<Object>) value : new ArrayList<Object>();
39
+ }
40
+
41
+ private Object readValue() {
42
+ skipWhitespace();
43
+ char ch = peek();
44
+ switch (ch) {
45
+ case '{':
46
+ return readObject();
47
+ case '[':
48
+ return readArray();
49
+ case '"':
50
+ return readString();
51
+ case 't':
52
+ expect("true");
53
+ return Boolean.TRUE;
54
+ case 'f':
55
+ expect("false");
56
+ return Boolean.FALSE;
57
+ case 'n':
58
+ expect("null");
59
+ return null;
60
+ default:
61
+ return readNumber();
62
+ }
63
+ }
64
+
65
+ private Map<String, Object> readObject() {
66
+ Map<String, Object> object = new LinkedHashMap<String, Object>();
67
+ position++;
68
+ skipWhitespace();
69
+ if (peek() == '}') {
70
+ position++;
71
+ return object;
72
+ }
73
+ while (true) {
74
+ skipWhitespace();
75
+ String key = readString();
76
+ skipWhitespace();
77
+ position++; // ':'
78
+ object.put(key, readValue());
79
+ skipWhitespace();
80
+ char ch = peek();
81
+ position++;
82
+ if (ch == '}') {
83
+ return object;
84
+ }
85
+ }
86
+ }
87
+
88
+ private List<Object> readArray() {
89
+ List<Object> array = new ArrayList<Object>();
90
+ position++;
91
+ skipWhitespace();
92
+ if (peek() == ']') {
93
+ position++;
94
+ return array;
95
+ }
96
+ while (true) {
97
+ array.add(readValue());
98
+ skipWhitespace();
99
+ char ch = peek();
100
+ position++;
101
+ if (ch == ']') {
102
+ return array;
103
+ }
104
+ }
105
+ }
106
+
107
+ private String readString() {
108
+ StringBuilder out = new StringBuilder();
109
+ position++;
110
+ while (true) {
111
+ char ch = source.charAt(position++);
112
+ if (ch == '"') {
113
+ return out.toString();
114
+ }
115
+ if (ch != '\\') {
116
+ out.append(ch);
117
+ continue;
118
+ }
119
+ char escaped = source.charAt(position++);
120
+ switch (escaped) {
121
+ case 'n':
122
+ out.append('\n');
123
+ break;
124
+ case 't':
125
+ out.append('\t');
126
+ break;
127
+ case 'r':
128
+ out.append('\r');
129
+ break;
130
+ case 'b':
131
+ out.append('\b');
132
+ break;
133
+ case 'f':
134
+ out.append('\f');
135
+ break;
136
+ case 'u':
137
+ out.append((char) Integer.parseInt(source.substring(position, position + 4), 16));
138
+ position += 4;
139
+ break;
140
+ default:
141
+ out.append(escaped);
142
+ }
143
+ }
144
+ }
145
+
146
+ private Object readNumber() {
147
+ int start = position;
148
+ while (position < source.length() && "+-.eE0123456789".indexOf(source.charAt(position)) >= 0) {
149
+ position++;
150
+ }
151
+ String text = source.substring(start, position);
152
+ if (text.indexOf('.') < 0 && text.indexOf('e') < 0 && text.indexOf('E') < 0) {
153
+ return Long.valueOf(text);
154
+ }
155
+ return Double.valueOf(text);
156
+ }
157
+
158
+ private void expect(String literal) {
159
+ if (!source.startsWith(literal, position)) {
160
+ throw new IllegalArgumentException("expected " + literal + " at offset " + position);
161
+ }
162
+ position += literal.length();
163
+ }
164
+
165
+ private char peek() {
166
+ return position < source.length() ? source.charAt(position) : '\0';
167
+ }
168
+
169
+ private void skipWhitespace() {
170
+ while (position < source.length() && Character.isWhitespace(source.charAt(position))) {
171
+ position++;
172
+ }
173
+ }
174
+ }
@@ -0,0 +1,111 @@
1
+ import an5.adapters.base.Dialect;
2
+ import an5.adapters.base.SqlBuilder;
3
+ import java.io.IOException;
4
+ import java.nio.charset.StandardCharsets;
5
+ import java.nio.file.Files;
6
+ import java.nio.file.Paths;
7
+ import java.sql.Connection;
8
+ import java.sql.DriverManager;
9
+ import java.sql.ResultSet;
10
+ import java.sql.Statement;
11
+ import java.util.ArrayList;
12
+ import java.util.LinkedHashMap;
13
+ import java.util.List;
14
+ import java.util.Map;
15
+
16
+ /**
17
+ * Runs the shared query contract in {@code test/fixtures/query-semantics.json} through the
18
+ * Java SQL builder and a real SQLite database.
19
+ *
20
+ * <p>The same fixture gates the TypeScript, Python and Go builders, which is the point: a
21
+ * where tree that means one thing in one client and another in the next is a bug in the
22
+ * client, and only a shared fixture catches it.
23
+ *
24
+ * <p>Reads the fixture path from {@code args[0]}, or from {@code ../test/fixtures} when run
25
+ * from the adapter directory.
26
+ */
27
+ public final class QuerySemantics {
28
+
29
+ private QuerySemantics() {}
30
+
31
+ public static void main(String[] args) throws Exception {
32
+ String fixturePath =
33
+ args.length > 0 ? args[0] : "../test/fixtures/query-semantics.json";
34
+ Map<String, Object> fixture = Json.asObject(Json.parse(read(fixturePath)));
35
+
36
+ List<Object> rows = Json.asList(fixture.get("rows"));
37
+ int failures = 0;
38
+ for (Object entry : Json.asList(fixture.get("cases"))) {
39
+ Map<String, Object> testCase = Json.asObject(entry);
40
+ String name = String.valueOf(testCase.get("name"));
41
+ Map<String, Object> where = Json.asObject(testCase.get("where"));
42
+ List<Object> expected = Json.asList(testCase.get("ids"));
43
+ try {
44
+ List<Long> actual = run(rows, where);
45
+ List<Long> expectedIds = new ArrayList<Long>();
46
+ for (Object id : expected) {
47
+ expectedIds.add(((Number) id).longValue());
48
+ }
49
+ if (!actual.equals(expectedIds)) {
50
+ System.out.println("FAIL " + name + ": expected " + expectedIds + ", got " + actual);
51
+ failures++;
52
+ } else {
53
+ System.out.println("ok " + name);
54
+ }
55
+ } catch (Exception error) {
56
+ System.out.println("FAIL " + name + ": " + error);
57
+ failures++;
58
+ }
59
+ }
60
+
61
+ if (failures > 0) {
62
+ System.out.println("java query contract: " + failures + " case(s) failed");
63
+ System.exit(1);
64
+ }
65
+ System.out.println("java query contract: all cases passed");
66
+ }
67
+
68
+ private static List<Long> run(List<Object> rows, Map<String, Object> where) throws Exception {
69
+ Connection connection = DriverManager.getConnection("jdbc:sqlite::memory:");
70
+ try {
71
+ Statement statement = connection.createStatement();
72
+ statement.execute("CREATE TABLE users (id INTEGER, score INTEGER)");
73
+ for (Object entry : rows) {
74
+ Map<String, Object> row = Json.asObject(entry);
75
+ statement.execute(
76
+ "INSERT INTO users VALUES ("
77
+ + ((Number) row.get("id")).longValue()
78
+ + ", "
79
+ + ((Number) row.get("score")).longValue()
80
+ + ")");
81
+ }
82
+
83
+ Map<String, Object> params = new LinkedHashMap<String, Object>();
84
+ SqlBuilder.Where clause = SqlBuilder.parseWhere(where, Dialect.SQLITE, params, "");
85
+ List<Object> values = new ArrayList<Object>(params.values());
86
+ String sql =
87
+ "SELECT id FROM users"
88
+ + (clause.isEmpty() ? "" : " WHERE " + clause.sql)
89
+ + " ORDER BY id";
90
+
91
+ java.sql.PreparedStatement prepared = connection.prepareStatement(sql);
92
+ for (int i = 0; i < values.size(); i++) {
93
+ prepared.setObject(i + 1, values.get(i));
94
+ }
95
+ ResultSet results = prepared.executeQuery();
96
+ List<Long> ids = new ArrayList<Long>();
97
+ while (results.next()) {
98
+ ids.add(results.getLong(1));
99
+ }
100
+ results.close();
101
+ prepared.close();
102
+ return ids;
103
+ } finally {
104
+ connection.close();
105
+ }
106
+ }
107
+
108
+ private static String read(String path) throws IOException {
109
+ return new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8);
110
+ }
111
+ }
@@ -0,0 +1,145 @@
1
+ plugins {
2
+ kotlin("jvm") version "2.0.21"
3
+ `java-library`
4
+ `maven-publish`
5
+ signing
6
+ }
7
+
8
+ // Gradle rather than Maven because Kotlin on Android is built with Gradle, and a runtime a
9
+ // phone app cannot resolve is not a mobile runtime. Maven consumers can still use the
10
+ // published Maven coordinates; this module publishes both.
11
+ group = "io.github.an5orm"
12
+ version = "0.2.13"
13
+
14
+ repositories {
15
+ // The Java runtime is a sibling checkout, not a published artifact during development:
16
+ // `mvn -f java/pom.xml install` puts it in ~/.m2 and this resolves it from there. Every
17
+ // other dependency still comes from Central, so a consumer without a local install is
18
+ // unaffected.
19
+ mavenLocal()
20
+ mavenCentral()
21
+ }
22
+
23
+ dependencies {
24
+ // The Java adapter is the implementation; this module is the Kotlin surface over it.
25
+ // One SQL builder and one set of dialect rules, so a where tree cannot mean one thing in
26
+ // Java and another in Kotlin.
27
+ api("io.github.an5orm:an5-adapters-java:${version}")
28
+
29
+ // `api`, not `implementation`: a row's accessors return stdlib types, so consumers see
30
+ // them in their own signatures.
31
+ api(kotlin("stdlib"))
32
+
33
+ // Tests run against the same SQLite the other runtimes use, which needs no licence and
34
+ // runs on every platform the workspace builds on.
35
+ testImplementation("org.xerial:sqlite-jdbc:3.46.1.3")
36
+ }
37
+
38
+ java {
39
+ // Java 8 bytecode: Android desugars Java 8 but not newer language features, so a runtime
40
+ // compiled to a newer target cannot be used on a device.
41
+ sourceCompatibility = JavaVersion.VERSION_1_8
42
+ targetCompatibility = JavaVersion.VERSION_1_8
43
+ // Sources are required by Maven Central; the javadoc jar is empty because the sources are
44
+ // Kotlin and only a Kotlin-aware generator would fill it.
45
+ withSourcesJar()
46
+ }
47
+
48
+ kotlin {
49
+ compilerOptions {
50
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8)
51
+ }
52
+ }
53
+
54
+ sourceSets {
55
+ named("main") { kotlin.srcDir("src/main/kotlin") }
56
+ named("test") { kotlin.srcDir("src/test/kotlin") }
57
+ }
58
+
59
+ // The runtime's tests are a `main()` program, not JUnit classes, so they need a runner
60
+ // rather than a test framework: `useJUnitPlatform()` with no engine on the classpath makes
61
+ // the launcher refuse to start, and an engine added to make it start would find zero tests
62
+ // and pass without running anything. `smoke` runs the program, and `check` — so `build` —
63
+ // depends on it.
64
+ val smoke = tasks.register<JavaExec>("smoke") {
65
+ group = "verification"
66
+ description = "Runs the Kotlin runtime smoke against a real SQLite database."
67
+ classpath = sourceSets.getByName("test").runtimeClasspath
68
+ mainClass.set("an5.adapters.An5Smoke")
69
+ }
70
+
71
+ tasks.named("check") {
72
+ dependsOn(smoke)
73
+ }
74
+
75
+ tasks.test {
76
+ // An5Smoke is executed by `smoke`; there is nothing here for a test framework to find.
77
+ testLogging {
78
+ events("passed", "failed", "skipped")
79
+ }
80
+ }
81
+
82
+ val javadocJar = tasks.register<Jar>("javadocJar") {
83
+ archiveClassifier.set("javadoc")
84
+ }
85
+
86
+ publishing {
87
+ publications {
88
+ create<MavenPublication>("maven") {
89
+ from(components["java"])
90
+ artifact(javadocJar)
91
+ pom {
92
+ name.set("AN5 ORM Kotlin adapter")
93
+ description.set(
94
+ "Kotlin runtime for AN5 ORM: dialect-aware CRUD, relation eager loading " +
95
+ "and vector search over the AN5 Java adapter.",
96
+ )
97
+ url.set("https://github.com/an5ORM/an5Adapters")
98
+ licenses {
99
+ license {
100
+ name.set("MIT License")
101
+ url.set("https://opensource.org/licenses/MIT")
102
+ }
103
+ }
104
+ developers {
105
+ developer {
106
+ name.set("AN5 ORM")
107
+ organization.set("AN5")
108
+ url.set("https://github.com/an5ORM")
109
+ }
110
+ }
111
+ scm {
112
+ connection.set("scm:git:https://github.com/an5ORM/an5Adapters.git")
113
+ developerConnection.set("scm:git:ssh://git@github.com/an5ORM/an5Adapters.git")
114
+ url.set("https://github.com/an5ORM/an5Adapters")
115
+ }
116
+ }
117
+ }
118
+ }
119
+ repositories {
120
+ // The Sonatype Central Portal accepts legacy OSSRH clients on this endpoint, so Gradle
121
+ // uploads reach the same repository as the Maven side's central-publishing plugin.
122
+ // Credentials come from the environment; `publishToMavenLocal` never touches them.
123
+ maven {
124
+ name = "central"
125
+ url = uri(
126
+ System.getenv("MAVEN_CENTRAL_URL")
127
+ ?: "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/",
128
+ )
129
+ credentials {
130
+ username = System.getenv("MAVEN_CENTRAL_USERNAME")
131
+ password = System.getenv("MAVEN_CENTRAL_TOKEN")
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ // Signing is conditional: a checkout without a key still runs `publishToMavenLocal` and
138
+ // `build`, and only a release machine supplies MAVEN_GPG_PRIVATE_KEY.
139
+ signing {
140
+ val signingKey = System.getenv("MAVEN_GPG_PRIVATE_KEY")
141
+ if (!signingKey.isNullOrBlank()) {
142
+ useInMemoryPgpKeys(signingKey, System.getenv("MAVEN_GPG_PASSPHRASE"))
143
+ sign(publishing.publications)
144
+ }
145
+ }
@@ -0,0 +1,10 @@
1
+ // Gradle needs this to treat the runtime as its own build rather than as part of whatever
2
+ // happens to be checked out next to it. Only the name and repositories: the build itself
3
+ // lives in build.gradle.kts.
4
+ rootProject.name = "an5-adapters-kotlin"
5
+
6
+ dependencyResolutionManagement {
7
+ repositories {
8
+ mavenCentral()
9
+ }
10
+ }
@@ -0,0 +1,89 @@
1
+ package an5.adapters
2
+
3
+ /**
4
+ * Builds an aggregate or a grouped aggregate.
5
+ *
6
+ * One builder covers both because the metrics are the same; [groupedBy] is what turns a
7
+ * flat `aggregate` into a `groupBy`.
8
+ *
9
+ * ```
10
+ * db.table("Order").aggregate {
11
+ * count()
12
+ * sum("total")
13
+ * where("status" eq "paid")
14
+ * }
15
+ *
16
+ * db.table("Order").groupBy("customerId") {
17
+ * sum("total")
18
+ * orderBy("_sum_total", Sort.DESC)
19
+ * take(5)
20
+ * }
21
+ * ```
22
+ */
23
+ class AggregateBuilder {
24
+ private var counts = false
25
+ private val sums = mutableListOf<String>()
26
+ private val averages = mutableListOf<String>()
27
+ private val minimums = mutableListOf<String>()
28
+ private val maximums = mutableListOf<String>()
29
+ private val clauses = mutableListOf<Where>()
30
+ private val groups = mutableListOf<String>()
31
+ private val order = mutableListOf<Map<String, String>>()
32
+ private var offset = 0
33
+ private var limit: Int? = null
34
+
35
+ /** Adds `COUNT(*)`. */
36
+ fun count() {
37
+ counts = true
38
+ }
39
+
40
+ fun sum(vararg columns: String) = apply { sums += columns }
41
+
42
+ fun avg(vararg columns: String) = apply { averages += columns }
43
+
44
+ fun min(vararg columns: String) = apply { minimums += columns }
45
+
46
+ fun max(vararg columns: String) = apply { maximums += columns }
47
+
48
+ /** Restricts the rows the aggregate runs over. */
49
+ fun where(clause: Where) {
50
+ clauses += clause
51
+ }
52
+
53
+ /** Orders the grouped rows; a flat aggregate returns one row, so it is ignored there. */
54
+ fun orderBy(column: String, direction: Sort = Sort.ASC) {
55
+ order += mapOf(column to direction.sql)
56
+ }
57
+
58
+ fun skip(count: Int) {
59
+ offset = count.coerceAtLeast(0)
60
+ }
61
+
62
+ fun take(count: Int?) {
63
+ limit = count
64
+ }
65
+
66
+ /** The `GROUP BY` columns. Empty means a flat aggregate rather than a grouped one. */
67
+ fun groupedBy(vararg columns: String): AggregateBuilder = apply { groups += columns }
68
+
69
+ internal fun build(): an5.adapters.An5GroupBy {
70
+ val aggregate = an5.adapters.An5GroupBy()
71
+ if (counts) aggregate.count()
72
+ aggregate.sum(*sums.toTypedArray())
73
+ aggregate.avg(*averages.toTypedArray())
74
+ aggregate.min(*minimums.toTypedArray())
75
+ aggregate.max(*maximums.toTypedArray())
76
+ if (clauses.isNotEmpty()) {
77
+ val filter =
78
+ if (clauses.size == 1) clauses[0] else andOf(*clauses.toTypedArray())
79
+ aggregate.where(filter.toJavaWhere())
80
+ }
81
+ aggregate.by(*groups.toTypedArray())
82
+ if (order.isNotEmpty()) {
83
+ aggregate.orderBy(order.toList())
84
+ }
85
+ aggregate.skip(offset)
86
+ aggregate.take(limit)
87
+ return aggregate
88
+ }
89
+ }
@@ -0,0 +1,91 @@
1
+ package an5.adapters
2
+
3
+ import java.util.UUID
4
+
5
+ /**
6
+ * The AN5 runtime for Kotlin.
7
+ *
8
+ * A thin, idiomatic front door over the JVM adapter: the dialect rules, the where builder
9
+ * and the row mapping all live in one place, so the Kotlin API can stay small enough to
10
+ * read in a sitting while still behaving exactly like the Java one.
11
+ *
12
+ * ```
13
+ * val db = An5("sqlite:///app.db")
14
+ * val users = db.table("User")
15
+ * users.create(mapOf("name" to "Ada", "score" to 10))
16
+ * val adults = users.findMany { where("age" gte 18); orderBy("name"); take(10) }
17
+ * ```
18
+ *
19
+ * Not thread-safe, exactly like the other AN5 runtimes: a transaction holds one open
20
+ * connection that every statement in the block has to share.
21
+ */
22
+ class An5 internal constructor(
23
+ private val delegate: an5.adapters.An5Adapter,
24
+ ) : AutoCloseable {
25
+
26
+ /** Opens a runtime for a connection string. */
27
+ constructor(connectionString: String) : this(an5.adapters.An5Adapter(connectionString))
28
+
29
+ /** The dialect the connection string points at. */
30
+ val dialect: Dialect get() = delegate.dialect()
31
+
32
+ /** The table client for a model. */
33
+ fun table(model: String): TableClient = TableClient(delegate, model)
34
+
35
+ /**
36
+ * Pins one SQLite vector strategy instead of probing for the fastest one available:
37
+ * `sqlite-vec`, `udf`, `sql` or `memory`.
38
+ *
39
+ * SQLite has no vector type, so a `VECTOR(n)` column is ranked by the sqlite-vec
40
+ * extension, a distance function the driver registers, `json_each` in plain SQL, or
41
+ * in the client. `null` probes. Plain JDBC reaches `json_each` and the in-memory
42
+ * path; the first two need a driver built for them.
43
+ */
44
+ fun vectorStrategy(strategy: String?) {
45
+ delegate.setVectorStrategy(strategy ?: "")
46
+ }
47
+
48
+ /** A read-only client for a database view. */
49
+ fun view(name: String): ViewClient = ViewClient(delegate, name)
50
+
51
+ /** Runs a query and returns its rows as ordered maps keyed by column label. */
52
+ fun queryRaw(sql: String, vararg params: Any?): List<Row> = delegate.queryRaw(sql, *params)
53
+
54
+ /** Runs a statement that returns no rows and gives back the affected row count. */
55
+ fun executeRaw(sql: String, vararg params: Any?): Int = delegate.executeRaw(sql, *params)
56
+
57
+ /**
58
+ * Calls a stored procedure and returns its rows.
59
+ *
60
+ * SQLite has none, and says so rather than emitting `EXEC`, which is not valid SQL.
61
+ */
62
+ fun queryProc(name: String, params: List<Any?> = emptyList()): List<Row> =
63
+ delegate.queryProc(name, params)
64
+
65
+ /** Calls a stored procedure for its effect. */
66
+ fun executeProc(name: String, params: List<Any?> = emptyList()): Int =
67
+ delegate.executeProc(name, params)
68
+
69
+ /**
70
+ * Runs [block] inside a transaction, committing on return and rolling back on failure.
71
+ *
72
+ * The [An5] handed to the block shares the transaction's connection, so statements
73
+ * inside it commit or roll back together. Nesting is rejected rather than flattened:
74
+ * an inner commit would make the outer rollback a partial save.
75
+ */
76
+ fun <T> transaction(block: (An5) -> T): T {
77
+ val result = arrayOfNulls<Any?>(1)
78
+ delegate.transaction<Unit> { scoped ->
79
+ result[0] = block(An5(scoped))
80
+ }
81
+ @Suppress("UNCHECKED_CAST")
82
+ return result[0] as T
83
+ }
84
+
85
+ override fun close() = delegate.close()
86
+
87
+ override fun toString(): String = "An5(${delegate.dialect().id()})"
88
+ }
89
+
90
+ /** A generated identifier, for callers that need the value before the insert. */
91
+ fun newId(): String = UUID.randomUUID().toString()