@an5/adapters 0.2.11 → 0.2.14

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 (91) hide show
  1. package/README.md +137 -5
  2. package/dist/an5Adapter.d.ts +42 -1
  3. package/dist/an5Adapter.js +146 -38
  4. package/dist/base/types.d.ts +14 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +8 -1
  7. package/dist/sqlite/browserEngine.d.ts +13 -0
  8. package/dist/sqlite/browserEngine.js +13 -0
  9. package/dist/sqlite/engine.d.ts +5 -0
  10. package/dist/sqlite/engine.js +10 -0
  11. package/dist/sqlite/index.d.ts +1 -0
  12. package/dist/sqlite/index.js +1 -0
  13. package/dist/sqlite/vector.d.ts +155 -0
  14. package/dist/sqlite/vector.js +432 -0
  15. package/dotnet/An5Adapter.cs +99 -28
  16. package/dotnet/Base/Core.cs +14 -0
  17. package/dotnet/Mssql/MssqlEngine.cs +44 -1
  18. package/dotnet/Postgres/PostgresEngine.cs +2 -0
  19. package/dotnet/Sqlite/SqliteEngine.cs +82 -2
  20. package/dotnet/Sqlite/SqliteVectors.cs +401 -0
  21. package/golang/an5_adapter.go +84 -32
  22. package/golang/base/vectors.go +263 -0
  23. package/golang/sqlite/vector.go +248 -0
  24. package/golang/sqlite_vector_test.go +288 -0
  25. package/golang/table_client.go +67 -3
  26. package/java/pom.xml +177 -0
  27. package/java/src/main/java/an5/adapters/An5Adapter.java +374 -0
  28. package/java/src/main/java/an5/adapters/An5Aggregate.java +116 -0
  29. package/java/src/main/java/an5/adapters/An5GroupBy.java +97 -0
  30. package/java/src/main/java/an5/adapters/An5Query.java +100 -0
  31. package/java/src/main/java/an5/adapters/An5TableClient.java +1030 -0
  32. package/java/src/main/java/an5/adapters/An5ViewClient.java +107 -0
  33. package/java/src/main/java/an5/adapters/base/Dialect.java +103 -0
  34. package/java/src/main/java/an5/adapters/base/FieldMeta.java +42 -0
  35. package/java/src/main/java/an5/adapters/base/JdbcUrls.java +201 -0
  36. package/java/src/main/java/an5/adapters/base/Metadata.java +195 -0
  37. package/java/src/main/java/an5/adapters/base/RelationDef.java +26 -0
  38. package/java/src/main/java/an5/adapters/base/SqlBuilder.java +422 -0
  39. package/java/src/main/java/an5/adapters/base/SqliteVectors.java +375 -0
  40. package/java/src/main/java/an5/adapters/base/Vectors.java +123 -0
  41. package/java/src/test/java/An5AdaptersSmoke.java +733 -0
  42. package/java/src/test/java/Json.java +174 -0
  43. package/java/src/test/java/QuerySemantics.java +111 -0
  44. package/kotlin/build.gradle.kts +145 -0
  45. package/kotlin/settings.gradle.kts +10 -0
  46. package/kotlin/src/main/kotlin/an5/adapters/Aggregate.kt +89 -0
  47. package/kotlin/src/main/kotlin/an5/adapters/An5.kt +91 -0
  48. package/kotlin/src/main/kotlin/an5/adapters/Query.kt +130 -0
  49. package/kotlin/src/main/kotlin/an5/adapters/Rows.kt +166 -0
  50. package/kotlin/src/main/kotlin/an5/adapters/TableClient.kt +169 -0
  51. package/kotlin/src/main/kotlin/an5/adapters/Types.kt +77 -0
  52. package/kotlin/src/test/kotlin/an5/adapters/An5Smoke.kt +329 -0
  53. package/native/sqlite/CMakeLists.txt +12 -0
  54. package/native/sqlite/README.md +117 -0
  55. package/native/sqlite/an5_vector.c +209 -0
  56. package/package.json +33 -7
  57. package/python/an5_adapter.py +37 -6
  58. package/python/base/__init__.py +15 -0
  59. package/python/base/vectors.py +431 -0
  60. package/python/sqlite/__init__.py +2 -2
  61. package/python/sqlite/provider.py +21 -1
  62. package/python/table_client.py +129 -59
  63. package/rust/src/adapter.rs +342 -24
  64. package/rust/src/base/mod.rs +7 -0
  65. package/rust/src/base/vectors.rs +293 -0
  66. package/rust/src/lib.rs +3 -3
  67. package/scripts/sqlite-native-build.js +32 -0
  68. package/swift/Package.swift +22 -0
  69. package/swift/Sources/An5Adapters/An5Adapter.swift +160 -0
  70. package/swift/Sources/An5Adapters/Metadata.swift +183 -0
  71. package/swift/Sources/An5Adapters/Query.swift +105 -0
  72. package/swift/Sources/An5Adapters/Row+Accessors.swift +129 -0
  73. package/swift/Sources/An5Adapters/SQLBuilder.swift +359 -0
  74. package/swift/Sources/An5Adapters/SQLiteDriver.swift +301 -0
  75. package/swift/Sources/An5Adapters/SqliteVectors.swift +243 -0
  76. package/swift/Sources/An5Adapters/TableClient.swift +671 -0
  77. package/swift/Sources/An5Adapters/Types.swift +127 -0
  78. package/swift/Sources/An5Adapters/Vectors.swift +86 -0
  79. package/swift/Sources/An5Adapters/ViewClient.swift +50 -0
  80. package/swift/Sources/CSQLite/module.modulemap +8 -0
  81. package/swift/Sources/CSQLite/shim.h +9 -0
  82. package/swift/Tests/An5AdaptersTests/QuerySemanticsTests.swift +51 -0
  83. package/swift/Tests/An5AdaptersTests/RuntimeSmokeTests.swift +488 -0
  84. package/typescript/src/an5Adapter.ts +155 -31
  85. package/typescript/src/base/types.ts +15 -0
  86. package/typescript/src/index.ts +13 -0
  87. package/typescript/src/sqlite/browserEngine.ts +24 -0
  88. package/typescript/src/sqlite/engine.ts +13 -0
  89. package/typescript/src/sqlite/index.ts +1 -1
  90. package/typescript/src/sqlite/vector.ts +537 -0
  91. package/golang/sqlite/sqlite.go +0 -14
