-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathquery.xi
More file actions
72 lines (64 loc) · 2.84 KB
/
Copy pathquery.xi
File metadata and controls
72 lines (64 loc) · 2.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// xi-sqlite: a QueryProvider that runs reified xi-query plans against sqlite.
//
// `query.from<T>("table").filter{...}.collect(provider)` reifies the chain into
// a QueryPlan; this provider renders it to parameterized SQL with the bundled
// SqliteDialect (values are bound, never spliced — injection-safe), runs it
// through the injected sqlite.SQLite, and returns the rows as a Json array so
// the compiler can decode them back into T.
//
// Following the std MemorySource pattern, the provider implements two
// interfaces over one instance: QueryProvider (run plans) and DatabaseBinder
// (attach the open database). Bind both to it `as singleton` so the instance
// you point at a database is the one `.collect` runs against:
//
// module App {
// bind QueryProvider -> SqliteQueryProvider as singleton
// bind DatabaseBinder -> SqliteQueryProvider as singleton
// }
//
// // inject both; they are the same singleton
// binder.useDatabase(sql.open("app.db")?)
// let picked = query.from<Note>("notes")
// .filter { it.stars >= 4.0 }.sortedBy { it.id }
// .collect(provider) // -> List<Note>
import "std/json.xi"
import "std/query.xi"
import "std/sql.xi"
// Attaches an open database to the query provider. Bind to SqliteQueryProvider
// `as singleton` alongside QueryProvider.
interface DatabaseBinder {
consumer useDatabase(db: sqlite.Database)
}
class SqliteQueryProvider implements QueryProvider, DatabaseBinder {
deps { sql: sqlite.SQLite, decoder: sqlite.ColumnDecoder }
state { db: sqlite.Database = empty sqlite.Database }
// Point the provider at an open database before running queries.
consumer useDatabase(database: sqlite.Database) { this.db = database }
producer run(plan: QueryPlan) -> Json {
let rendered = sqlRender(plan, SqliteDialect {} as SqlDialect)
if isErr(rendered) { return sqliteQueryFail(rendered.err) }
let st = rendered.value
let rows = sql.queryBound(this.db, st.text, st.params)
if isErr(rows) { return sqliteQueryFail(rows.err) }
return sqliteRowsToJson(rows.value, decoder)
}
}
// Turn typed rows back into the Json array shape `collect` decodes.
mapper sqliteRowsToJson(rows: sqlite.Rows, decoder: sqlite.ColumnDecoder) -> Json {
let out = json.array()
for row in rows.items {
let obj = json.object()
for name in row.columns.keys() {
obj = json.set(obj, name, decoder.toJson(row.columns.get(name)))
}
out = json.push(out, obj)
}
return out
}
// A provider cannot honor this plan — fail loudly (the run contract has no
// error channel), naming the SQL/render error.
producer sqliteQueryFail(message: String) -> Json {
system.stdout.writeln("xi-sqlite: query provider could not run plan: " + message)
assert false
return json.array()
}