@@ -0,0 +1,374 @@
1
+ package an5.adapters;
2
+
3
+ import an5.adapters.base.Dialect;
4
+ import an5.adapters.base.JdbcUrls;
5
+ import java.sql.Connection;
6
+ import java.sql.DriverManager;
7
+ import java.sql.PreparedStatement;
8
+ import java.sql.ResultSet;
9
+ import java.sql.ResultSetMetaData;
10
+ import java.sql.SQLException;
11
+ import java.sql.Types;
12
+ import java.util.ArrayList;
13
+ import java.util.Collections;
14
+ import java.util.LinkedHashMap;
15
+ import java.util.List;
16
+ import java.util.Map;
17
+ import java.util.Properties;
18
+
19
+ /**
20
+ * The AN5 runtime adapter for Java: a connection holder plus raw SQL execution.
21
+ *
22
+ * <p>Model knowledge lives in {@link an5.adapters.base.Metadata}, which the generated
23
+ * client fills in at start-up. Queries go through {@link #table(String)}; this class is
24
+ * only the layer underneath that needs to know about connections and dialect.
25
+ *
26
+ * <p>Instances are not thread-safe, exactly like the other AN5 adapters: the transaction
27
+ * slot below holds one open connection that every statement has to share.
28
+ */
29
+ public class An5Adapter implements AutoCloseable {
30
+
31
+ /** The unit of work a {@link #transaction} runs. */
32
+ public interface TransactionWork<T> {
33
+ T run(An5Adapter adapter) throws SQLException;
34
+ }
35
+
36
+ private final Dialect dialect;
37
+ private final String connectionString;
38
+
39
+ /**
40
+ * Pins one SQLite vector strategy instead of probing for the fastest one available:
41
+ * {@code sqlite-vec}, {@code udf}, {@code sql} or {@code memory}. Empty probes.
42
+ */
43
+ private String vectorStrategy = "";
44
+
45
+ /**
46
+ * What this SQLite connection can do, probed once with cheap {@code SELECT}s.
47
+ *
48
+ * <p>JDBC has no API for registering a user function or loading an extension, so a stock
49
+ * adapter normally reports only JSON1. That is enough to rank with {@code json_each}; the
50
+ * sqlite-vec and UDF strategies need a driver built for them (the Xerial driver, for
51
+ * instance, loads an extension through a {@code _load_extension} connection property).
52
+ */
53
+ private an5.adapters.base.SqliteVectors.Capabilities sqliteCapabilities;
54
+
55
+ /** The SQLite vector strategy to pin, or {@code null} to probe. */
56
+ public String vectorStrategy() {
57
+ return vectorStrategy;
58
+ }
59
+
60
+ /** Pins one SQLite vector strategy instead of probing. */
61
+ public void setVectorStrategy(String strategy) {
62
+ this.vectorStrategy = strategy == null ? "" : strategy;
63
+ }
64
+
65
+ /** What this SQLite connection can do for vector search. */
66
+ public an5.adapters.base.SqliteVectors.Capabilities sqliteVectorCapabilities() {
67
+ if (dialect != Dialect.SQLITE) {
68
+ return new an5.adapters.base.SqliteVectors.Capabilities(false, false, false);
69
+ }
70
+ if (sqliteCapabilities == null) {
71
+ sqliteCapabilities =
72
+ new an5.adapters.base.SqliteVectors.Capabilities(
73
+ probe("SELECT vec_version()"),
74
+ // The probe has to reach a real distance: a driver that only knows the name
75
+ // would still fail on the arguments.
76
+ probe("SELECT an5_vec_cosine(zeroblob(4), zeroblob(4))"),
77
+ probe("SELECT json_valid('[1]')"));
78
+ }
79
+ return sqliteCapabilities;
80
+ }
81
+
82
+ /** A vector column's declared DDL type, or {@code null} when it cannot be read. */
83
+ public String sqliteColumnType(String table, String column) {
84
+ try {
85
+ List<Map<String, Object>> rows =
86
+ exec(
87
+ "SELECT type FROM pragma_table_info(?) WHERE name = ?",
88
+ java.util.Arrays.<Object>asList(table, column));
89
+ if (!rows.isEmpty()) {
90
+ Object type = rows.get(0).get("type");
91
+ if (type != null) {
92
+ return String.valueOf(type);
93
+ }
94
+ }
95
+ } catch (SQLException ignored) {
96
+ // An older SQLite without table-valued pragmas just means no declared type, and the
97
+ // caller then keeps every strategy in the plan.
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /** True when a probe statement ran, whatever it returned. */
103
+ private boolean probe(String sql) {
104
+ try {
105
+ exec(sql);
106
+ return true;
107
+ } catch (SQLException ignored) {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * The open connection of {@link #transaction}, when there is one. Every statement in the
114
+ * callback has to use this very connection, otherwise a rollback returns to a different,
115
+ * already committed connection — that is "the error was raised but the data is still there".
116
+ */
117
+ private Connection transactionConnection;
118
+
119
+ /** Long-lived connection for in-memory databases, where closing loses the data. */
120
+ private Connection sharedConnection;
121
+
122
+ public An5Adapter(String connectionString) {
123
+ this.connectionString = connectionString;
124
+ this.dialect = Dialect.detect(connectionString);
125
+ }
126
+
127
+ public Dialect dialect() {
128
+ return dialect;
129
+ }
130
+
131
+ /** Opens and returns a fresh connection; the caller closes it. */
132
+ public Connection connect() throws SQLException {
133
+ String url = JdbcUrls.url(connectionString);
134
+ String[] credentials = JdbcUrls.credentials(connectionString);
135
+ Properties properties = JdbcUrls.properties(credentials[0], credentials[1]);
136
+ Connection connection = DriverManager.getConnection(url, properties);
137
+ if (dialect == Dialect.SQLITE) {
138
+ // SQLite defaults foreign keys OFF, unlike every other dialect, so a schema that
139
+ // relies on them would silently accept rows nothing points at.
140
+ try {
141
+ connection.createStatement().execute("PRAGMA foreign_keys = ON");
142
+ } catch (SQLException ignored) {
143
+ // A driver without PRAGMA support is not a reason to fail the connection.
144
+ }
145
+ }
146
+ return connection;
147
+ }
148
+
149
+ /**
150
+ * The connection a statement runs on, and whether this call opened it.
151
+ *
152
+ * <p>An in-memory SQLite database is gone once every connection is closed, so
153
+ * {@link #sharedConnection} is kept open for its whole lifetime.
154
+ */
155
+ private Connection acquire(boolean[] owned) throws SQLException {
156
+ if (transactionConnection != null) {
157
+ owned[0] = false;
158
+ return transactionConnection;
159
+ }
160
+ if (dialect == Dialect.SQLITE && JdbcUrls.isMemory(connectionString)) {
161
+ if (sharedConnection == null) {
162
+ sharedConnection = connect();
163
+ }
164
+ owned[0] = false;
165
+ return sharedConnection;
166
+ }
167
+ owned[0] = true;
168
+ return connect();
169
+ }
170
+
171
+ @Override
172
+ public void close() {
173
+ if (sharedConnection != null) {
174
+ try {
175
+ sharedConnection.close();
176
+ } catch (SQLException ignored) {
177
+ // Closing is best effort; the pool owner reports real failures.
178
+ }
179
+ sharedConnection = null;
180
+ }
181
+ }
182
+
183
+ /** Runs a query and returns its rows as ordered maps keyed by column label. */
184
+ public List<Map<String, Object>> exec(String query) throws SQLException {
185
+ return exec(query, Collections.<Object>emptyList());
186
+ }
187
+
188
+ public List<Map<String, Object>> exec(String query, List<Object> params) throws SQLException {
189
+ boolean[] owned = new boolean[1];
190
+ Connection connection = acquire(owned);
191
+ try {
192
+ PreparedStatement statement = prepare(connection, query, params);
193
+ try {
194
+ ResultSet results = statement.executeQuery();
195
+ try {
196
+ return readRows(results);
197
+ } finally {
198
+ results.close();
199
+ }
200
+ } finally {
201
+ statement.close();
202
+ }
203
+ } finally {
204
+ if (owned[0]) {
205
+ connection.close();
206
+ }
207
+ }
208
+ }
209
+
210
+ /** Runs a statement that returns no rows and gives back the affected row count. */
211
+ public int execute(String query) throws SQLException {
212
+ return execute(query, Collections.<Object>emptyList());
213
+ }
214
+
215
+ public int execute(String query, List<Object> params) throws SQLException {
216
+ boolean[] owned = new boolean[1];
217
+ Connection connection = acquire(owned);
218
+ try {
219
+ PreparedStatement statement = prepare(connection, query, params);
220
+ try {
221
+ return statement.executeUpdate();
222
+ } finally {
223
+ statement.close();
224
+ }
225
+ } finally {
226
+ if (owned[0]) {
227
+ connection.close();
228
+ }
229
+ }
230
+ }
231
+
232
+ public List<Map<String, Object>> queryRaw(String query, Object... params) throws SQLException {
233
+ return exec(query, toList(params));
234
+ }
235
+
236
+ public int executeRaw(String query, Object... params) throws SQLException {
237
+ return execute(query, toList(params));
238
+ }
239
+
240
+ /** The table client for a model, or for a view name on {@link #view(String)}. */
241
+ public An5TableClient table(String modelName) {
242
+ return new An5TableClient(this, modelName);
243
+ }
244
+
245
+ /** A read-only client for a database view. */
246
+ public An5ViewClient view(String viewName) {
247
+ return new An5ViewClient(this, viewName);
248
+ }
249
+
250
+ /**
251
+ * Runs work inside a transaction, committing on return and rolling back on failure.
252
+ *
253
+ * <p>Nesting is rejected rather than flattened: an inner commit would make the outer
254
+ * rollback a partial save, which is the opposite of what the caller asked for.
255
+ */
256
+ public <T> T transaction(TransactionWork<T> work) throws SQLException {
257
+ if (transactionConnection != null) {
258
+ throw new IllegalStateException("Nested transaction() is not supported");
259
+ }
260
+ boolean[] owned = new boolean[1];
261
+ Connection connection = acquire(owned);
262
+ boolean previousAutoCommit = connection.getAutoCommit();
263
+ connection.setAutoCommit(false);
264
+ transactionConnection = connection;
265
+ try {
266
+ T result = work.run(this);
267
+ connection.commit();
268
+ return result;
269
+ } catch (SQLException error) {
270
+ connection.rollback();
271
+ throw error;
272
+ } catch (RuntimeException error) {
273
+ connection.rollback();
274
+ throw error;
275
+ } catch (Error error) {
276
+ connection.rollback();
277
+ throw error;
278
+ } finally {
279
+ transactionConnection = null;
280
+ connection.setAutoCommit(previousAutoCommit);
281
+ if (owned[0]) {
282
+ connection.close();
283
+ }
284
+ }
285
+ }
286
+
287
+ public List<Map<String, Object>> queryProc(String procName, List<Object> params) throws SQLException {
288
+ return exec(callStatement(procName, params, false), params);
289
+ }
290
+
291
+ public int executeProc(String procName, List<Object> params) throws SQLException {
292
+ return execute(callStatement(procName, params, true), params);
293
+ }
294
+
295
+ /**
296
+ * SQLite has no stored procedures.
297
+ *
298
+ * <p>This used to fall through to the MSSQL branch and emit {@code EXEC ...}, which is not
299
+ * valid SQL anywhere; the error only surfaced at the call. Saying so beats sending wrong SQL.
300
+ */
301
+ private String callStatement(String procName, List<Object> params, boolean forExecute) {
302
+ if (dialect == Dialect.SQLITE) {
303
+ throw new UnsupportedOperationException(
304
+ "SQLite has no stored procedures. Use the generated table client "
305
+ + "(db.table(\"Model\").findMany()) or db.exec() with a parameterised query.");
306
+ }
307
+ int count = params == null ? 0 : params.size();
308
+ List<String> placeholders = new ArrayList<String>();
309
+ for (int i = 0; i < count; i++) {
310
+ placeholders.add(dialect.placeholder());
311
+ }
312
+ String joined = placeholders.isEmpty() ? "" : join(placeholders, ", ");
313
+ if (dialect == Dialect.POSTGRES) {
314
+ return joined.isEmpty()
315
+ ? "CALL " + procName + "()"
316
+ : "CALL " + procName + "(" + joined + ")";
317
+ }
318
+ return joined.isEmpty() ? "EXEC " + procName : "EXEC " + procName + " " + joined;
319
+ }
320
+
321
+ private PreparedStatement prepare(Connection connection, String query, List<Object> params)
322
+ throws SQLException {
323
+ PreparedStatement statement = connection.prepareStatement(query);
324
+ if (params != null) {
325
+ for (int i = 0; i < params.size(); i++) {
326
+ Object value = params.get(i);
327
+ if (value == null) {
328
+ statement.setNull(i + 1, Types.NULL);
329
+ } else {
330
+ statement.setObject(i + 1, value);
331
+ }
332
+ }
333
+ }
334
+ return statement;
335
+ }
336
+
337
+ private static List<Map<String, Object>> readRows(ResultSet results) throws SQLException {
338
+ List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
339
+ ResultSetMetaData meta = results.getMetaData();
340
+ int columns = meta.getColumnCount();
341
+ while (results.next()) {
342
+ // LinkedHashMap so a row keeps the column order of the SELECT, which is what makes a
343
+ // serialised row read the same way every time.
344
+ Map<String, Object> row = new LinkedHashMap<String, Object>();
345
+ for (int i = 1; i <= columns; i++) {
346
+ row.put(meta.getColumnLabel(i), results.getObject(i));
347
+ }
348
+ rows.add(row);
349
+ }
350
+ return rows;
351
+ }
352
+
353
+ private static List<Object> toList(Object[] params) {
354
+ if (params == null || params.length == 0) {
355
+ return Collections.emptyList();
356
+ }
357
+ List<Object> list = new ArrayList<Object>(params.length);
358
+ for (Object param : params) {
359
+ list.add(param);
360
+ }
361
+ return list;
362
+ }
363
+
364
+ private static String join(List<String> parts, String separator) {
365
+ StringBuilder out = new StringBuilder();
366
+ for (int i = 0; i < parts.size(); i++) {
367
+ if (i > 0) {
368
+ out.append(separator);
369
+ }
370
+ out.append(parts.get(i));
371
+ }
372
+ return out.toString();
373
+ }
374
+ }
@@ -0,0 +1,116 @@
1
+ package an5.adapters;
2
+
3
+ import an5.adapters.base.Dialect;
4
+ import java.util.ArrayList;
5
+ import java.util.Arrays;
6
+ import java.util.LinkedHashMap;
7
+ import java.util.List;
8
+ import java.util.Map;
9
+
10
+ /**
11
+ * Which aggregates a query computes.
12
+ *
13
+ * <p>Each list names columns to apply the aggregate to; the first list added wins for a
14
+ * given metric, so calling {@link #sum} twice widens the first call rather than replacing it.
15
+ */
16
+ public class An5Aggregate {
17
+
18
+ private boolean count;
19
+ private List<String> sum = new ArrayList<String>();
20
+ private List<String> avg = new ArrayList<String>();
21
+ private List<String> min = new ArrayList<String>();
22
+ private List<String> max = new ArrayList<String>();
23
+ private Map<String, Object> where;
24
+
25
+ /** Adds {@code COUNT(*)}. */
26
+ public An5Aggregate count() {
27
+ this.count = true;
28
+ return this;
29
+ }
30
+
31
+ public An5Aggregate sum(String... columns) {
32
+ this.sum.addAll(Arrays.asList(columns));
33
+ return this;
34
+ }
35
+
36
+ public An5Aggregate avg(String... columns) {
37
+ this.avg.addAll(Arrays.asList(columns));
38
+ return this;
39
+ }
40
+
41
+ public An5Aggregate min(String... columns) {
42
+ this.min.addAll(Arrays.asList(columns));
43
+ return this;
44
+ }
45
+
46
+ public An5Aggregate max(String... columns) {
47
+ this.max.addAll(Arrays.asList(columns));
48
+ return this;
49
+ }
50
+
51
+ /** Restricts the rows the aggregate runs over. */
52
+ public An5Aggregate where(Map<String, Object> where) {
53
+ this.where = where;
54
+ return this;
55
+ }
56
+
57
+ public boolean countEnabled() {
58
+ return count;
59
+ }
60
+
61
+ public List<String> sumFields() {
62
+ return sum;
63
+ }
64
+
65
+ public List<String> avgFields() {
66
+ return avg;
67
+ }
68
+
69
+ public List<String> minFields() {
70
+ return min;
71
+ }
72
+
73
+ public List<String> maxFields() {
74
+ return max;
75
+ }
76
+
77
+ public Map<String, Object> whereMap() {
78
+ return where;
79
+ }
80
+
81
+ /**
82
+ * The {@code SELECT} list for this aggregate, in a stable order.
83
+ *
84
+ * <p>Order is fixed so the same aggregate always produces the same SQL, which is what
85
+ * makes the generated query comparable between runs and cacheable.
86
+ */
87
+ public List<String> selectExpressions(Dialect dialect) {
88
+ List<String> expressions = new ArrayList<String>();
89
+ if (count) {
90
+ expressions.add("COUNT(*) AS _count");
91
+ }
92
+ addFor(expressions, "SUM", sumFields(), dialect);
93
+ addFor(expressions, "AVG", avgFields(), dialect);
94
+ addFor(expressions, "MIN", minFields(), dialect);
95
+ addFor(expressions, "MAX", maxFields(), dialect);
96
+ return expressions;
97
+ }
98
+
99
+ private static void addFor(
100
+ List<String> expressions, String function, List<String> fields, Dialect dialect) {
101
+ for (String field : fields) {
102
+ expressions.add(function + "(" + dialect.quoteIdentifier(field) + ") AS _" + lower(function) + "_" + field);
103
+ }
104
+ }
105
+
106
+ private static String lower(String value) {
107
+ return value.toLowerCase(java.util.Locale.ROOT);
108
+ }
109
+
110
+ /** Convenience for the one-map-per-column form used by several adapters' {@code orderBy}. */
111
+ public static Map<String, Object> orderEntry(String column, String direction) {
112
+ Map<String, Object> entry = new LinkedHashMap<String, Object>();
113
+ entry.put(column, direction);
114
+ return entry;
115
+ }
116
+ }
@@ -0,0 +1,97 @@
1
+ package an5.adapters;
2
+
3
+ import java.util.ArrayList;
4
+ import java.util.Arrays;
5
+ import java.util.List;
6
+
7
+ /**
8
+ * A grouped aggregate: the {@code GROUP BY} columns plus the aggregates over each group.
9
+ *
10
+ * <p>Extends {@link An5Aggregate} so the same {@code sum}/{@code avg} calls describe both a
11
+ * flat aggregate and a grouped one.
12
+ */
13
+ public class An5GroupBy extends An5Aggregate {
14
+
15
+ private List<String> by = new ArrayList<String>();
16
+ private Object orderBy;
17
+ private int skip;
18
+ private Integer take;
19
+
20
+ /** The columns to group on. At least one is required. */
21
+ public An5GroupBy by(String... columns) {
22
+ this.by.addAll(Arrays.asList(columns));
23
+ return this;
24
+ }
25
+
26
+ @Override
27
+ public An5GroupBy count() {
28
+ super.count();
29
+ return this;
30
+ }
31
+
32
+ @Override
33
+ public An5GroupBy sum(String... columns) {
34
+ super.sum(columns);
35
+ return this;
36
+ }
37
+
38
+ @Override
39
+ public An5GroupBy avg(String... columns) {
40
+ super.avg(columns);
41
+ return this;
42
+ }
43
+
44
+ @Override
45
+ public An5GroupBy min(String... columns) {
46
+ super.min(columns);
47
+ return this;
48
+ }
49
+
50
+ @Override
51
+ public An5GroupBy max(String... columns) {
52
+ super.max(columns);
53
+ return this;
54
+ }
55
+
56
+ @Override
57
+ public An5GroupBy where(java.util.Map<String, Object> where) {
58
+ super.where(where);
59
+ return this;
60
+ }
61
+
62
+ public An5GroupBy orderBy(String column, String direction) {
63
+ this.orderBy = An5Aggregate.orderEntry(column, direction);
64
+ return this;
65
+ }
66
+
67
+ public An5GroupBy orderBy(Object orderBy) {
68
+ this.orderBy = orderBy;
69
+ return this;
70
+ }
71
+
72
+ public An5GroupBy skip(int skip) {
73
+ this.skip = Math.max(0, skip);
74
+ return this;
75
+ }
76
+
77
+ public An5GroupBy take(Integer take) {
78
+ this.take = take;
79
+ return this;
80
+ }
81
+
82
+ public List<String> byFields() {
83
+ return by;
84
+ }
85
+
86
+ public Object orderByValue() {
87
+ return orderBy;
88
+ }
89
+
90
+ public int skipValue() {
91
+ return skip;
92
+ }
93
+
94
+ public Integer takeValue() {
95
+ return take;
96
+ }
97
+ }
@@ -0,0 +1,100 @@
1
+ package an5.adapters;
2
+
3
+ import java.util.LinkedHashMap;
4
+ import java.util.List;
5
+ import java.util.Map;
6
+
7
+ /**
8
+ * A query built fluently and handed to a table client.
9
+ *
10
+ * <p>Every field is optional, so {@code new An5Query()} means "no filter, no order, no
11
+ * limit" — the same thing the other adapters read from an omitted argument.
12
+ */
13
+ public class An5Query {
14
+
15
+ private Map<String, Object> where;
16
+ private Object orderBy;
17
+ private int skip;
18
+ private Integer take;
19
+ private List<String> select;
20
+ private Map<String, Object> include;
21
+
22
+ public An5Query where(Map<String, Object> where) {
23
+ this.where = where;
24
+ return this;
25
+ }
26
+
27
+ /**
28
+ * Orders by a single column, ascending.
29
+ *
30
+ * <p>For several columns, or a descending one, pass the map form:
31
+ * {@code orderBy(Map.of("name", "asc", "createdAt", "desc"))}.
32
+ */
33
+ public An5Query orderBy(String column) {
34
+ Map<String, Object> entry = new LinkedHashMap<String, Object>();
35
+ entry.put(column, "asc");
36
+ this.orderBy = entry;
37
+ return this;
38
+ }
39
+
40
+ public An5Query orderBy(String column, String direction) {
41
+ Map<String, Object> entry = new LinkedHashMap<String, Object>();
42
+ entry.put(column, direction);
43
+ this.orderBy = entry;
44
+ return this;
45
+ }
46
+
47
+ /** Accepts one entry per column; a {@code List} of them is treated as a multi-column sort. */
48
+ public An5Query orderBy(Object orderBy) {
49
+ this.orderBy = orderBy;
50
+ return this;
51
+ }
52
+
53
+ public An5Query skip(int skip) {
54
+ this.skip = Math.max(0, skip);
55
+ return this;
56
+ }
57
+
58
+ /** Caps the number of rows. {@code null} — the default — means no limit. */
59
+ public An5Query take(Integer take) {
60
+ this.take = take;
61
+ return this;
62
+ }
63
+
64
+ /** Restricts the returned columns; relations and {@code _count} are eager-loaded instead. */
65
+ public An5Query select(String... columns) {
66
+ this.select = columns == null || columns.length == 0 ? null : java.util.Arrays.asList(columns);
67
+ return this;
68
+ }
69
+
70
+ /** Eager-loads relations, mirroring the other adapters' {@code include} argument. */
71
+ public An5Query include(Map<String, Object> include) {
72
+ this.include = include;
73
+ return this;
74
+ }
75
+
76
+ public Map<String, Object> whereMap() {
77
+ return where;
78
+ }
79
+
80
+ public Object orderByValue() {
81
+ return orderBy;
82
+ }
83
+
84
+ public int skipValue() {
85
+ return skip;
86
+ }
87
+
88
+ /** The row cap, or {@code null} for no limit. */
89
+ public Integer takeValue() {
90
+ return take;
91
+ }
92
+
93
+ public List<String> selectValue() {
94
+ return select;
95
+ }
96
+
97
+ public Map<String, Object> includeValue() {
98
+ return include;
99
+ }
100
+ }