Delete the ATProto half
Ten modules and eight probes go: OAuth, DPoP, XRPC, identity resolution, Jetstream and its supervisor, the session store, the social records, and the clustering engine. None of it is reachable from a reader that has no accounts. That cascades further than the file list. The schema loses sixteen tables -- oauth, follows, likes, annotations, settings, and every derived recommendation table -- which in turn removes the whole `recs` database, so two SQLite files instead of three. sqlite-vec goes with the embeddings it existed for, taking 305KB of vendored C and the SQLITE_CORE define that had to be got exactly right. exp() and log() were registered only for the clustering SQL, so they go too; this time the claim that nothing calls them is backed by having deleted every caller. articlestore no longer counts likes, so Article loses likeCount and hasLiked, and each query that selected them loses a bind parameter. `atproto/` is renamed `reader/`, since nothing in it is ATProto any more and the old name would actively mislead. Trimming the schema by filtering the generated statements needed two passes: the first dropped the users table, because its follows_dirty column matched a naive "follows" marker, and the second had to catch indexes whose ON clause names an unqualified table the CREATE INDEX prefixes with a schema. Verified after each step: both suites pass, and a clean run subscribes, ingests, searches, scrapes and marks read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
775d793 parent: 4cd4748 deleted
spike/nim/atproto/cluster.nim +0 -276 | deleted file mode 100644 | ||
| @@ -1,276 +0,0 @@ | ||
| 1 | -## The similarity computations behind recommendations, ported from | |
| 2 | -## internal/cluster/jaccard.go and the pure helpers of scoring.go. | |
| 3 | -## | |
| 4 | -## Two kinds of similarity are precomputed on a schedule, because both are | |
| 5 | -## O(pairs) and neither needs to be fresh to the second: | |
| 6 | -## | |
| 7 | -## feed_similarity which feeds are subscribed to by the same people | |
| 8 | -## user_similarity which people subscribe to the same feeds | |
| 9 | -## | |
| 10 | -## Both are time-decayed. An overlap from three years ago says much less | |
| 11 | -## about what someone wants now than one from last week, and without decay a | |
| 12 | -## long-lived account's early subscriptions dominate its recommendations | |
| 13 | -## forever. | |
| 14 | -## | |
| 15 | -## The decay constant is `exp(-0.023 * days)`, a half-life of about a month | |
| 16 | -## (ln 2 / 0.023 ≈ 30). It is written inline in the SQL rather than named, | |
| 17 | -## the same way the Go implementation writes it. | |
| 18 | - | |
| 19 | -import std/[algorithm, math, options, random] | |
| 20 | -import ./gleandb | |
| 21 | -import ./sqlite | |
| 22 | - | |
| 23 | -type | |
| 24 | - Config* = object | |
| 25 | - followBoost*: float | |
| 26 | - likesWeight*: float | |
| 27 | - tagsWeight*: float | |
| 28 | - descriptionWeight*: float | |
| 29 | - | |
| 30 | - SignalWeights* = object | |
| 31 | - wSub*: float | |
| 32 | - wLike*: float | |
| 33 | - wTag*: float | |
| 34 | - wSocial*: float | |
| 35 | - wPop*: float | |
| 36 | - wCategory*: float | |
| 37 | - wContent*: float | |
| 38 | - | |
| 39 | - FeedRecommendation* = object | |
| 40 | - feedUrl*: string | |
| 41 | - title*: string | |
| 42 | - siteUrl*: string | |
| 43 | - description*: string | |
| 44 | - subscriberCount*: int | |
| 45 | - faviconUrl*: string | |
| 46 | - score*: float | |
| 47 | - | |
| 48 | - PersonRecommendation* = object | |
| 49 | - did*: string | |
| 50 | - handle*: string | |
| 51 | - displayName*: string | |
| 52 | - avatarUrl*: string | |
| 53 | - commonFeeds*: int | |
| 54 | - commonLikes*: int | |
| 55 | - commonTags*: int | |
| 56 | - isFollowed*: bool | |
| 57 | - score*: float | |
| 58 | - | |
| 59 | -const | |
| 60 | - ## Half-life of roughly 30 days: ln(2) / 0.023. | |
| 61 | - DecayPerDay* = 0.023 | |
| 62 | - | |
| 63 | -proc defaultConfig*(): Config = | |
| 64 | - Config(followBoost: 0.5, likesWeight: 0.3, tagsWeight: 0.2, | |
| 65 | - descriptionWeight: 0.15) | |
| 66 | - | |
| 67 | -proc defaultWeights*(): SignalWeights = | |
| 68 | - SignalWeights(wSub: 1.0, wLike: 0.5, wTag: 0.3, wSocial: 0.7, | |
| 69 | - wPop: 0.2, wCategory: 0.4, wContent: 0.4) | |
| 70 | - | |
| 71 | -proc decay*(ageDays: float): float = | |
| 72 | - ## The weight an event of this age still carries. | |
| 73 | - ## | |
| 74 | - ## Exposed so the decay can be checked directly; the queries compute it in | |
| 75 | - ## SQL because pulling every like into the application to weight it would | |
| 76 | - ## defeat the point of precomputing. | |
| 77 | - exp(-DecayPerDay * ageDays) | |
| 78 | - | |
| 79 | -# --- similarity ------------------------------------------------------------ | |
| 80 | - | |
| 81 | -## Both computations stage into a temp table and swap in a second | |
| 82 | -## transaction. The first pass is the expensive one and holds no lock on the | |
| 83 | -## live table; readers keep seeing the previous generation until the swap, | |
| 84 | -## rather than an empty table for the length of the computation. | |
| 85 | - | |
| 86 | -proc computeFeedSimilarity*(g: GleanDb) = | |
| 87 | - ## For each pair of feeds, how strongly their subscriber sets overlap. | |
| 88 | - ## | |
| 89 | - ## The numerator is a decayed count of shared subscribers and the | |
| 90 | - ## denominator is the union of the two subscriber counts, which makes this | |
| 91 | - ## a Jaccard index with recency folded into the intersection. | |
| 92 | - g.db.transaction: | |
| 93 | - g.db.exec """CREATE TEMP TABLE IF NOT EXISTS _feed_sim_staging ( | |
| 94 | - feed_a TEXT NOT NULL, feed_b TEXT NOT NULL, jaccard REAL NOT NULL, | |
| 95 | - PRIMARY KEY (feed_a, feed_b))""" | |
| 96 | - g.db.exec "DELETE FROM _feed_sim_staging" | |
| 97 | - g.db.exec """ | |
| 98 | - INSERT INTO _feed_sim_staging (feed_a, feed_b, jaccard) | |
| 99 | - SELECT feed_a, feed_b, jaccard FROM ( | |
| 100 | - SELECT | |
| 101 | - s1.feed_url AS feed_a, | |
| 102 | - s2.feed_url AS feed_b, | |
| 103 | - SUM(EXP(-0.023 * CAST(julianday('now') - | |
| 104 | - julianday(MIN(s1.added_at, s2.added_at)) AS REAL))) | |
| 105 | - / NULLIF(f1.subscriber_count + f2.subscriber_count | |
| 106 | - - CAST(COUNT(*) AS REAL), 0) AS jaccard | |
| 107 | - FROM articles.subscriptions s1 | |
| 108 | - -- feed_url < feed_url keeps one row per unordered pair, and drops | |
| 109 | - -- the self-pair without a separate predicate. | |
| 110 | - JOIN articles.subscriptions s2 | |
| 111 | - ON s1.user_did = s2.user_did AND s1.feed_url < s2.feed_url | |
| 112 | - JOIN articles.feeds f1 ON f1.feed_url = s1.feed_url | |
| 113 | - JOIN articles.feeds f2 ON f2.feed_url = s2.feed_url | |
| 114 | - WHERE s1.added_at IS NOT NULL AND s2.added_at IS NOT NULL | |
| 115 | - GROUP BY s1.feed_url, s2.feed_url | |
| 116 | - ) WHERE jaccard IS NOT NULL""" | |
| 117 | - | |
| 118 | - g.db.transaction: | |
| 119 | - g.db.exec "DELETE FROM recs.feed_similarity" | |
| 120 | - g.db.exec """INSERT INTO recs.feed_similarity (feed_a, feed_b, jaccard) | |
| 121 | - SELECT feed_a, feed_b, jaccard FROM _feed_sim_staging""" | |
| 122 | - | |
| 123 | -proc computeUserSimilarity*(g: GleanDb) = | |
| 124 | - ## For each pair of people, how strongly their subscriptions overlap. | |
| 125 | - ## | |
| 126 | - ## Undecayed, unlike feed similarity: a subscription is a standing choice | |
| 127 | - ## rather than an event, so its age says little. Shared *likes* are decayed | |
| 128 | - ## separately below, because those are events. | |
| 129 | - g.db.transaction: | |
| 130 | - g.db.exec """CREATE TEMP TABLE IF NOT EXISTS _user_sim_staging ( | |
| 131 | - user_a TEXT NOT NULL, user_b TEXT NOT NULL, jaccard REAL NOT NULL, | |
| 132 | - common_feeds INT NOT NULL DEFAULT 0, | |
| 133 | - common_likes INT NOT NULL DEFAULT 0, | |
| 134 | - common_tags INT NOT NULL DEFAULT 0, | |
| 135 | - PRIMARY KEY (user_a, user_b))""" | |
| 136 | - g.db.exec "DELETE FROM _user_sim_staging" | |
| 137 | - g.db.exec """ | |
| 138 | - INSERT INTO _user_sim_staging | |
| 139 | - (user_a, user_b, jaccard, common_feeds, common_likes, common_tags) | |
| 140 | - SELECT | |
| 141 | - s1.user_did, s2.user_did, | |
| 142 | - CAST(COUNT(*) AS REAL) / ( | |
| 143 | - (SELECT COUNT(*) FROM articles.subscriptions WHERE user_did = s1.user_did) + | |
| 144 | - (SELECT COUNT(*) FROM articles.subscriptions WHERE user_did = s2.user_did) - | |
| 145 | - CAST(COUNT(*) AS REAL) | |
| 146 | - ), | |
| 147 | - COUNT(*), 0, 0 | |
| 148 | - FROM articles.subscriptions s1 | |
| 149 | - JOIN articles.subscriptions s2 | |
| 150 | - ON s1.feed_url = s2.feed_url AND s1.user_did < s2.user_did | |
| 151 | - GROUP BY s1.user_did, s2.user_did""" | |
| 152 | - | |
| 153 | - # Shared likes, decayed on both sides: two people who liked the same | |
| 154 | - # thing years apart have less in common than two who liked it the same | |
| 155 | - # week, and the product of the two decays expresses that. | |
| 156 | - # | |
| 157 | - # ROUND, not CAST AS INTEGER. The Go implementation truncates, and since | |
| 158 | - # each decay factor is at most 1 their product is always just under it -- | |
| 159 | - # so a pair sharing one recent like sums to 0.9999 and stores 0. That | |
| 160 | - # number is shown to readers as the reason for a recommendation ("N | |
| 161 | - # shared likes"), so truncation makes a real overlap read as none. | |
| 162 | - g.db.exec """CREATE TEMP TABLE IF NOT EXISTS _likes_overlap ( | |
| 163 | - user_a TEXT, user_b TEXT, common INT, PRIMARY KEY(user_a, user_b))""" | |
| 164 | - g.db.exec "DELETE FROM _likes_overlap" | |
| 165 | - g.db.exec """ | |
| 166 | - INSERT INTO _likes_overlap (user_a, user_b, common) | |
| 167 | - SELECT l1.author_did, l2.author_did, | |
| 168 | - CAST(ROUND(SUM( | |
| 169 | - EXP(-0.023 * CAST(julianday('now') - julianday(l1.created_at) AS REAL)) | |
| 170 | - * EXP(-0.023 * CAST(julianday('now') - julianday(l2.created_at) AS REAL)) | |
| 171 | - )) AS INTEGER) | |
| 172 | - FROM articles.likes l1 | |
| 173 | - JOIN articles.likes l2 | |
| 174 | - ON l1.feed_url = l2.feed_url AND l1.article_url = l2.article_url | |
| 175 | - AND l1.author_did < l2.author_did | |
| 176 | - WHERE l1.created_at IS NOT NULL AND l2.created_at IS NOT NULL | |
| 177 | - GROUP BY l1.author_did, l2.author_did""" | |
| 178 | - | |
| 179 | - # A pair that shares likes but no feeds has no staging row yet, so this | |
| 180 | - # inserts before it updates. | |
| 181 | - g.db.exec """ | |
| 182 | - INSERT INTO _user_sim_staging (user_a, user_b, jaccard, common_feeds, | |
| 183 | - common_likes, common_tags) | |
| 184 | - SELECT o.user_a, o.user_b, 0, 0, o.common, 0 | |
| 185 | - FROM _likes_overlap o | |
| 186 | - WHERE NOT EXISTS (SELECT 1 FROM _user_sim_staging s | |
| 187 | - WHERE s.user_a = o.user_a AND s.user_b = o.user_b)""" | |
| 188 | - g.db.exec """ | |
| 189 | - UPDATE _user_sim_staging SET common_likes = ( | |
| 190 | - SELECT o.common FROM _likes_overlap o | |
| 191 | - WHERE o.user_a = _user_sim_staging.user_a | |
| 192 | - AND o.user_b = _user_sim_staging.user_b) | |
| 193 | - WHERE EXISTS (SELECT 1 FROM _likes_overlap o | |
| 194 | - WHERE o.user_a = _user_sim_staging.user_a | |
| 195 | - AND o.user_b = _user_sim_staging.user_b)""" | |
| 196 | - | |
| 197 | - g.db.transaction: | |
| 198 | - g.db.exec "DELETE FROM recs.user_similarity" | |
| 199 | - g.db.exec """INSERT INTO recs.user_similarity | |
| 200 | - (user_a, user_b, jaccard, common_feeds, common_likes, common_tags) | |
| 201 | - SELECT user_a, user_b, jaccard, common_feeds, common_likes, | |
| 202 | - common_tags | |
| 203 | - FROM _user_sim_staging""" | |
| 204 | - | |
| 205 | -proc feedSimilarity*(g: GleanDb, feedA, feedB: string): float = | |
| 206 | - ## Stored pairs are ordered, so look up whichever way round the caller asks. | |
| 207 | - let (a, b) = if feedA < feedB: (feedA, feedB) else: (feedB, feedA) | |
| 208 | - let row = g.db.queryFirst( | |
| 209 | - "SELECT jaccard FROM recs.feed_similarity WHERE feed_a = ? AND feed_b = ?", | |
| 210 | - [p(a), p(b)], proc(r: Row): float = r.f(0)) | |
| 211 | - if row.isSome: row.get else: 0.0 | |
| 212 | - | |
| 213 | -proc userSimilarity*(g: GleanDb, userA, userB: string): float = | |
| 214 | - let (a, b) = if userA < userB: (userA, userB) else: (userB, userA) | |
| 215 | - let row = g.db.queryFirst( | |
| 216 | - "SELECT jaccard FROM recs.user_similarity WHERE user_a = ? AND user_b = ?", | |
| 217 | - [p(a), p(b)], proc(r: Row): float = r.f(0)) | |
| 218 | - if row.isSome: row.get else: 0.0 | |
| 219 | - | |
| 220 | -# --- scoring helpers ------------------------------------------------------- | |
| 221 | - | |
| 222 | -proc normalizeScores*(scores: var openArray[float]) = | |
| 223 | - ## Rescale to 0..1. | |
| 224 | - ## | |
| 225 | - ## Scores from different signals are not comparable in magnitude -- a | |
| 226 | - ## like-overlap sum and a log-popularity term live on unrelated scales -- | |
| 227 | - ## so they are normalised before being blended or ranked against each | |
| 228 | - ## other. A single item, or a set where every score is equal, is left | |
| 229 | - ## alone: there is no spread to stretch, and dividing by zero would turn | |
| 230 | - ## the whole list into NaN. | |
| 231 | - if scores.len < 2: return | |
| 232 | - var lo = scores[0] | |
| 233 | - var hi = scores[0] | |
| 234 | - for s in scores: | |
| 235 | - lo = min(lo, s) | |
| 236 | - hi = max(hi, s) | |
| 237 | - if hi == lo: return | |
| 238 | - let span = hi - lo | |
| 239 | - for i in 0 ..< scores.len: | |
| 240 | - scores[i] = (scores[i] - lo) / span | |
| 241 | - | |
| 242 | -proc normalizeFeedScores*(recs: var seq[FeedRecommendation]) = | |
| 243 | - var s = newSeq[float](recs.len) | |
| 244 | - for i, r in recs: s[i] = r.score | |
| 245 | - normalizeScores(s) | |
| 246 | - for i in 0 ..< recs.len: recs[i].score = s[i] | |
| 247 | - | |
| 248 | -proc normalizePersonScores*(recs: var seq[PersonRecommendation]) = | |
| 249 | - var s = newSeq[float](recs.len) | |
| 250 | - for i, r in recs: s[i] = r.score | |
| 251 | - normalizeScores(s) | |
| 252 | - for i in 0 ..< recs.len: recs[i].score = s[i] | |
| 253 | - | |
| 254 | -proc samplePeople*(pool: seq[PersonRecommendation], half: int, | |
| 255 | - rng: var Rand): seq[PersonRecommendation] = | |
| 256 | - ## Up to `half` from inside the reader's network and `half` from outside. | |
| 257 | - ## | |
| 258 | - ## The split is the point: ranking people purely by similarity returns the | |
| 259 | - ## reader's existing network, which they already have. Reserving half the | |
| 260 | - ## slots for strangers is what makes this discovery rather than a mirror. | |
| 261 | - ## | |
| 262 | - ## Shuffled rather than taken in order so the same faces do not appear | |
| 263 | - ## every time -- the pool changes far more slowly than the reader revisits. | |
| 264 | - var inNet, outNet: seq[PersonRecommendation] | |
| 265 | - for p in pool: | |
| 266 | - if p.isFollowed: inNet.add p else: outNet.add p | |
| 267 | - | |
| 268 | - rng.shuffle(inNet) | |
| 269 | - rng.shuffle(outNet) | |
| 270 | - if inNet.len > half: inNet.setLen(half) | |
| 271 | - if outNet.len > half: outNet.setLen(half) | |
| 272 | - inNet & outNet | |
| 273 | - | |
| 274 | -proc rankByScore*[T](items: var seq[T]) = | |
| 275 | - ## Highest first, which is what every recommendation listing wants. | |
| 276 | - items.sort(proc(a, b: T): int = cmp(b.score, a.score)) | |
| deleted file mode 100644 | |||
| @@ -1,276 +0,0 @@ | |||
| 1 | -## The similarity computations behind recommendations, ported from | ||
| 2 | -## internal/cluster/jaccard.go and the pure helpers of scoring.go. | ||
| 3 | -## | ||
| 4 | -## Two kinds of similarity are precomputed on a schedule, because both are | ||
| 5 | -## O(pairs) and neither needs to be fresh to the second: | ||
| 6 | -## | ||
| 7 | -## feed_similarity which feeds are subscribed to by the same people | ||
| 8 | -## user_similarity which people subscribe to the same feeds | ||
| 9 | -## | ||
| 10 | -## Both are time-decayed. An overlap from three years ago says much less | ||
| 11 | -## about what someone wants now than one from last week, and without decay a | ||
| 12 | -## long-lived account's early subscriptions dominate its recommendations | ||
| 13 | -## forever. | ||
| 14 | -## | ||
| 15 | -## The decay constant is `exp(-0.023 * days)`, a half-life of about a month | ||
| 16 | -## (ln 2 / 0.023 ≈ 30). It is written inline in the SQL rather than named, | ||
| 17 | -## the same way the Go implementation writes it. | ||
| 18 | - | ||
| 19 | -import std/[algorithm, math, options, random] | ||
| 20 | -import ./gleandb | ||
| 21 | -import ./sqlite | ||
| 22 | - | ||
| 23 | -type | ||
| 24 | - Config* = object | ||
| 25 | - followBoost*: float | ||
| 26 | - likesWeight*: float | ||
| 27 | - tagsWeight*: float | ||
| 28 | - descriptionWeight*: float | ||
| 29 | - | ||
| 30 | - SignalWeights* = object | ||
| 31 | - wSub*: float | ||
| 32 | - wLike*: float | ||
| 33 | - wTag*: float | ||
| 34 | - wSocial*: float | ||
| 35 | - wPop*: float | ||
| 36 | - wCategory*: float | ||
| 37 | - wContent*: float | ||
| 38 | - | ||
| 39 | - FeedRecommendation* = object | ||
| 40 | - feedUrl*: string | ||
| 41 | - title*: string | ||
| 42 | - siteUrl*: string | ||
| 43 | - description*: string | ||
| 44 | - subscriberCount*: int | ||
| 45 | - faviconUrl*: string | ||
| 46 | - score*: float | ||
| 47 | - | ||
| 48 | - PersonRecommendation* = object | ||
| 49 | - did*: string | ||
| 50 | - handle*: string | ||
| 51 | - displayName*: string | ||
| 52 | - avatarUrl*: string | ||
| 53 | - commonFeeds*: int | ||
| 54 | - commonLikes*: int | ||
| 55 | - commonTags*: int | ||
| 56 | - isFollowed*: bool | ||
| 57 | - score*: float | ||
| 58 | - | ||
| 59 | -const | ||
| 60 | - ## Half-life of roughly 30 days: ln(2) / 0.023. | ||
| 61 | - DecayPerDay* = 0.023 | ||
| 62 | - | ||
| 63 | -proc defaultConfig*(): Config = | ||
| 64 | - Config(followBoost: 0.5, likesWeight: 0.3, tagsWeight: 0.2, | ||
| 65 | - descriptionWeight: 0.15) | ||
| 66 | - | ||
| 67 | -proc defaultWeights*(): SignalWeights = | ||
| 68 | - SignalWeights(wSub: 1.0, wLike: 0.5, wTag: 0.3, wSocial: 0.7, | ||
| 69 | - wPop: 0.2, wCategory: 0.4, wContent: 0.4) | ||
| 70 | - | ||
| 71 | -proc decay*(ageDays: float): float = | ||
| 72 | - ## The weight an event of this age still carries. | ||
| 73 | - ## | ||
| 74 | - ## Exposed so the decay can be checked directly; the queries compute it in | ||
| 75 | - ## SQL because pulling every like into the application to weight it would | ||
| 76 | - ## defeat the point of precomputing. | ||
| 77 | - exp(-DecayPerDay * ageDays) | ||
| 78 | - | ||
| 79 | -# --- similarity ------------------------------------------------------------ | ||
| 80 | - | ||
| 81 | -## Both computations stage into a temp table and swap in a second | ||
| 82 | -## transaction. The first pass is the expensive one and holds no lock on the | ||
| 83 | -## live table; readers keep seeing the previous generation until the swap, | ||
| 84 | -## rather than an empty table for the length of the computation. | ||
| 85 | - | ||
| 86 | -proc computeFeedSimilarity*(g: GleanDb) = | ||
| 87 | - ## For each pair of feeds, how strongly their subscriber sets overlap. | ||
| 88 | - ## | ||
| 89 | - ## The numerator is a decayed count of shared subscribers and the | ||
| 90 | - ## denominator is the union of the two subscriber counts, which makes this | ||
| 91 | - ## a Jaccard index with recency folded into the intersection. | ||
| 92 | - g.db.transaction: | ||
| 93 | - g.db.exec """CREATE TEMP TABLE IF NOT EXISTS _feed_sim_staging ( | ||
| 94 | - feed_a TEXT NOT NULL, feed_b TEXT NOT NULL, jaccard REAL NOT NULL, | ||
| 95 | - PRIMARY KEY (feed_a, feed_b))""" | ||
| 96 | - g.db.exec "DELETE FROM _feed_sim_staging" | ||
| 97 | - g.db.exec """ | ||
| 98 | - INSERT INTO _feed_sim_staging (feed_a, feed_b, jaccard) | ||
| 99 | - SELECT feed_a, feed_b, jaccard FROM ( | ||
| 100 | - SELECT | ||
| 101 | - s1.feed_url AS feed_a, | ||
| 102 | - s2.feed_url AS feed_b, | ||
| 103 | - SUM(EXP(-0.023 * CAST(julianday('now') - | ||
| 104 | - julianday(MIN(s1.added_at, s2.added_at)) AS REAL))) | ||
| 105 | - / NULLIF(f1.subscriber_count + f2.subscriber_count | ||
| 106 | - - CAST(COUNT(*) AS REAL), 0) AS jaccard | ||
| 107 | - FROM articles.subscriptions s1 | ||
| 108 | - -- feed_url < feed_url keeps one row per unordered pair, and drops | ||
| 109 | - -- the self-pair without a separate predicate. | ||
| 110 | - JOIN articles.subscriptions s2 | ||
| 111 | - ON s1.user_did = s2.user_did AND s1.feed_url < s2.feed_url | ||
| 112 | - JOIN articles.feeds f1 ON f1.feed_url = s1.feed_url | ||
| 113 | - JOIN articles.feeds f2 ON f2.feed_url = s2.feed_url | ||
| 114 | - WHERE s1.added_at IS NOT NULL AND s2.added_at IS NOT NULL | ||
| 115 | - GROUP BY s1.feed_url, s2.feed_url | ||
| 116 | - ) WHERE jaccard IS NOT NULL""" | ||
| 117 | - | ||
| 118 | - g.db.transaction: | ||
| 119 | - g.db.exec "DELETE FROM recs.feed_similarity" | ||
| 120 | - g.db.exec """INSERT INTO recs.feed_similarity (feed_a, feed_b, jaccard) | ||
| 121 | - SELECT feed_a, feed_b, jaccard FROM _feed_sim_staging""" | ||
| 122 | - | ||
| 123 | -proc computeUserSimilarity*(g: GleanDb) = | ||
| 124 | - ## For each pair of people, how strongly their subscriptions overlap. | ||
| 125 | - ## | ||
| 126 | - ## Undecayed, unlike feed similarity: a subscription is a standing choice | ||
| 127 | - ## rather than an event, so its age says little. Shared *likes* are decayed | ||
| 128 | - ## separately below, because those are events. | ||
| 129 | - g.db.transaction: | ||
| 130 | - g.db.exec """CREATE TEMP TABLE IF NOT EXISTS _user_sim_staging ( | ||
| 131 | - user_a TEXT NOT NULL, user_b TEXT NOT NULL, jaccard REAL NOT NULL, | ||
| 132 | - common_feeds INT NOT NULL DEFAULT 0, | ||
| 133 | - common_likes INT NOT NULL DEFAULT 0, | ||
| 134 | - common_tags INT NOT NULL DEFAULT 0, | ||
| 135 | - PRIMARY KEY (user_a, user_b))""" | ||
| 136 | - g.db.exec "DELETE FROM _user_sim_staging" | ||
| 137 | - g.db.exec """ | ||
| 138 | - INSERT INTO _user_sim_staging | ||
| 139 | - (user_a, user_b, jaccard, common_feeds, common_likes, common_tags) | ||
| 140 | - SELECT | ||
| 141 | - s1.user_did, s2.user_did, | ||
| 142 | - CAST(COUNT(*) AS REAL) / ( | ||
| 143 | - (SELECT COUNT(*) FROM articles.subscriptions WHERE user_did = s1.user_did) + | ||
| 144 | - (SELECT COUNT(*) FROM articles.subscriptions WHERE user_did = s2.user_did) - | ||
| 145 | - CAST(COUNT(*) AS REAL) | ||
| 146 | - ), | ||
| 147 | - COUNT(*), 0, 0 | ||
| 148 | - FROM articles.subscriptions s1 | ||
| 149 | - JOIN articles.subscriptions s2 | ||
| 150 | - ON s1.feed_url = s2.feed_url AND s1.user_did < s2.user_did | ||
| 151 | - GROUP BY s1.user_did, s2.user_did""" | ||
| 152 | - | ||
| 153 | - # Shared likes, decayed on both sides: two people who liked the same | ||
| 154 | - # thing years apart have less in common than two who liked it the same | ||
| 155 | - # week, and the product of the two decays expresses that. | ||
| 156 | - # | ||
| 157 | - # ROUND, not CAST AS INTEGER. The Go implementation truncates, and since | ||
| 158 | - # each decay factor is at most 1 their product is always just under it -- | ||
| 159 | - # so a pair sharing one recent like sums to 0.9999 and stores 0. That | ||
| 160 | - # number is shown to readers as the reason for a recommendation ("N | ||
| 161 | - # shared likes"), so truncation makes a real overlap read as none. | ||
| 162 | - g.db.exec """CREATE TEMP TABLE IF NOT EXISTS _likes_overlap ( | ||
| 163 | - user_a TEXT, user_b TEXT, common INT, PRIMARY KEY(user_a, user_b))""" | ||
| 164 | - g.db.exec "DELETE FROM _likes_overlap" | ||
| 165 | - g.db.exec """ | ||
| 166 | - INSERT INTO _likes_overlap (user_a, user_b, common) | ||
| 167 | - SELECT l1.author_did, l2.author_did, | ||
| 168 | - CAST(ROUND(SUM( | ||
| 169 | - EXP(-0.023 * CAST(julianday('now') - julianday(l1.created_at) AS REAL)) | ||
| 170 | - * EXP(-0.023 * CAST(julianday('now') - julianday(l2.created_at) AS REAL)) | ||
| 171 | - )) AS INTEGER) | ||
| 172 | - FROM articles.likes l1 | ||
| 173 | - JOIN articles.likes l2 | ||
| 174 | - ON l1.feed_url = l2.feed_url AND l1.article_url = l2.article_url | ||
| 175 | - AND l1.author_did < l2.author_did | ||
| 176 | - WHERE l1.created_at IS NOT NULL AND l2.created_at IS NOT NULL | ||
| 177 | - GROUP BY l1.author_did, l2.author_did""" | ||
| 178 | - | ||
| 179 | - # A pair that shares likes but no feeds has no staging row yet, so this | ||
| 180 | - # inserts before it updates. | ||
| 181 | - g.db.exec """ | ||
| 182 | - INSERT INTO _user_sim_staging (user_a, user_b, jaccard, common_feeds, | ||
| 183 | - common_likes, common_tags) | ||
| 184 | - SELECT o.user_a, o.user_b, 0, 0, o.common, 0 | ||
| 185 | - FROM _likes_overlap o | ||
| 186 | - WHERE NOT EXISTS (SELECT 1 FROM _user_sim_staging s | ||
| 187 | - WHERE s.user_a = o.user_a AND s.user_b = o.user_b)""" | ||
| 188 | - g.db.exec """ | ||
| 189 | - UPDATE _user_sim_staging SET common_likes = ( | ||
| 190 | - SELECT o.common FROM _likes_overlap o | ||
| 191 | - WHERE o.user_a = _user_sim_staging.user_a | ||
| 192 | - AND o.user_b = _user_sim_staging.user_b) | ||
| 193 | - WHERE EXISTS (SELECT 1 FROM _likes_overlap o | ||
| 194 | - WHERE o.user_a = _user_sim_staging.user_a | ||
| 195 | - AND o.user_b = _user_sim_staging.user_b)""" | ||
| 196 | - | ||
| 197 | - g.db.transaction: | ||
| 198 | - g.db.exec "DELETE FROM recs.user_similarity" | ||
| 199 | - g.db.exec """INSERT INTO recs.user_similarity | ||
| 200 | - (user_a, user_b, jaccard, common_feeds, common_likes, common_tags) | ||
| 201 | - SELECT user_a, user_b, jaccard, common_feeds, common_likes, | ||
| 202 | - common_tags | ||
| 203 | - FROM _user_sim_staging""" | ||
| 204 | - | ||
| 205 | -proc feedSimilarity*(g: GleanDb, feedA, feedB: string): float = | ||
| 206 | - ## Stored pairs are ordered, so look up whichever way round the caller asks. | ||
| 207 | - let (a, b) = if feedA < feedB: (feedA, feedB) else: (feedB, feedA) | ||
| 208 | - let row = g.db.queryFirst( | ||
| 209 | - "SELECT jaccard FROM recs.feed_similarity WHERE feed_a = ? AND feed_b = ?", | ||
| 210 | - [p(a), p(b)], proc(r: Row): float = r.f(0)) | ||
| 211 | - if row.isSome: row.get else: 0.0 | ||
| 212 | - | ||
| 213 | -proc userSimilarity*(g: GleanDb, userA, userB: string): float = | ||
| 214 | - let (a, b) = if userA < userB: (userA, userB) else: (userB, userA) | ||
| 215 | - let row = g.db.queryFirst( | ||
| 216 | - "SELECT jaccard FROM recs.user_similarity WHERE user_a = ? AND user_b = ?", | ||
| 217 | - [p(a), p(b)], proc(r: Row): float = r.f(0)) | ||
| 218 | - if row.isSome: row.get else: 0.0 | ||
| 219 | - | ||
| 220 | -# --- scoring helpers ------------------------------------------------------- | ||
| 221 | - | ||
| 222 | -proc normalizeScores*(scores: var openArray[float]) = | ||
| 223 | - ## Rescale to 0..1. | ||
| 224 | - ## | ||
| 225 | - ## Scores from different signals are not comparable in magnitude -- a | ||
| 226 | - ## like-overlap sum and a log-popularity term live on unrelated scales -- | ||
| 227 | - ## so they are normalised before being blended or ranked against each | ||
| 228 | - ## other. A single item, or a set where every score is equal, is left | ||
| 229 | - ## alone: there is no spread to stretch, and dividing by zero would turn | ||
| 230 | - ## the whole list into NaN. | ||
| 231 | - if scores.len < 2: return | ||
| 232 | - var lo = scores[0] | ||
| 233 | - var hi = scores[0] | ||
| 234 | - for s in scores: | ||
| 235 | - lo = min(lo, s) | ||
| 236 | - hi = max(hi, s) | ||
| 237 | - if hi == lo: return | ||
| 238 | - let span = hi - lo | ||
| 239 | - for i in 0 ..< scores.len: | ||
| 240 | - scores[i] = (scores[i] - lo) / span | ||
| 241 | - | ||
| 242 | -proc normalizeFeedScores*(recs: var seq[FeedRecommendation]) = | ||
| 243 | - var s = newSeq[float](recs.len) | ||
| 244 | - for i, r in recs: s[i] = r.score | ||
| 245 | - normalizeScores(s) | ||
| 246 | - for i in 0 ..< recs.len: recs[i].score = s[i] | ||
| 247 | - | ||
| 248 | -proc normalizePersonScores*(recs: var seq[PersonRecommendation]) = | ||
| 249 | - var s = newSeq[float](recs.len) | ||
| 250 | - for i, r in recs: s[i] = r.score | ||
| 251 | - normalizeScores(s) | ||
| 252 | - for i in 0 ..< recs.len: recs[i].score = s[i] | ||
| 253 | - | ||
| 254 | -proc samplePeople*(pool: seq[PersonRecommendation], half: int, | ||
| 255 | - rng: var Rand): seq[PersonRecommendation] = | ||
| 256 | - ## Up to `half` from inside the reader's network and `half` from outside. | ||
| 257 | - ## | ||
| 258 | - ## The split is the point: ranking people purely by similarity returns the | ||
| 259 | - ## reader's existing network, which they already have. Reserving half the | ||
| 260 | - ## slots for strangers is what makes this discovery rather than a mirror. | ||
| 261 | - ## | ||
| 262 | - ## Shuffled rather than taken in order so the same faces do not appear | ||
| 263 | - ## every time -- the pool changes far more slowly than the reader revisits. | ||
| 264 | - var inNet, outNet: seq[PersonRecommendation] | ||
| 265 | - for p in pool: | ||
| 266 | - if p.isFollowed: inNet.add p else: outNet.add p | ||
| 267 | - | ||
| 268 | - rng.shuffle(inNet) | ||
| 269 | - rng.shuffle(outNet) | ||
| 270 | - if inNet.len > half: inNet.setLen(half) | ||
| 271 | - if outNet.len > half: outNet.setLen(half) | ||
| 272 | - inNet & outNet | ||
| 273 | - | ||
| 274 | -proc rankByScore*[T](items: var seq[T]) = | ||
| 275 | - ## Highest first, which is what every recommendation listing wants. | ||
| 276 | - items.sort(proc(a, b: T): int = cmp(b.score, a.score)) | ||
deleted
spike/nim/atproto/cursor_store.nim +0 -59 | deleted file mode 100644 | ||
| @@ -1,59 +0,0 @@ | ||
| 1 | -## A Jetstream cursor store backed by SQLite, with batched writes. | |
| 2 | -## | |
| 3 | -## Bridges `store` and `jetstream_run`. Separate from both so the supervisor | |
| 4 | -## does not drag a database dependency behind it, and so an application can | |
| 5 | -## substitute its own persistence. | |
| 6 | -## | |
| 7 | -## Writes are batched because the cursor advances on *every* event -- often | |
| 8 | -## hundreds a second -- and a synchronous write per event would make the | |
| 9 | -## database the bottleneck for a firehose. The cost is that the stored cursor | |
| 10 | -## trails the live one, which is exactly the gap the supervisor's rewind | |
| 11 | -## exists to cover. | |
| 12 | - | |
| 13 | -import std/times | |
| 14 | -import ./jetstream_run | |
| 15 | -import ./sqlite | |
| 16 | -import ./store | |
| 17 | - | |
| 18 | -type DbCursorStore* = ref object of CursorStore | |
| 19 | - db*: Db | |
| 20 | - flushEvery*: int64 ## seconds | |
| 21 | - lastFlush: int64 | |
| 22 | - pending: int64 | |
| 23 | - dirty: bool | |
| 24 | - | |
| 25 | -proc newDbCursorStore*(db: Db, flushEvery = 5'i64): DbCursorStore = | |
| 26 | - result = DbCursorStore(db: db, flushEvery: flushEvery, | |
| 27 | - lastFlush: getTime().toUnix) | |
| 28 | - result.cursor = loadCursor(db) | |
| 29 | - result.pending = result.cursor | |
| 30 | - | |
| 31 | -method load*(s: DbCursorStore): int64 {.gcsafe, raises: [].} = | |
| 32 | - s.cursor | |
| 33 | - | |
| 34 | -proc flushNow*(s: DbCursorStore) = | |
| 35 | - ## Write the newest cursor out. Safe to call at any time; a failure is | |
| 36 | - ## swallowed, because losing a cursor update costs a replay and raising | |
| 37 | - ## here would cost the subscription. | |
| 38 | - if not s.dirty: return | |
| 39 | - try: | |
| 40 | - saveCursor(s.db, s.pending) | |
| 41 | - s.cursor = s.pending | |
| 42 | - s.dirty = false | |
| 43 | - s.lastFlush = getTime().toUnix | |
| 44 | - except CatchableError: | |
| 45 | - discard | |
| 46 | - | |
| 47 | -method save*(s: DbCursorStore, cursor: int64) {.gcsafe, raises: [].} = | |
| 48 | - ## Called per event. Records in memory and writes at most once per | |
| 49 | - ## flushEvery seconds. | |
| 50 | - if cursor <= s.pending: return | |
| 51 | - s.pending = cursor | |
| 52 | - s.dirty = true | |
| 53 | - let now = try: getTime().toUnix except CatchableError: return | |
| 54 | - if now - s.lastFlush >= s.flushEvery: | |
| 55 | - s.flushNow() | |
| 56 | - | |
| 57 | -proc close*(s: DbCursorStore) = | |
| 58 | - ## Final flush, so a clean shutdown does not throw away the last batch. | |
| 59 | - s.flushNow() | |
| deleted file mode 100644 | |||
| @@ -1,59 +0,0 @@ | |||
| 1 | -## A Jetstream cursor store backed by SQLite, with batched writes. | ||
| 2 | -## | ||
| 3 | -## Bridges `store` and `jetstream_run`. Separate from both so the supervisor | ||
| 4 | -## does not drag a database dependency behind it, and so an application can | ||
| 5 | -## substitute its own persistence. | ||
| 6 | -## | ||
| 7 | -## Writes are batched because the cursor advances on *every* event -- often | ||
| 8 | -## hundreds a second -- and a synchronous write per event would make the | ||
| 9 | -## database the bottleneck for a firehose. The cost is that the stored cursor | ||
| 10 | -## trails the live one, which is exactly the gap the supervisor's rewind | ||
| 11 | -## exists to cover. | ||
| 12 | - | ||
| 13 | -import std/times | ||
| 14 | -import ./jetstream_run | ||
| 15 | -import ./sqlite | ||
| 16 | -import ./store | ||
| 17 | - | ||
| 18 | -type DbCursorStore* = ref object of CursorStore | ||
| 19 | - db*: Db | ||
| 20 | - flushEvery*: int64 ## seconds | ||
| 21 | - lastFlush: int64 | ||
| 22 | - pending: int64 | ||
| 23 | - dirty: bool | ||
| 24 | - | ||
| 25 | -proc newDbCursorStore*(db: Db, flushEvery = 5'i64): DbCursorStore = | ||
| 26 | - result = DbCursorStore(db: db, flushEvery: flushEvery, | ||
| 27 | - lastFlush: getTime().toUnix) | ||
| 28 | - result.cursor = loadCursor(db) | ||
| 29 | - result.pending = result.cursor | ||
| 30 | - | ||
| 31 | -method load*(s: DbCursorStore): int64 {.gcsafe, raises: [].} = | ||
| 32 | - s.cursor | ||
| 33 | - | ||
| 34 | -proc flushNow*(s: DbCursorStore) = | ||
| 35 | - ## Write the newest cursor out. Safe to call at any time; a failure is | ||
| 36 | - ## swallowed, because losing a cursor update costs a replay and raising | ||
| 37 | - ## here would cost the subscription. | ||
| 38 | - if not s.dirty: return | ||
| 39 | - try: | ||
| 40 | - saveCursor(s.db, s.pending) | ||
| 41 | - s.cursor = s.pending | ||
| 42 | - s.dirty = false | ||
| 43 | - s.lastFlush = getTime().toUnix | ||
| 44 | - except CatchableError: | ||
| 45 | - discard | ||
| 46 | - | ||
| 47 | -method save*(s: DbCursorStore, cursor: int64) {.gcsafe, raises: [].} = | ||
| 48 | - ## Called per event. Records in memory and writes at most once per | ||
| 49 | - ## flushEvery seconds. | ||
| 50 | - if cursor <= s.pending: return | ||
| 51 | - s.pending = cursor | ||
| 52 | - s.dirty = true | ||
| 53 | - let now = try: getTime().toUnix except CatchableError: return | ||
| 54 | - if now - s.lastFlush >= s.flushEvery: | ||
| 55 | - s.flushNow() | ||
| 56 | - | ||
| 57 | -proc close*(s: DbCursorStore) = | ||
| 58 | - ## Final flush, so a clean shutdown does not throw away the last batch. | ||
| 59 | - s.flushNow() | ||
deleted
spike/nim/atproto/dpop.nim +0 -153 | deleted file mode 100644 | ||
| @@ -1,153 +0,0 @@ | ||
| 1 | -## ES256 keys and DPoP proofs (RFC 9449) for ATProto OAuth. | |
| 2 | -## | |
| 3 | -## What has to be exactly right: | |
| 4 | -## * ES256 signatures in JOSE form -- raw R||S, 64 bytes, NOT the DER that | |
| 5 | -## most crypto libraries hand you. | |
| 6 | -## * An RFC 7638 JWK thumbprint, which is a SHA-256 over canonical JSON: | |
| 7 | -## exactly the members crv/kty/x/y, lexicographically ordered, no spaces. | |
| 8 | -## * base64url without padding, everywhere. | |
| 9 | -## | |
| 10 | -## bearssl is used because br_ecdsa_sign_raw already emits R||S, so there is no | |
| 11 | -## DER unwrapping step to get wrong. dpop_probe.nim cross-checks the output | |
| 12 | -## against a Go verifier. | |
| 13 | - | |
| 14 | -import std/[base64, json, strutils, times, strformat] | |
| 15 | -import bearssl/[ec, rand] | |
| 16 | -import bearssl/abi/[bearssl_ec, bearssl_rand, bearssl_hash] | |
| 17 | -import nimcrypto/[sha2, hash] | |
| 18 | - | |
| 19 | -type P256Key* = object | |
| 20 | - priv*: array[32, byte] | |
| 21 | - pubX*: array[32, byte] | |
| 22 | - pubY*: array[32, byte] | |
| 23 | - | |
| 24 | -proc toSeq(a: openArray[byte]): seq[byte] = | |
| 25 | - result = newSeq[byte](a.len) | |
| 26 | - for i, b in a: result[i] = b | |
| 27 | - | |
| 28 | -proc b64u*(data: openArray[byte]): string = | |
| 29 | - ## base64url, unpadded (RFC 7515 §2). | |
| 30 | - base64.encode(data, safe = true).strip(chars = {'='}) | |
| 31 | - | |
| 32 | -proc b64u*(s: string): string = | |
| 33 | - b64u(s.toOpenArrayByte(0, s.high).toSeq) | |
| 34 | - | |
| 35 | -proc generateKey*(rng: var HmacDrbgContext): P256Key = | |
| 36 | - ## P-256 keypair. bearssl returns the public point uncompressed: | |
| 37 | - ## 0x04 || X(32) || Y(32). | |
| 38 | - var | |
| 39 | - skBuf: array[EC_KBUF_PRIV_MAX_SIZE, byte] | |
| 40 | - pkBuf: array[EC_KBUF_PUB_MAX_SIZE, byte] | |
| 41 | - sk: EcPrivateKey | |
| 42 | - pk: EcPublicKey | |
| 43 | - | |
| 44 | - let n = ecKeygen(PrngClassPointerConst(addr rng.vtable), addr ecPrimeI31, addr sk, | |
| 45 | - addr skBuf[0], EC_secp256r1.cint) | |
| 46 | - doAssert n > 0, "ecKeygen failed" | |
| 47 | - let m = ecComputePub(addr ecPrimeI31, addr pk, addr pkBuf[0], addr sk) | |
| 48 | - doAssert m > 0, "ecComputePub failed" | |
| 49 | - | |
| 50 | - doAssert sk.xlen == 32, &"unexpected private key length {sk.xlen}" | |
| 51 | - doAssert pk.qlen == 65 and cast[ptr UncheckedArray[byte]](pk.q)[0] == 0x04'u8, | |
| 52 | - "expected an uncompressed public point" | |
| 53 | - | |
| 54 | - let | |
| 55 | - skBytes = cast[ptr UncheckedArray[byte]](sk.x) | |
| 56 | - pkBytes = cast[ptr UncheckedArray[byte]](pk.q) | |
| 57 | - for i in 0 ..< 32: | |
| 58 | - result.priv[i] = skBytes[i] | |
| 59 | - result.pubX[i] = pkBytes[1 + i] | |
| 60 | - result.pubY[i] = pkBytes[33 + i] | |
| 61 | - | |
| 62 | -proc publicJwk*(k: P256Key): JsonNode = | |
| 63 | - %*{"crv": "P-256", "kty": "EC", "x": b64u(k.pubX), "y": b64u(k.pubY)} | |
| 64 | - | |
| 65 | -proc thumbprint*(k: P256Key): string = | |
| 66 | - ## RFC 7638: SHA-256 over the canonical JWK. The member set and ordering are | |
| 67 | - ## fixed by the spec, so this is built by hand rather than serialised from a | |
| 68 | - ## JsonNode whose key order is incidental. | |
| 69 | - let canonical = &"""{{"crv":"P-256","kty":"EC","x":"{b64u(k.pubX)}","y":"{b64u(k.pubY)}"}}""" | |
| 70 | - b64u(sha256.digest(canonical).data) | |
| 71 | - | |
| 72 | -proc signEs256*(k: P256Key, rng: var HmacDrbgContext, signingInput: string): seq[byte] = | |
| 73 | - var | |
| 74 | - sk: EcPrivateKey | |
| 75 | - priv = k.priv | |
| 76 | - sk.curve = EC_secp256r1.cint | |
| 77 | - sk.x = cast[ptr byte](addr priv[0]) | |
| 78 | - sk.xlen = 32 | |
| 79 | - | |
| 80 | - let digest = sha256.digest(signingInput).data | |
| 81 | - var sig: array[64, byte] | |
| 82 | - let n = ecdsaSignRawGetDefault()( | |
| 83 | - addr ecPrimeI31, addr sha256Vtable, unsafeAddr digest[0], addr sk, addr sig[0]) | |
| 84 | - doAssert n == 64, &"expected a 64-byte raw signature, got {n}" | |
| 85 | - result = sig.toSeq | |
| 86 | - | |
| 87 | -proc b64uDecode*(s: string): string = | |
| 88 | - ## Inverse of b64u: restore the padding base64url drops, then decode. | |
| 89 | - var t = s.replace('-', '+').replace('_', '/') | |
| 90 | - while t.len mod 4 != 0: t.add '=' | |
| 91 | - base64.decode(t) | |
| 92 | - | |
| 93 | -proc accessTokenHash*(accessToken: string): string = | |
| 94 | - ## The `ath` claim: base64url(SHA-256(access token)). | |
| 95 | - ## | |
| 96 | - ## This is what binds a proof to one specific token. A resource-server proof | |
| 97 | - ## without it is rejected, and a proof carrying the wrong one lets a stolen | |
| 98 | - ## token be replayed with a fresh proof -- which is the whole thing DPoP | |
| 99 | - ## exists to prevent. | |
| 100 | - b64u(sha256.digest(accessToken).data) | |
| 101 | - | |
| 102 | -proc verifyEs256*(k: P256Key, signingInput: string, | |
| 103 | - sig: openArray[byte]): bool = | |
| 104 | - ## Verify a raw R||S signature against this key's public half. | |
| 105 | - ## | |
| 106 | - ## Mostly useful for proving a key survived a round trip: ECDSA is | |
| 107 | - ## randomised, so a reloaded key cannot be checked by re-signing and | |
| 108 | - ## comparing bytes. Signing with the reloaded key and verifying against the | |
| 109 | - ## original public point does establish the private scalar came back intact. | |
| 110 | - if sig.len != 64: return false | |
| 111 | - var q: array[65, byte] | |
| 112 | - q[0] = 0x04 | |
| 113 | - for i in 0 ..< 32: | |
| 114 | - q[1 + i] = k.pubX[i] | |
| 115 | - q[33 + i] = k.pubY[i] | |
| 116 | - | |
| 117 | - var pk: EcPublicKey | |
| 118 | - pk.curve = EC_secp256r1.cint | |
| 119 | - pk.q = cast[ptr byte](addr q[0]) | |
| 120 | - pk.qlen = 65 | |
| 121 | - | |
| 122 | - let digest = sha256.digest(signingInput).data | |
| 123 | - ecdsaVrfyRawGetDefault()( | |
| 124 | - addr ecPrimeI31, unsafeAddr digest[0], digest.len.uint, | |
| 125 | - addr pk, unsafeAddr sig[0], sig.len.uint) == 1 | |
| 126 | - | |
| 127 | -proc dpopProof*(k: P256Key, rng: var HmacDrbgContext, htm, htu: string, | |
| 128 | - nonce = "", accessToken = "", issuer = ""): string = | |
| 129 | - ## A DPoP proof JWT (RFC 9449). | |
| 130 | - ## | |
| 131 | - ## Auth-server and resource-server proofs differ, even when the same host | |
| 132 | - ## fills both roles: a request to a PDS carries `ath` (bound to the access | |
| 133 | - ## token) and `iss` (the auth server that issued it), while the PAR and | |
| 134 | - ## token requests that mint that token carry neither. | |
| 135 | - let header = %*{"typ": "dpop+jwt", "alg": "ES256", "jwk": k.publicJwk} | |
| 136 | - let issued = now().utc.toTime.toUnix | |
| 137 | - var payload = %*{ | |
| 138 | - "jti": b64u(sha256.digest(&"{htm}{htu}{epochTime()}").data)[0 ..< 16], | |
| 139 | - "htm": htm, | |
| 140 | - "htu": htu, | |
| 141 | - "iat": issued, | |
| 142 | - "exp": issued + 300, | |
| 143 | - } | |
| 144 | - if nonce.len > 0: | |
| 145 | - payload["nonce"] = %nonce | |
| 146 | - if accessToken.len > 0: | |
| 147 | - payload["ath"] = %accessTokenHash(accessToken) | |
| 148 | - if issuer.len > 0: | |
| 149 | - payload["iss"] = %issuer | |
| 150 | - | |
| 151 | - let signingInput = b64u($header) & "." & b64u($payload) | |
| 152 | - signingInput & "." & b64u(k.signEs256(rng, signingInput)) | |
| 153 | - | |
| deleted file mode 100644 | |||
| @@ -1,153 +0,0 @@ | |||
| 1 | -## ES256 keys and DPoP proofs (RFC 9449) for ATProto OAuth. | ||
| 2 | -## | ||
| 3 | -## What has to be exactly right: | ||
| 4 | -## * ES256 signatures in JOSE form -- raw R||S, 64 bytes, NOT the DER that | ||
| 5 | -## most crypto libraries hand you. | ||
| 6 | -## * An RFC 7638 JWK thumbprint, which is a SHA-256 over canonical JSON: | ||
| 7 | -## exactly the members crv/kty/x/y, lexicographically ordered, no spaces. | ||
| 8 | -## * base64url without padding, everywhere. | ||
| 9 | -## | ||
| 10 | -## bearssl is used because br_ecdsa_sign_raw already emits R||S, so there is no | ||
| 11 | -## DER unwrapping step to get wrong. dpop_probe.nim cross-checks the output | ||
| 12 | -## against a Go verifier. | ||
| 13 | - | ||
| 14 | -import std/[base64, json, strutils, times, strformat] | ||
| 15 | -import bearssl/[ec, rand] | ||
| 16 | -import bearssl/abi/[bearssl_ec, bearssl_rand, bearssl_hash] | ||
| 17 | -import nimcrypto/[sha2, hash] | ||
| 18 | - | ||
| 19 | -type P256Key* = object | ||
| 20 | - priv*: array[32, byte] | ||
| 21 | - pubX*: array[32, byte] | ||
| 22 | - pubY*: array[32, byte] | ||
| 23 | - | ||
| 24 | -proc toSeq(a: openArray[byte]): seq[byte] = | ||
| 25 | - result = newSeq[byte](a.len) | ||
| 26 | - for i, b in a: result[i] = b | ||
| 27 | - | ||
| 28 | -proc b64u*(data: openArray[byte]): string = | ||
| 29 | - ## base64url, unpadded (RFC 7515 §2). | ||
| 30 | - base64.encode(data, safe = true).strip(chars = {'='}) | ||
| 31 | - | ||
| 32 | -proc b64u*(s: string): string = | ||
| 33 | - b64u(s.toOpenArrayByte(0, s.high).toSeq) | ||
| 34 | - | ||
| 35 | -proc generateKey*(rng: var HmacDrbgContext): P256Key = | ||
| 36 | - ## P-256 keypair. bearssl returns the public point uncompressed: | ||
| 37 | - ## 0x04 || X(32) || Y(32). | ||
| 38 | - var | ||
| 39 | - skBuf: array[EC_KBUF_PRIV_MAX_SIZE, byte] | ||
| 40 | - pkBuf: array[EC_KBUF_PUB_MAX_SIZE, byte] | ||
| 41 | - sk: EcPrivateKey | ||
| 42 | - pk: EcPublicKey | ||
| 43 | - | ||
| 44 | - let n = ecKeygen(PrngClassPointerConst(addr rng.vtable), addr ecPrimeI31, addr sk, | ||
| 45 | - addr skBuf[0], EC_secp256r1.cint) | ||
| 46 | - doAssert n > 0, "ecKeygen failed" | ||
| 47 | - let m = ecComputePub(addr ecPrimeI31, addr pk, addr pkBuf[0], addr sk) | ||
| 48 | - doAssert m > 0, "ecComputePub failed" | ||
| 49 | - | ||
| 50 | - doAssert sk.xlen == 32, &"unexpected private key length {sk.xlen}" | ||
| 51 | - doAssert pk.qlen == 65 and cast[ptr UncheckedArray[byte]](pk.q)[0] == 0x04'u8, | ||
| 52 | - "expected an uncompressed public point" | ||
| 53 | - | ||
| 54 | - let | ||
| 55 | - skBytes = cast[ptr UncheckedArray[byte]](sk.x) | ||
| 56 | - pkBytes = cast[ptr UncheckedArray[byte]](pk.q) | ||
| 57 | - for i in 0 ..< 32: | ||
| 58 | - result.priv[i] = skBytes[i] | ||
| 59 | - result.pubX[i] = pkBytes[1 + i] | ||
| 60 | - result.pubY[i] = pkBytes[33 + i] | ||
| 61 | - | ||
| 62 | -proc publicJwk*(k: P256Key): JsonNode = | ||
| 63 | - %*{"crv": "P-256", "kty": "EC", "x": b64u(k.pubX), "y": b64u(k.pubY)} | ||
| 64 | - | ||
| 65 | -proc thumbprint*(k: P256Key): string = | ||
| 66 | - ## RFC 7638: SHA-256 over the canonical JWK. The member set and ordering are | ||
| 67 | - ## fixed by the spec, so this is built by hand rather than serialised from a | ||
| 68 | - ## JsonNode whose key order is incidental. | ||
| 69 | - let canonical = &"""{{"crv":"P-256","kty":"EC","x":"{b64u(k.pubX)}","y":"{b64u(k.pubY)}"}}""" | ||
| 70 | - b64u(sha256.digest(canonical).data) | ||
| 71 | - | ||
| 72 | -proc signEs256*(k: P256Key, rng: var HmacDrbgContext, signingInput: string): seq[byte] = | ||
| 73 | - var | ||
| 74 | - sk: EcPrivateKey | ||
| 75 | - priv = k.priv | ||
| 76 | - sk.curve = EC_secp256r1.cint | ||
| 77 | - sk.x = cast[ptr byte](addr priv[0]) | ||
| 78 | - sk.xlen = 32 | ||
| 79 | - | ||
| 80 | - let digest = sha256.digest(signingInput).data | ||
| 81 | - var sig: array[64, byte] | ||
| 82 | - let n = ecdsaSignRawGetDefault()( | ||
| 83 | - addr ecPrimeI31, addr sha256Vtable, unsafeAddr digest[0], addr sk, addr sig[0]) | ||
| 84 | - doAssert n == 64, &"expected a 64-byte raw signature, got {n}" | ||
| 85 | - result = sig.toSeq | ||
| 86 | - | ||
| 87 | -proc b64uDecode*(s: string): string = | ||
| 88 | - ## Inverse of b64u: restore the padding base64url drops, then decode. | ||
| 89 | - var t = s.replace('-', '+').replace('_', '/') | ||
| 90 | - while t.len mod 4 != 0: t.add '=' | ||
| 91 | - base64.decode(t) | ||
| 92 | - | ||
| 93 | -proc accessTokenHash*(accessToken: string): string = | ||
| 94 | - ## The `ath` claim: base64url(SHA-256(access token)). | ||
| 95 | - ## | ||
| 96 | - ## This is what binds a proof to one specific token. A resource-server proof | ||
| 97 | - ## without it is rejected, and a proof carrying the wrong one lets a stolen | ||
| 98 | - ## token be replayed with a fresh proof -- which is the whole thing DPoP | ||
| 99 | - ## exists to prevent. | ||
| 100 | - b64u(sha256.digest(accessToken).data) | ||
| 101 | - | ||
| 102 | -proc verifyEs256*(k: P256Key, signingInput: string, | ||
| 103 | - sig: openArray[byte]): bool = | ||
| 104 | - ## Verify a raw R||S signature against this key's public half. | ||
| 105 | - ## | ||
| 106 | - ## Mostly useful for proving a key survived a round trip: ECDSA is | ||
| 107 | - ## randomised, so a reloaded key cannot be checked by re-signing and | ||
| 108 | - ## comparing bytes. Signing with the reloaded key and verifying against the | ||
| 109 | - ## original public point does establish the private scalar came back intact. | ||
| 110 | - if sig.len != 64: return false | ||
| 111 | - var q: array[65, byte] | ||
| 112 | - q[0] = 0x04 | ||
| 113 | - for i in 0 ..< 32: | ||
| 114 | - q[1 + i] = k.pubX[i] | ||
| 115 | - q[33 + i] = k.pubY[i] | ||
| 116 | - | ||
| 117 | - var pk: EcPublicKey | ||
| 118 | - pk.curve = EC_secp256r1.cint | ||
| 119 | - pk.q = cast[ptr byte](addr q[0]) | ||
| 120 | - pk.qlen = 65 | ||
| 121 | - | ||
| 122 | - let digest = sha256.digest(signingInput).data | ||
| 123 | - ecdsaVrfyRawGetDefault()( | ||
| 124 | - addr ecPrimeI31, unsafeAddr digest[0], digest.len.uint, | ||
| 125 | - addr pk, unsafeAddr sig[0], sig.len.uint) == 1 | ||
| 126 | - | ||
| 127 | -proc dpopProof*(k: P256Key, rng: var HmacDrbgContext, htm, htu: string, | ||
| 128 | - nonce = "", accessToken = "", issuer = ""): string = | ||
| 129 | - ## A DPoP proof JWT (RFC 9449). | ||
| 130 | - ## | ||
| 131 | - ## Auth-server and resource-server proofs differ, even when the same host | ||
| 132 | - ## fills both roles: a request to a PDS carries `ath` (bound to the access | ||
| 133 | - ## token) and `iss` (the auth server that issued it), while the PAR and | ||
| 134 | - ## token requests that mint that token carry neither. | ||
| 135 | - let header = %*{"typ": "dpop+jwt", "alg": "ES256", "jwk": k.publicJwk} | ||
| 136 | - let issued = now().utc.toTime.toUnix | ||
| 137 | - var payload = %*{ | ||
| 138 | - "jti": b64u(sha256.digest(&"{htm}{htu}{epochTime()}").data)[0 ..< 16], | ||
| 139 | - "htm": htm, | ||
| 140 | - "htu": htu, | ||
| 141 | - "iat": issued, | ||
| 142 | - "exp": issued + 300, | ||
| 143 | - } | ||
| 144 | - if nonce.len > 0: | ||
| 145 | - payload["nonce"] = %nonce | ||
| 146 | - if accessToken.len > 0: | ||
| 147 | - payload["ath"] = %accessTokenHash(accessToken) | ||
| 148 | - if issuer.len > 0: | ||
| 149 | - payload["iss"] = %issuer | ||
| 150 | - | ||
| 151 | - let signingInput = b64u($header) & "." & b64u($payload) | ||
| 152 | - signingInput & "." & b64u(k.signEs256(rng, signingInput)) | ||
| 153 | - | ||
deleted
spike/nim/atproto/gleandb.nim +0 -150 | deleted file mode 100644 | ||
| @@ -1,150 +0,0 @@ | ||
| 1 | -## Glean's database: three SQLite files behind one connection. | |
| 2 | -## | |
| 3 | -## `main` holds users and auth, `articles` the feeds and their contents, and | |
| 4 | -## `recs` the derived recommendation tables. They are separate files attached | |
| 5 | -## together, so a query can join across them while each still checkpoints and | |
| 6 | -## vacuums on its own -- and, more practically, so the derived tables can be | |
| 7 | -## deleted and rebuilt without touching anything a user wrote. | |
| 8 | -## | |
| 9 | -## The pragmas are copied from internal/db/db.go. They are not decoration: | |
| 10 | -## WAL is what lets readers run during a write, and the per-database repeats | |
| 11 | -## matter because PRAGMA applies to one schema at a time -- setting WAL on | |
| 12 | -## `main` leaves `articles` journalling the slow way. | |
| 13 | - | |
| 14 | -import std/[math, options, strformat, strutils] | |
| 15 | -import ./schema | |
| 16 | -import ./sqlite | |
| 17 | - | |
| 18 | -type | |
| 19 | - GleanDb* = object | |
| 20 | - db*: Db | |
| 21 | - basePath*: string | |
| 22 | - | |
| 23 | -{.push importc, cdecl.} | |
| 24 | -proc sqlite3_create_function_v2(db: pointer, name: cstring, nArg: cint, | |
| 25 | - eTextRep: cint, pApp: pointer, | |
| 26 | - xFunc, xStep, xFinal, xDestroy: pointer): cint | |
| 27 | -proc sqlite3_value_double(v: pointer): cdouble | |
| 28 | -proc sqlite3_result_double(ctx: pointer, v: cdouble) | |
| 29 | -proc sqlite3_result_null(ctx: pointer) | |
| 30 | -{.pop.} | |
| 31 | - | |
| 32 | -const | |
| 33 | - SQLITE_UTF8 = 1.cint | |
| 34 | - SQLITE_DETERMINISTIC = 0x000000800.cint | |
| 35 | - | |
| 36 | -# SQLite ships maths functions only when built with | |
| 37 | -# SQLITE_ENABLE_MATH_FUNCTIONS, which the library here is not, so these are | |
| 38 | -# registered exactly as the Go connection registers them. They are not | |
| 39 | -# optional: the clustering SQL calls EXP and LOG in seven places for time | |
| 40 | -# decay and popularity normalisation, and without them every recommendation | |
| 41 | -# query fails at runtime rather than at startup. | |
| 42 | -proc sqlExp(ctx: pointer, n: cint, args: ptr UncheckedArray[pointer]) | |
| 43 | - {.cdecl.} = | |
| 44 | - if n != 1: | |
| 45 | - sqlite3_result_null(ctx) | |
| 46 | - return | |
| 47 | - sqlite3_result_double(ctx, exp(sqlite3_value_double(args[0])).cdouble) | |
| 48 | - | |
| 49 | -proc sqlLog(ctx: pointer, n: cint, args: ptr UncheckedArray[pointer]) | |
| 50 | - {.cdecl.} = | |
| 51 | - if n != 1: | |
| 52 | - sqlite3_result_null(ctx) | |
| 53 | - return | |
| 54 | - let v = sqlite3_value_double(args[0]).float | |
| 55 | - # log(0) is -inf and log(negative) is NaN; SQL callers want neither, and a | |
| 56 | - # NULL propagates through an ORDER BY far more gracefully. | |
| 57 | - if v <= 0: | |
| 58 | - sqlite3_result_null(ctx) | |
| 59 | - else: | |
| 60 | - sqlite3_result_double(ctx, ln(v).cdouble) | |
| 61 | - | |
| 62 | -proc registerFunctions*(db: Db) = | |
| 63 | - for (name, fn) in [("exp", sqlExp), ("log", sqlLog)]: | |
| 64 | - let rc = sqlite3_create_function_v2( | |
| 65 | - db.handle, name.cstring, 1, | |
| 66 | - SQLITE_UTF8 or SQLITE_DETERMINISTIC, | |
| 67 | - nil, cast[pointer](fn), nil, nil, nil) | |
| 68 | - if rc != SQLITE_OK: | |
| 69 | - raise newException(SqliteError, "registering SQL function " & name) | |
| 70 | - | |
| 71 | -const | |
| 72 | - MainPragmas = [ | |
| 73 | - "journal_mode=WAL", | |
| 74 | - "wal_autocheckpoint=1000", | |
| 75 | - "busy_timeout=30000", | |
| 76 | - "synchronous=NORMAL", | |
| 77 | - "temp_store=FILE", | |
| 78 | - "mmap_size=268435456", | |
| 79 | - "auto_vacuum=INCREMENTAL", | |
| 80 | - ] | |
| 81 | - AttachedPragmas = [ | |
| 82 | - "journal_mode=WAL", | |
| 83 | - "wal_autocheckpoint=1000", | |
| 84 | - "busy_timeout=30000", | |
| 85 | - "synchronous=NORMAL", | |
| 86 | - "temp_store=FILE", | |
| 87 | - "mmap_size=268435456", | |
| 88 | - ] | |
| 89 | - | |
| 90 | -proc applyPragmas(db: Db, schemaName: string, pragmas: openArray[string]) = | |
| 91 | - for pragma in pragmas: | |
| 92 | - # A pragma that will not take is not worth failing a boot over -- some are | |
| 93 | - # advisory, and mmap_size in particular is refused on filesystems that | |
| 94 | - # cannot map. The ones that matter are asserted after opening instead. | |
| 95 | - try: | |
| 96 | - if schemaName.len == 0: | |
| 97 | - db.exec &"PRAGMA {pragma}" | |
| 98 | - else: | |
| 99 | - db.exec &"PRAGMA {schemaName}.{pragma}" | |
| 100 | - except SqliteError: | |
| 101 | - discard | |
| 102 | - | |
| 103 | -proc open*(basePath: string): GleanDb = | |
| 104 | - ## Open `<basePath>_users` and attach `_articles` and `_recs`. | |
| 105 | - ## | |
| 106 | - ## The suffixes match the Go layout, so a database written by either side is | |
| 107 | - ## readable by the other. | |
| 108 | - let | |
| 109 | - usersPath = basePath & "_users" | |
| 110 | - articlesPath = basePath & "_articles" | |
| 111 | - recsPath = basePath & "_recs" | |
| 112 | - | |
| 113 | - var db = sqlite.open(usersPath) | |
| 114 | - registerFunctions(db) | |
| 115 | - applyPragmas(db, "", MainPragmas) | |
| 116 | - | |
| 117 | - # Quoted and single-quote-escaped: a path is not necessarily tame, and this | |
| 118 | - # one can come from configuration. | |
| 119 | - for (name, path) in [("articles", articlesPath), ("recs", recsPath)]: | |
| 120 | - db.exec &"ATTACH DATABASE '{path.replace(\"'\", \"''\")}' AS {name}" | |
| 121 | - applyPragmas(db, name, AttachedPragmas) | |
| 122 | - | |
| 123 | - GleanDb(db: db, basePath: basePath) | |
| 124 | - | |
| 125 | -proc migrate*(g: GleanDb) = | |
| 126 | - ## Apply the schema. Every statement is IF NOT EXISTS, so this runs on every | |
| 127 | - ## boot rather than being gated on a version. | |
| 128 | - for stmt in usersSchema: g.db.exec stmt | |
| 129 | - for stmt in articlesSchema: g.db.exec stmt | |
| 130 | - for stmt in recsSchema: g.db.exec stmt | |
| 131 | - | |
| 132 | -proc createEmbeddingTables*(g: GleanDb, dimension: int) = | |
| 133 | - ## The vec0 tables carry their dimension in the DDL, so they cannot live in | |
| 134 | - ## the static schema -- it comes from GLEAN_EMBED_DIMENSION. | |
| 135 | - g.db.exec &"""CREATE VIRTUAL TABLE IF NOT EXISTS recs.feed_embeddings | |
| 136 | - USING vec0(feed_url TEXT PRIMARY KEY, embedding float[{dimension}])""" | |
| 137 | - g.db.exec &"""CREATE VIRTUAL TABLE IF NOT EXISTS recs.article_embeddings | |
| 138 | - USING vec0(article_id INTEGER PRIMARY KEY, embedding float[{dimension}])""" | |
| 139 | - | |
| 140 | -proc close*(g: var GleanDb) = | |
| 141 | - g.db.close() | |
| 142 | - | |
| 143 | -proc journalMode*(g: GleanDb, schemaName = ""): string = | |
| 144 | - let sql = if schemaName.len == 0: "PRAGMA journal_mode" | |
| 145 | - else: &"PRAGMA {schemaName}.journal_mode" | |
| 146 | - g.db.queryText(sql).get("") | |
| 147 | - | |
| 148 | -proc attachedSchemas*(g: GleanDb): seq[string] = | |
| 149 | - for row in g.db.rows("PRAGMA database_list"): | |
| 150 | - result.add row[1] | |
| deleted file mode 100644 | |||
| @@ -1,150 +0,0 @@ | |||
| 1 | -## Glean's database: three SQLite files behind one connection. | ||
| 2 | -## | ||
| 3 | -## `main` holds users and auth, `articles` the feeds and their contents, and | ||
| 4 | -## `recs` the derived recommendation tables. They are separate files attached | ||
| 5 | -## together, so a query can join across them while each still checkpoints and | ||
| 6 | -## vacuums on its own -- and, more practically, so the derived tables can be | ||
| 7 | -## deleted and rebuilt without touching anything a user wrote. | ||
| 8 | -## | ||
| 9 | -## The pragmas are copied from internal/db/db.go. They are not decoration: | ||
| 10 | -## WAL is what lets readers run during a write, and the per-database repeats | ||
| 11 | -## matter because PRAGMA applies to one schema at a time -- setting WAL on | ||
| 12 | -## `main` leaves `articles` journalling the slow way. | ||
| 13 | - | ||
| 14 | -import std/[math, options, strformat, strutils] | ||
| 15 | -import ./schema | ||
| 16 | -import ./sqlite | ||
| 17 | - | ||
| 18 | -type | ||
| 19 | - GleanDb* = object | ||
| 20 | - db*: Db | ||
| 21 | - basePath*: string | ||
| 22 | - | ||
| 23 | -{.push importc, cdecl.} | ||
| 24 | -proc sqlite3_create_function_v2(db: pointer, name: cstring, nArg: cint, | ||
| 25 | - eTextRep: cint, pApp: pointer, | ||
| 26 | - xFunc, xStep, xFinal, xDestroy: pointer): cint | ||
| 27 | -proc sqlite3_value_double(v: pointer): cdouble | ||
| 28 | -proc sqlite3_result_double(ctx: pointer, v: cdouble) | ||
| 29 | -proc sqlite3_result_null(ctx: pointer) | ||
| 30 | -{.pop.} | ||
| 31 | - | ||
| 32 | -const | ||
| 33 | - SQLITE_UTF8 = 1.cint | ||
| 34 | - SQLITE_DETERMINISTIC = 0x000000800.cint | ||
| 35 | - | ||
| 36 | -# SQLite ships maths functions only when built with | ||
| 37 | -# SQLITE_ENABLE_MATH_FUNCTIONS, which the library here is not, so these are | ||
| 38 | -# registered exactly as the Go connection registers them. They are not | ||
| 39 | -# optional: the clustering SQL calls EXP and LOG in seven places for time | ||
| 40 | -# decay and popularity normalisation, and without them every recommendation | ||
| 41 | -# query fails at runtime rather than at startup. | ||
| 42 | -proc sqlExp(ctx: pointer, n: cint, args: ptr UncheckedArray[pointer]) | ||
| 43 | - {.cdecl.} = | ||
| 44 | - if n != 1: | ||
| 45 | - sqlite3_result_null(ctx) | ||
| 46 | - return | ||
| 47 | - sqlite3_result_double(ctx, exp(sqlite3_value_double(args[0])).cdouble) | ||
| 48 | - | ||
| 49 | -proc sqlLog(ctx: pointer, n: cint, args: ptr UncheckedArray[pointer]) | ||
| 50 | - {.cdecl.} = | ||
| 51 | - if n != 1: | ||
| 52 | - sqlite3_result_null(ctx) | ||
| 53 | - return | ||
| 54 | - let v = sqlite3_value_double(args[0]).float | ||
| 55 | - # log(0) is -inf and log(negative) is NaN; SQL callers want neither, and a | ||
| 56 | - # NULL propagates through an ORDER BY far more gracefully. | ||
| 57 | - if v <= 0: | ||
| 58 | - sqlite3_result_null(ctx) | ||
| 59 | - else: | ||
| 60 | - sqlite3_result_double(ctx, ln(v).cdouble) | ||
| 61 | - | ||
| 62 | -proc registerFunctions*(db: Db) = | ||
| 63 | - for (name, fn) in [("exp", sqlExp), ("log", sqlLog)]: | ||
| 64 | - let rc = sqlite3_create_function_v2( | ||
| 65 | - db.handle, name.cstring, 1, | ||
| 66 | - SQLITE_UTF8 or SQLITE_DETERMINISTIC, | ||
| 67 | - nil, cast[pointer](fn), nil, nil, nil) | ||
| 68 | - if rc != SQLITE_OK: | ||
| 69 | - raise newException(SqliteError, "registering SQL function " & name) | ||
| 70 | - | ||
| 71 | -const | ||
| 72 | - MainPragmas = [ | ||
| 73 | - "journal_mode=WAL", | ||
| 74 | - "wal_autocheckpoint=1000", | ||
| 75 | - "busy_timeout=30000", | ||
| 76 | - "synchronous=NORMAL", | ||
| 77 | - "temp_store=FILE", | ||
| 78 | - "mmap_size=268435456", | ||
| 79 | - "auto_vacuum=INCREMENTAL", | ||
| 80 | - ] | ||
| 81 | - AttachedPragmas = [ | ||
| 82 | - "journal_mode=WAL", | ||
| 83 | - "wal_autocheckpoint=1000", | ||
| 84 | - "busy_timeout=30000", | ||
| 85 | - "synchronous=NORMAL", | ||
| 86 | - "temp_store=FILE", | ||
| 87 | - "mmap_size=268435456", | ||
| 88 | - ] | ||
| 89 | - | ||
| 90 | -proc applyPragmas(db: Db, schemaName: string, pragmas: openArray[string]) = | ||
| 91 | - for pragma in pragmas: | ||
| 92 | - # A pragma that will not take is not worth failing a boot over -- some are | ||
| 93 | - # advisory, and mmap_size in particular is refused on filesystems that | ||
| 94 | - # cannot map. The ones that matter are asserted after opening instead. | ||
| 95 | - try: | ||
| 96 | - if schemaName.len == 0: | ||
| 97 | - db.exec &"PRAGMA {pragma}" | ||
| 98 | - else: | ||
| 99 | - db.exec &"PRAGMA {schemaName}.{pragma}" | ||
| 100 | - except SqliteError: | ||
| 101 | - discard | ||
| 102 | - | ||
| 103 | -proc open*(basePath: string): GleanDb = | ||
| 104 | - ## Open `<basePath>_users` and attach `_articles` and `_recs`. | ||
| 105 | - ## | ||
| 106 | - ## The suffixes match the Go layout, so a database written by either side is | ||
| 107 | - ## readable by the other. | ||
| 108 | - let | ||
| 109 | - usersPath = basePath & "_users" | ||
| 110 | - articlesPath = basePath & "_articles" | ||
| 111 | - recsPath = basePath & "_recs" | ||
| 112 | - | ||
| 113 | - var db = sqlite.open(usersPath) | ||
| 114 | - registerFunctions(db) | ||
| 115 | - applyPragmas(db, "", MainPragmas) | ||
| 116 | - | ||
| 117 | - # Quoted and single-quote-escaped: a path is not necessarily tame, and this | ||
| 118 | - # one can come from configuration. | ||
| 119 | - for (name, path) in [("articles", articlesPath), ("recs", recsPath)]: | ||
| 120 | - db.exec &"ATTACH DATABASE '{path.replace(\"'\", \"''\")}' AS {name}" | ||
| 121 | - applyPragmas(db, name, AttachedPragmas) | ||
| 122 | - | ||
| 123 | - GleanDb(db: db, basePath: basePath) | ||
| 124 | - | ||
| 125 | -proc migrate*(g: GleanDb) = | ||
| 126 | - ## Apply the schema. Every statement is IF NOT EXISTS, so this runs on every | ||
| 127 | - ## boot rather than being gated on a version. | ||
| 128 | - for stmt in usersSchema: g.db.exec stmt | ||
| 129 | - for stmt in articlesSchema: g.db.exec stmt | ||
| 130 | - for stmt in recsSchema: g.db.exec stmt | ||
| 131 | - | ||
| 132 | -proc createEmbeddingTables*(g: GleanDb, dimension: int) = | ||
| 133 | - ## The vec0 tables carry their dimension in the DDL, so they cannot live in | ||
| 134 | - ## the static schema -- it comes from GLEAN_EMBED_DIMENSION. | ||
| 135 | - g.db.exec &"""CREATE VIRTUAL TABLE IF NOT EXISTS recs.feed_embeddings | ||
| 136 | - USING vec0(feed_url TEXT PRIMARY KEY, embedding float[{dimension}])""" | ||
| 137 | - g.db.exec &"""CREATE VIRTUAL TABLE IF NOT EXISTS recs.article_embeddings | ||
| 138 | - USING vec0(article_id INTEGER PRIMARY KEY, embedding float[{dimension}])""" | ||
| 139 | - | ||
| 140 | -proc close*(g: var GleanDb) = | ||
| 141 | - g.db.close() | ||
| 142 | - | ||
| 143 | -proc journalMode*(g: GleanDb, schemaName = ""): string = | ||
| 144 | - let sql = if schemaName.len == 0: "PRAGMA journal_mode" | ||
| 145 | - else: &"PRAGMA {schemaName}.journal_mode" | ||
| 146 | - g.db.queryText(sql).get("") | ||
| 147 | - | ||
| 148 | -proc attachedSchemas*(g: GleanDb): seq[string] = | ||
| 149 | - for row in g.db.rows("PRAGMA database_list"): | ||
| 150 | - result.add row[1] | ||
deleted
spike/nim/atproto/identity.nim +0 -149 | deleted file mode 100644 | ||
| @@ -1,149 +0,0 @@ | ||
| 1 | -## Handle and DID resolution: the chain that turns "alice.bsky.social" into | |
| 2 | -## the PDS host an OAuth flow has to talk to. | |
| 3 | -## | |
| 4 | -## The steps, matching what indigo's identity directory does: | |
| 5 | -## handle -> DID via DNS TXT _atproto.<handle>, else | |
| 6 | -## https://<handle>/.well-known/atproto-did | |
| 7 | -## DID -> doc via the PLC directory for did:plc, or | |
| 8 | -## https://<host>/.well-known/did.json for did:web | |
| 9 | -## doc -> PDS the service entry with id "#atproto_pds" | |
| 10 | - | |
| 11 | -import std/[httpclient, json, options, os, osproc, strutils, uri] | |
| 12 | - | |
| 13 | -const | |
| 14 | - DefaultPlcUrl* = "https://plc.directory" | |
| 15 | - UserAgent* = "glean-nim/0.1" | |
| 16 | - HttpTimeoutMs = 10_000 | |
| 17 | - | |
| 18 | -type | |
| 19 | - IdentityError* = object of CatchableError | |
| 20 | - | |
| 21 | - Identity* = object | |
| 22 | - did*: string | |
| 23 | - handle*: string | |
| 24 | - pdsEndpoint*: string | |
| 25 | - | |
| 26 | -proc newClient(): HttpClient = | |
| 27 | - newHttpClient(userAgent = UserAgent, timeout = HttpTimeoutMs) | |
| 28 | - | |
| 29 | -proc isValidHandle*(handle: string): bool = | |
| 30 | - ## Deliberately loose: a dotted, non-empty ASCII domain. The authoritative | |
| 31 | - ## check is whether it resolves. | |
| 32 | - if handle.len == 0 or handle.len > 253 or '.' notin handle: | |
| 33 | - return false | |
| 34 | - for label in handle.split('.'): | |
| 35 | - if label.len == 0: return false | |
| 36 | - for ch in label: | |
| 37 | - if ch notin {'a'..'z', 'A'..'Z', '0'..'9', '-'}: return false | |
| 38 | - true | |
| 39 | - | |
| 40 | -proc isDid*(s: string): bool = | |
| 41 | - s.startsWith("did:plc:") or s.startsWith("did:web:") | |
| 42 | - | |
| 43 | -proc resolveHandleDns(handle: string): Option[string] = | |
| 44 | - ## _atproto.<handle> TXT record containing "did=did:plc:...". | |
| 45 | - ## | |
| 46 | - ## Nim has no resolver in the stdlib, so this shells out to `dig`. A real | |
| 47 | - ## port should bind a resolver rather than depend on a binary being present; | |
| 48 | - ## a missing `dig` is treated as "no DNS answer" so the HTTP fallback runs. | |
| 49 | - let dig = findExe("dig") | |
| 50 | - if dig.len == 0: | |
| 51 | - return none(string) | |
| 52 | - let (output, code) = execCmdEx(dig & " +short +time=3 +tries=1 TXT _atproto." & | |
| 53 | - quoteShell(handle)) | |
| 54 | - if code != 0: | |
| 55 | - return none(string) | |
| 56 | - for rawLine in output.splitLines: | |
| 57 | - let line = rawLine.strip(chars = {'"', ' ', '\t'}) | |
| 58 | - if line.startsWith("did="): | |
| 59 | - let did = line[4 .. ^1].strip(chars = {'"'}) | |
| 60 | - if did.isDid: | |
| 61 | - return some(did) | |
| 62 | - none(string) | |
| 63 | - | |
| 64 | -proc resolveHandleHttp(handle: string): Option[string] = | |
| 65 | - let client = newClient() | |
| 66 | - defer: client.close() | |
| 67 | - try: | |
| 68 | - let res = client.get("https://" & handle & "/.well-known/atproto-did") | |
| 69 | - if res.code.int != 200: | |
| 70 | - return none(string) | |
| 71 | - let did = res.body.strip() | |
| 72 | - if did.isDid: some(did) else: none(string) | |
| 73 | - except CatchableError: | |
| 74 | - none(string) | |
| 75 | - | |
| 76 | -proc resolveHandle*(handle: string): string = | |
| 77 | - ## DNS first, then the well-known endpoint, as the spec prefers. | |
| 78 | - if not handle.isValidHandle: | |
| 79 | - raise newException(IdentityError, "not a valid handle: " & handle) | |
| 80 | - let viaDns = resolveHandleDns(handle) | |
| 81 | - if viaDns.isSome: | |
| 82 | - return viaDns.get | |
| 83 | - let viaHttp = resolveHandleHttp(handle) | |
| 84 | - if viaHttp.isSome: | |
| 85 | - return viaHttp.get | |
| 86 | - raise newException(IdentityError, "could not resolve handle: " & handle) | |
| 87 | - | |
| 88 | -proc resolveDidDoc*(did: string, plcUrl = DefaultPlcUrl): JsonNode = | |
| 89 | - if not did.isDid: | |
| 90 | - raise newException(IdentityError, "unsupported DID method: " & did) | |
| 91 | - | |
| 92 | - let url = | |
| 93 | - if did.startsWith("did:plc:"): | |
| 94 | - plcUrl & "/" & did | |
| 95 | - else: | |
| 96 | - # did:web:example.com -> https://example.com/.well-known/did.json | |
| 97 | - let host = did["did:web:".len .. ^1].replace("%3A", ":") | |
| 98 | - "https://" & host & "/.well-known/did.json" | |
| 99 | - | |
| 100 | - let client = newClient() | |
| 101 | - defer: client.close() | |
| 102 | - let res = client.get(url) | |
| 103 | - if res.code.int != 200: | |
| 104 | - raise newException(IdentityError, | |
| 105 | - "DID document lookup failed (HTTP " & $res.code.int & "): " & did) | |
| 106 | - try: | |
| 107 | - result = parseJson(res.body) | |
| 108 | - except JsonParsingError: | |
| 109 | - raise newException(IdentityError, "DID document is not valid JSON: " & did) | |
| 110 | - | |
| 111 | -proc pdsEndpoint*(doc: JsonNode): string = | |
| 112 | - ## The service entry whose id ends in "#atproto_pds". | |
| 113 | - if doc.kind != JObject or "service" notin doc: | |
| 114 | - return "" | |
| 115 | - for svc in doc["service"]: | |
| 116 | - let id = svc{"id"}.getStr | |
| 117 | - if id.endsWith("#atproto_pds"): | |
| 118 | - return svc{"serviceEndpoint"}.getStr.strip(chars = {'/'}) | |
| 119 | - "" | |
| 120 | - | |
| 121 | -proc handleFromDoc(doc: JsonNode): string = | |
| 122 | - for aka in doc{"alsoKnownAs"}: | |
| 123 | - let v = aka.getStr | |
| 124 | - if v.startsWith("at://"): | |
| 125 | - return v["at://".len .. ^1] | |
| 126 | - "" | |
| 127 | - | |
| 128 | -proc lookup*(identifier: string, plcUrl = DefaultPlcUrl): Identity = | |
| 129 | - ## Resolve a handle or DID all the way to a PDS endpoint. | |
| 130 | - let did = if identifier.isDid: identifier else: resolveHandle(identifier) | |
| 131 | - let doc = resolveDidDoc(did, plcUrl) | |
| 132 | - result = Identity( | |
| 133 | - did: did, | |
| 134 | - handle: if identifier.isDid: handleFromDoc(doc) else: identifier, | |
| 135 | - pdsEndpoint: pdsEndpoint(doc), | |
| 136 | - ) | |
| 137 | - if result.pdsEndpoint.len == 0: | |
| 138 | - raise newException(IdentityError, "identity has no atproto PDS: " & did) | |
| 139 | - | |
| 140 | -proc hostOf*(url: string): string = | |
| 141 | - ## Host (with port, if given) from a URL. | |
| 142 | - ## | |
| 143 | - ## parseUri treats a scheme-less string as a path, so "example.com" would | |
| 144 | - ## otherwise yield an empty host and build URLs like "https:///xrpc/...". | |
| 145 | - ## Callers legitimately pass bare hostnames, so assume https when no scheme | |
| 146 | - ## is present. | |
| 147 | - let withScheme = if "://" in url: url else: "https://" & url | |
| 148 | - let u = parseUri(withScheme) | |
| 149 | - if u.port.len > 0: u.hostname & ":" & u.port else: u.hostname | |
| deleted file mode 100644 | |||
| @@ -1,149 +0,0 @@ | |||
| 1 | -## Handle and DID resolution: the chain that turns "alice.bsky.social" into | ||
| 2 | -## the PDS host an OAuth flow has to talk to. | ||
| 3 | -## | ||
| 4 | -## The steps, matching what indigo's identity directory does: | ||
| 5 | -## handle -> DID via DNS TXT _atproto.<handle>, else | ||
| 6 | -## https://<handle>/.well-known/atproto-did | ||
| 7 | -## DID -> doc via the PLC directory for did:plc, or | ||
| 8 | -## https://<host>/.well-known/did.json for did:web | ||
| 9 | -## doc -> PDS the service entry with id "#atproto_pds" | ||
| 10 | - | ||
| 11 | -import std/[httpclient, json, options, os, osproc, strutils, uri] | ||
| 12 | - | ||
| 13 | -const | ||
| 14 | - DefaultPlcUrl* = "https://plc.directory" | ||
| 15 | - UserAgent* = "glean-nim/0.1" | ||
| 16 | - HttpTimeoutMs = 10_000 | ||
| 17 | - | ||
| 18 | -type | ||
| 19 | - IdentityError* = object of CatchableError | ||
| 20 | - | ||
| 21 | - Identity* = object | ||
| 22 | - did*: string | ||
| 23 | - handle*: string | ||
| 24 | - pdsEndpoint*: string | ||
| 25 | - | ||
| 26 | -proc newClient(): HttpClient = | ||
| 27 | - newHttpClient(userAgent = UserAgent, timeout = HttpTimeoutMs) | ||
| 28 | - | ||
| 29 | -proc isValidHandle*(handle: string): bool = | ||
| 30 | - ## Deliberately loose: a dotted, non-empty ASCII domain. The authoritative | ||
| 31 | - ## check is whether it resolves. | ||
| 32 | - if handle.len == 0 or handle.len > 253 or '.' notin handle: | ||
| 33 | - return false | ||
| 34 | - for label in handle.split('.'): | ||
| 35 | - if label.len == 0: return false | ||
| 36 | - for ch in label: | ||
| 37 | - if ch notin {'a'..'z', 'A'..'Z', '0'..'9', '-'}: return false | ||
| 38 | - true | ||
| 39 | - | ||
| 40 | -proc isDid*(s: string): bool = | ||
| 41 | - s.startsWith("did:plc:") or s.startsWith("did:web:") | ||
| 42 | - | ||
| 43 | -proc resolveHandleDns(handle: string): Option[string] = | ||
| 44 | - ## _atproto.<handle> TXT record containing "did=did:plc:...". | ||
| 45 | - ## | ||
| 46 | - ## Nim has no resolver in the stdlib, so this shells out to `dig`. A real | ||
| 47 | - ## port should bind a resolver rather than depend on a binary being present; | ||
| 48 | - ## a missing `dig` is treated as "no DNS answer" so the HTTP fallback runs. | ||
| 49 | - let dig = findExe("dig") | ||
| 50 | - if dig.len == 0: | ||
| 51 | - return none(string) | ||
| 52 | - let (output, code) = execCmdEx(dig & " +short +time=3 +tries=1 TXT _atproto." & | ||
| 53 | - quoteShell(handle)) | ||
| 54 | - if code != 0: | ||
| 55 | - return none(string) | ||
| 56 | - for rawLine in output.splitLines: | ||
| 57 | - let line = rawLine.strip(chars = {'"', ' ', '\t'}) | ||
| 58 | - if line.startsWith("did="): | ||
| 59 | - let did = line[4 .. ^1].strip(chars = {'"'}) | ||
| 60 | - if did.isDid: | ||
| 61 | - return some(did) | ||
| 62 | - none(string) | ||
| 63 | - | ||
| 64 | -proc resolveHandleHttp(handle: string): Option[string] = | ||
| 65 | - let client = newClient() | ||
| 66 | - defer: client.close() | ||
| 67 | - try: | ||
| 68 | - let res = client.get("https://" & handle & "/.well-known/atproto-did") | ||
| 69 | - if res.code.int != 200: | ||
| 70 | - return none(string) | ||
| 71 | - let did = res.body.strip() | ||
| 72 | - if did.isDid: some(did) else: none(string) | ||
| 73 | - except CatchableError: | ||
| 74 | - none(string) | ||
| 75 | - | ||
| 76 | -proc resolveHandle*(handle: string): string = | ||
| 77 | - ## DNS first, then the well-known endpoint, as the spec prefers. | ||
| 78 | - if not handle.isValidHandle: | ||
| 79 | - raise newException(IdentityError, "not a valid handle: " & handle) | ||
| 80 | - let viaDns = resolveHandleDns(handle) | ||
| 81 | - if viaDns.isSome: | ||
| 82 | - return viaDns.get | ||
| 83 | - let viaHttp = resolveHandleHttp(handle) | ||
| 84 | - if viaHttp.isSome: | ||
| 85 | - return viaHttp.get | ||
| 86 | - raise newException(IdentityError, "could not resolve handle: " & handle) | ||
| 87 | - | ||
| 88 | -proc resolveDidDoc*(did: string, plcUrl = DefaultPlcUrl): JsonNode = | ||
| 89 | - if not did.isDid: | ||
| 90 | - raise newException(IdentityError, "unsupported DID method: " & did) | ||
| 91 | - | ||
| 92 | - let url = | ||
| 93 | - if did.startsWith("did:plc:"): | ||
| 94 | - plcUrl & "/" & did | ||
| 95 | - else: | ||
| 96 | - # did:web:example.com -> https://example.com/.well-known/did.json | ||
| 97 | - let host = did["did:web:".len .. ^1].replace("%3A", ":") | ||
| 98 | - "https://" & host & "/.well-known/did.json" | ||
| 99 | - | ||
| 100 | - let client = newClient() | ||
| 101 | - defer: client.close() | ||
| 102 | - let res = client.get(url) | ||
| 103 | - if res.code.int != 200: | ||
| 104 | - raise newException(IdentityError, | ||
| 105 | - "DID document lookup failed (HTTP " & $res.code.int & "): " & did) | ||
| 106 | - try: | ||
| 107 | - result = parseJson(res.body) | ||
| 108 | - except JsonParsingError: | ||
| 109 | - raise newException(IdentityError, "DID document is not valid JSON: " & did) | ||
| 110 | - | ||
| 111 | -proc pdsEndpoint*(doc: JsonNode): string = | ||
| 112 | - ## The service entry whose id ends in "#atproto_pds". | ||
| 113 | - if doc.kind != JObject or "service" notin doc: | ||
| 114 | - return "" | ||
| 115 | - for svc in doc["service"]: | ||
| 116 | - let id = svc{"id"}.getStr | ||
| 117 | - if id.endsWith("#atproto_pds"): | ||
| 118 | - return svc{"serviceEndpoint"}.getStr.strip(chars = {'/'}) | ||
| 119 | - "" | ||
| 120 | - | ||
| 121 | -proc handleFromDoc(doc: JsonNode): string = | ||
| 122 | - for aka in doc{"alsoKnownAs"}: | ||
| 123 | - let v = aka.getStr | ||
| 124 | - if v.startsWith("at://"): | ||
| 125 | - return v["at://".len .. ^1] | ||
| 126 | - "" | ||
| 127 | - | ||
| 128 | -proc lookup*(identifier: string, plcUrl = DefaultPlcUrl): Identity = | ||
| 129 | - ## Resolve a handle or DID all the way to a PDS endpoint. | ||
| 130 | - let did = if identifier.isDid: identifier else: resolveHandle(identifier) | ||
| 131 | - let doc = resolveDidDoc(did, plcUrl) | ||
| 132 | - result = Identity( | ||
| 133 | - did: did, | ||
| 134 | - handle: if identifier.isDid: handleFromDoc(doc) else: identifier, | ||
| 135 | - pdsEndpoint: pdsEndpoint(doc), | ||
| 136 | - ) | ||
| 137 | - if result.pdsEndpoint.len == 0: | ||
| 138 | - raise newException(IdentityError, "identity has no atproto PDS: " & did) | ||
| 139 | - | ||
| 140 | -proc hostOf*(url: string): string = | ||
| 141 | - ## Host (with port, if given) from a URL. | ||
| 142 | - ## | ||
| 143 | - ## parseUri treats a scheme-less string as a path, so "example.com" would | ||
| 144 | - ## otherwise yield an empty host and build URLs like "https:///xrpc/...". | ||
| 145 | - ## Callers legitimately pass bare hostnames, so assume https when no scheme | ||
| 146 | - ## is present. | ||
| 147 | - let withScheme = if "://" in url: url else: "https://" & url | ||
| 148 | - let u = parseUri(withScheme) | ||
| 149 | - if u.port.len > 0: u.hostname & ":" & u.port else: u.hostname | ||
deleted
spike/nim/atproto/jetstream.nim +0 -184 | deleted file mode 100644 | ||
| @@ -1,184 +0,0 @@ | ||
| 1 | -## Jetstream consumer: the ATProto firehose as JSON over a websocket. | |
| 2 | -## | |
| 3 | -## Jetstream is a public, unauthenticated feed of repository commits, so this | |
| 4 | -## is the one part of the port that can be verified end to end without a | |
| 5 | -## consented session. | |
| 6 | -## | |
| 7 | -## Filtering happens server-side via query parameters (wantedCollections, | |
| 8 | -## wantedDids), which matters: the unfiltered firehose is the entire network. | |
| 9 | -## A subscription that silently loses its filters still delivers events, so a | |
| 10 | -## consumer that looks healthy can be reading the wrong stream entirely -- | |
| 11 | -## see the note on websock's Uri overload in `connect`. | |
| 12 | - | |
| 13 | -import std/[json, strutils, uri] | |
| 14 | -import pkg/chronicles | |
| 15 | -import pkg/chronos | |
| 16 | -import pkg/websock/websock | |
| 17 | - | |
| 18 | -type | |
| 19 | - JetstreamError* = object of CatchableError | |
| 20 | - | |
| 21 | - EventKind* = enum | |
| 22 | - ekCommit = "commit" | |
| 23 | - ekIdentity = "identity" | |
| 24 | - ekAccount = "account" | |
| 25 | - ekUnknown = "unknown" | |
| 26 | - | |
| 27 | - CommitOperation* = enum | |
| 28 | - coCreate = "create" | |
| 29 | - coUpdate = "update" | |
| 30 | - coDelete = "delete" | |
| 31 | - | |
| 32 | - Commit* = object | |
| 33 | - rev*: string | |
| 34 | - operation*: CommitOperation | |
| 35 | - collection*: string | |
| 36 | - rkey*: string | |
| 37 | - cid*: string | |
| 38 | - record*: JsonNode ## nil for a delete | |
| 39 | - | |
| 40 | - Event* = object | |
| 41 | - did*: string | |
| 42 | - timeUs*: int64 ## microseconds; also the replay cursor | |
| 43 | - kind*: EventKind | |
| 44 | - commit*: Commit ## meaningful only when kind is ekCommit | |
| 45 | - raw*: JsonNode | |
| 46 | - | |
| 47 | - JetstreamConfig* = object | |
| 48 | - url*: string | |
| 49 | - wantedCollections*: seq[string] | |
| 50 | - wantedDids*: seq[string] | |
| 51 | - cursor*: int64 ## 0 to start live | |
| 52 | - userAgent*: string | |
| 53 | - | |
| 54 | - EventHandler* = proc(ev: Event) {.gcsafe, raises: [].} | |
| 55 | - | |
| 56 | -const | |
| 57 | - DefaultJetstream* = "wss://jetstream1.us-east.bsky.network" | |
| 58 | - DefaultUserAgent* = "glean-nim/0.1" | |
| 59 | - | |
| 60 | -proc bytesToString(bs: openArray[byte]): string = | |
| 61 | - ## websock hands back seq[byte]; Jetstream frames are UTF-8 JSON text. | |
| 62 | - result = newString(bs.len) | |
| 63 | - if bs.len > 0: | |
| 64 | - copyMem(result[0].addr, bs[0].unsafeAddr, bs.len) | |
| 65 | - | |
| 66 | -proc parseEventKind(s: string): EventKind = | |
| 67 | - case s | |
| 68 | - of "commit": ekCommit | |
| 69 | - of "identity": ekIdentity | |
| 70 | - of "account": ekAccount | |
| 71 | - else: ekUnknown | |
| 72 | - | |
| 73 | -proc parseOperation(s: string): CommitOperation = | |
| 74 | - case s | |
| 75 | - of "create": coCreate | |
| 76 | - of "update": coUpdate | |
| 77 | - of "delete": coDelete | |
| 78 | - else: coCreate | |
| 79 | - | |
| 80 | -proc parseEvent*(raw: string): Event = | |
| 81 | - ## Decode one Jetstream frame. | |
| 82 | - let j = | |
| 83 | - try: parseJson(raw) | |
| 84 | - except CatchableError: | |
| 85 | - raise newException(JetstreamError, "frame is not valid JSON") | |
| 86 | - | |
| 87 | - result = Event( | |
| 88 | - did: j{"did"}.getStr, | |
| 89 | - timeUs: j{"time_us"}.getBiggestInt.int64, | |
| 90 | - kind: parseEventKind(j{"kind"}.getStr), | |
| 91 | - raw: j, | |
| 92 | - ) | |
| 93 | - | |
| 94 | - if result.kind == ekCommit and j.hasKey("commit"): | |
| 95 | - let c = j["commit"] | |
| 96 | - result.commit = Commit( | |
| 97 | - rev: c{"rev"}.getStr, | |
| 98 | - operation: parseOperation(c{"operation"}.getStr), | |
| 99 | - collection: c{"collection"}.getStr, | |
| 100 | - rkey: c{"rkey"}.getStr, | |
| 101 | - cid: c{"cid"}.getStr, | |
| 102 | - record: c{"record"}, | |
| 103 | - ) | |
| 104 | - | |
| 105 | -proc subscribeUrl*(cfg: JetstreamConfig): string = | |
| 106 | - ## Build the /subscribe URL, including filters. | |
| 107 | - var base = cfg.url.strip(chars = {'/'}) | |
| 108 | - if not base.endsWith("/subscribe"): | |
| 109 | - base &= "/subscribe" | |
| 110 | - | |
| 111 | - var params: seq[(string, string)] | |
| 112 | - # Repeated keys, not a comma-joined list: Jetstream reads these as a | |
| 113 | - # multi-valued parameter. | |
| 114 | - for c in cfg.wantedCollections: | |
| 115 | - params.add ("wantedCollections", c) | |
| 116 | - for d in cfg.wantedDids: | |
| 117 | - params.add ("wantedDids", d) | |
| 118 | - if cfg.cursor > 0: | |
| 119 | - params.add ("cursor", $cfg.cursor) | |
| 120 | - | |
| 121 | - if params.len > 0: base & "?" & params.encodeQuery else: base | |
| 122 | - | |
| 123 | -proc connect*(cfg: JetstreamConfig): Future[WSSession] {.async.} = | |
| 124 | - ## Open the websocket. | |
| 125 | - ## | |
| 126 | - ## Deliberately not websock's `Uri` overload: it forwards only `uri.path` | |
| 127 | - ## and drops `uri.query`, which for Jetstream means losing every filter and | |
| 128 | - ## silently subscribing to the entire network firehose instead of the | |
| 129 | - ## handful of collections asked for. | |
| 130 | - let full = subscribeUrl(cfg) | |
| 131 | - let u = parseUri(full) | |
| 132 | - if u.scheme notin ["ws", "wss"]: | |
| 133 | - raise newException(JetstreamError, "not a websocket URL: " & full) | |
| 134 | - | |
| 135 | - let | |
| 136 | - secure = u.scheme == "wss" | |
| 137 | - port = if u.port.len > 0: u.port else: (if secure: "443" else: "80") | |
| 138 | - pathAndQuery = (if u.path.len > 0: u.path else: "/") & | |
| 139 | - (if u.query.len > 0: "?" & u.query else: "") | |
| 140 | - | |
| 141 | - await WebSocket.connect( | |
| 142 | - host = u.hostname & ":" & port, | |
| 143 | - path = pathAndQuery, | |
| 144 | - hostName = u.hostname, | |
| 145 | - secure = secure, | |
| 146 | - ) | |
| 147 | - | |
| 148 | -proc readEvents*(ws: WSSession, handler: EventHandler, | |
| 149 | - maxEvents = 0) {.async.} = | |
| 150 | - ## Pump frames until the socket closes, or until `maxEvents` have been | |
| 151 | - ## handled when that is non-zero. | |
| 152 | - var seen = 0 | |
| 153 | - while ws.readyState != ReadyState.Closed: | |
| 154 | - let frame = await ws.recvMsg() | |
| 155 | - if frame.len == 0: | |
| 156 | - # A zero-length read means the peer closed; recvMsg does not raise. | |
| 157 | - break | |
| 158 | - var ev: Event | |
| 159 | - try: | |
| 160 | - ev = parseEvent(bytesToString(frame)) | |
| 161 | - except JetstreamError: | |
| 162 | - # One malformed frame should not end a long-lived subscription. | |
| 163 | - continue | |
| 164 | - handler(ev) | |
| 165 | - inc seen | |
| 166 | - if maxEvents > 0 and seen >= maxEvents: | |
| 167 | - break | |
| 168 | - | |
| 169 | -proc collectEvents*(cfg: JetstreamConfig, count: int, | |
| 170 | - timeout = 30.seconds): Future[seq[Event]] {.async.} = | |
| 171 | - ## Connect, gather `count` events, and close. For probes and tests. | |
| 172 | - var got: seq[Event] | |
| 173 | - let ws = await connect(cfg) | |
| 174 | - defer: | |
| 175 | - try: await ws.close() | |
| 176 | - except CatchableError: discard | |
| 177 | - | |
| 178 | - proc onEvent(ev: Event) {.gcsafe, raises: [].} = | |
| 179 | - got.add ev | |
| 180 | - | |
| 181 | - let pump = readEvents(ws, onEvent, maxEvents = count) | |
| 182 | - if not await pump.withTimeout(timeout): | |
| 183 | - await pump.cancelAndWait() | |
| 184 | - got | |
| deleted file mode 100644 | |||
| @@ -1,184 +0,0 @@ | |||
| 1 | -## Jetstream consumer: the ATProto firehose as JSON over a websocket. | ||
| 2 | -## | ||
| 3 | -## Jetstream is a public, unauthenticated feed of repository commits, so this | ||
| 4 | -## is the one part of the port that can be verified end to end without a | ||
| 5 | -## consented session. | ||
| 6 | -## | ||
| 7 | -## Filtering happens server-side via query parameters (wantedCollections, | ||
| 8 | -## wantedDids), which matters: the unfiltered firehose is the entire network. | ||
| 9 | -## A subscription that silently loses its filters still delivers events, so a | ||
| 10 | -## consumer that looks healthy can be reading the wrong stream entirely -- | ||
| 11 | -## see the note on websock's Uri overload in `connect`. | ||
| 12 | - | ||
| 13 | -import std/[json, strutils, uri] | ||
| 14 | -import pkg/chronicles | ||
| 15 | -import pkg/chronos | ||
| 16 | -import pkg/websock/websock | ||
| 17 | - | ||
| 18 | -type | ||
| 19 | - JetstreamError* = object of CatchableError | ||
| 20 | - | ||
| 21 | - EventKind* = enum | ||
| 22 | - ekCommit = "commit" | ||
| 23 | - ekIdentity = "identity" | ||
| 24 | - ekAccount = "account" | ||
| 25 | - ekUnknown = "unknown" | ||
| 26 | - | ||
| 27 | - CommitOperation* = enum | ||
| 28 | - coCreate = "create" | ||
| 29 | - coUpdate = "update" | ||
| 30 | - coDelete = "delete" | ||
| 31 | - | ||
| 32 | - Commit* = object | ||
| 33 | - rev*: string | ||
| 34 | - operation*: CommitOperation | ||
| 35 | - collection*: string | ||
| 36 | - rkey*: string | ||
| 37 | - cid*: string | ||
| 38 | - record*: JsonNode ## nil for a delete | ||
| 39 | - | ||
| 40 | - Event* = object | ||
| 41 | - did*: string | ||
| 42 | - timeUs*: int64 ## microseconds; also the replay cursor | ||
| 43 | - kind*: EventKind | ||
| 44 | - commit*: Commit ## meaningful only when kind is ekCommit | ||
| 45 | - raw*: JsonNode | ||
| 46 | - | ||
| 47 | - JetstreamConfig* = object | ||
| 48 | - url*: string | ||
| 49 | - wantedCollections*: seq[string] | ||
| 50 | - wantedDids*: seq[string] | ||
| 51 | - cursor*: int64 ## 0 to start live | ||
| 52 | - userAgent*: string | ||
| 53 | - | ||
| 54 | - EventHandler* = proc(ev: Event) {.gcsafe, raises: [].} | ||
| 55 | - | ||
| 56 | -const | ||
| 57 | - DefaultJetstream* = "wss://jetstream1.us-east.bsky.network" | ||
| 58 | - DefaultUserAgent* = "glean-nim/0.1" | ||
| 59 | - | ||
| 60 | -proc bytesToString(bs: openArray[byte]): string = | ||
| 61 | - ## websock hands back seq[byte]; Jetstream frames are UTF-8 JSON text. | ||
| 62 | - result = newString(bs.len) | ||
| 63 | - if bs.len > 0: | ||
| 64 | - copyMem(result[0].addr, bs[0].unsafeAddr, bs.len) | ||
| 65 | - | ||
| 66 | -proc parseEventKind(s: string): EventKind = | ||
| 67 | - case s | ||
| 68 | - of "commit": ekCommit | ||
| 69 | - of "identity": ekIdentity | ||
| 70 | - of "account": ekAccount | ||
| 71 | - else: ekUnknown | ||
| 72 | - | ||
| 73 | -proc parseOperation(s: string): CommitOperation = | ||
| 74 | - case s | ||
| 75 | - of "create": coCreate | ||
| 76 | - of "update": coUpdate | ||
| 77 | - of "delete": coDelete | ||
| 78 | - else: coCreate | ||
| 79 | - | ||
| 80 | -proc parseEvent*(raw: string): Event = | ||
| 81 | - ## Decode one Jetstream frame. | ||
| 82 | - let j = | ||
| 83 | - try: parseJson(raw) | ||
| 84 | - except CatchableError: | ||
| 85 | - raise newException(JetstreamError, "frame is not valid JSON") | ||
| 86 | - | ||
| 87 | - result = Event( | ||
| 88 | - did: j{"did"}.getStr, | ||
| 89 | - timeUs: j{"time_us"}.getBiggestInt.int64, | ||
| 90 | - kind: parseEventKind(j{"kind"}.getStr), | ||
| 91 | - raw: j, | ||
| 92 | - ) | ||
| 93 | - | ||
| 94 | - if result.kind == ekCommit and j.hasKey("commit"): | ||
| 95 | - let c = j["commit"] | ||
| 96 | - result.commit = Commit( | ||
| 97 | - rev: c{"rev"}.getStr, | ||
| 98 | - operation: parseOperation(c{"operation"}.getStr), | ||
| 99 | - collection: c{"collection"}.getStr, | ||
| 100 | - rkey: c{"rkey"}.getStr, | ||
| 101 | - cid: c{"cid"}.getStr, | ||
| 102 | - record: c{"record"}, | ||
| 103 | - ) | ||
| 104 | - | ||
| 105 | -proc subscribeUrl*(cfg: JetstreamConfig): string = | ||
| 106 | - ## Build the /subscribe URL, including filters. | ||
| 107 | - var base = cfg.url.strip(chars = {'/'}) | ||
| 108 | - if not base.endsWith("/subscribe"): | ||
| 109 | - base &= "/subscribe" | ||
| 110 | - | ||
| 111 | - var params: seq[(string, string)] | ||
| 112 | - # Repeated keys, not a comma-joined list: Jetstream reads these as a | ||
| 113 | - # multi-valued parameter. | ||
| 114 | - for c in cfg.wantedCollections: | ||
| 115 | - params.add ("wantedCollections", c) | ||
| 116 | - for d in cfg.wantedDids: | ||
| 117 | - params.add ("wantedDids", d) | ||
| 118 | - if cfg.cursor > 0: | ||
| 119 | - params.add ("cursor", $cfg.cursor) | ||
| 120 | - | ||
| 121 | - if params.len > 0: base & "?" & params.encodeQuery else: base | ||
| 122 | - | ||
| 123 | -proc connect*(cfg: JetstreamConfig): Future[WSSession] {.async.} = | ||
| 124 | - ## Open the websocket. | ||
| 125 | - ## | ||
| 126 | - ## Deliberately not websock's `Uri` overload: it forwards only `uri.path` | ||
| 127 | - ## and drops `uri.query`, which for Jetstream means losing every filter and | ||
| 128 | - ## silently subscribing to the entire network firehose instead of the | ||
| 129 | - ## handful of collections asked for. | ||
| 130 | - let full = subscribeUrl(cfg) | ||
| 131 | - let u = parseUri(full) | ||
| 132 | - if u.scheme notin ["ws", "wss"]: | ||
| 133 | - raise newException(JetstreamError, "not a websocket URL: " & full) | ||
| 134 | - | ||
| 135 | - let | ||
| 136 | - secure = u.scheme == "wss" | ||
| 137 | - port = if u.port.len > 0: u.port else: (if secure: "443" else: "80") | ||
| 138 | - pathAndQuery = (if u.path.len > 0: u.path else: "/") & | ||
| 139 | - (if u.query.len > 0: "?" & u.query else: "") | ||
| 140 | - | ||
| 141 | - await WebSocket.connect( | ||
| 142 | - host = u.hostname & ":" & port, | ||
| 143 | - path = pathAndQuery, | ||
| 144 | - hostName = u.hostname, | ||
| 145 | - secure = secure, | ||
| 146 | - ) | ||
| 147 | - | ||
| 148 | -proc readEvents*(ws: WSSession, handler: EventHandler, | ||
| 149 | - maxEvents = 0) {.async.} = | ||
| 150 | - ## Pump frames until the socket closes, or until `maxEvents` have been | ||
| 151 | - ## handled when that is non-zero. | ||
| 152 | - var seen = 0 | ||
| 153 | - while ws.readyState != ReadyState.Closed: | ||
| 154 | - let frame = await ws.recvMsg() | ||
| 155 | - if frame.len == 0: | ||
| 156 | - # A zero-length read means the peer closed; recvMsg does not raise. | ||
| 157 | - break | ||
| 158 | - var ev: Event | ||
| 159 | - try: | ||
| 160 | - ev = parseEvent(bytesToString(frame)) | ||
| 161 | - except JetstreamError: | ||
| 162 | - # One malformed frame should not end a long-lived subscription. | ||
| 163 | - continue | ||
| 164 | - handler(ev) | ||
| 165 | - inc seen | ||
| 166 | - if maxEvents > 0 and seen >= maxEvents: | ||
| 167 | - break | ||
| 168 | - | ||
| 169 | -proc collectEvents*(cfg: JetstreamConfig, count: int, | ||
| 170 | - timeout = 30.seconds): Future[seq[Event]] {.async.} = | ||
| 171 | - ## Connect, gather `count` events, and close. For probes and tests. | ||
| 172 | - var got: seq[Event] | ||
| 173 | - let ws = await connect(cfg) | ||
| 174 | - defer: | ||
| 175 | - try: await ws.close() | ||
| 176 | - except CatchableError: discard | ||
| 177 | - | ||
| 178 | - proc onEvent(ev: Event) {.gcsafe, raises: [].} = | ||
| 179 | - got.add ev | ||
| 180 | - | ||
| 181 | - let pump = readEvents(ws, onEvent, maxEvents = count) | ||
| 182 | - if not await pump.withTimeout(timeout): | ||
| 183 | - await pump.cancelAndWait() | ||
| 184 | - got | ||
deleted
spike/nim/atproto/jetstream_run.nim +0 -167 | deleted file mode 100644 | ||
| @@ -1,167 +0,0 @@ | ||
| 1 | -## Keeping a Jetstream subscription alive. | |
| 2 | -## | |
| 3 | -## `jetstream.nim` reads a stream; this survives it ending. Three concerns, | |
| 4 | -## all of which are about not losing events rather than about uptime: | |
| 5 | -## | |
| 6 | -## 1. **Resume from a cursor, rewound.** Cursors are persisted in batches, so | |
| 7 | -## the last one written is always slightly behind the last one *seen*. On | |
| 8 | -## reconnect the consumer rewinds past that gap and accepts re-delivery -- | |
| 9 | -## duplicate events are cheap when handling is idempotent, and a hole in | |
| 10 | -## the record is not recoverable. | |
| 11 | -## | |
| 12 | -## 2. **Rotate connections on a timer.** Server-side filters are fixed for the | |
| 13 | -## life of a connection, so a subscription that never reconnects never | |
| 14 | -## learns about newly-known DIDs. Glean's Go consumer rotates every 15 | |
| 15 | -## minutes for exactly this reason. | |
| 16 | -## | |
| 17 | -## 3. **Back off, with jitter.** The Go consumer waits a flat 5s between | |
| 18 | -## attempts, which is fine against one healthy server and unkind to one | |
| 19 | -## that is down. This grows the delay to a cap, and jitters it so a fleet | |
| 20 | -## of consumers does not reconnect in lockstep. | |
| 21 | - | |
| 22 | -import std/[math, random] | |
| 23 | -import pkg/chronicles | |
| 24 | -import pkg/chronos | |
| 25 | -import pkg/websock/websock | |
| 26 | -import ./jetstream | |
| 27 | - | |
| 28 | -type | |
| 29 | - BackoffConfig* = object | |
| 30 | - initial*: Duration | |
| 31 | - maxDelay*: Duration | |
| 32 | - factor*: float | |
| 33 | - jitter*: float ## 0.0-1.0 fraction of the delay to randomise | |
| 34 | - | |
| 35 | - SupervisorConfig* = object | |
| 36 | - jetstream*: JetstreamConfig | |
| 37 | - backoff*: BackoffConfig | |
| 38 | - rewind*: Duration | |
| 39 | - ## How far behind the persisted cursor to resume. Covers the window | |
| 40 | - ## between the last cursor flush and the disconnection. | |
| 41 | - rotateEvery*: Duration | |
| 42 | - ## Force a reconnect this often so filter changes take effect. Zero | |
| 43 | - ## disables rotation. | |
| 44 | - | |
| 45 | - CursorStore* = ref object of RootObj ## Override to persist across restarts. | |
| 46 | - cursor*: int64 | |
| 47 | - | |
| 48 | - Supervisor* = ref object | |
| 49 | - cfg*: SupervisorConfig | |
| 50 | - store*: CursorStore | |
| 51 | - handler*: EventHandler | |
| 52 | - running*: bool | |
| 53 | - reconnects*: int ## observable for tests and metrics | |
| 54 | - | |
| 55 | -# Both are `raises: []` on purpose. Saving a cursor happens on the event path, | |
| 56 | -# and a persistent store that throws -- a locked database, a full disk -- must | |
| 57 | -# not tear down the subscription. An implementation that can fail is expected | |
| 58 | -# to swallow and log, and to lose at most the rewind window. | |
| 59 | -method load*(s: CursorStore): int64 {.base, gcsafe, raises: [].} = | |
| 60 | - s.cursor | |
| 61 | -method save*(s: CursorStore, cursor: int64) {.base, gcsafe, raises: [].} = | |
| 62 | - s.cursor = cursor | |
| 63 | - | |
| 64 | -const | |
| 65 | - DefaultBackoff* = BackoffConfig( | |
| 66 | - initial: 1.seconds, maxDelay: 60.seconds, factor: 2.0, jitter: 0.2) | |
| 67 | - DefaultRewind* = 5.seconds | |
| 68 | - DefaultRotateEvery* = 15.minutes | |
| 69 | - | |
| 70 | -proc newSupervisorConfig*(js: JetstreamConfig): SupervisorConfig = | |
| 71 | - SupervisorConfig( | |
| 72 | - jetstream: js, | |
| 73 | - backoff: DefaultBackoff, | |
| 74 | - rewind: DefaultRewind, | |
| 75 | - rotateEvery: DefaultRotateEvery, | |
| 76 | - ) | |
| 77 | - | |
| 78 | -proc delayFor*(b: BackoffConfig, attempt: int, rand01: float = -1.0): Duration = | |
| 79 | - ## Delay before attempt `attempt` (0 = first retry). | |
| 80 | - ## | |
| 81 | - ## `rand01` is injectable so the jitter can be tested deterministically; | |
| 82 | - ## left at -1 it draws from the global RNG. | |
| 83 | - ## | |
| 84 | - ## The first retry is jittered like every other. Short-circuiting it to the | |
| 85 | - ## bare initial delay would leave the one moment a herd is most likely -- | |
| 86 | - ## every consumer dropped at once by the same server restart -- as the one | |
| 87 | - ## moment they all wake together. | |
| 88 | - let | |
| 89 | - exp = max(attempt, 0).float | |
| 90 | - grown = b.initial.milliseconds.float * pow(b.factor, exp) | |
| 91 | - capped = min(grown, b.maxDelay.milliseconds.float) | |
| 92 | - if b.jitter <= 0: | |
| 93 | - return milliseconds(capped.int64) | |
| 94 | - # Jitter downward only, so the cap stays a real ceiling. | |
| 95 | - let r = if rand01 >= 0: rand01 else: rand(1.0) | |
| 96 | - let jittered = capped * (1.0 - b.jitter * r) | |
| 97 | - milliseconds(max(jittered.int64, 1'i64)) | |
| 98 | - | |
| 99 | -proc rewoundCursor*(store: CursorStore, rewind: Duration): int64 = | |
| 100 | - ## The persisted cursor, moved back to cover the unflushed window. Zero | |
| 101 | - ## (start live) stays zero -- there is nothing to rewind to. | |
| 102 | - let c = store.load() | |
| 103 | - if c <= 0: return 0 | |
| 104 | - max(c - rewind.microseconds, 0'i64) | |
| 105 | - | |
| 106 | -proc newSupervisor*(cfg: SupervisorConfig, handler: EventHandler, | |
| 107 | - store: CursorStore = nil): Supervisor = | |
| 108 | - Supervisor( | |
| 109 | - cfg: cfg, | |
| 110 | - store: if store != nil: store else: CursorStore(), | |
| 111 | - handler: handler, | |
| 112 | - ) | |
| 113 | - | |
| 114 | -proc runOnce(sup: Supervisor) {.async.} = | |
| 115 | - ## One connection, read until it ends or the rotation timer fires. | |
| 116 | - var cfg = sup.cfg.jetstream | |
| 117 | - cfg.cursor = rewoundCursor(sup.store, sup.cfg.rewind) | |
| 118 | - | |
| 119 | - let ws = await connect(cfg) | |
| 120 | - defer: | |
| 121 | - try: await ws.close() | |
| 122 | - except CatchableError: discard | |
| 123 | - | |
| 124 | - # Record every event's cursor, so a drop resumes near where it stopped. | |
| 125 | - let store = sup.store | |
| 126 | - let inner = sup.handler | |
| 127 | - proc onEvent(ev: Event) {.gcsafe, raises: [].} = | |
| 128 | - if ev.timeUs > 0: | |
| 129 | - store.save(ev.timeUs) | |
| 130 | - inner(ev) | |
| 131 | - | |
| 132 | - let pump = readEvents(ws, onEvent) | |
| 133 | - if sup.cfg.rotateEvery > ZeroDuration: | |
| 134 | - if not await pump.withTimeout(sup.cfg.rotateEvery): | |
| 135 | - # Rotation is a normal end to a connection, not a failure. | |
| 136 | - await pump.cancelAndWait() | |
| 137 | - else: | |
| 138 | - await pump | |
| 139 | - | |
| 140 | -proc run*(sup: Supervisor) {.async.} = | |
| 141 | - ## Reconnect forever. Stop by setting `running` to false. | |
| 142 | - sup.running = true | |
| 143 | - var attempt = 0 | |
| 144 | - while sup.running: | |
| 145 | - try: | |
| 146 | - await sup.runOnce() | |
| 147 | - # A clean end (rotation, or the server closing) is not a failure, so | |
| 148 | - # the next attempt starts from the bottom of the backoff curve. | |
| 149 | - attempt = 0 | |
| 150 | - except CancelledError as e: | |
| 151 | - raise e | |
| 152 | - except CatchableError as e: | |
| 153 | - inc sup.reconnects | |
| 154 | - warn "jetstream connection lost", err = e.msg, attempt = attempt | |
| 155 | - let delay = delayFor(sup.cfg.backoff, attempt) | |
| 156 | - inc attempt | |
| 157 | - await sleepAsync(delay) | |
| 158 | - continue | |
| 159 | - | |
| 160 | - if not sup.running: | |
| 161 | - break | |
| 162 | - # Even a clean rotation waits a moment, so a server that closes | |
| 163 | - # immediately cannot spin this loop. | |
| 164 | - await sleepAsync(sup.cfg.backoff.initial) | |
| 165 | - | |
| 166 | -proc stop*(sup: Supervisor) = | |
| 167 | - sup.running = false | |
| deleted file mode 100644 | |||
| @@ -1,167 +0,0 @@ | |||
| 1 | -## Keeping a Jetstream subscription alive. | ||
| 2 | -## | ||
| 3 | -## `jetstream.nim` reads a stream; this survives it ending. Three concerns, | ||
| 4 | -## all of which are about not losing events rather than about uptime: | ||
| 5 | -## | ||
| 6 | -## 1. **Resume from a cursor, rewound.** Cursors are persisted in batches, so | ||
| 7 | -## the last one written is always slightly behind the last one *seen*. On | ||
| 8 | -## reconnect the consumer rewinds past that gap and accepts re-delivery -- | ||
| 9 | -## duplicate events are cheap when handling is idempotent, and a hole in | ||
| 10 | -## the record is not recoverable. | ||
| 11 | -## | ||
| 12 | -## 2. **Rotate connections on a timer.** Server-side filters are fixed for the | ||
| 13 | -## life of a connection, so a subscription that never reconnects never | ||
| 14 | -## learns about newly-known DIDs. Glean's Go consumer rotates every 15 | ||
| 15 | -## minutes for exactly this reason. | ||
| 16 | -## | ||
| 17 | -## 3. **Back off, with jitter.** The Go consumer waits a flat 5s between | ||
| 18 | -## attempts, which is fine against one healthy server and unkind to one | ||
| 19 | -## that is down. This grows the delay to a cap, and jitters it so a fleet | ||
| 20 | -## of consumers does not reconnect in lockstep. | ||
| 21 | - | ||
| 22 | -import std/[math, random] | ||
| 23 | -import pkg/chronicles | ||
| 24 | -import pkg/chronos | ||
| 25 | -import pkg/websock/websock | ||
| 26 | -import ./jetstream | ||
| 27 | - | ||
| 28 | -type | ||
| 29 | - BackoffConfig* = object | ||
| 30 | - initial*: Duration | ||
| 31 | - maxDelay*: Duration | ||
| 32 | - factor*: float | ||
| 33 | - jitter*: float ## 0.0-1.0 fraction of the delay to randomise | ||
| 34 | - | ||
| 35 | - SupervisorConfig* = object | ||
| 36 | - jetstream*: JetstreamConfig | ||
| 37 | - backoff*: BackoffConfig | ||
| 38 | - rewind*: Duration | ||
| 39 | - ## How far behind the persisted cursor to resume. Covers the window | ||
| 40 | - ## between the last cursor flush and the disconnection. | ||
| 41 | - rotateEvery*: Duration | ||
| 42 | - ## Force a reconnect this often so filter changes take effect. Zero | ||
| 43 | - ## disables rotation. | ||
| 44 | - | ||
| 45 | - CursorStore* = ref object of RootObj ## Override to persist across restarts. | ||
| 46 | - cursor*: int64 | ||
| 47 | - | ||
| 48 | - Supervisor* = ref object | ||
| 49 | - cfg*: SupervisorConfig | ||
| 50 | - store*: CursorStore | ||
| 51 | - handler*: EventHandler | ||
| 52 | - running*: bool | ||
| 53 | - reconnects*: int ## observable for tests and metrics | ||
| 54 | - | ||
| 55 | -# Both are `raises: []` on purpose. Saving a cursor happens on the event path, | ||
| 56 | -# and a persistent store that throws -- a locked database, a full disk -- must | ||
| 57 | -# not tear down the subscription. An implementation that can fail is expected | ||
| 58 | -# to swallow and log, and to lose at most the rewind window. | ||
| 59 | -method load*(s: CursorStore): int64 {.base, gcsafe, raises: [].} = | ||
| 60 | - s.cursor | ||
| 61 | -method save*(s: CursorStore, cursor: int64) {.base, gcsafe, raises: [].} = | ||
| 62 | - s.cursor = cursor | ||
| 63 | - | ||
| 64 | -const | ||
| 65 | - DefaultBackoff* = BackoffConfig( | ||
| 66 | - initial: 1.seconds, maxDelay: 60.seconds, factor: 2.0, jitter: 0.2) | ||
| 67 | - DefaultRewind* = 5.seconds | ||
| 68 | - DefaultRotateEvery* = 15.minutes | ||
| 69 | - | ||
| 70 | -proc newSupervisorConfig*(js: JetstreamConfig): SupervisorConfig = | ||
| 71 | - SupervisorConfig( | ||
| 72 | - jetstream: js, | ||
| 73 | - backoff: DefaultBackoff, | ||
| 74 | - rewind: DefaultRewind, | ||
| 75 | - rotateEvery: DefaultRotateEvery, | ||
| 76 | - ) | ||
| 77 | - | ||
| 78 | -proc delayFor*(b: BackoffConfig, attempt: int, rand01: float = -1.0): Duration = | ||
| 79 | - ## Delay before attempt `attempt` (0 = first retry). | ||
| 80 | - ## | ||
| 81 | - ## `rand01` is injectable so the jitter can be tested deterministically; | ||
| 82 | - ## left at -1 it draws from the global RNG. | ||
| 83 | - ## | ||
| 84 | - ## The first retry is jittered like every other. Short-circuiting it to the | ||
| 85 | - ## bare initial delay would leave the one moment a herd is most likely -- | ||
| 86 | - ## every consumer dropped at once by the same server restart -- as the one | ||
| 87 | - ## moment they all wake together. | ||
| 88 | - let | ||
| 89 | - exp = max(attempt, 0).float | ||
| 90 | - grown = b.initial.milliseconds.float * pow(b.factor, exp) | ||
| 91 | - capped = min(grown, b.maxDelay.milliseconds.float) | ||
| 92 | - if b.jitter <= 0: | ||
| 93 | - return milliseconds(capped.int64) | ||
| 94 | - # Jitter downward only, so the cap stays a real ceiling. | ||
| 95 | - let r = if rand01 >= 0: rand01 else: rand(1.0) | ||
| 96 | - let jittered = capped * (1.0 - b.jitter * r) | ||
| 97 | - milliseconds(max(jittered.int64, 1'i64)) | ||
| 98 | - | ||
| 99 | -proc rewoundCursor*(store: CursorStore, rewind: Duration): int64 = | ||
| 100 | - ## The persisted cursor, moved back to cover the unflushed window. Zero | ||
| 101 | - ## (start live) stays zero -- there is nothing to rewind to. | ||
| 102 | - let c = store.load() | ||
| 103 | - if c <= 0: return 0 | ||
| 104 | - max(c - rewind.microseconds, 0'i64) | ||
| 105 | - | ||
| 106 | -proc newSupervisor*(cfg: SupervisorConfig, handler: EventHandler, | ||
| 107 | - store: CursorStore = nil): Supervisor = | ||
| 108 | - Supervisor( | ||
| 109 | - cfg: cfg, | ||
| 110 | - store: if store != nil: store else: CursorStore(), | ||
| 111 | - handler: handler, | ||
| 112 | - ) | ||
| 113 | - | ||
| 114 | -proc runOnce(sup: Supervisor) {.async.} = | ||
| 115 | - ## One connection, read until it ends or the rotation timer fires. | ||
| 116 | - var cfg = sup.cfg.jetstream | ||
| 117 | - cfg.cursor = rewoundCursor(sup.store, sup.cfg.rewind) | ||
| 118 | - | ||
| 119 | - let ws = await connect(cfg) | ||
| 120 | - defer: | ||
| 121 | - try: await ws.close() | ||
| 122 | - except CatchableError: discard | ||
| 123 | - | ||
| 124 | - # Record every event's cursor, so a drop resumes near where it stopped. | ||
| 125 | - let store = sup.store | ||
| 126 | - let inner = sup.handler | ||
| 127 | - proc onEvent(ev: Event) {.gcsafe, raises: [].} = | ||
| 128 | - if ev.timeUs > 0: | ||
| 129 | - store.save(ev.timeUs) | ||
| 130 | - inner(ev) | ||
| 131 | - | ||
| 132 | - let pump = readEvents(ws, onEvent) | ||
| 133 | - if sup.cfg.rotateEvery > ZeroDuration: | ||
| 134 | - if not await pump.withTimeout(sup.cfg.rotateEvery): | ||
| 135 | - # Rotation is a normal end to a connection, not a failure. | ||
| 136 | - await pump.cancelAndWait() | ||
| 137 | - else: | ||
| 138 | - await pump | ||
| 139 | - | ||
| 140 | -proc run*(sup: Supervisor) {.async.} = | ||
| 141 | - ## Reconnect forever. Stop by setting `running` to false. | ||
| 142 | - sup.running = true | ||
| 143 | - var attempt = 0 | ||
| 144 | - while sup.running: | ||
| 145 | - try: | ||
| 146 | - await sup.runOnce() | ||
| 147 | - # A clean end (rotation, or the server closing) is not a failure, so | ||
| 148 | - # the next attempt starts from the bottom of the backoff curve. | ||
| 149 | - attempt = 0 | ||
| 150 | - except CancelledError as e: | ||
| 151 | - raise e | ||
| 152 | - except CatchableError as e: | ||
| 153 | - inc sup.reconnects | ||
| 154 | - warn "jetstream connection lost", err = e.msg, attempt = attempt | ||
| 155 | - let delay = delayFor(sup.cfg.backoff, attempt) | ||
| 156 | - inc attempt | ||
| 157 | - await sleepAsync(delay) | ||
| 158 | - continue | ||
| 159 | - | ||
| 160 | - if not sup.running: | ||
| 161 | - break | ||
| 162 | - # Even a clean rotation waits a moment, so a server that closes | ||
| 163 | - # immediately cannot spin this loop. | ||
| 164 | - await sleepAsync(sup.cfg.backoff.initial) | ||
| 165 | - | ||
| 166 | -proc stop*(sup: Supervisor) = | ||
| 167 | - sup.running = false | ||
deleted
spike/nim/atproto/oauth.nim +0 -329 | deleted file mode 100644 | ||
| @@ -1,329 +0,0 @@ | ||
| 1 | -## ATProto OAuth client: metadata discovery, PAR, authorization URL, and the | |
| 2 | -## token exchange, ported from the shape of bluesky-social/indigo's | |
| 3 | -## atproto/auth/oauth package. | |
| 4 | -## | |
| 5 | -## Two details drive most of the design: | |
| 6 | -## | |
| 7 | -## 1. Every request to the auth server carries a DPoP proof bound to a | |
| 8 | -## per-session P-256 key, and the server will reject the first one with | |
| 9 | -## `use_dpop_nonce` plus a DPoP-Nonce header. That is normal, not an error: | |
| 10 | -## the request must be retried once with the supplied nonce. Note the | |
| 11 | -## status is 400 on the auth server (not the 401 a resource server uses). | |
| 12 | -## | |
| 13 | -## 2. PKCE is mandatory. The verifier is kept alongside the request so the | |
| 14 | -## callback can present it. | |
| 15 | - | |
| 16 | -import std/[httpclient, json, strutils, strformat, sysrand, uri] | |
| 17 | -import nimcrypto/[sha2, hash] | |
| 18 | -import bearssl/rand | |
| 19 | -import ./dpop | |
| 20 | -import ./identity | |
| 21 | - | |
| 22 | -type | |
| 23 | - OAuthError* = object of CatchableError | |
| 24 | - | |
| 25 | - ClientConfig* = object | |
| 26 | - clientId*: string | |
| 27 | - callbackUrl*: string | |
| 28 | - scopes*: seq[string] | |
| 29 | - | |
| 30 | - AuthServerMetadata* = object | |
| 31 | - issuer*: string | |
| 32 | - authorizationEndpoint*: string | |
| 33 | - tokenEndpoint*: string | |
| 34 | - parEndpoint*: string | |
| 35 | - revocationEndpoint*: string | |
| 36 | - | |
| 37 | - AuthRequestData* = object ## Persisted between the redirect and the callback. | |
| 38 | - state*: string | |
| 39 | - authServerUrl*: string | |
| 40 | - scopes*: seq[string] | |
| 41 | - pkceVerifier*: string | |
| 42 | - requestUri*: string | |
| 43 | - tokenEndpoint*: string | |
| 44 | - revocationEndpoint*: string | |
| 45 | - dpopNonce*: string | |
| 46 | - dpopKey*: P256Key | |
| 47 | - accountDid*: string | |
| 48 | - | |
| 49 | - TokenResponse* = object | |
| 50 | - accessToken*: string | |
| 51 | - refreshToken*: string | |
| 52 | - tokenType*: string | |
| 53 | - expiresIn*: int | |
| 54 | - scope*: string | |
| 55 | - sub*: string | |
| 56 | - | |
| 57 | -const HttpTimeoutMs = 15_000 | |
| 58 | - | |
| 59 | -proc newClient(): HttpClient = | |
| 60 | - newHttpClient(userAgent = UserAgent, timeout = HttpTimeoutMs) | |
| 61 | - | |
| 62 | -proc scopeStr*(scopes: seq[string]): string = scopes.join(" ") | |
| 63 | - | |
| 64 | -proc newPublicConfig*(clientId, callbackUrl: string, | |
| 65 | - scopes = @["atproto", "transition:generic"]): ClientConfig = | |
| 66 | - ClientConfig(clientId: clientId, callbackUrl: callbackUrl, scopes: scopes) | |
| 67 | - | |
| 68 | -proc newLocalhostConfig*(callbackUrl: string, | |
| 69 | - scopes = @["atproto", "transition:generic"]): ClientConfig = | |
| 70 | - ## Localhost development clients have no hosted metadata document; the | |
| 71 | - ## client_id encodes the redirect and scopes instead. | |
| 72 | - var params = @[("redirect_uri", callbackUrl), ("scope", scopeStr(scopes))] | |
| 73 | - ClientConfig( | |
| 74 | - clientId: "http://localhost" & "?" & params.encodeQuery, | |
| 75 | - callbackUrl: callbackUrl, | |
| 76 | - scopes: scopes, | |
| 77 | - ) | |
| 78 | - | |
| 79 | -proc clientMetadata*(cfg: ClientConfig): JsonNode = | |
| 80 | - ## Served at the client_id URL for a public (non-confidential) client. | |
| 81 | - %*{ | |
| 82 | - "client_id": cfg.clientId, | |
| 83 | - "application_type": "web", | |
| 84 | - "client_name": "glean", | |
| 85 | - "grant_types": ["authorization_code", "refresh_token"], | |
| 86 | - "scope": scopeStr(cfg.scopes), | |
| 87 | - "response_types": ["code"], | |
| 88 | - "redirect_uris": [cfg.callbackUrl], | |
| 89 | - "token_endpoint_auth_method": "none", | |
| 90 | - "dpop_bound_access_tokens": true, | |
| 91 | - } | |
| 92 | - | |
| 93 | -proc secureRandomB64*(n: int): string = | |
| 94 | - var buf = newSeq[byte](n) | |
| 95 | - doAssert urandom(buf), "system randomness unavailable" | |
| 96 | - b64u(buf) | |
| 97 | - | |
| 98 | -proc s256Challenge*(verifier: string): string = | |
| 99 | - b64u(sha256.digest(verifier).data) | |
| 100 | - | |
| 101 | -# --- discovery ------------------------------------------------------------- | |
| 102 | - | |
| 103 | -proc resolveAuthServerUrl*(pdsUrl: string): string = | |
| 104 | - ## The PDS advertises which authorization server governs it. | |
| 105 | - let url = &"https://{hostOf(pdsUrl)}/.well-known/oauth-protected-resource" | |
| 106 | - let client = newClient() | |
| 107 | - defer: client.close() | |
| 108 | - let res = client.get(url) | |
| 109 | - if res.code.int != 200: | |
| 110 | - raise newException(OAuthError, | |
| 111 | - &"protected resource metadata failed (HTTP {res.code.int}) at {url}") | |
| 112 | - let body = parseJson(res.body) | |
| 113 | - let servers = body{"authorization_servers"} | |
| 114 | - if servers == nil or servers.kind != JArray or servers.len == 0: | |
| 115 | - raise newException(OAuthError, "PDS lists no authorization servers: " & pdsUrl) | |
| 116 | - servers[0].getStr.strip(chars = {'/'}) | |
| 117 | - | |
| 118 | -proc resolveAuthServerMetadata*(authServerUrl: string): AuthServerMetadata = | |
| 119 | - let url = &"https://{hostOf(authServerUrl)}/.well-known/oauth-authorization-server" | |
| 120 | - let client = newClient() | |
| 121 | - defer: client.close() | |
| 122 | - let res = client.get(url) | |
| 123 | - if res.code.int != 200: | |
| 124 | - raise newException(OAuthError, | |
| 125 | - &"auth server metadata failed (HTTP {res.code.int}) at {url}") | |
| 126 | - let b = parseJson(res.body) | |
| 127 | - | |
| 128 | - result = AuthServerMetadata( | |
| 129 | - issuer: b{"issuer"}.getStr, | |
| 130 | - authorizationEndpoint: b{"authorization_endpoint"}.getStr, | |
| 131 | - tokenEndpoint: b{"token_endpoint"}.getStr, | |
| 132 | - parEndpoint: b{"pushed_authorization_request_endpoint"}.getStr, | |
| 133 | - revocationEndpoint: b{"revocation_endpoint"}.getStr, | |
| 134 | - ) | |
| 135 | - | |
| 136 | - # An auth server missing any of these cannot complete an ATProto flow, and | |
| 137 | - # failing here beats a confusing error three requests later. | |
| 138 | - for (name, value) in { | |
| 139 | - "issuer": result.issuer, | |
| 140 | - "authorization_endpoint": result.authorizationEndpoint, | |
| 141 | - "token_endpoint": result.tokenEndpoint, | |
| 142 | - "pushed_authorization_request_endpoint": result.parEndpoint, | |
| 143 | - }: | |
| 144 | - if value.len == 0: | |
| 145 | - raise newException(OAuthError, "auth server metadata is missing " & name) | |
| 146 | - | |
| 147 | - if result.issuer.strip(chars = {'/'}) != authServerUrl.strip(chars = {'/'}): | |
| 148 | - raise newException(OAuthError, | |
| 149 | - &"auth server issuer mismatch: metadata says {result.issuer}, fetched from {authServerUrl}") | |
| 150 | - | |
| 151 | -# --- requests -------------------------------------------------------------- | |
| 152 | - | |
| 153 | -proc errorReason(body: string): string = | |
| 154 | - try: | |
| 155 | - let j = parseJson(body) | |
| 156 | - result = j{"error"}.getStr | |
| 157 | - let desc = j{"error_description"}.getStr | |
| 158 | - if desc.len > 0: | |
| 159 | - result &= ": " & desc | |
| 160 | - except CatchableError: | |
| 161 | - result = "unknown" | |
| 162 | - if result.len == 0: | |
| 163 | - result = "unknown" | |
| 164 | - | |
| 165 | -proc errorCode(body: string): string = | |
| 166 | - try: parseJson(body){"error"}.getStr | |
| 167 | - except CatchableError: "" | |
| 168 | - | |
| 169 | -proc postWithDpop(url, body: string, key: P256Key, rng: var HmacDrbgContext, | |
| 170 | - nonce: var string): Response = | |
| 171 | - ## POST with a DPoP proof, retrying once when the server demands a nonce. | |
| 172 | - ## | |
| 173 | - ## The first request of a session always lacks a nonce, so the retry is the | |
| 174 | - ## expected path rather than an error case. The auth server signals this | |
| 175 | - ## with 400 + error=use_dpop_nonce and supplies DPoP-Nonce. | |
| 176 | - let client = newClient() | |
| 177 | - defer: client.close() | |
| 178 | - | |
| 179 | - for attempt in 0 .. 1: | |
| 180 | - let proof = dpopProof(key, rng, "POST", url, nonce = nonce) | |
| 181 | - client.headers = newHttpHeaders({ | |
| 182 | - "Content-Type": "application/x-www-form-urlencoded", | |
| 183 | - "DPoP": proof, | |
| 184 | - }) | |
| 185 | - result = client.post(url, body = body) | |
| 186 | - | |
| 187 | - let supplied = result.headers.getOrDefault("DPoP-Nonce").string | |
| 188 | - if supplied.len > 0: | |
| 189 | - nonce = supplied | |
| 190 | - | |
| 191 | - if attempt == 0 and result.code.int == 400 and supplied.len > 0: | |
| 192 | - if errorCode(result.body) == "use_dpop_nonce": | |
| 193 | - continue | |
| 194 | - return result | |
| 195 | - | |
| 196 | -proc sendAuthRequest*(cfg: ClientConfig, meta: AuthServerMetadata, | |
| 197 | - rng: var HmacDrbgContext, loginHint = ""): AuthRequestData = | |
| 198 | - ## Pushed Authorization Request: hand the parameters to the auth server up | |
| 199 | - ## front and get back a short-lived request_uri to redirect the user with. | |
| 200 | - let | |
| 201 | - state = secureRandomB64(16) | |
| 202 | - verifier = secureRandomB64(48) | |
| 203 | - key = generateKey(rng) | |
| 204 | - | |
| 205 | - var params = @[ | |
| 206 | - ("client_id", cfg.clientId), | |
| 207 | - ("state", state), | |
| 208 | - ("redirect_uri", cfg.callbackUrl), | |
| 209 | - ("scope", scopeStr(cfg.scopes)), | |
| 210 | - ("response_type", "code"), | |
| 211 | - ("code_challenge", s256Challenge(verifier)), | |
| 212 | - ("code_challenge_method", "S256"), | |
| 213 | - ] | |
| 214 | - if loginHint.len > 0: | |
| 215 | - params.add ("login_hint", loginHint) | |
| 216 | - | |
| 217 | - var nonce = "" | |
| 218 | - let res = postWithDpop(meta.parEndpoint, params.encodeQuery, key, rng, nonce) | |
| 219 | - if res.code.int notin [200, 201]: | |
| 220 | - raise newException(OAuthError, | |
| 221 | - &"PAR failed (HTTP {res.code.int}): {errorReason(res.body)}") | |
| 222 | - | |
| 223 | - let requestUri = parseJson(res.body){"request_uri"}.getStr | |
| 224 | - if requestUri.len == 0: | |
| 225 | - raise newException(OAuthError, "PAR response has no request_uri") | |
| 226 | - | |
| 227 | - AuthRequestData( | |
| 228 | - state: state, | |
| 229 | - authServerUrl: meta.issuer, | |
| 230 | - scopes: cfg.scopes, | |
| 231 | - pkceVerifier: verifier, | |
| 232 | - requestUri: requestUri, | |
| 233 | - tokenEndpoint: meta.tokenEndpoint, | |
| 234 | - revocationEndpoint: meta.revocationEndpoint, | |
| 235 | - dpopNonce: nonce, | |
| 236 | - dpopKey: key, | |
| 237 | - ) | |
| 238 | - | |
| 239 | -proc authorizeUrl*(cfg: ClientConfig, meta: AuthServerMetadata, | |
| 240 | - info: AuthRequestData): string = | |
| 241 | - let params = @[("client_id", cfg.clientId), ("request_uri", info.requestUri)] | |
| 242 | - meta.authorizationEndpoint & "?" & params.encodeQuery | |
| 243 | - | |
| 244 | -proc startAuthFlow*(cfg: ClientConfig, identifier: string, | |
| 245 | - rng: var HmacDrbgContext, | |
| 246 | - plcUrl = DefaultPlcUrl): (string, AuthRequestData) = | |
| 247 | - ## Resolve an account (or take an auth server URL directly), push the | |
| 248 | - ## request, and return the URL to send the user to. | |
| 249 | - var | |
| 250 | - authServerUrl: string | |
| 251 | - accountDid: string | |
| 252 | - loginHint: string | |
| 253 | - | |
| 254 | - if identifier.startsWith("https://"): | |
| 255 | - authServerUrl = identifier.strip(chars = {'/'}) | |
| 256 | - else: | |
| 257 | - let ident = lookup(identifier, plcUrl) | |
| 258 | - accountDid = ident.did | |
| 259 | - loginHint = identifier | |
| 260 | - authServerUrl = resolveAuthServerUrl(ident.pdsEndpoint) | |
| 261 | - | |
| 262 | - let meta = resolveAuthServerMetadata(authServerUrl) | |
| 263 | - var info = sendAuthRequest(cfg, meta, rng, loginHint) | |
| 264 | - info.accountDid = accountDid | |
| 265 | - (authorizeUrl(cfg, meta, info), info) | |
| 266 | - | |
| 267 | -proc exchangeCode*(cfg: ClientConfig, info: var AuthRequestData, | |
| 268 | - code: string, rng: var HmacDrbgContext): TokenResponse = | |
| 269 | - ## Trade the authorization code for tokens, proving possession of both the | |
| 270 | - ## PKCE verifier and the session's DPoP key. | |
| 271 | - let params = @[ | |
| 272 | - ("client_id", cfg.clientId), | |
| 273 | - ("redirect_uri", cfg.callbackUrl), | |
| 274 | - ("grant_type", "authorization_code"), | |
| 275 | - ("code", code), | |
| 276 | - ("code_verifier", info.pkceVerifier), | |
| 277 | - ] | |
| 278 | - var nonce = info.dpopNonce | |
| 279 | - let res = postWithDpop(info.tokenEndpoint, params.encodeQuery, | |
| 280 | - info.dpopKey, rng, nonce) | |
| 281 | - info.dpopNonce = nonce | |
| 282 | - if res.code.int != 200: | |
| 283 | - raise newException(OAuthError, | |
| 284 | - &"token request failed (HTTP {res.code.int}): {errorReason(res.body)}") | |
| 285 | - | |
| 286 | - let b = parseJson(res.body) | |
| 287 | - result = TokenResponse( | |
| 288 | - accessToken: b{"access_token"}.getStr, | |
| 289 | - refreshToken: b{"refresh_token"}.getStr, | |
| 290 | - tokenType: b{"token_type"}.getStr, | |
| 291 | - expiresIn: b{"expires_in"}.getInt, | |
| 292 | - scope: b{"scope"}.getStr, | |
| 293 | - sub: b{"sub"}.getStr, | |
| 294 | - ) | |
| 295 | - | |
| 296 | - # DPoP-bound tokens are useless as bearer tokens; a server answering | |
| 297 | - # token_type=Bearer means the binding silently did not happen. | |
| 298 | - if not result.tokenType.toLowerAscii.startsWith("dpop"): | |
| 299 | - raise newException(OAuthError, | |
| 300 | - "expected a DPoP-bound token, got token_type=" & result.tokenType) | |
| 301 | - if result.sub.len == 0: | |
| 302 | - raise newException(OAuthError, "token response has no subject DID") | |
| 303 | - if info.accountDid.len > 0 and result.sub != info.accountDid: | |
| 304 | - raise newException(OAuthError, | |
| 305 | - &"token subject {result.sub} does not match requested account {info.accountDid}") | |
| 306 | - | |
| 307 | -proc refresh*(cfg: ClientConfig, info: var AuthRequestData, | |
| 308 | - refreshToken: string, rng: var HmacDrbgContext): TokenResponse = | |
| 309 | - let params = @[ | |
| 310 | - ("client_id", cfg.clientId), | |
| 311 | - ("grant_type", "refresh_token"), | |
| 312 | - ("refresh_token", refreshToken), | |
| 313 | - ] | |
| 314 | - var nonce = info.dpopNonce | |
| 315 | - let res = postWithDpop(info.tokenEndpoint, params.encodeQuery, | |
| 316 | - info.dpopKey, rng, nonce) | |
| 317 | - info.dpopNonce = nonce | |
| 318 | - if res.code.int != 200: | |
| 319 | - raise newException(OAuthError, | |
| 320 | - &"refresh failed (HTTP {res.code.int}): {errorReason(res.body)}") | |
| 321 | - let b = parseJson(res.body) | |
| 322 | - TokenResponse( | |
| 323 | - accessToken: b{"access_token"}.getStr, | |
| 324 | - refreshToken: b{"refresh_token"}.getStr, | |
| 325 | - tokenType: b{"token_type"}.getStr, | |
| 326 | - expiresIn: b{"expires_in"}.getInt, | |
| 327 | - scope: b{"scope"}.getStr, | |
| 328 | - sub: b{"sub"}.getStr, | |
| 329 | - ) | |
| deleted file mode 100644 | |||
| @@ -1,329 +0,0 @@ | |||
| 1 | -## ATProto OAuth client: metadata discovery, PAR, authorization URL, and the | ||
| 2 | -## token exchange, ported from the shape of bluesky-social/indigo's | ||
| 3 | -## atproto/auth/oauth package. | ||
| 4 | -## | ||
| 5 | -## Two details drive most of the design: | ||
| 6 | -## | ||
| 7 | -## 1. Every request to the auth server carries a DPoP proof bound to a | ||
| 8 | -## per-session P-256 key, and the server will reject the first one with | ||
| 9 | -## `use_dpop_nonce` plus a DPoP-Nonce header. That is normal, not an error: | ||
| 10 | -## the request must be retried once with the supplied nonce. Note the | ||
| 11 | -## status is 400 on the auth server (not the 401 a resource server uses). | ||
| 12 | -## | ||
| 13 | -## 2. PKCE is mandatory. The verifier is kept alongside the request so the | ||
| 14 | -## callback can present it. | ||
| 15 | - | ||
| 16 | -import std/[httpclient, json, strutils, strformat, sysrand, uri] | ||
| 17 | -import nimcrypto/[sha2, hash] | ||
| 18 | -import bearssl/rand | ||
| 19 | -import ./dpop | ||
| 20 | -import ./identity | ||
| 21 | - | ||
| 22 | -type | ||
| 23 | - OAuthError* = object of CatchableError | ||
| 24 | - | ||
| 25 | - ClientConfig* = object | ||
| 26 | - clientId*: string | ||
| 27 | - callbackUrl*: string | ||
| 28 | - scopes*: seq[string] | ||
| 29 | - | ||
| 30 | - AuthServerMetadata* = object | ||
| 31 | - issuer*: string | ||
| 32 | - authorizationEndpoint*: string | ||
| 33 | - tokenEndpoint*: string | ||
| 34 | - parEndpoint*: string | ||
| 35 | - revocationEndpoint*: string | ||
| 36 | - | ||
| 37 | - AuthRequestData* = object ## Persisted between the redirect and the callback. | ||
| 38 | - state*: string | ||
| 39 | - authServerUrl*: string | ||
| 40 | - scopes*: seq[string] | ||
| 41 | - pkceVerifier*: string | ||
| 42 | - requestUri*: string | ||
| 43 | - tokenEndpoint*: string | ||
| 44 | - revocationEndpoint*: string | ||
| 45 | - dpopNonce*: string | ||
| 46 | - dpopKey*: P256Key | ||
| 47 | - accountDid*: string | ||
| 48 | - | ||
| 49 | - TokenResponse* = object | ||
| 50 | - accessToken*: string | ||
| 51 | - refreshToken*: string | ||
| 52 | - tokenType*: string | ||
| 53 | - expiresIn*: int | ||
| 54 | - scope*: string | ||
| 55 | - sub*: string | ||
| 56 | - | ||
| 57 | -const HttpTimeoutMs = 15_000 | ||
| 58 | - | ||
| 59 | -proc newClient(): HttpClient = | ||
| 60 | - newHttpClient(userAgent = UserAgent, timeout = HttpTimeoutMs) | ||
| 61 | - | ||
| 62 | -proc scopeStr*(scopes: seq[string]): string = scopes.join(" ") | ||
| 63 | - | ||
| 64 | -proc newPublicConfig*(clientId, callbackUrl: string, | ||
| 65 | - scopes = @["atproto", "transition:generic"]): ClientConfig = | ||
| 66 | - ClientConfig(clientId: clientId, callbackUrl: callbackUrl, scopes: scopes) | ||
| 67 | - | ||
| 68 | -proc newLocalhostConfig*(callbackUrl: string, | ||
| 69 | - scopes = @["atproto", "transition:generic"]): ClientConfig = | ||
| 70 | - ## Localhost development clients have no hosted metadata document; the | ||
| 71 | - ## client_id encodes the redirect and scopes instead. | ||
| 72 | - var params = @[("redirect_uri", callbackUrl), ("scope", scopeStr(scopes))] | ||
| 73 | - ClientConfig( | ||
| 74 | - clientId: "http://localhost" & "?" & params.encodeQuery, | ||
| 75 | - callbackUrl: callbackUrl, | ||
| 76 | - scopes: scopes, | ||
| 77 | - ) | ||
| 78 | - | ||
| 79 | -proc clientMetadata*(cfg: ClientConfig): JsonNode = | ||
| 80 | - ## Served at the client_id URL for a public (non-confidential) client. | ||
| 81 | - %*{ | ||
| 82 | - "client_id": cfg.clientId, | ||
| 83 | - "application_type": "web", | ||
| 84 | - "client_name": "glean", | ||
| 85 | - "grant_types": ["authorization_code", "refresh_token"], | ||
| 86 | - "scope": scopeStr(cfg.scopes), | ||
| 87 | - "response_types": ["code"], | ||
| 88 | - "redirect_uris": [cfg.callbackUrl], | ||
| 89 | - "token_endpoint_auth_method": "none", | ||
| 90 | - "dpop_bound_access_tokens": true, | ||
| 91 | - } | ||
| 92 | - | ||
| 93 | -proc secureRandomB64*(n: int): string = | ||
| 94 | - var buf = newSeq[byte](n) | ||
| 95 | - doAssert urandom(buf), "system randomness unavailable" | ||
| 96 | - b64u(buf) | ||
| 97 | - | ||
| 98 | -proc s256Challenge*(verifier: string): string = | ||
| 99 | - b64u(sha256.digest(verifier).data) | ||
| 100 | - | ||
| 101 | -# --- discovery ------------------------------------------------------------- | ||
| 102 | - | ||
| 103 | -proc resolveAuthServerUrl*(pdsUrl: string): string = | ||
| 104 | - ## The PDS advertises which authorization server governs it. | ||
| 105 | - let url = &"https://{hostOf(pdsUrl)}/.well-known/oauth-protected-resource" | ||
| 106 | - let client = newClient() | ||
| 107 | - defer: client.close() | ||
| 108 | - let res = client.get(url) | ||
| 109 | - if res.code.int != 200: | ||
| 110 | - raise newException(OAuthError, | ||
| 111 | - &"protected resource metadata failed (HTTP {res.code.int}) at {url}") | ||
| 112 | - let body = parseJson(res.body) | ||
| 113 | - let servers = body{"authorization_servers"} | ||
| 114 | - if servers == nil or servers.kind != JArray or servers.len == 0: | ||
| 115 | - raise newException(OAuthError, "PDS lists no authorization servers: " & pdsUrl) | ||
| 116 | - servers[0].getStr.strip(chars = {'/'}) | ||
| 117 | - | ||
| 118 | -proc resolveAuthServerMetadata*(authServerUrl: string): AuthServerMetadata = | ||
| 119 | - let url = &"https://{hostOf(authServerUrl)}/.well-known/oauth-authorization-server" | ||
| 120 | - let client = newClient() | ||
| 121 | - defer: client.close() | ||
| 122 | - let res = client.get(url) | ||
| 123 | - if res.code.int != 200: | ||
| 124 | - raise newException(OAuthError, | ||
| 125 | - &"auth server metadata failed (HTTP {res.code.int}) at {url}") | ||
| 126 | - let b = parseJson(res.body) | ||
| 127 | - | ||
| 128 | - result = AuthServerMetadata( | ||
| 129 | - issuer: b{"issuer"}.getStr, | ||
| 130 | - authorizationEndpoint: b{"authorization_endpoint"}.getStr, | ||
| 131 | - tokenEndpoint: b{"token_endpoint"}.getStr, | ||
| 132 | - parEndpoint: b{"pushed_authorization_request_endpoint"}.getStr, | ||
| 133 | - revocationEndpoint: b{"revocation_endpoint"}.getStr, | ||
| 134 | - ) | ||
| 135 | - | ||
| 136 | - # An auth server missing any of these cannot complete an ATProto flow, and | ||
| 137 | - # failing here beats a confusing error three requests later. | ||
| 138 | - for (name, value) in { | ||
| 139 | - "issuer": result.issuer, | ||
| 140 | - "authorization_endpoint": result.authorizationEndpoint, | ||
| 141 | - "token_endpoint": result.tokenEndpoint, | ||
| 142 | - "pushed_authorization_request_endpoint": result.parEndpoint, | ||
| 143 | - }: | ||
| 144 | - if value.len == 0: | ||
| 145 | - raise newException(OAuthError, "auth server metadata is missing " & name) | ||
| 146 | - | ||
| 147 | - if result.issuer.strip(chars = {'/'}) != authServerUrl.strip(chars = {'/'}): | ||
| 148 | - raise newException(OAuthError, | ||
| 149 | - &"auth server issuer mismatch: metadata says {result.issuer}, fetched from {authServerUrl}") | ||
| 150 | - | ||
| 151 | -# --- requests -------------------------------------------------------------- | ||
| 152 | - | ||
| 153 | -proc errorReason(body: string): string = | ||
| 154 | - try: | ||
| 155 | - let j = parseJson(body) | ||
| 156 | - result = j{"error"}.getStr | ||
| 157 | - let desc = j{"error_description"}.getStr | ||
| 158 | - if desc.len > 0: | ||
| 159 | - result &= ": " & desc | ||
| 160 | - except CatchableError: | ||
| 161 | - result = "unknown" | ||
| 162 | - if result.len == 0: | ||
| 163 | - result = "unknown" | ||
| 164 | - | ||
| 165 | -proc errorCode(body: string): string = | ||
| 166 | - try: parseJson(body){"error"}.getStr | ||
| 167 | - except CatchableError: "" | ||
| 168 | - | ||
| 169 | -proc postWithDpop(url, body: string, key: P256Key, rng: var HmacDrbgContext, | ||
| 170 | - nonce: var string): Response = | ||
| 171 | - ## POST with a DPoP proof, retrying once when the server demands a nonce. | ||
| 172 | - ## | ||
| 173 | - ## The first request of a session always lacks a nonce, so the retry is the | ||
| 174 | - ## expected path rather than an error case. The auth server signals this | ||
| 175 | - ## with 400 + error=use_dpop_nonce and supplies DPoP-Nonce. | ||
| 176 | - let client = newClient() | ||
| 177 | - defer: client.close() | ||
| 178 | - | ||
| 179 | - for attempt in 0 .. 1: | ||
| 180 | - let proof = dpopProof(key, rng, "POST", url, nonce = nonce) | ||
| 181 | - client.headers = newHttpHeaders({ | ||
| 182 | - "Content-Type": "application/x-www-form-urlencoded", | ||
| 183 | - "DPoP": proof, | ||
| 184 | - }) | ||
| 185 | - result = client.post(url, body = body) | ||
| 186 | - | ||
| 187 | - let supplied = result.headers.getOrDefault("DPoP-Nonce").string | ||
| 188 | - if supplied.len > 0: | ||
| 189 | - nonce = supplied | ||
| 190 | - | ||
| 191 | - if attempt == 0 and result.code.int == 400 and supplied.len > 0: | ||
| 192 | - if errorCode(result.body) == "use_dpop_nonce": | ||
| 193 | - continue | ||
| 194 | - return result | ||
| 195 | - | ||
| 196 | -proc sendAuthRequest*(cfg: ClientConfig, meta: AuthServerMetadata, | ||
| 197 | - rng: var HmacDrbgContext, loginHint = ""): AuthRequestData = | ||
| 198 | - ## Pushed Authorization Request: hand the parameters to the auth server up | ||
| 199 | - ## front and get back a short-lived request_uri to redirect the user with. | ||
| 200 | - let | ||
| 201 | - state = secureRandomB64(16) | ||
| 202 | - verifier = secureRandomB64(48) | ||
| 203 | - key = generateKey(rng) | ||
| 204 | - | ||
| 205 | - var params = @[ | ||
| 206 | - ("client_id", cfg.clientId), | ||
| 207 | - ("state", state), | ||
| 208 | - ("redirect_uri", cfg.callbackUrl), | ||
| 209 | - ("scope", scopeStr(cfg.scopes)), | ||
| 210 | - ("response_type", "code"), | ||
| 211 | - ("code_challenge", s256Challenge(verifier)), | ||
| 212 | - ("code_challenge_method", "S256"), | ||
| 213 | - ] | ||
| 214 | - if loginHint.len > 0: | ||
| 215 | - params.add ("login_hint", loginHint) | ||
| 216 | - | ||
| 217 | - var nonce = "" | ||
| 218 | - let res = postWithDpop(meta.parEndpoint, params.encodeQuery, key, rng, nonce) | ||
| 219 | - if res.code.int notin [200, 201]: | ||
| 220 | - raise newException(OAuthError, | ||
| 221 | - &"PAR failed (HTTP {res.code.int}): {errorReason(res.body)}") | ||
| 222 | - | ||
| 223 | - let requestUri = parseJson(res.body){"request_uri"}.getStr | ||
| 224 | - if requestUri.len == 0: | ||
| 225 | - raise newException(OAuthError, "PAR response has no request_uri") | ||
| 226 | - | ||
| 227 | - AuthRequestData( | ||
| 228 | - state: state, | ||
| 229 | - authServerUrl: meta.issuer, | ||
| 230 | - scopes: cfg.scopes, | ||
| 231 | - pkceVerifier: verifier, | ||
| 232 | - requestUri: requestUri, | ||
| 233 | - tokenEndpoint: meta.tokenEndpoint, | ||
| 234 | - revocationEndpoint: meta.revocationEndpoint, | ||
| 235 | - dpopNonce: nonce, | ||
| 236 | - dpopKey: key, | ||
| 237 | - ) | ||
| 238 | - | ||
| 239 | -proc authorizeUrl*(cfg: ClientConfig, meta: AuthServerMetadata, | ||
| 240 | - info: AuthRequestData): string = | ||
| 241 | - let params = @[("client_id", cfg.clientId), ("request_uri", info.requestUri)] | ||
| 242 | - meta.authorizationEndpoint & "?" & params.encodeQuery | ||
| 243 | - | ||
| 244 | -proc startAuthFlow*(cfg: ClientConfig, identifier: string, | ||
| 245 | - rng: var HmacDrbgContext, | ||
| 246 | - plcUrl = DefaultPlcUrl): (string, AuthRequestData) = | ||
| 247 | - ## Resolve an account (or take an auth server URL directly), push the | ||
| 248 | - ## request, and return the URL to send the user to. | ||
| 249 | - var | ||
| 250 | - authServerUrl: string | ||
| 251 | - accountDid: string | ||
| 252 | - loginHint: string | ||
| 253 | - | ||
| 254 | - if identifier.startsWith("https://"): | ||
| 255 | - authServerUrl = identifier.strip(chars = {'/'}) | ||
| 256 | - else: | ||
| 257 | - let ident = lookup(identifier, plcUrl) | ||
| 258 | - accountDid = ident.did | ||
| 259 | - loginHint = identifier | ||
| 260 | - authServerUrl = resolveAuthServerUrl(ident.pdsEndpoint) | ||
| 261 | - | ||
| 262 | - let meta = resolveAuthServerMetadata(authServerUrl) | ||
| 263 | - var info = sendAuthRequest(cfg, meta, rng, loginHint) | ||
| 264 | - info.accountDid = accountDid | ||
| 265 | - (authorizeUrl(cfg, meta, info), info) | ||
| 266 | - | ||
| 267 | -proc exchangeCode*(cfg: ClientConfig, info: var AuthRequestData, | ||
| 268 | - code: string, rng: var HmacDrbgContext): TokenResponse = | ||
| 269 | - ## Trade the authorization code for tokens, proving possession of both the | ||
| 270 | - ## PKCE verifier and the session's DPoP key. | ||
| 271 | - let params = @[ | ||
| 272 | - ("client_id", cfg.clientId), | ||
| 273 | - ("redirect_uri", cfg.callbackUrl), | ||
| 274 | - ("grant_type", "authorization_code"), | ||
| 275 | - ("code", code), | ||
| 276 | - ("code_verifier", info.pkceVerifier), | ||
| 277 | - ] | ||
| 278 | - var nonce = info.dpopNonce | ||
| 279 | - let res = postWithDpop(info.tokenEndpoint, params.encodeQuery, | ||
| 280 | - info.dpopKey, rng, nonce) | ||
| 281 | - info.dpopNonce = nonce | ||
| 282 | - if res.code.int != 200: | ||
| 283 | - raise newException(OAuthError, | ||
| 284 | - &"token request failed (HTTP {res.code.int}): {errorReason(res.body)}") | ||
| 285 | - | ||
| 286 | - let b = parseJson(res.body) | ||
| 287 | - result = TokenResponse( | ||
| 288 | - accessToken: b{"access_token"}.getStr, | ||
| 289 | - refreshToken: b{"refresh_token"}.getStr, | ||
| 290 | - tokenType: b{"token_type"}.getStr, | ||
| 291 | - expiresIn: b{"expires_in"}.getInt, | ||
| 292 | - scope: b{"scope"}.getStr, | ||
| 293 | - sub: b{"sub"}.getStr, | ||
| 294 | - ) | ||
| 295 | - | ||
| 296 | - # DPoP-bound tokens are useless as bearer tokens; a server answering | ||
| 297 | - # token_type=Bearer means the binding silently did not happen. | ||
| 298 | - if not result.tokenType.toLowerAscii.startsWith("dpop"): | ||
| 299 | - raise newException(OAuthError, | ||
| 300 | - "expected a DPoP-bound token, got token_type=" & result.tokenType) | ||
| 301 | - if result.sub.len == 0: | ||
| 302 | - raise newException(OAuthError, "token response has no subject DID") | ||
| 303 | - if info.accountDid.len > 0 and result.sub != info.accountDid: | ||
| 304 | - raise newException(OAuthError, | ||
| 305 | - &"token subject {result.sub} does not match requested account {info.accountDid}") | ||
| 306 | - | ||
| 307 | -proc refresh*(cfg: ClientConfig, info: var AuthRequestData, | ||
| 308 | - refreshToken: string, rng: var HmacDrbgContext): TokenResponse = | ||
| 309 | - let params = @[ | ||
| 310 | - ("client_id", cfg.clientId), | ||
| 311 | - ("grant_type", "refresh_token"), | ||
| 312 | - ("refresh_token", refreshToken), | ||
| 313 | - ] | ||
| 314 | - var nonce = info.dpopNonce | ||
| 315 | - let res = postWithDpop(info.tokenEndpoint, params.encodeQuery, | ||
| 316 | - info.dpopKey, rng, nonce) | ||
| 317 | - info.dpopNonce = nonce | ||
| 318 | - if res.code.int != 200: | ||
| 319 | - raise newException(OAuthError, | ||
| 320 | - &"refresh failed (HTTP {res.code.int}): {errorReason(res.body)}") | ||
| 321 | - let b = parseJson(res.body) | ||
| 322 | - TokenResponse( | ||
| 323 | - accessToken: b{"access_token"}.getStr, | ||
| 324 | - refreshToken: b{"refresh_token"}.getStr, | ||
| 325 | - tokenType: b{"token_type"}.getStr, | ||
| 326 | - expiresIn: b{"expires_in"}.getInt, | ||
| 327 | - scope: b{"scope"}.getStr, | ||
| 328 | - sub: b{"sub"}.getStr, | ||
| 329 | - ) | ||
deleted
spike/nim/atproto/schema.nim +0 -232 | deleted file mode 100644 | ||
| @@ -1,232 +0,0 @@ | ||
| 1 | -## Glean's schema, in the three databases it uses. | |
| 2 | -## | |
| 3 | -## Extracted mechanically from internal/db/db.go rather than retyped: 44 DDL | |
| 4 | -## statements transcribed by hand would drift from the Go definition, and a | |
| 5 | -## column that differs by a default or a CHECK is the kind of thing that only | |
| 6 | -## shows up as a constraint violation months later. | |
| 7 | -## | |
| 8 | -## Regenerate with tools/extract_schema.py after changing the Go schema. | |
| 9 | - | |
| 10 | -const usersSchema* = [ | |
| 11 | - """CREATE TABLE IF NOT EXISTS users ( | |
| 12 | - did TEXT PRIMARY KEY, | |
| 13 | - indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 14 | - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 15 | - follows_dirty BOOLEAN NOT NULL DEFAULT 1 | |
| 16 | - )""", | |
| 17 | - """CREATE TABLE IF NOT EXISTS follows ( | |
| 18 | - user_did TEXT NOT NULL, | |
| 19 | - target_did TEXT NOT NULL, | |
| 20 | - uri TEXT, | |
| 21 | - cid TEXT, | |
| 22 | - followed_at DATETIME, | |
| 23 | - PRIMARY KEY (user_did, target_did) | |
| 24 | - )""", | |
| 25 | - """CREATE TABLE IF NOT EXISTS oauth_auth_requests ( | |
| 26 | - state TEXT PRIMARY KEY, | |
| 27 | - data TEXT NOT NULL | |
| 28 | - )""", | |
| 29 | - """CREATE TABLE IF NOT EXISTS oauth_sessions ( | |
| 30 | - account_did TEXT NOT NULL, | |
| 31 | - session_id TEXT NOT NULL, | |
| 32 | - data TEXT NOT NULL, | |
| 33 | - PRIMARY KEY (account_did, session_id) | |
| 34 | - )""", | |
| 35 | - """CREATE INDEX IF NOT EXISTS idx_follows_target ON follows(target_did)""", | |
| 36 | - """CREATE INDEX IF NOT EXISTS idx_follows_uri ON follows(uri)""", | |
| 37 | - """CREATE TABLE IF NOT EXISTS user_settings ( | |
| 38 | - did TEXT PRIMARY KEY, | |
| 39 | - languages TEXT, | |
| 40 | - expanded_view BOOLEAN NOT NULL DEFAULT 0, | |
| 41 | - digest_enabled BOOLEAN NOT NULL DEFAULT 0, | |
| 42 | - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | |
| 43 | - )""", | |
| 44 | - """CREATE TABLE IF NOT EXISTS dismissed_recommendations ( | |
| 45 | - user_did TEXT NOT NULL, | |
| 46 | - target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article', 'person')), | |
| 47 | - target_id TEXT NOT NULL, | |
| 48 | - reason TEXT, | |
| 49 | - dismissed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 50 | - PRIMARY KEY (user_did, target_type, target_id) | |
| 51 | - )""", | |
| 52 | - """CREATE TABLE IF NOT EXISTS recommendation_impressions ( | |
| 53 | - user_did TEXT NOT NULL, | |
| 54 | - target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article', 'person')), | |
| 55 | - target_id TEXT NOT NULL, | |
| 56 | - first_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 57 | - last_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 58 | - shown_count INTEGER NOT NULL DEFAULT 1, | |
| 59 | - acted BOOLEAN NOT NULL DEFAULT 0, | |
| 60 | - PRIMARY KEY (user_did, target_type, target_id) | |
| 61 | - )""", | |
| 62 | - """CREATE INDEX IF NOT EXISTS idx_impressions_user_unacted ON recommendation_impressions(user_did, acted, shown_count)""", | |
| 63 | - """CREATE TABLE IF NOT EXISTS jetstream_cursor ( | |
| 64 | - id INTEGER PRIMARY KEY CHECK(id = 1), | |
| 65 | - cursor_us INTEGER NOT NULL | |
| 66 | - )""", | |
| 67 | -] | |
| 68 | - | |
| 69 | -const articlesSchema* = [ | |
| 70 | - """CREATE TABLE IF NOT EXISTS articles.feeds ( | |
| 71 | - feed_url TEXT PRIMARY KEY, | |
| 72 | - title TEXT, | |
| 73 | - site_url TEXT, | |
| 74 | - description TEXT, | |
| 75 | - feed_type TEXT CHECK(feed_type IN ('rss', 'atom', 'json', 'atproto')), | |
| 76 | - last_fetched_at DATETIME, | |
| 77 | - last_error TEXT, | |
| 78 | - subscriber_count INTEGER NOT NULL DEFAULT 0, | |
| 79 | - consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0, | |
| 80 | - error_count INTEGER NOT NULL DEFAULT 0, | |
| 81 | - favicon_url TEXT | |
| 82 | - )""", | |
| 83 | - """CREATE TABLE IF NOT EXISTS articles.subscriptions ( | |
| 84 | - id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 85 | - user_did TEXT NOT NULL, | |
| 86 | - feed_url TEXT NOT NULL, | |
| 87 | - title TEXT, | |
| 88 | - category TEXT, | |
| 89 | - added_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 90 | - uri TEXT, | |
| 91 | - cid TEXT, | |
| 92 | - UNIQUE(user_did, feed_url) | |
| 93 | - )""", | |
| 94 | - """CREATE TABLE IF NOT EXISTS articles.articles ( | |
| 95 | - id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 96 | - feed_url TEXT NOT NULL, | |
| 97 | - guid TEXT NOT NULL, | |
| 98 | - title TEXT NOT NULL DEFAULT '', | |
| 99 | - url TEXT, | |
| 100 | - author TEXT, | |
| 101 | - summary TEXT, | |
| 102 | - content TEXT, | |
| 103 | - full_content TEXT, | |
| 104 | - published DATETIME, | |
| 105 | - updated DATETIME, | |
| 106 | - fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 107 | - language TEXT NOT NULL DEFAULT '', | |
| 108 | - UNIQUE(feed_url, guid) | |
| 109 | - )""", | |
| 110 | - """CREATE TABLE IF NOT EXISTS articles.read_state ( | |
| 111 | - user_did TEXT NOT NULL, | |
| 112 | - article_id INTEGER NOT NULL, | |
| 113 | - is_read BOOLEAN NOT NULL DEFAULT 0, | |
| 114 | - read_at DATETIME, | |
| 115 | - PRIMARY KEY (user_did, article_id) | |
| 116 | - )""", | |
| 117 | - """CREATE TABLE IF NOT EXISTS articles.read_state_history ( | |
| 118 | - user_did TEXT NOT NULL, | |
| 119 | - feed_url TEXT NOT NULL, | |
| 120 | - guid TEXT NOT NULL, | |
| 121 | - is_read BOOLEAN NOT NULL DEFAULT 0, | |
| 122 | - read_at DATETIME, | |
| 123 | - PRIMARY KEY (user_did, feed_url, guid) | |
| 124 | - )""", | |
| 125 | - """CREATE TABLE IF NOT EXISTS articles.annotations ( | |
| 126 | - id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 127 | - uri TEXT NOT NULL UNIQUE, | |
| 128 | - author_did TEXT NOT NULL, | |
| 129 | - feed_url TEXT NOT NULL, | |
| 130 | - article_url TEXT NOT NULL, | |
| 131 | - quote TEXT, | |
| 132 | - note TEXT, | |
| 133 | - tags TEXT, | |
| 134 | - rating INTEGER, | |
| 135 | - created_at DATETIME NOT NULL, | |
| 136 | - cid TEXT | |
| 137 | - )""", | |
| 138 | - """CREATE TABLE IF NOT EXISTS articles.likes ( | |
| 139 | - id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 140 | - uri TEXT NOT NULL UNIQUE, | |
| 141 | - author_did TEXT NOT NULL, | |
| 142 | - feed_url TEXT NOT NULL, | |
| 143 | - article_url TEXT NOT NULL, | |
| 144 | - created_at DATETIME NOT NULL, | |
| 145 | - cid TEXT, | |
| 146 | - UNIQUE(author_did, feed_url, article_url) | |
| 147 | - )""", | |
| 148 | - """CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed_user ON subscriptions(feed_url, user_did)""", | |
| 149 | - """CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_uri ON subscriptions(uri)""", | |
| 150 | - """CREATE INDEX IF NOT EXISTS articles.idx_likes_author_feed ON likes(author_did, feed_url, created_at)""", | |
| 151 | - """CREATE INDEX IF NOT EXISTS articles.idx_articles_url ON articles(url)""", | |
| 152 | - """CREATE INDEX IF NOT EXISTS articles.idx_read_state_unread ON read_state(user_did, is_read) WHERE is_read = 0""", | |
| 153 | - """CREATE INDEX IF NOT EXISTS articles.idx_annotations_article ON annotations(article_url)""", | |
| 154 | - """CREATE INDEX IF NOT EXISTS articles.idx_annotations_author ON annotations(author_did)""", | |
| 155 | - """CREATE INDEX IF NOT EXISTS articles.idx_annotations_created_at ON annotations(created_at DESC)""", | |
| 156 | - """CREATE INDEX IF NOT EXISTS articles.idx_likes_created_at ON likes(created_at DESC)""", | |
| 157 | - """CREATE INDEX IF NOT EXISTS articles.idx_articles_language ON articles(language)""", | |
| 158 | - """CREATE INDEX IF NOT EXISTS articles.idx_likes_article_created ON likes(feed_url, article_url, created_at)""", | |
| 159 | - """CREATE INDEX IF NOT EXISTS articles.idx_annotations_feed_article_created ON annotations(feed_url, article_url, created_at)""", | |
| 160 | - """CREATE INDEX IF NOT EXISTS articles.idx_likes_created_author ON likes(created_at, author_did)""", | |
| 161 | - """CREATE VIRTUAL TABLE IF NOT EXISTS articles.articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)""", | |
| 162 | - """CREATE TRIGGER IF NOT EXISTS articles.articles_ai AFTER INSERT ON articles BEGIN | |
| 163 | - INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); | |
| 164 | - END""", | |
| 165 | - """CREATE TRIGGER IF NOT EXISTS articles.articles_ad AFTER DELETE ON articles BEGIN | |
| 166 | - INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); | |
| 167 | - END""", | |
| 168 | - """CREATE TRIGGER IF NOT EXISTS articles.articles_au AFTER UPDATE ON articles BEGIN | |
| 169 | - INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); | |
| 170 | - INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); | |
| 171 | - END""", | |
| 172 | -] | |
| 173 | - | |
| 174 | -const recsSchema* = [ | |
| 175 | - """CREATE TABLE IF NOT EXISTS recs.feed_similarity ( | |
| 176 | - feed_a TEXT NOT NULL, | |
| 177 | - feed_b TEXT NOT NULL, | |
| 178 | - jaccard REAL NOT NULL, | |
| 179 | - computed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 180 | - PRIMARY KEY (feed_a, feed_b), | |
| 181 | - CHECK(feed_a < feed_b) | |
| 182 | - )""", | |
| 183 | - """CREATE TABLE IF NOT EXISTS recs.user_similarity ( | |
| 184 | - user_a TEXT NOT NULL, | |
| 185 | - user_b TEXT NOT NULL, | |
| 186 | - jaccard REAL NOT NULL, | |
| 187 | - common_feeds INTEGER NOT NULL, | |
| 188 | - common_likes INTEGER NOT NULL DEFAULT 0, | |
| 189 | - common_tags INTEGER NOT NULL DEFAULT 0, | |
| 190 | - computed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 191 | - PRIMARY KEY (user_a, user_b), | |
| 192 | - CHECK(user_a < user_b) | |
| 193 | - )""", | |
| 194 | - """CREATE TABLE IF NOT EXISTS recs.follow_distances ( | |
| 195 | - user_a TEXT NOT NULL, | |
| 196 | - user_b TEXT NOT NULL, | |
| 197 | - distance INTEGER NOT NULL CHECK(distance IN (1, 2, 3)), | |
| 198 | - PRIMARY KEY (user_a, user_b) | |
| 199 | - )""", | |
| 200 | - """CREATE TABLE IF NOT EXISTS recs.user_signal_weights ( | |
| 201 | - user_did TEXT PRIMARY KEY, | |
| 202 | - w_sub REAL NOT NULL DEFAULT 1.0, | |
| 203 | - w_like REAL NOT NULL DEFAULT 0.5, | |
| 204 | - w_tag REAL NOT NULL DEFAULT 0.3, | |
| 205 | - w_social REAL NOT NULL DEFAULT 0.7, | |
| 206 | - w_pop REAL NOT NULL DEFAULT 0.2, | |
| 207 | - w_category REAL NOT NULL DEFAULT 0.4, | |
| 208 | - w_content REAL NOT NULL DEFAULT 0.4, | |
| 209 | - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | |
| 210 | - )""", | |
| 211 | - """CREATE TABLE IF NOT EXISTS recs.user_signal_profiles ( | |
| 212 | - user_did TEXT PRIMARY KEY, | |
| 213 | - total_likes INTEGER NOT NULL DEFAULT 0, | |
| 214 | - total_tags INTEGER NOT NULL DEFAULT 0, | |
| 215 | - top_categories TEXT, | |
| 216 | - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | |
| 217 | - )""", | |
| 218 | - """CREATE TABLE IF NOT EXISTS recs.feed_embedding_meta ( | |
| 219 | - feed_url TEXT PRIMARY KEY, | |
| 220 | - source_text TEXT NOT NULL DEFAULT '' | |
| 221 | - )""", | |
| 222 | - """CREATE INDEX IF NOT EXISTS recs.idx_follow_distances_a_dist ON follow_distances(user_a, distance)""", | |
| 223 | - """CREATE INDEX IF NOT EXISTS recs.idx_user_similarity_b ON user_similarity(user_b)""", | |
| 224 | - """CREATE TABLE IF NOT EXISTS recs.precomputed_recommendations ( | |
| 225 | - user_did TEXT NOT NULL, | |
| 226 | - rec_type TEXT NOT NULL CHECK(rec_type IN ('feed', 'article', 'person')), | |
| 227 | - data TEXT NOT NULL, | |
| 228 | - computed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 229 | - PRIMARY KEY (user_did, rec_type) | |
| 230 | - )""", | |
| 231 | -] | |
| 232 | - | |
| deleted file mode 100644 | |||
| @@ -1,232 +0,0 @@ | |||
| 1 | -## Glean's schema, in the three databases it uses. | ||
| 2 | -## | ||
| 3 | -## Extracted mechanically from internal/db/db.go rather than retyped: 44 DDL | ||
| 4 | -## statements transcribed by hand would drift from the Go definition, and a | ||
| 5 | -## column that differs by a default or a CHECK is the kind of thing that only | ||
| 6 | -## shows up as a constraint violation months later. | ||
| 7 | -## | ||
| 8 | -## Regenerate with tools/extract_schema.py after changing the Go schema. | ||
| 9 | - | ||
| 10 | -const usersSchema* = [ | ||
| 11 | - """CREATE TABLE IF NOT EXISTS users ( | ||
| 12 | - did TEXT PRIMARY KEY, | ||
| 13 | - indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 14 | - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 15 | - follows_dirty BOOLEAN NOT NULL DEFAULT 1 | ||
| 16 | - )""", | ||
| 17 | - """CREATE TABLE IF NOT EXISTS follows ( | ||
| 18 | - user_did TEXT NOT NULL, | ||
| 19 | - target_did TEXT NOT NULL, | ||
| 20 | - uri TEXT, | ||
| 21 | - cid TEXT, | ||
| 22 | - followed_at DATETIME, | ||
| 23 | - PRIMARY KEY (user_did, target_did) | ||
| 24 | - )""", | ||
| 25 | - """CREATE TABLE IF NOT EXISTS oauth_auth_requests ( | ||
| 26 | - state TEXT PRIMARY KEY, | ||
| 27 | - data TEXT NOT NULL | ||
| 28 | - )""", | ||
| 29 | - """CREATE TABLE IF NOT EXISTS oauth_sessions ( | ||
| 30 | - account_did TEXT NOT NULL, | ||
| 31 | - session_id TEXT NOT NULL, | ||
| 32 | - data TEXT NOT NULL, | ||
| 33 | - PRIMARY KEY (account_did, session_id) | ||
| 34 | - )""", | ||
| 35 | - """CREATE INDEX IF NOT EXISTS idx_follows_target ON follows(target_did)""", | ||
| 36 | - """CREATE INDEX IF NOT EXISTS idx_follows_uri ON follows(uri)""", | ||
| 37 | - """CREATE TABLE IF NOT EXISTS user_settings ( | ||
| 38 | - did TEXT PRIMARY KEY, | ||
| 39 | - languages TEXT, | ||
| 40 | - expanded_view BOOLEAN NOT NULL DEFAULT 0, | ||
| 41 | - digest_enabled BOOLEAN NOT NULL DEFAULT 0, | ||
| 42 | - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | ||
| 43 | - )""", | ||
| 44 | - """CREATE TABLE IF NOT EXISTS dismissed_recommendations ( | ||
| 45 | - user_did TEXT NOT NULL, | ||
| 46 | - target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article', 'person')), | ||
| 47 | - target_id TEXT NOT NULL, | ||
| 48 | - reason TEXT, | ||
| 49 | - dismissed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 50 | - PRIMARY KEY (user_did, target_type, target_id) | ||
| 51 | - )""", | ||
| 52 | - """CREATE TABLE IF NOT EXISTS recommendation_impressions ( | ||
| 53 | - user_did TEXT NOT NULL, | ||
| 54 | - target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article', 'person')), | ||
| 55 | - target_id TEXT NOT NULL, | ||
| 56 | - first_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 57 | - last_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 58 | - shown_count INTEGER NOT NULL DEFAULT 1, | ||
| 59 | - acted BOOLEAN NOT NULL DEFAULT 0, | ||
| 60 | - PRIMARY KEY (user_did, target_type, target_id) | ||
| 61 | - )""", | ||
| 62 | - """CREATE INDEX IF NOT EXISTS idx_impressions_user_unacted ON recommendation_impressions(user_did, acted, shown_count)""", | ||
| 63 | - """CREATE TABLE IF NOT EXISTS jetstream_cursor ( | ||
| 64 | - id INTEGER PRIMARY KEY CHECK(id = 1), | ||
| 65 | - cursor_us INTEGER NOT NULL | ||
| 66 | - )""", | ||
| 67 | -] | ||
| 68 | - | ||
| 69 | -const articlesSchema* = [ | ||
| 70 | - """CREATE TABLE IF NOT EXISTS articles.feeds ( | ||
| 71 | - feed_url TEXT PRIMARY KEY, | ||
| 72 | - title TEXT, | ||
| 73 | - site_url TEXT, | ||
| 74 | - description TEXT, | ||
| 75 | - feed_type TEXT CHECK(feed_type IN ('rss', 'atom', 'json', 'atproto')), | ||
| 76 | - last_fetched_at DATETIME, | ||
| 77 | - last_error TEXT, | ||
| 78 | - subscriber_count INTEGER NOT NULL DEFAULT 0, | ||
| 79 | - consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0, | ||
| 80 | - error_count INTEGER NOT NULL DEFAULT 0, | ||
| 81 | - favicon_url TEXT | ||
| 82 | - )""", | ||
| 83 | - """CREATE TABLE IF NOT EXISTS articles.subscriptions ( | ||
| 84 | - id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 85 | - user_did TEXT NOT NULL, | ||
| 86 | - feed_url TEXT NOT NULL, | ||
| 87 | - title TEXT, | ||
| 88 | - category TEXT, | ||
| 89 | - added_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 90 | - uri TEXT, | ||
| 91 | - cid TEXT, | ||
| 92 | - UNIQUE(user_did, feed_url) | ||
| 93 | - )""", | ||
| 94 | - """CREATE TABLE IF NOT EXISTS articles.articles ( | ||
| 95 | - id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 96 | - feed_url TEXT NOT NULL, | ||
| 97 | - guid TEXT NOT NULL, | ||
| 98 | - title TEXT NOT NULL DEFAULT '', | ||
| 99 | - url TEXT, | ||
| 100 | - author TEXT, | ||
| 101 | - summary TEXT, | ||
| 102 | - content TEXT, | ||
| 103 | - full_content TEXT, | ||
| 104 | - published DATETIME, | ||
| 105 | - updated DATETIME, | ||
| 106 | - fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 107 | - language TEXT NOT NULL DEFAULT '', | ||
| 108 | - UNIQUE(feed_url, guid) | ||
| 109 | - )""", | ||
| 110 | - """CREATE TABLE IF NOT EXISTS articles.read_state ( | ||
| 111 | - user_did TEXT NOT NULL, | ||
| 112 | - article_id INTEGER NOT NULL, | ||
| 113 | - is_read BOOLEAN NOT NULL DEFAULT 0, | ||
| 114 | - read_at DATETIME, | ||
| 115 | - PRIMARY KEY (user_did, article_id) | ||
| 116 | - )""", | ||
| 117 | - """CREATE TABLE IF NOT EXISTS articles.read_state_history ( | ||
| 118 | - user_did TEXT NOT NULL, | ||
| 119 | - feed_url TEXT NOT NULL, | ||
| 120 | - guid TEXT NOT NULL, | ||
| 121 | - is_read BOOLEAN NOT NULL DEFAULT 0, | ||
| 122 | - read_at DATETIME, | ||
| 123 | - PRIMARY KEY (user_did, feed_url, guid) | ||
| 124 | - )""", | ||
| 125 | - """CREATE TABLE IF NOT EXISTS articles.annotations ( | ||
| 126 | - id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 127 | - uri TEXT NOT NULL UNIQUE, | ||
| 128 | - author_did TEXT NOT NULL, | ||
| 129 | - feed_url TEXT NOT NULL, | ||
| 130 | - article_url TEXT NOT NULL, | ||
| 131 | - quote TEXT, | ||
| 132 | - note TEXT, | ||
| 133 | - tags TEXT, | ||
| 134 | - rating INTEGER, | ||
| 135 | - created_at DATETIME NOT NULL, | ||
| 136 | - cid TEXT | ||
| 137 | - )""", | ||
| 138 | - """CREATE TABLE IF NOT EXISTS articles.likes ( | ||
| 139 | - id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 140 | - uri TEXT NOT NULL UNIQUE, | ||
| 141 | - author_did TEXT NOT NULL, | ||
| 142 | - feed_url TEXT NOT NULL, | ||
| 143 | - article_url TEXT NOT NULL, | ||
| 144 | - created_at DATETIME NOT NULL, | ||
| 145 | - cid TEXT, | ||
| 146 | - UNIQUE(author_did, feed_url, article_url) | ||
| 147 | - )""", | ||
| 148 | - """CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed_user ON subscriptions(feed_url, user_did)""", | ||
| 149 | - """CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_uri ON subscriptions(uri)""", | ||
| 150 | - """CREATE INDEX IF NOT EXISTS articles.idx_likes_author_feed ON likes(author_did, feed_url, created_at)""", | ||
| 151 | - """CREATE INDEX IF NOT EXISTS articles.idx_articles_url ON articles(url)""", | ||
| 152 | - """CREATE INDEX IF NOT EXISTS articles.idx_read_state_unread ON read_state(user_did, is_read) WHERE is_read = 0""", | ||
| 153 | - """CREATE INDEX IF NOT EXISTS articles.idx_annotations_article ON annotations(article_url)""", | ||
| 154 | - """CREATE INDEX IF NOT EXISTS articles.idx_annotations_author ON annotations(author_did)""", | ||
| 155 | - """CREATE INDEX IF NOT EXISTS articles.idx_annotations_created_at ON annotations(created_at DESC)""", | ||
| 156 | - """CREATE INDEX IF NOT EXISTS articles.idx_likes_created_at ON likes(created_at DESC)""", | ||
| 157 | - """CREATE INDEX IF NOT EXISTS articles.idx_articles_language ON articles(language)""", | ||
| 158 | - """CREATE INDEX IF NOT EXISTS articles.idx_likes_article_created ON likes(feed_url, article_url, created_at)""", | ||
| 159 | - """CREATE INDEX IF NOT EXISTS articles.idx_annotations_feed_article_created ON annotations(feed_url, article_url, created_at)""", | ||
| 160 | - """CREATE INDEX IF NOT EXISTS articles.idx_likes_created_author ON likes(created_at, author_did)""", | ||
| 161 | - """CREATE VIRTUAL TABLE IF NOT EXISTS articles.articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)""", | ||
| 162 | - """CREATE TRIGGER IF NOT EXISTS articles.articles_ai AFTER INSERT ON articles BEGIN | ||
| 163 | - INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); | ||
| 164 | - END""", | ||
| 165 | - """CREATE TRIGGER IF NOT EXISTS articles.articles_ad AFTER DELETE ON articles BEGIN | ||
| 166 | - INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); | ||
| 167 | - END""", | ||
| 168 | - """CREATE TRIGGER IF NOT EXISTS articles.articles_au AFTER UPDATE ON articles BEGIN | ||
| 169 | - INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); | ||
| 170 | - INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); | ||
| 171 | - END""", | ||
| 172 | -] | ||
| 173 | - | ||
| 174 | -const recsSchema* = [ | ||
| 175 | - """CREATE TABLE IF NOT EXISTS recs.feed_similarity ( | ||
| 176 | - feed_a TEXT NOT NULL, | ||
| 177 | - feed_b TEXT NOT NULL, | ||
| 178 | - jaccard REAL NOT NULL, | ||
| 179 | - computed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 180 | - PRIMARY KEY (feed_a, feed_b), | ||
| 181 | - CHECK(feed_a < feed_b) | ||
| 182 | - )""", | ||
| 183 | - """CREATE TABLE IF NOT EXISTS recs.user_similarity ( | ||
| 184 | - user_a TEXT NOT NULL, | ||
| 185 | - user_b TEXT NOT NULL, | ||
| 186 | - jaccard REAL NOT NULL, | ||
| 187 | - common_feeds INTEGER NOT NULL, | ||
| 188 | - common_likes INTEGER NOT NULL DEFAULT 0, | ||
| 189 | - common_tags INTEGER NOT NULL DEFAULT 0, | ||
| 190 | - computed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 191 | - PRIMARY KEY (user_a, user_b), | ||
| 192 | - CHECK(user_a < user_b) | ||
| 193 | - )""", | ||
| 194 | - """CREATE TABLE IF NOT EXISTS recs.follow_distances ( | ||
| 195 | - user_a TEXT NOT NULL, | ||
| 196 | - user_b TEXT NOT NULL, | ||
| 197 | - distance INTEGER NOT NULL CHECK(distance IN (1, 2, 3)), | ||
| 198 | - PRIMARY KEY (user_a, user_b) | ||
| 199 | - )""", | ||
| 200 | - """CREATE TABLE IF NOT EXISTS recs.user_signal_weights ( | ||
| 201 | - user_did TEXT PRIMARY KEY, | ||
| 202 | - w_sub REAL NOT NULL DEFAULT 1.0, | ||
| 203 | - w_like REAL NOT NULL DEFAULT 0.5, | ||
| 204 | - w_tag REAL NOT NULL DEFAULT 0.3, | ||
| 205 | - w_social REAL NOT NULL DEFAULT 0.7, | ||
| 206 | - w_pop REAL NOT NULL DEFAULT 0.2, | ||
| 207 | - w_category REAL NOT NULL DEFAULT 0.4, | ||
| 208 | - w_content REAL NOT NULL DEFAULT 0.4, | ||
| 209 | - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | ||
| 210 | - )""", | ||
| 211 | - """CREATE TABLE IF NOT EXISTS recs.user_signal_profiles ( | ||
| 212 | - user_did TEXT PRIMARY KEY, | ||
| 213 | - total_likes INTEGER NOT NULL DEFAULT 0, | ||
| 214 | - total_tags INTEGER NOT NULL DEFAULT 0, | ||
| 215 | - top_categories TEXT, | ||
| 216 | - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | ||
| 217 | - )""", | ||
| 218 | - """CREATE TABLE IF NOT EXISTS recs.feed_embedding_meta ( | ||
| 219 | - feed_url TEXT PRIMARY KEY, | ||
| 220 | - source_text TEXT NOT NULL DEFAULT '' | ||
| 221 | - )""", | ||
| 222 | - """CREATE INDEX IF NOT EXISTS recs.idx_follow_distances_a_dist ON follow_distances(user_a, distance)""", | ||
| 223 | - """CREATE INDEX IF NOT EXISTS recs.idx_user_similarity_b ON user_similarity(user_b)""", | ||
| 224 | - """CREATE TABLE IF NOT EXISTS recs.precomputed_recommendations ( | ||
| 225 | - user_did TEXT NOT NULL, | ||
| 226 | - rec_type TEXT NOT NULL CHECK(rec_type IN ('feed', 'article', 'person')), | ||
| 227 | - data TEXT NOT NULL, | ||
| 228 | - computed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 229 | - PRIMARY KEY (user_did, rec_type) | ||
| 230 | - )""", | ||
| 231 | -] | ||
| 232 | - | ||
deleted
spike/nim/atproto/socialstore.nim +0 -388 | deleted file mode 100644 | ||
| @@ -1,388 +0,0 @@ | ||
| 1 | -## Likes, annotations and the social reads built on them, ported from | |
| 2 | -## internal/db/social.go. | |
| 3 | -## | |
| 4 | -## Likes and annotations are ATProto records first and rows second. They are | |
| 5 | -## keyed by `uri` (the at:// record URI) because that is the identity the PDS | |
| 6 | -## owns, and every one of them can arrive twice -- once from the Jetstream | |
| 7 | -## firehose and once from a PDS sync -- so every write here is idempotent by | |
| 8 | -## construction rather than by checking first. | |
| 9 | - | |
| 10 | -import std/[options, strutils, times] | |
| 11 | -import ./articlestore | |
| 12 | -import ./feedstore | |
| 13 | -import ./gleandb | |
| 14 | -import ./sqlite | |
| 15 | - | |
| 16 | -type | |
| 17 | - Annotation* = object | |
| 18 | - id*: int64 | |
| 19 | - uri*: string | |
| 20 | - authorDid*: string | |
| 21 | - feedUrl*: string | |
| 22 | - articleUrl*: string | |
| 23 | - quote*: string | |
| 24 | - note*: string | |
| 25 | - tags*: seq[string] | |
| 26 | - rating*: Option[int] | |
| 27 | - createdAt*: Option[DateTime] | |
| 28 | - cid*: string | |
| 29 | - | |
| 30 | - Like* = object | |
| 31 | - id*: int64 | |
| 32 | - uri*: string | |
| 33 | - authorDid*: string | |
| 34 | - feedUrl*: string | |
| 35 | - articleUrl*: string | |
| 36 | - createdAt*: Option[DateTime] | |
| 37 | - cid*: string | |
| 38 | - | |
| 39 | - TrendingItem* = object | |
| 40 | - articleId*: int64 | |
| 41 | - title*: string | |
| 42 | - url*: string | |
| 43 | - author*: string | |
| 44 | - summary*: string | |
| 45 | - feedUrl*: string | |
| 46 | - feedTitle*: string | |
| 47 | - faviconUrl*: string | |
| 48 | - likeCount*: int | |
| 49 | - annotationCount*: int | |
| 50 | - hasLiked*: bool | |
| 51 | - | |
| 52 | -# --- helpers --------------------------------------------------------------- | |
| 53 | - | |
| 54 | -proc splitTags(s: string): seq[string] = | |
| 55 | - for t in s.split(','): | |
| 56 | - let v = t.strip() | |
| 57 | - if v.len > 0: result.add v | |
| 58 | - | |
| 59 | -proc joinTags(tags: seq[string]): string = tags.join(",") | |
| 60 | - | |
| 61 | -proc langFilter*(languages: seq[string], prefix: string): (string, seq[Param]) = | |
| 62 | - ## Restrict to the reader's languages, always keeping unknown ones. | |
| 63 | - ## | |
| 64 | - ## `language = ''` means detection has not run yet, not "no language". It is | |
| 65 | - ## included deliberately: excluding it would make new articles vanish for | |
| 66 | - ## anyone with a filter set, and reappear minutes later once classified. | |
| 67 | - if languages.len == 0: | |
| 68 | - return ("", @[]) | |
| 69 | - var holes: seq[string] | |
| 70 | - var params: seq[Param] | |
| 71 | - for l in languages: | |
| 72 | - holes.add "?" | |
| 73 | - params.add p(l) | |
| 74 | - (" AND (" & prefix & "language IN (" & holes.join(",") & ") OR " & | |
| 75 | - prefix & "language = '')", params) | |
| 76 | - | |
| 77 | -# --- annotations ----------------------------------------------------------- | |
| 78 | - | |
| 79 | -proc readAnnotation(r: Row): Annotation = | |
| 80 | - Annotation( | |
| 81 | - id: r.i64(0), | |
| 82 | - uri: r.str(1), | |
| 83 | - authorDid: r.str(2), | |
| 84 | - feedUrl: r.str(3), | |
| 85 | - articleUrl: r.str(4), | |
| 86 | - quote: r.str(5), | |
| 87 | - note: r.str(6), | |
| 88 | - tags: splitTags(r.str(7)), | |
| 89 | - rating: (if r.isNull(8): none(int) else: some(r.i(8))), | |
| 90 | - createdAt: parseSqliteTime(r.str(9)), | |
| 91 | - cid: r.str(10), | |
| 92 | - ) | |
| 93 | - | |
| 94 | -const annotationColumns = """an.id, an.uri, an.author_did, an.feed_url, | |
| 95 | - an.article_url, an.quote, an.note, an.tags, an.rating, an.created_at, an.cid""" | |
| 96 | - | |
| 97 | -proc createAnnotation*(g: GleanDb, a: Annotation) = | |
| 98 | - ## Upsert on `uri`: the same record can arrive from Jetstream and from a | |
| 99 | - ## sync, and an edit keeps the URI while changing the content. | |
| 100 | - g.db.run("""INSERT INTO articles.annotations | |
| 101 | - (uri, author_did, feed_url, article_url, quote, note, tags, | |
| 102 | - rating, created_at, cid) | |
| 103 | - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| 104 | - ON CONFLICT(uri) DO UPDATE SET | |
| 105 | - quote = excluded.quote, | |
| 106 | - note = excluded.note, | |
| 107 | - tags = excluded.tags, | |
| 108 | - rating = excluded.rating, | |
| 109 | - cid = excluded.cid""", | |
| 110 | - p(a.uri), p(a.authorDid), p(a.feedUrl), p(a.articleUrl), | |
| 111 | - pOrNull(a.quote), pOrNull(a.note), pOrNull(joinTags(a.tags)), | |
| 112 | - (if a.rating.isSome: p(a.rating.get.int64) else: nullParam()), | |
| 113 | - p(formatSqliteTime(a.createdAt.get(now().utc))), | |
| 114 | - pOrNull(a.cid)) | |
| 115 | - | |
| 116 | -proc getAnnotation*(g: GleanDb, id: int64): Option[Annotation] = | |
| 117 | - g.db.queryFirst("SELECT " & annotationColumns & | |
| 118 | - " FROM articles.annotations an WHERE an.id = ?", | |
| 119 | - [p(id)], readAnnotation) | |
| 120 | - | |
| 121 | -proc annotationExists*(g: GleanDb, uri: string): bool = | |
| 122 | - g.db.queryInt("SELECT 1 FROM articles.annotations WHERE uri = ?", | |
| 123 | - p(uri)).isSome | |
| 124 | - | |
| 125 | -proc deleteAnnotation*(g: GleanDb, uri: string) = | |
| 126 | - g.db.run("DELETE FROM articles.annotations WHERE uri = ?", p(uri)) | |
| 127 | - | |
| 128 | -proc listAnnotations*(g: GleanDb, feedUrl = "", articleUrl = "", | |
| 129 | - authorDid = "", limit = 50, offset = 0): seq[Annotation] = | |
| 130 | - var sql = "SELECT " & annotationColumns & | |
| 131 | - " FROM articles.annotations an WHERE 1=1" | |
| 132 | - var params: seq[Param] | |
| 133 | - if feedUrl.len > 0: | |
| 134 | - sql &= " AND an.feed_url = ?" | |
| 135 | - params.add p(feedUrl) | |
| 136 | - if articleUrl.len > 0: | |
| 137 | - sql &= " AND an.article_url = ?" | |
| 138 | - params.add p(articleUrl) | |
| 139 | - if authorDid.len > 0: | |
| 140 | - sql &= " AND an.author_did = ?" | |
| 141 | - params.add p(authorDid) | |
| 142 | - sql &= " ORDER BY an.created_at DESC LIMIT ? OFFSET ?" | |
| 143 | - params.add p(limit.int64) | |
| 144 | - params.add p(offset.int64) | |
| 145 | - g.db.queryAll(sql, params, readAnnotation) | |
| 146 | - | |
| 147 | -proc batchCreateAnnotations*(g: GleanDb, annotations: seq[Annotation]) = | |
| 148 | - if annotations.len == 0: return | |
| 149 | - g.db.transaction: | |
| 150 | - var ps = g.db.prepared(""" | |
| 151 | - INSERT INTO articles.annotations | |
| 152 | - (uri, author_did, feed_url, article_url, quote, note, tags, rating, | |
| 153 | - created_at, cid) | |
| 154 | - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| 155 | - ON CONFLICT(uri) DO NOTHING""") | |
| 156 | - defer: ps.finalize() | |
| 157 | - for a in annotations: | |
| 158 | - ps.exec(p(a.uri), p(a.authorDid), p(a.feedUrl), p(a.articleUrl), | |
| 159 | - pOrNull(a.quote), pOrNull(a.note), pOrNull(joinTags(a.tags)), | |
| 160 | - (if a.rating.isSome: p(a.rating.get.int64) else: nullParam()), | |
| 161 | - p(formatSqliteTime(a.createdAt.get(now().utc))), pOrNull(a.cid)) | |
| 162 | - | |
| 163 | -# --- likes ----------------------------------------------------------------- | |
| 164 | - | |
| 165 | -proc readLike(r: Row): Like = | |
| 166 | - Like( | |
| 167 | - id: r.i64(0), | |
| 168 | - uri: r.str(1), | |
| 169 | - authorDid: r.str(2), | |
| 170 | - feedUrl: r.str(3), | |
| 171 | - articleUrl: r.str(4), | |
| 172 | - createdAt: parseSqliteTime(r.str(5)), | |
| 173 | - cid: r.str(6), | |
| 174 | - ) | |
| 175 | - | |
| 176 | -const likeColumns = """l.id, l.uri, l.author_did, l.feed_url, l.article_url, | |
| 177 | - l.created_at, l.cid""" | |
| 178 | - | |
| 179 | -proc createLike*(g: GleanDb, l: Like) = | |
| 180 | - g.db.run("""INSERT INTO articles.likes | |
| 181 | - (uri, author_did, feed_url, article_url, created_at, cid) | |
| 182 | - VALUES (?, ?, ?, ?, ?, ?) | |
| 183 | - ON CONFLICT(uri) DO NOTHING""", | |
| 184 | - p(l.uri), p(l.authorDid), p(l.feedUrl), p(l.articleUrl), | |
| 185 | - p(formatSqliteTime(l.createdAt.get(now().utc))), pOrNull(l.cid)) | |
| 186 | - | |
| 187 | -proc batchCreateLikes*(g: GleanDb, likes: seq[Like]) = | |
| 188 | - if likes.len == 0: return | |
| 189 | - g.db.transaction: | |
| 190 | - var ps = g.db.prepared(""" | |
| 191 | - INSERT INTO articles.likes | |
| 192 | - (uri, author_did, feed_url, article_url, created_at, cid) | |
| 193 | - VALUES (?, ?, ?, ?, ?, ?) | |
| 194 | - ON CONFLICT(uri) DO NOTHING""") | |
| 195 | - defer: ps.finalize() | |
| 196 | - for l in likes: | |
| 197 | - ps.exec(p(l.uri), p(l.authorDid), p(l.feedUrl), p(l.articleUrl), | |
| 198 | - p(formatSqliteTime(l.createdAt.get(now().utc))), pOrNull(l.cid)) | |
| 199 | - | |
| 200 | -proc deleteLike*(g: GleanDb, uri: string) = | |
| 201 | - g.db.run("DELETE FROM articles.likes WHERE uri = ?", p(uri)) | |
| 202 | - | |
| 203 | -proc deleteLikeByUserArticle*(g: GleanDb, authorDid, feedUrl, articleUrl: string) = | |
| 204 | - g.db.run("""DELETE FROM articles.likes | |
| 205 | - WHERE author_did = ? AND feed_url = ? AND article_url = ?""", | |
| 206 | - p(authorDid), p(feedUrl), p(articleUrl)) | |
| 207 | - | |
| 208 | -proc getLike*(g: GleanDb, authorDid, feedUrl, articleUrl: string): Option[Like] = | |
| 209 | - g.db.queryFirst("SELECT " & likeColumns & """ | |
| 210 | - FROM articles.likes l | |
| 211 | - WHERE l.author_did = ? AND l.feed_url = ? AND l.article_url = ?""", | |
| 212 | - [p(authorDid), p(feedUrl), p(articleUrl)], readLike) | |
| 213 | - | |
| 214 | -proc hasLiked*(g: GleanDb, authorDid, feedUrl, articleUrl: string): bool = | |
| 215 | - getLike(g, authorDid, feedUrl, articleUrl).isSome | |
| 216 | - | |
| 217 | -proc getLikeCount*(g: GleanDb, feedUrl, articleUrl: string): int = | |
| 218 | - g.db.queryInt("""SELECT count(*) FROM articles.likes | |
| 219 | - WHERE feed_url = ? AND article_url = ?""", | |
| 220 | - p(feedUrl), p(articleUrl)).get(0).int | |
| 221 | - | |
| 222 | -proc listLikes*(g: GleanDb, authorDid: string; feedUrl = "", | |
| 223 | - limit = 50, offset = 0): seq[Like] = | |
| 224 | - var sql = "SELECT " & likeColumns & | |
| 225 | - " FROM articles.likes l WHERE l.author_did = ?" | |
| 226 | - var params = @[p(authorDid)] | |
| 227 | - if feedUrl.len > 0: | |
| 228 | - sql &= " AND l.feed_url = ?" | |
| 229 | - params.add p(feedUrl) | |
| 230 | - sql &= " ORDER BY l.created_at DESC LIMIT ? OFFSET ?" | |
| 231 | - params.add p(limit.int64) | |
| 232 | - params.add p(offset.int64) | |
| 233 | - g.db.queryAll(sql, params, readLike) | |
| 234 | - | |
| 235 | -# --- reconciliation -------------------------------------------------------- | |
| 236 | - | |
| 237 | -proc deleteOrphanedLikes*(g: GleanDb, userDid: string, activeUris: seq[string]) = | |
| 238 | - ## Remove local likes the user's PDS no longer has. | |
| 239 | - ## | |
| 240 | - ## This is the destructive half of a sync, so an empty active set is treated | |
| 241 | - ## as "delete everything this user liked" only because that is what an empty | |
| 242 | - ## PDS collection means. A caller that failed to *fetch* the collection must | |
| 243 | - ## not reach here -- the difference between "you unliked everything" and | |
| 244 | - ## "the request failed" is not visible from this side. | |
| 245 | - if activeUris.len == 0: | |
| 246 | - g.db.run("DELETE FROM articles.likes WHERE author_did = ?", p(userDid)) | |
| 247 | - return | |
| 248 | - g.db.transaction: | |
| 249 | - g.db.run("CREATE TEMP TABLE IF NOT EXISTS active_uris (uri TEXT PRIMARY KEY)") | |
| 250 | - g.db.run("DELETE FROM active_uris") | |
| 251 | - var ps = g.db.prepared( | |
| 252 | - "INSERT OR IGNORE INTO active_uris (uri) VALUES (?)") | |
| 253 | - defer: ps.finalize() | |
| 254 | - for uri in activeUris: | |
| 255 | - ps.exec(p(uri)) | |
| 256 | - g.db.run("""DELETE FROM articles.likes | |
| 257 | - WHERE author_did = ? | |
| 258 | - AND uri NOT IN (SELECT uri FROM active_uris)""", p(userDid)) | |
| 259 | - | |
| 260 | -proc deleteOrphanedAnnotations*(g: GleanDb, userDid: string, | |
| 261 | - activeUris: seq[string]) = | |
| 262 | - if activeUris.len == 0: | |
| 263 | - g.db.run("DELETE FROM articles.annotations WHERE author_did = ?", p(userDid)) | |
| 264 | - return | |
| 265 | - g.db.transaction: | |
| 266 | - g.db.run("CREATE TEMP TABLE IF NOT EXISTS active_uris (uri TEXT PRIMARY KEY)") | |
| 267 | - g.db.run("DELETE FROM active_uris") | |
| 268 | - var ps = g.db.prepared( | |
| 269 | - "INSERT OR IGNORE INTO active_uris (uri) VALUES (?)") | |
| 270 | - defer: ps.finalize() | |
| 271 | - for uri in activeUris: | |
| 272 | - ps.exec(p(uri)) | |
| 273 | - g.db.run("""DELETE FROM articles.annotations | |
| 274 | - WHERE author_did = ? | |
| 275 | - AND uri NOT IN (SELECT uri FROM active_uris)""", p(userDid)) | |
| 276 | - | |
| 277 | -# --- follows --------------------------------------------------------------- | |
| 278 | - | |
| 279 | -proc upsertFollow*(g: GleanDb, userDid, targetDid, uri, cid: string) = | |
| 280 | - g.db.run("""INSERT INTO follows (user_did, target_did, uri, cid, followed_at) | |
| 281 | - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) | |
| 282 | - ON CONFLICT(user_did, target_did) DO UPDATE SET | |
| 283 | - uri = COALESCE(excluded.uri, follows.uri), | |
| 284 | - cid = COALESCE(excluded.cid, follows.cid)""", | |
| 285 | - p(userDid), p(targetDid), pOrNull(uri), pOrNull(cid)) | |
| 286 | - | |
| 287 | -proc deleteFollow*(g: GleanDb, userDid, targetDid: string) = | |
| 288 | - g.db.run("DELETE FROM follows WHERE user_did = ? AND target_did = ?", | |
| 289 | - p(userDid), p(targetDid)) | |
| 290 | - | |
| 291 | -proc listFollows*(g: GleanDb, userDid: string): seq[string] = | |
| 292 | - for r in g.db.query( | |
| 293 | - "SELECT target_did FROM follows WHERE user_did = ? ORDER BY target_did", | |
| 294 | - p(userDid)): | |
| 295 | - result.add r.str(0) | |
| 296 | - | |
| 297 | -# --- trending -------------------------------------------------------------- | |
| 298 | - | |
| 299 | -proc readTrending(r: Row): TrendingItem = | |
| 300 | - TrendingItem( | |
| 301 | - articleId: r.i64(0), | |
| 302 | - title: r.str(1), | |
| 303 | - url: r.str(2), | |
| 304 | - author: r.str(3), | |
| 305 | - summary: r.str(4), | |
| 306 | - feedUrl: r.str(5), | |
| 307 | - feedTitle: r.str(6), | |
| 308 | - faviconUrl: r.str(7), | |
| 309 | - likeCount: r.i(8), | |
| 310 | - annotationCount: r.i(9), | |
| 311 | - hasLiked: r.b(10), | |
| 312 | - ) | |
| 313 | - | |
| 314 | -const trendingSelect = """ | |
| 315 | - SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), | |
| 316 | - COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), | |
| 317 | - COALESCE(f.favicon_url, ''), | |
| 318 | - COUNT(DISTINCT l.id) AS like_count, | |
| 319 | - COUNT(DISTINCT an.id) AS annotation_count, | |
| 320 | - COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0) | |
| 321 | - FROM articles.likes l | |
| 322 | - JOIN articles.articles ar | |
| 323 | - ON ar.url = l.article_url AND ar.feed_url = l.feed_url | |
| 324 | - LEFT JOIN articles.feeds f ON f.feed_url = l.feed_url | |
| 325 | - LEFT JOIN articles.annotations an | |
| 326 | - ON an.feed_url = l.feed_url AND an.article_url = l.article_url | |
| 327 | - AND an.created_at >= ? | |
| 328 | - LEFT JOIN articles.likes ul | |
| 329 | - ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url | |
| 330 | - AND ul.author_did = ? | |
| 331 | - WHERE l.created_at >= ?""" | |
| 332 | - | |
| 333 | -## Future-dated articles sort last: a feed that publishes with a scheduled | |
| 334 | -## timestamp would otherwise pin itself to the top of trending indefinitely. | |
| 335 | -const trendingOrder = """ | |
| 336 | - GROUP BY ar.id | |
| 337 | - ORDER BY like_count DESC, annotation_count DESC, | |
| 338 | - (CASE WHEN ar.published > datetime('now') THEN 1 ELSE 0 END), | |
| 339 | - ar.published DESC | |
| 340 | - LIMIT ? OFFSET ?""" | |
| 341 | - | |
| 342 | -proc listTrendingArticles*(g: GleanDb, userDid, since: string, | |
| 343 | - limit = 25, offset = 0): seq[TrendingItem] = | |
| 344 | - ## Global trending: everyone's likes. | |
| 345 | - g.db.queryAll(trendingSelect & trendingOrder, | |
| 346 | - [p(since), p(userDid), p(since), p(limit.int64), p(offset.int64)], | |
| 347 | - readTrending) | |
| 348 | - | |
| 349 | -proc listTrendingArticlesForUser*(g: GleanDb, userDid, since: string, | |
| 350 | - languages: seq[string] = @[], | |
| 351 | - limit = 25, offset = 0): seq[TrendingItem] = | |
| 352 | - ## Trending within the reader's own graph: people they follow, people the | |
| 353 | - ## similarity table pairs them with, and themselves. | |
| 354 | - let (filter, langParams) = langFilter(languages, "ar.") | |
| 355 | - var params = @[p(since), p(userDid), p(since), | |
| 356 | - p(userDid), p(userDid), p(userDid), p(userDid), p(userDid)] | |
| 357 | - params.add langParams | |
| 358 | - params.add p(limit.int64) | |
| 359 | - params.add p(offset.int64) | |
| 360 | - g.db.queryAll(trendingSelect & """ | |
| 361 | - AND l.author_did IN ( | |
| 362 | - SELECT CASE WHEN us.user_a = ? THEN us.user_b ELSE us.user_a END | |
| 363 | - FROM recs.user_similarity us | |
| 364 | - WHERE us.user_a = ? OR us.user_b = ? | |
| 365 | - UNION SELECT ? | |
| 366 | - UNION SELECT fo.target_did FROM follows fo WHERE fo.user_did = ? | |
| 367 | - )""" & filter & trendingOrder, params, readTrending) | |
| 368 | - | |
| 369 | -proc listLikedArticles*(g: GleanDb, userDid: string, | |
| 370 | - limit = 25, offset = 0): seq[Article] = | |
| 371 | - ## The reader's own likes, as articles. | |
| 372 | - g.db.queryAll(""" | |
| 373 | - SELECT a.id, a.feed_url, COALESCE(f.title, a.feed_url), f.favicon_url, | |
| 374 | - a.guid, a.title, a.url, a.author, a.summary, a.content, | |
| 375 | - a.full_content, a.published, a.updated, | |
| 376 | - COALESCE(r.is_read, 0), | |
| 377 | - (SELECT count(*) FROM articles.likes l2 WHERE l2.article_url = a.url), | |
| 378 | - 1 | |
| 379 | - FROM articles.likes l | |
| 380 | - JOIN articles.articles a | |
| 381 | - ON a.url = l.article_url AND a.feed_url = l.feed_url | |
| 382 | - LEFT JOIN articles.feeds f ON f.feed_url = a.feed_url | |
| 383 | - LEFT JOIN articles.read_state r ON r.article_id = a.id AND r.user_did = ? | |
| 384 | - WHERE l.author_did = ? | |
| 385 | - ORDER BY l.created_at DESC | |
| 386 | - LIMIT ? OFFSET ?""", | |
| 387 | - [p(userDid), p(userDid), p(limit.int64), p(offset.int64)], | |
| 388 | - proc(r: Row): Article = readArticleRow(r)) | |
| deleted file mode 100644 | |||
| @@ -1,388 +0,0 @@ | |||
| 1 | -## Likes, annotations and the social reads built on them, ported from | ||
| 2 | -## internal/db/social.go. | ||
| 3 | -## | ||
| 4 | -## Likes and annotations are ATProto records first and rows second. They are | ||
| 5 | -## keyed by `uri` (the at:// record URI) because that is the identity the PDS | ||
| 6 | -## owns, and every one of them can arrive twice -- once from the Jetstream | ||
| 7 | -## firehose and once from a PDS sync -- so every write here is idempotent by | ||
| 8 | -## construction rather than by checking first. | ||
| 9 | - | ||
| 10 | -import std/[options, strutils, times] | ||
| 11 | -import ./articlestore | ||
| 12 | -import ./feedstore | ||
| 13 | -import ./gleandb | ||
| 14 | -import ./sqlite | ||
| 15 | - | ||
| 16 | -type | ||
| 17 | - Annotation* = object | ||
| 18 | - id*: int64 | ||
| 19 | - uri*: string | ||
| 20 | - authorDid*: string | ||
| 21 | - feedUrl*: string | ||
| 22 | - articleUrl*: string | ||
| 23 | - quote*: string | ||
| 24 | - note*: string | ||
| 25 | - tags*: seq[string] | ||
| 26 | - rating*: Option[int] | ||
| 27 | - createdAt*: Option[DateTime] | ||
| 28 | - cid*: string | ||
| 29 | - | ||
| 30 | - Like* = object | ||
| 31 | - id*: int64 | ||
| 32 | - uri*: string | ||
| 33 | - authorDid*: string | ||
| 34 | - feedUrl*: string | ||
| 35 | - articleUrl*: string | ||
| 36 | - createdAt*: Option[DateTime] | ||
| 37 | - cid*: string | ||
| 38 | - | ||
| 39 | - TrendingItem* = object | ||
| 40 | - articleId*: int64 | ||
| 41 | - title*: string | ||
| 42 | - url*: string | ||
| 43 | - author*: string | ||
| 44 | - summary*: string | ||
| 45 | - feedUrl*: string | ||
| 46 | - feedTitle*: string | ||
| 47 | - faviconUrl*: string | ||
| 48 | - likeCount*: int | ||
| 49 | - annotationCount*: int | ||
| 50 | - hasLiked*: bool | ||
| 51 | - | ||
| 52 | -# --- helpers --------------------------------------------------------------- | ||
| 53 | - | ||
| 54 | -proc splitTags(s: string): seq[string] = | ||
| 55 | - for t in s.split(','): | ||
| 56 | - let v = t.strip() | ||
| 57 | - if v.len > 0: result.add v | ||
| 58 | - | ||
| 59 | -proc joinTags(tags: seq[string]): string = tags.join(",") | ||
| 60 | - | ||
| 61 | -proc langFilter*(languages: seq[string], prefix: string): (string, seq[Param]) = | ||
| 62 | - ## Restrict to the reader's languages, always keeping unknown ones. | ||
| 63 | - ## | ||
| 64 | - ## `language = ''` means detection has not run yet, not "no language". It is | ||
| 65 | - ## included deliberately: excluding it would make new articles vanish for | ||
| 66 | - ## anyone with a filter set, and reappear minutes later once classified. | ||
| 67 | - if languages.len == 0: | ||
| 68 | - return ("", @[]) | ||
| 69 | - var holes: seq[string] | ||
| 70 | - var params: seq[Param] | ||
| 71 | - for l in languages: | ||
| 72 | - holes.add "?" | ||
| 73 | - params.add p(l) | ||
| 74 | - (" AND (" & prefix & "language IN (" & holes.join(",") & ") OR " & | ||
| 75 | - prefix & "language = '')", params) | ||
| 76 | - | ||
| 77 | -# --- annotations ----------------------------------------------------------- | ||
| 78 | - | ||
| 79 | -proc readAnnotation(r: Row): Annotation = | ||
| 80 | - Annotation( | ||
| 81 | - id: r.i64(0), | ||
| 82 | - uri: r.str(1), | ||
| 83 | - authorDid: r.str(2), | ||
| 84 | - feedUrl: r.str(3), | ||
| 85 | - articleUrl: r.str(4), | ||
| 86 | - quote: r.str(5), | ||
| 87 | - note: r.str(6), | ||
| 88 | - tags: splitTags(r.str(7)), | ||
| 89 | - rating: (if r.isNull(8): none(int) else: some(r.i(8))), | ||
| 90 | - createdAt: parseSqliteTime(r.str(9)), | ||
| 91 | - cid: r.str(10), | ||
| 92 | - ) | ||
| 93 | - | ||
| 94 | -const annotationColumns = """an.id, an.uri, an.author_did, an.feed_url, | ||
| 95 | - an.article_url, an.quote, an.note, an.tags, an.rating, an.created_at, an.cid""" | ||
| 96 | - | ||
| 97 | -proc createAnnotation*(g: GleanDb, a: Annotation) = | ||
| 98 | - ## Upsert on `uri`: the same record can arrive from Jetstream and from a | ||
| 99 | - ## sync, and an edit keeps the URI while changing the content. | ||
| 100 | - g.db.run("""INSERT INTO articles.annotations | ||
| 101 | - (uri, author_did, feed_url, article_url, quote, note, tags, | ||
| 102 | - rating, created_at, cid) | ||
| 103 | - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||
| 104 | - ON CONFLICT(uri) DO UPDATE SET | ||
| 105 | - quote = excluded.quote, | ||
| 106 | - note = excluded.note, | ||
| 107 | - tags = excluded.tags, | ||
| 108 | - rating = excluded.rating, | ||
| 109 | - cid = excluded.cid""", | ||
| 110 | - p(a.uri), p(a.authorDid), p(a.feedUrl), p(a.articleUrl), | ||
| 111 | - pOrNull(a.quote), pOrNull(a.note), pOrNull(joinTags(a.tags)), | ||
| 112 | - (if a.rating.isSome: p(a.rating.get.int64) else: nullParam()), | ||
| 113 | - p(formatSqliteTime(a.createdAt.get(now().utc))), | ||
| 114 | - pOrNull(a.cid)) | ||
| 115 | - | ||
| 116 | -proc getAnnotation*(g: GleanDb, id: int64): Option[Annotation] = | ||
| 117 | - g.db.queryFirst("SELECT " & annotationColumns & | ||
| 118 | - " FROM articles.annotations an WHERE an.id = ?", | ||
| 119 | - [p(id)], readAnnotation) | ||
| 120 | - | ||
| 121 | -proc annotationExists*(g: GleanDb, uri: string): bool = | ||
| 122 | - g.db.queryInt("SELECT 1 FROM articles.annotations WHERE uri = ?", | ||
| 123 | - p(uri)).isSome | ||
| 124 | - | ||
| 125 | -proc deleteAnnotation*(g: GleanDb, uri: string) = | ||
| 126 | - g.db.run("DELETE FROM articles.annotations WHERE uri = ?", p(uri)) | ||
| 127 | - | ||
| 128 | -proc listAnnotations*(g: GleanDb, feedUrl = "", articleUrl = "", | ||
| 129 | - authorDid = "", limit = 50, offset = 0): seq[Annotation] = | ||
| 130 | - var sql = "SELECT " & annotationColumns & | ||
| 131 | - " FROM articles.annotations an WHERE 1=1" | ||
| 132 | - var params: seq[Param] | ||
| 133 | - if feedUrl.len > 0: | ||
| 134 | - sql &= " AND an.feed_url = ?" | ||
| 135 | - params.add p(feedUrl) | ||
| 136 | - if articleUrl.len > 0: | ||
| 137 | - sql &= " AND an.article_url = ?" | ||
| 138 | - params.add p(articleUrl) | ||
| 139 | - if authorDid.len > 0: | ||
| 140 | - sql &= " AND an.author_did = ?" | ||
| 141 | - params.add p(authorDid) | ||
| 142 | - sql &= " ORDER BY an.created_at DESC LIMIT ? OFFSET ?" | ||
| 143 | - params.add p(limit.int64) | ||
| 144 | - params.add p(offset.int64) | ||
| 145 | - g.db.queryAll(sql, params, readAnnotation) | ||
| 146 | - | ||
| 147 | -proc batchCreateAnnotations*(g: GleanDb, annotations: seq[Annotation]) = | ||
| 148 | - if annotations.len == 0: return | ||
| 149 | - g.db.transaction: | ||
| 150 | - var ps = g.db.prepared(""" | ||
| 151 | - INSERT INTO articles.annotations | ||
| 152 | - (uri, author_did, feed_url, article_url, quote, note, tags, rating, | ||
| 153 | - created_at, cid) | ||
| 154 | - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||
| 155 | - ON CONFLICT(uri) DO NOTHING""") | ||
| 156 | - defer: ps.finalize() | ||
| 157 | - for a in annotations: | ||
| 158 | - ps.exec(p(a.uri), p(a.authorDid), p(a.feedUrl), p(a.articleUrl), | ||
| 159 | - pOrNull(a.quote), pOrNull(a.note), pOrNull(joinTags(a.tags)), | ||
| 160 | - (if a.rating.isSome: p(a.rating.get.int64) else: nullParam()), | ||
| 161 | - p(formatSqliteTime(a.createdAt.get(now().utc))), pOrNull(a.cid)) | ||
| 162 | - | ||
| 163 | -# --- likes ----------------------------------------------------------------- | ||
| 164 | - | ||
| 165 | -proc readLike(r: Row): Like = | ||
| 166 | - Like( | ||
| 167 | - id: r.i64(0), | ||
| 168 | - uri: r.str(1), | ||
| 169 | - authorDid: r.str(2), | ||
| 170 | - feedUrl: r.str(3), | ||
| 171 | - articleUrl: r.str(4), | ||
| 172 | - createdAt: parseSqliteTime(r.str(5)), | ||
| 173 | - cid: r.str(6), | ||
| 174 | - ) | ||
| 175 | - | ||
| 176 | -const likeColumns = """l.id, l.uri, l.author_did, l.feed_url, l.article_url, | ||
| 177 | - l.created_at, l.cid""" | ||
| 178 | - | ||
| 179 | -proc createLike*(g: GleanDb, l: Like) = | ||
| 180 | - g.db.run("""INSERT INTO articles.likes | ||
| 181 | - (uri, author_did, feed_url, article_url, created_at, cid) | ||
| 182 | - VALUES (?, ?, ?, ?, ?, ?) | ||
| 183 | - ON CONFLICT(uri) DO NOTHING""", | ||
| 184 | - p(l.uri), p(l.authorDid), p(l.feedUrl), p(l.articleUrl), | ||
| 185 | - p(formatSqliteTime(l.createdAt.get(now().utc))), pOrNull(l.cid)) | ||
| 186 | - | ||
| 187 | -proc batchCreateLikes*(g: GleanDb, likes: seq[Like]) = | ||
| 188 | - if likes.len == 0: return | ||
| 189 | - g.db.transaction: | ||
| 190 | - var ps = g.db.prepared(""" | ||
| 191 | - INSERT INTO articles.likes | ||
| 192 | - (uri, author_did, feed_url, article_url, created_at, cid) | ||
| 193 | - VALUES (?, ?, ?, ?, ?, ?) | ||
| 194 | - ON CONFLICT(uri) DO NOTHING""") | ||
| 195 | - defer: ps.finalize() | ||
| 196 | - for l in likes: | ||
| 197 | - ps.exec(p(l.uri), p(l.authorDid), p(l.feedUrl), p(l.articleUrl), | ||
| 198 | - p(formatSqliteTime(l.createdAt.get(now().utc))), pOrNull(l.cid)) | ||
| 199 | - | ||
| 200 | -proc deleteLike*(g: GleanDb, uri: string) = | ||
| 201 | - g.db.run("DELETE FROM articles.likes WHERE uri = ?", p(uri)) | ||
| 202 | - | ||
| 203 | -proc deleteLikeByUserArticle*(g: GleanDb, authorDid, feedUrl, articleUrl: string) = | ||
| 204 | - g.db.run("""DELETE FROM articles.likes | ||
| 205 | - WHERE author_did = ? AND feed_url = ? AND article_url = ?""", | ||
| 206 | - p(authorDid), p(feedUrl), p(articleUrl)) | ||
| 207 | - | ||
| 208 | -proc getLike*(g: GleanDb, authorDid, feedUrl, articleUrl: string): Option[Like] = | ||
| 209 | - g.db.queryFirst("SELECT " & likeColumns & """ | ||
| 210 | - FROM articles.likes l | ||
| 211 | - WHERE l.author_did = ? AND l.feed_url = ? AND l.article_url = ?""", | ||
| 212 | - [p(authorDid), p(feedUrl), p(articleUrl)], readLike) | ||
| 213 | - | ||
| 214 | -proc hasLiked*(g: GleanDb, authorDid, feedUrl, articleUrl: string): bool = | ||
| 215 | - getLike(g, authorDid, feedUrl, articleUrl).isSome | ||
| 216 | - | ||
| 217 | -proc getLikeCount*(g: GleanDb, feedUrl, articleUrl: string): int = | ||
| 218 | - g.db.queryInt("""SELECT count(*) FROM articles.likes | ||
| 219 | - WHERE feed_url = ? AND article_url = ?""", | ||
| 220 | - p(feedUrl), p(articleUrl)).get(0).int | ||
| 221 | - | ||
| 222 | -proc listLikes*(g: GleanDb, authorDid: string; feedUrl = "", | ||
| 223 | - limit = 50, offset = 0): seq[Like] = | ||
| 224 | - var sql = "SELECT " & likeColumns & | ||
| 225 | - " FROM articles.likes l WHERE l.author_did = ?" | ||
| 226 | - var params = @[p(authorDid)] | ||
| 227 | - if feedUrl.len > 0: | ||
| 228 | - sql &= " AND l.feed_url = ?" | ||
| 229 | - params.add p(feedUrl) | ||
| 230 | - sql &= " ORDER BY l.created_at DESC LIMIT ? OFFSET ?" | ||
| 231 | - params.add p(limit.int64) | ||
| 232 | - params.add p(offset.int64) | ||
| 233 | - g.db.queryAll(sql, params, readLike) | ||
| 234 | - | ||
| 235 | -# --- reconciliation -------------------------------------------------------- | ||
| 236 | - | ||
| 237 | -proc deleteOrphanedLikes*(g: GleanDb, userDid: string, activeUris: seq[string]) = | ||
| 238 | - ## Remove local likes the user's PDS no longer has. | ||
| 239 | - ## | ||
| 240 | - ## This is the destructive half of a sync, so an empty active set is treated | ||
| 241 | - ## as "delete everything this user liked" only because that is what an empty | ||
| 242 | - ## PDS collection means. A caller that failed to *fetch* the collection must | ||
| 243 | - ## not reach here -- the difference between "you unliked everything" and | ||
| 244 | - ## "the request failed" is not visible from this side. | ||
| 245 | - if activeUris.len == 0: | ||
| 246 | - g.db.run("DELETE FROM articles.likes WHERE author_did = ?", p(userDid)) | ||
| 247 | - return | ||
| 248 | - g.db.transaction: | ||
| 249 | - g.db.run("CREATE TEMP TABLE IF NOT EXISTS active_uris (uri TEXT PRIMARY KEY)") | ||
| 250 | - g.db.run("DELETE FROM active_uris") | ||
| 251 | - var ps = g.db.prepared( | ||
| 252 | - "INSERT OR IGNORE INTO active_uris (uri) VALUES (?)") | ||
| 253 | - defer: ps.finalize() | ||
| 254 | - for uri in activeUris: | ||
| 255 | - ps.exec(p(uri)) | ||
| 256 | - g.db.run("""DELETE FROM articles.likes | ||
| 257 | - WHERE author_did = ? | ||
| 258 | - AND uri NOT IN (SELECT uri FROM active_uris)""", p(userDid)) | ||
| 259 | - | ||
| 260 | -proc deleteOrphanedAnnotations*(g: GleanDb, userDid: string, | ||
| 261 | - activeUris: seq[string]) = | ||
| 262 | - if activeUris.len == 0: | ||
| 263 | - g.db.run("DELETE FROM articles.annotations WHERE author_did = ?", p(userDid)) | ||
| 264 | - return | ||
| 265 | - g.db.transaction: | ||
| 266 | - g.db.run("CREATE TEMP TABLE IF NOT EXISTS active_uris (uri TEXT PRIMARY KEY)") | ||
| 267 | - g.db.run("DELETE FROM active_uris") | ||
| 268 | - var ps = g.db.prepared( | ||
| 269 | - "INSERT OR IGNORE INTO active_uris (uri) VALUES (?)") | ||
| 270 | - defer: ps.finalize() | ||
| 271 | - for uri in activeUris: | ||
| 272 | - ps.exec(p(uri)) | ||
| 273 | - g.db.run("""DELETE FROM articles.annotations | ||
| 274 | - WHERE author_did = ? | ||
| 275 | - AND uri NOT IN (SELECT uri FROM active_uris)""", p(userDid)) | ||
| 276 | - | ||
| 277 | -# --- follows --------------------------------------------------------------- | ||
| 278 | - | ||
| 279 | -proc upsertFollow*(g: GleanDb, userDid, targetDid, uri, cid: string) = | ||
| 280 | - g.db.run("""INSERT INTO follows (user_did, target_did, uri, cid, followed_at) | ||
| 281 | - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) | ||
| 282 | - ON CONFLICT(user_did, target_did) DO UPDATE SET | ||
| 283 | - uri = COALESCE(excluded.uri, follows.uri), | ||
| 284 | - cid = COALESCE(excluded.cid, follows.cid)""", | ||
| 285 | - p(userDid), p(targetDid), pOrNull(uri), pOrNull(cid)) | ||
| 286 | - | ||
| 287 | -proc deleteFollow*(g: GleanDb, userDid, targetDid: string) = | ||
| 288 | - g.db.run("DELETE FROM follows WHERE user_did = ? AND target_did = ?", | ||
| 289 | - p(userDid), p(targetDid)) | ||
| 290 | - | ||
| 291 | -proc listFollows*(g: GleanDb, userDid: string): seq[string] = | ||
| 292 | - for r in g.db.query( | ||
| 293 | - "SELECT target_did FROM follows WHERE user_did = ? ORDER BY target_did", | ||
| 294 | - p(userDid)): | ||
| 295 | - result.add r.str(0) | ||
| 296 | - | ||
| 297 | -# --- trending -------------------------------------------------------------- | ||
| 298 | - | ||
| 299 | -proc readTrending(r: Row): TrendingItem = | ||
| 300 | - TrendingItem( | ||
| 301 | - articleId: r.i64(0), | ||
| 302 | - title: r.str(1), | ||
| 303 | - url: r.str(2), | ||
| 304 | - author: r.str(3), | ||
| 305 | - summary: r.str(4), | ||
| 306 | - feedUrl: r.str(5), | ||
| 307 | - feedTitle: r.str(6), | ||
| 308 | - faviconUrl: r.str(7), | ||
| 309 | - likeCount: r.i(8), | ||
| 310 | - annotationCount: r.i(9), | ||
| 311 | - hasLiked: r.b(10), | ||
| 312 | - ) | ||
| 313 | - | ||
| 314 | -const trendingSelect = """ | ||
| 315 | - SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), | ||
| 316 | - COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), | ||
| 317 | - COALESCE(f.favicon_url, ''), | ||
| 318 | - COUNT(DISTINCT l.id) AS like_count, | ||
| 319 | - COUNT(DISTINCT an.id) AS annotation_count, | ||
| 320 | - COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0) | ||
| 321 | - FROM articles.likes l | ||
| 322 | - JOIN articles.articles ar | ||
| 323 | - ON ar.url = l.article_url AND ar.feed_url = l.feed_url | ||
| 324 | - LEFT JOIN articles.feeds f ON f.feed_url = l.feed_url | ||
| 325 | - LEFT JOIN articles.annotations an | ||
| 326 | - ON an.feed_url = l.feed_url AND an.article_url = l.article_url | ||
| 327 | - AND an.created_at >= ? | ||
| 328 | - LEFT JOIN articles.likes ul | ||
| 329 | - ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url | ||
| 330 | - AND ul.author_did = ? | ||
| 331 | - WHERE l.created_at >= ?""" | ||
| 332 | - | ||
| 333 | -## Future-dated articles sort last: a feed that publishes with a scheduled | ||
| 334 | -## timestamp would otherwise pin itself to the top of trending indefinitely. | ||
| 335 | -const trendingOrder = """ | ||
| 336 | - GROUP BY ar.id | ||
| 337 | - ORDER BY like_count DESC, annotation_count DESC, | ||
| 338 | - (CASE WHEN ar.published > datetime('now') THEN 1 ELSE 0 END), | ||
| 339 | - ar.published DESC | ||
| 340 | - LIMIT ? OFFSET ?""" | ||
| 341 | - | ||
| 342 | -proc listTrendingArticles*(g: GleanDb, userDid, since: string, | ||
| 343 | - limit = 25, offset = 0): seq[TrendingItem] = | ||
| 344 | - ## Global trending: everyone's likes. | ||
| 345 | - g.db.queryAll(trendingSelect & trendingOrder, | ||
| 346 | - [p(since), p(userDid), p(since), p(limit.int64), p(offset.int64)], | ||
| 347 | - readTrending) | ||
| 348 | - | ||
| 349 | -proc listTrendingArticlesForUser*(g: GleanDb, userDid, since: string, | ||
| 350 | - languages: seq[string] = @[], | ||
| 351 | - limit = 25, offset = 0): seq[TrendingItem] = | ||
| 352 | - ## Trending within the reader's own graph: people they follow, people the | ||
| 353 | - ## similarity table pairs them with, and themselves. | ||
| 354 | - let (filter, langParams) = langFilter(languages, "ar.") | ||
| 355 | - var params = @[p(since), p(userDid), p(since), | ||
| 356 | - p(userDid), p(userDid), p(userDid), p(userDid), p(userDid)] | ||
| 357 | - params.add langParams | ||
| 358 | - params.add p(limit.int64) | ||
| 359 | - params.add p(offset.int64) | ||
| 360 | - g.db.queryAll(trendingSelect & """ | ||
| 361 | - AND l.author_did IN ( | ||
| 362 | - SELECT CASE WHEN us.user_a = ? THEN us.user_b ELSE us.user_a END | ||
| 363 | - FROM recs.user_similarity us | ||
| 364 | - WHERE us.user_a = ? OR us.user_b = ? | ||
| 365 | - UNION SELECT ? | ||
| 366 | - UNION SELECT fo.target_did FROM follows fo WHERE fo.user_did = ? | ||
| 367 | - )""" & filter & trendingOrder, params, readTrending) | ||
| 368 | - | ||
| 369 | -proc listLikedArticles*(g: GleanDb, userDid: string, | ||
| 370 | - limit = 25, offset = 0): seq[Article] = | ||
| 371 | - ## The reader's own likes, as articles. | ||
| 372 | - g.db.queryAll(""" | ||
| 373 | - SELECT a.id, a.feed_url, COALESCE(f.title, a.feed_url), f.favicon_url, | ||
| 374 | - a.guid, a.title, a.url, a.author, a.summary, a.content, | ||
| 375 | - a.full_content, a.published, a.updated, | ||
| 376 | - COALESCE(r.is_read, 0), | ||
| 377 | - (SELECT count(*) FROM articles.likes l2 WHERE l2.article_url = a.url), | ||
| 378 | - 1 | ||
| 379 | - FROM articles.likes l | ||
| 380 | - JOIN articles.articles a | ||
| 381 | - ON a.url = l.article_url AND a.feed_url = l.feed_url | ||
| 382 | - LEFT JOIN articles.feeds f ON f.feed_url = a.feed_url | ||
| 383 | - LEFT JOIN articles.read_state r ON r.article_id = a.id AND r.user_did = ? | ||
| 384 | - WHERE l.author_did = ? | ||
| 385 | - ORDER BY l.created_at DESC | ||
| 386 | - LIMIT ? OFFSET ?""", | ||
| 387 | - [p(userDid), p(userDid), p(limit.int64), p(offset.int64)], | ||
| 388 | - proc(r: Row): Article = readArticleRow(r)) | ||
deleted
spike/nim/atproto/store.nim +0 -217 | deleted file mode 100644 | ||
| @@ -1,217 +0,0 @@ | ||
| 1 | -## Persisting what an OAuth session and a firehose subscription need to | |
| 2 | -## survive a restart. | |
| 3 | -## | |
| 4 | -## Three things are stored, matching the tables Glean's Go backend uses: | |
| 5 | -## | |
| 6 | -## oauth_auth_requests a flow in progress, keyed by `state` | |
| 7 | -## oauth_sessions a live session, keyed by (account_did, session_id) | |
| 8 | -## jetstream_cursor a single row: where the firehose got to | |
| 9 | -## | |
| 10 | -## The rows hold JSON, as the Go side does, so the schema does not have to | |
| 11 | -## change every time a field is added to a session. | |
| 12 | -## | |
| 13 | -## **This stores key material.** A session row contains the DPoP private key | |
| 14 | -## and the refresh token; together they are the account. The file wants the | |
| 15 | -## same protection as a password store: not world-readable, and not in a | |
| 16 | -## backup that travels somewhere less careful. | |
| 17 | - | |
| 18 | -import std/[json, options, sequtils, times] | |
| 19 | -import ./dpop | |
| 20 | -import ./oauth | |
| 21 | -import ./sqlite | |
| 22 | - | |
| 23 | -type | |
| 24 | - StoreError* = object of CatchableError | |
| 25 | - | |
| 26 | - SessionRecord* = object ## A usable OAuth session. | |
| 27 | - accountDid*: string | |
| 28 | - sessionId*: string | |
| 29 | - pdsUrl*: string | |
| 30 | - authServerUrl*: string | |
| 31 | - tokenEndpoint*: string | |
| 32 | - accessToken*: string | |
| 33 | - refreshToken*: string | |
| 34 | - dpopKey*: P256Key | |
| 35 | - hostNonce*: string | |
| 36 | - authNonce*: string | |
| 37 | - scopes*: seq[string] | |
| 38 | - createdAt*: int64 | |
| 39 | - | |
| 40 | -const Schema = [ | |
| 41 | - """CREATE TABLE IF NOT EXISTS oauth_auth_requests ( | |
| 42 | - state TEXT PRIMARY KEY, | |
| 43 | - data TEXT NOT NULL, | |
| 44 | - created_at INTEGER NOT NULL | |
| 45 | - )""", | |
| 46 | - """CREATE TABLE IF NOT EXISTS oauth_sessions ( | |
| 47 | - account_did TEXT NOT NULL, | |
| 48 | - session_id TEXT NOT NULL, | |
| 49 | - data TEXT NOT NULL, | |
| 50 | - PRIMARY KEY (account_did, session_id) | |
| 51 | - )""", | |
| 52 | - """CREATE TABLE IF NOT EXISTS jetstream_cursor ( | |
| 53 | - id INTEGER PRIMARY KEY CHECK(id = 1), | |
| 54 | - cursor_us INTEGER NOT NULL | |
| 55 | - )""", | |
| 56 | -] | |
| 57 | - | |
| 58 | -proc migrate*(db: Db) = | |
| 59 | - for stmt in Schema: | |
| 60 | - db.exec stmt | |
| 61 | - | |
| 62 | -# --- key serialisation ----------------------------------------------------- | |
| 63 | - | |
| 64 | -proc toJson*(k: P256Key): JsonNode = | |
| 65 | - ## The private scalar plus the public point. | |
| 66 | - ## | |
| 67 | - ## The public half is stored rather than re-derived on load: deriving it | |
| 68 | - ## needs a curve operation, and a stored key that cannot be read back | |
| 69 | - ## without one is a restart that silently drops every session. | |
| 70 | - %*{"d": b64u(k.priv), "x": b64u(k.pubX), "y": b64u(k.pubY)} | |
| 71 | - | |
| 72 | -proc keyFromJson*(j: JsonNode): P256Key = | |
| 73 | - proc part(name: string): seq[byte] = | |
| 74 | - let raw = b64uDecode(j{name}.getStr) | |
| 75 | - if raw.len != 32: | |
| 76 | - raise newException(StoreError, "stored key field " & name & " is not 32 bytes") | |
| 77 | - result = newSeq[byte](32) | |
| 78 | - copyMem(result[0].addr, raw[0].unsafeAddr, 32) | |
| 79 | - | |
| 80 | - let d = part("d") | |
| 81 | - let x = part("x") | |
| 82 | - let y = part("y") | |
| 83 | - for i in 0 ..< 32: | |
| 84 | - result.priv[i] = d[i] | |
| 85 | - result.pubX[i] = x[i] | |
| 86 | - result.pubY[i] = y[i] | |
| 87 | - | |
| 88 | -# --- auth requests --------------------------------------------------------- | |
| 89 | - | |
| 90 | -proc toJson(info: AuthRequestData): JsonNode = | |
| 91 | - %*{ | |
| 92 | - "state": info.state, | |
| 93 | - "auth_server_url": info.authServerUrl, | |
| 94 | - "scopes": info.scopes, | |
| 95 | - "pkce_verifier": info.pkceVerifier, | |
| 96 | - "request_uri": info.requestUri, | |
| 97 | - "token_endpoint": info.tokenEndpoint, | |
| 98 | - "revocation_endpoint": info.revocationEndpoint, | |
| 99 | - "dpop_nonce": info.dpopNonce, | |
| 100 | - "dpop_key": info.dpopKey.toJson, | |
| 101 | - "account_did": info.accountDid, | |
| 102 | - } | |
| 103 | - | |
| 104 | -proc authRequestFromJson(j: JsonNode): AuthRequestData = | |
| 105 | - AuthRequestData( | |
| 106 | - state: j{"state"}.getStr, | |
| 107 | - authServerUrl: j{"auth_server_url"}.getStr, | |
| 108 | - scopes: j{"scopes"}.getElems.mapIt(it.getStr), | |
| 109 | - pkceVerifier: j{"pkce_verifier"}.getStr, | |
| 110 | - requestUri: j{"request_uri"}.getStr, | |
| 111 | - tokenEndpoint: j{"token_endpoint"}.getStr, | |
| 112 | - revocationEndpoint: j{"revocation_endpoint"}.getStr, | |
| 113 | - dpopNonce: j{"dpop_nonce"}.getStr, | |
| 114 | - dpopKey: keyFromJson(j{"dpop_key"}), | |
| 115 | - accountDid: j{"account_did"}.getStr, | |
| 116 | - ) | |
| 117 | - | |
| 118 | -proc saveAuthRequest*(db: Db, info: AuthRequestData) = | |
| 119 | - db.run("""INSERT INTO oauth_auth_requests (state, data, created_at) | |
| 120 | - VALUES (?, ?, ?) | |
| 121 | - ON CONFLICT(state) DO UPDATE SET data = excluded.data""", | |
| 122 | - p(info.state), p($info.toJson), p(getTime().toUnix)) | |
| 123 | - | |
| 124 | -proc takeAuthRequest*(db: Db, state: string): Option[AuthRequestData] = | |
| 125 | - ## Read **and delete**: an authorization code is single-use, so leaving the | |
| 126 | - ## request behind invites a replay of the callback. | |
| 127 | - let row = db.queryText( | |
| 128 | - "SELECT data FROM oauth_auth_requests WHERE state = ?", p(state)) | |
| 129 | - if row.isNone: | |
| 130 | - return none(AuthRequestData) | |
| 131 | - db.run("DELETE FROM oauth_auth_requests WHERE state = ?", p(state)) | |
| 132 | - try: | |
| 133 | - some(authRequestFromJson(parseJson(row.get))) | |
| 134 | - except JsonParsingError: | |
| 135 | - raise newException(StoreError, "stored auth request is not valid JSON") | |
| 136 | - | |
| 137 | -proc purgeAuthRequests*(db: Db, olderThan: int64) = | |
| 138 | - ## Flows that were started and never finished. Without this the table grows | |
| 139 | - ## forever, one row per abandoned sign-in. | |
| 140 | - db.run("DELETE FROM oauth_auth_requests WHERE created_at < ?", | |
| 141 | - p(getTime().toUnix - olderThan)) | |
| 142 | - | |
| 143 | -# --- sessions -------------------------------------------------------------- | |
| 144 | - | |
| 145 | -proc toJson(s: SessionRecord): JsonNode = | |
| 146 | - %*{ | |
| 147 | - "account_did": s.accountDid, | |
| 148 | - "session_id": s.sessionId, | |
| 149 | - "pds_url": s.pdsUrl, | |
| 150 | - "auth_server_url": s.authServerUrl, | |
| 151 | - "token_endpoint": s.tokenEndpoint, | |
| 152 | - "access_token": s.accessToken, | |
| 153 | - "refresh_token": s.refreshToken, | |
| 154 | - "dpop_key": s.dpopKey.toJson, | |
| 155 | - "host_nonce": s.hostNonce, | |
| 156 | - "auth_nonce": s.authNonce, | |
| 157 | - "scopes": s.scopes, | |
| 158 | - "created_at": s.createdAt, | |
| 159 | - } | |
| 160 | - | |
| 161 | -proc sessionFromJson(j: JsonNode): SessionRecord = | |
| 162 | - SessionRecord( | |
| 163 | - accountDid: j{"account_did"}.getStr, | |
| 164 | - sessionId: j{"session_id"}.getStr, | |
| 165 | - pdsUrl: j{"pds_url"}.getStr, | |
| 166 | - authServerUrl: j{"auth_server_url"}.getStr, | |
| 167 | - tokenEndpoint: j{"token_endpoint"}.getStr, | |
| 168 | - accessToken: j{"access_token"}.getStr, | |
| 169 | - refreshToken: j{"refresh_token"}.getStr, | |
| 170 | - dpopKey: keyFromJson(j{"dpop_key"}), | |
| 171 | - hostNonce: j{"host_nonce"}.getStr, | |
| 172 | - authNonce: j{"auth_nonce"}.getStr, | |
| 173 | - scopes: j{"scopes"}.getElems.mapIt(it.getStr), | |
| 174 | - createdAt: j{"created_at"}.getBiggestInt.int64, | |
| 175 | - ) | |
| 176 | - | |
| 177 | -proc saveSession*(db: Db, s: SessionRecord) = | |
| 178 | - if s.accountDid.len == 0 or s.sessionId.len == 0: | |
| 179 | - raise newException(StoreError, "a session needs both a DID and a session id") | |
| 180 | - db.run("""INSERT INTO oauth_sessions (account_did, session_id, data) | |
| 181 | - VALUES (?, ?, ?) | |
| 182 | - ON CONFLICT(account_did, session_id) DO UPDATE SET data = excluded.data""", | |
| 183 | - p(s.accountDid), p(s.sessionId), p($s.toJson)) | |
| 184 | - | |
| 185 | -proc getSession*(db: Db, did, sessionId: string): Option[SessionRecord] = | |
| 186 | - let row = db.queryText( | |
| 187 | - "SELECT data FROM oauth_sessions WHERE account_did = ? AND session_id = ?", | |
| 188 | - p(did), p(sessionId)) | |
| 189 | - if row.isNone: | |
| 190 | - return none(SessionRecord) | |
| 191 | - try: | |
| 192 | - some(sessionFromJson(parseJson(row.get))) | |
| 193 | - except JsonParsingError: | |
| 194 | - raise newException(StoreError, "stored session is not valid JSON") | |
| 195 | - | |
| 196 | -proc deleteSession*(db: Db, did, sessionId: string) = | |
| 197 | - db.run("DELETE FROM oauth_sessions WHERE account_did = ? AND session_id = ?", | |
| 198 | - p(did), p(sessionId)) | |
| 199 | - | |
| 200 | -proc listSessions*(db: Db, did: string): seq[string] = | |
| 201 | - for row in db.rows( | |
| 202 | - "SELECT session_id FROM oauth_sessions WHERE account_did = ?", p(did)): | |
| 203 | - result.add row[0] | |
| 204 | - | |
| 205 | -proc listAccounts*(db: Db): seq[string] = | |
| 206 | - for row in db.rows("SELECT DISTINCT account_did FROM oauth_sessions"): | |
| 207 | - result.add row[0] | |
| 208 | - | |
| 209 | -# --- jetstream cursor ------------------------------------------------------ | |
| 210 | - | |
| 211 | -proc saveCursor*(db: Db, cursorUs: int64) = | |
| 212 | - db.run("""INSERT INTO jetstream_cursor (id, cursor_us) VALUES (1, ?) | |
| 213 | - ON CONFLICT(id) DO UPDATE SET cursor_us = excluded.cursor_us""", | |
| 214 | - p(cursorUs)) | |
| 215 | - | |
| 216 | -proc loadCursor*(db: Db): int64 = | |
| 217 | - db.queryInt("SELECT cursor_us FROM jetstream_cursor WHERE id = 1").get(0'i64) | |
| deleted file mode 100644 | |||
| @@ -1,217 +0,0 @@ | |||
| 1 | -## Persisting what an OAuth session and a firehose subscription need to | ||
| 2 | -## survive a restart. | ||
| 3 | -## | ||
| 4 | -## Three things are stored, matching the tables Glean's Go backend uses: | ||
| 5 | -## | ||
| 6 | -## oauth_auth_requests a flow in progress, keyed by `state` | ||
| 7 | -## oauth_sessions a live session, keyed by (account_did, session_id) | ||
| 8 | -## jetstream_cursor a single row: where the firehose got to | ||
| 9 | -## | ||
| 10 | -## The rows hold JSON, as the Go side does, so the schema does not have to | ||
| 11 | -## change every time a field is added to a session. | ||
| 12 | -## | ||
| 13 | -## **This stores key material.** A session row contains the DPoP private key | ||
| 14 | -## and the refresh token; together they are the account. The file wants the | ||
| 15 | -## same protection as a password store: not world-readable, and not in a | ||
| 16 | -## backup that travels somewhere less careful. | ||
| 17 | - | ||
| 18 | -import std/[json, options, sequtils, times] | ||
| 19 | -import ./dpop | ||
| 20 | -import ./oauth | ||
| 21 | -import ./sqlite | ||
| 22 | - | ||
| 23 | -type | ||
| 24 | - StoreError* = object of CatchableError | ||
| 25 | - | ||
| 26 | - SessionRecord* = object ## A usable OAuth session. | ||
| 27 | - accountDid*: string | ||
| 28 | - sessionId*: string | ||
| 29 | - pdsUrl*: string | ||
| 30 | - authServerUrl*: string | ||
| 31 | - tokenEndpoint*: string | ||
| 32 | - accessToken*: string | ||
| 33 | - refreshToken*: string | ||
| 34 | - dpopKey*: P256Key | ||
| 35 | - hostNonce*: string | ||
| 36 | - authNonce*: string | ||
| 37 | - scopes*: seq[string] | ||
| 38 | - createdAt*: int64 | ||
| 39 | - | ||
| 40 | -const Schema = [ | ||
| 41 | - """CREATE TABLE IF NOT EXISTS oauth_auth_requests ( | ||
| 42 | - state TEXT PRIMARY KEY, | ||
| 43 | - data TEXT NOT NULL, | ||
| 44 | - created_at INTEGER NOT NULL | ||
| 45 | - )""", | ||
| 46 | - """CREATE TABLE IF NOT EXISTS oauth_sessions ( | ||
| 47 | - account_did TEXT NOT NULL, | ||
| 48 | - session_id TEXT NOT NULL, | ||
| 49 | - data TEXT NOT NULL, | ||
| 50 | - PRIMARY KEY (account_did, session_id) | ||
| 51 | - )""", | ||
| 52 | - """CREATE TABLE IF NOT EXISTS jetstream_cursor ( | ||
| 53 | - id INTEGER PRIMARY KEY CHECK(id = 1), | ||
| 54 | - cursor_us INTEGER NOT NULL | ||
| 55 | - )""", | ||
| 56 | -] | ||
| 57 | - | ||
| 58 | -proc migrate*(db: Db) = | ||
| 59 | - for stmt in Schema: | ||
| 60 | - db.exec stmt | ||
| 61 | - | ||
| 62 | -# --- key serialisation ----------------------------------------------------- | ||
| 63 | - | ||
| 64 | -proc toJson*(k: P256Key): JsonNode = | ||
| 65 | - ## The private scalar plus the public point. | ||
| 66 | - ## | ||
| 67 | - ## The public half is stored rather than re-derived on load: deriving it | ||
| 68 | - ## needs a curve operation, and a stored key that cannot be read back | ||
| 69 | - ## without one is a restart that silently drops every session. | ||
| 70 | - %*{"d": b64u(k.priv), "x": b64u(k.pubX), "y": b64u(k.pubY)} | ||
| 71 | - | ||
| 72 | -proc keyFromJson*(j: JsonNode): P256Key = | ||
| 73 | - proc part(name: string): seq[byte] = | ||
| 74 | - let raw = b64uDecode(j{name}.getStr) | ||
| 75 | - if raw.len != 32: | ||
| 76 | - raise newException(StoreError, "stored key field " & name & " is not 32 bytes") | ||
| 77 | - result = newSeq[byte](32) | ||
| 78 | - copyMem(result[0].addr, raw[0].unsafeAddr, 32) | ||
| 79 | - | ||
| 80 | - let d = part("d") | ||
| 81 | - let x = part("x") | ||
| 82 | - let y = part("y") | ||
| 83 | - for i in 0 ..< 32: | ||
| 84 | - result.priv[i] = d[i] | ||
| 85 | - result.pubX[i] = x[i] | ||
| 86 | - result.pubY[i] = y[i] | ||
| 87 | - | ||
| 88 | -# --- auth requests --------------------------------------------------------- | ||
| 89 | - | ||
| 90 | -proc toJson(info: AuthRequestData): JsonNode = | ||
| 91 | - %*{ | ||
| 92 | - "state": info.state, | ||
| 93 | - "auth_server_url": info.authServerUrl, | ||
| 94 | - "scopes": info.scopes, | ||
| 95 | - "pkce_verifier": info.pkceVerifier, | ||
| 96 | - "request_uri": info.requestUri, | ||
| 97 | - "token_endpoint": info.tokenEndpoint, | ||
| 98 | - "revocation_endpoint": info.revocationEndpoint, | ||
| 99 | - "dpop_nonce": info.dpopNonce, | ||
| 100 | - "dpop_key": info.dpopKey.toJson, | ||
| 101 | - "account_did": info.accountDid, | ||
| 102 | - } | ||
| 103 | - | ||
| 104 | -proc authRequestFromJson(j: JsonNode): AuthRequestData = | ||
| 105 | - AuthRequestData( | ||
| 106 | - state: j{"state"}.getStr, | ||
| 107 | - authServerUrl: j{"auth_server_url"}.getStr, | ||
| 108 | - scopes: j{"scopes"}.getElems.mapIt(it.getStr), | ||
| 109 | - pkceVerifier: j{"pkce_verifier"}.getStr, | ||
| 110 | - requestUri: j{"request_uri"}.getStr, | ||
| 111 | - tokenEndpoint: j{"token_endpoint"}.getStr, | ||
| 112 | - revocationEndpoint: j{"revocation_endpoint"}.getStr, | ||
| 113 | - dpopNonce: j{"dpop_nonce"}.getStr, | ||
| 114 | - dpopKey: keyFromJson(j{"dpop_key"}), | ||
| 115 | - accountDid: j{"account_did"}.getStr, | ||
| 116 | - ) | ||
| 117 | - | ||
| 118 | -proc saveAuthRequest*(db: Db, info: AuthRequestData) = | ||
| 119 | - db.run("""INSERT INTO oauth_auth_requests (state, data, created_at) | ||
| 120 | - VALUES (?, ?, ?) | ||
| 121 | - ON CONFLICT(state) DO UPDATE SET data = excluded.data""", | ||
| 122 | - p(info.state), p($info.toJson), p(getTime().toUnix)) | ||
| 123 | - | ||
| 124 | -proc takeAuthRequest*(db: Db, state: string): Option[AuthRequestData] = | ||
| 125 | - ## Read **and delete**: an authorization code is single-use, so leaving the | ||
| 126 | - ## request behind invites a replay of the callback. | ||
| 127 | - let row = db.queryText( | ||
| 128 | - "SELECT data FROM oauth_auth_requests WHERE state = ?", p(state)) | ||
| 129 | - if row.isNone: | ||
| 130 | - return none(AuthRequestData) | ||
| 131 | - db.run("DELETE FROM oauth_auth_requests WHERE state = ?", p(state)) | ||
| 132 | - try: | ||
| 133 | - some(authRequestFromJson(parseJson(row.get))) | ||
| 134 | - except JsonParsingError: | ||
| 135 | - raise newException(StoreError, "stored auth request is not valid JSON") | ||
| 136 | - | ||
| 137 | -proc purgeAuthRequests*(db: Db, olderThan: int64) = | ||
| 138 | - ## Flows that were started and never finished. Without this the table grows | ||
| 139 | - ## forever, one row per abandoned sign-in. | ||
| 140 | - db.run("DELETE FROM oauth_auth_requests WHERE created_at < ?", | ||
| 141 | - p(getTime().toUnix - olderThan)) | ||
| 142 | - | ||
| 143 | -# --- sessions -------------------------------------------------------------- | ||
| 144 | - | ||
| 145 | -proc toJson(s: SessionRecord): JsonNode = | ||
| 146 | - %*{ | ||
| 147 | - "account_did": s.accountDid, | ||
| 148 | - "session_id": s.sessionId, | ||
| 149 | - "pds_url": s.pdsUrl, | ||
| 150 | - "auth_server_url": s.authServerUrl, | ||
| 151 | - "token_endpoint": s.tokenEndpoint, | ||
| 152 | - "access_token": s.accessToken, | ||
| 153 | - "refresh_token": s.refreshToken, | ||
| 154 | - "dpop_key": s.dpopKey.toJson, | ||
| 155 | - "host_nonce": s.hostNonce, | ||
| 156 | - "auth_nonce": s.authNonce, | ||
| 157 | - "scopes": s.scopes, | ||
| 158 | - "created_at": s.createdAt, | ||
| 159 | - } | ||
| 160 | - | ||
| 161 | -proc sessionFromJson(j: JsonNode): SessionRecord = | ||
| 162 | - SessionRecord( | ||
| 163 | - accountDid: j{"account_did"}.getStr, | ||
| 164 | - sessionId: j{"session_id"}.getStr, | ||
| 165 | - pdsUrl: j{"pds_url"}.getStr, | ||
| 166 | - authServerUrl: j{"auth_server_url"}.getStr, | ||
| 167 | - tokenEndpoint: j{"token_endpoint"}.getStr, | ||
| 168 | - accessToken: j{"access_token"}.getStr, | ||
| 169 | - refreshToken: j{"refresh_token"}.getStr, | ||
| 170 | - dpopKey: keyFromJson(j{"dpop_key"}), | ||
| 171 | - hostNonce: j{"host_nonce"}.getStr, | ||
| 172 | - authNonce: j{"auth_nonce"}.getStr, | ||
| 173 | - scopes: j{"scopes"}.getElems.mapIt(it.getStr), | ||
| 174 | - createdAt: j{"created_at"}.getBiggestInt.int64, | ||
| 175 | - ) | ||
| 176 | - | ||
| 177 | -proc saveSession*(db: Db, s: SessionRecord) = | ||
| 178 | - if s.accountDid.len == 0 or s.sessionId.len == 0: | ||
| 179 | - raise newException(StoreError, "a session needs both a DID and a session id") | ||
| 180 | - db.run("""INSERT INTO oauth_sessions (account_did, session_id, data) | ||
| 181 | - VALUES (?, ?, ?) | ||
| 182 | - ON CONFLICT(account_did, session_id) DO UPDATE SET data = excluded.data""", | ||
| 183 | - p(s.accountDid), p(s.sessionId), p($s.toJson)) | ||
| 184 | - | ||
| 185 | -proc getSession*(db: Db, did, sessionId: string): Option[SessionRecord] = | ||
| 186 | - let row = db.queryText( | ||
| 187 | - "SELECT data FROM oauth_sessions WHERE account_did = ? AND session_id = ?", | ||
| 188 | - p(did), p(sessionId)) | ||
| 189 | - if row.isNone: | ||
| 190 | - return none(SessionRecord) | ||
| 191 | - try: | ||
| 192 | - some(sessionFromJson(parseJson(row.get))) | ||
| 193 | - except JsonParsingError: | ||
| 194 | - raise newException(StoreError, "stored session is not valid JSON") | ||
| 195 | - | ||
| 196 | -proc deleteSession*(db: Db, did, sessionId: string) = | ||
| 197 | - db.run("DELETE FROM oauth_sessions WHERE account_did = ? AND session_id = ?", | ||
| 198 | - p(did), p(sessionId)) | ||
| 199 | - | ||
| 200 | -proc listSessions*(db: Db, did: string): seq[string] = | ||
| 201 | - for row in db.rows( | ||
| 202 | - "SELECT session_id FROM oauth_sessions WHERE account_did = ?", p(did)): | ||
| 203 | - result.add row[0] | ||
| 204 | - | ||
| 205 | -proc listAccounts*(db: Db): seq[string] = | ||
| 206 | - for row in db.rows("SELECT DISTINCT account_did FROM oauth_sessions"): | ||
| 207 | - result.add row[0] | ||
| 208 | - | ||
| 209 | -# --- jetstream cursor ------------------------------------------------------ | ||
| 210 | - | ||
| 211 | -proc saveCursor*(db: Db, cursorUs: int64) = | ||
| 212 | - db.run("""INSERT INTO jetstream_cursor (id, cursor_us) VALUES (1, ?) | ||
| 213 | - ON CONFLICT(id) DO UPDATE SET cursor_us = excluded.cursor_us""", | ||
| 214 | - p(cursorUs)) | ||
| 215 | - | ||
| 216 | -proc loadCursor*(db: Db): int64 = | ||
| 217 | - db.queryInt("SELECT cursor_us FROM jetstream_cursor WHERE id = 1").get(0'i64) | ||
deleted
spike/nim/atproto/xrpc.nim +0 -204 | deleted file mode 100644 | ||
| @@ -1,204 +0,0 @@ | ||
| 1 | -## Authenticated XRPC against a PDS, using a DPoP-bound OAuth access token. | |
| 2 | -## | |
| 3 | -## The important difference from the OAuth endpoints: a resource server | |
| 4 | -## signals a stale nonce with **401** and a `WWW-Authenticate: DPoP | |
| 5 | -## error="use_dpop_nonce"` header, where the auth server used 400 and a JSON | |
| 6 | -## body. Same protocol, different shape, so the retry logic cannot be shared | |
| 7 | -## by pattern-matching the status code alone. | |
| 8 | -## | |
| 9 | -## A session therefore tracks two nonces: one for the auth server (token | |
| 10 | -## refresh) and one for the PDS. They are issued independently and are not | |
| 11 | -## interchangeable. | |
| 12 | - | |
| 13 | -import std/[httpclient, json, strformat, strutils, uri] | |
| 14 | -import bearssl/rand | |
| 15 | -import ./dpop | |
| 16 | -import ./identity | |
| 17 | -import ./oauth | |
| 18 | - | |
| 19 | -type | |
| 20 | - XrpcError* = object of CatchableError | |
| 21 | - status*: int | |
| 22 | - xrpcError*: string ## the `error` field of an XRPC error body | |
| 23 | - | |
| 24 | - Session* = object ## Everything needed to make an authenticated call. | |
| 25 | - did*: string | |
| 26 | - pdsUrl*: string | |
| 27 | - authServerUrl*: string | |
| 28 | - tokenEndpoint*: string | |
| 29 | - accessToken*: string | |
| 30 | - refreshToken*: string | |
| 31 | - dpopKey*: P256Key | |
| 32 | - hostNonce*: string ## nonce issued by the PDS | |
| 33 | - authNonce*: string ## nonce issued by the auth server | |
| 34 | - clientId*: string | |
| 35 | - | |
| 36 | -const HttpTimeoutMs = 30_000 | |
| 37 | - | |
| 38 | -proc sessionFromTokens*(info: AuthRequestData, tokens: TokenResponse, | |
| 39 | - pdsUrl, clientId: string): Session = | |
| 40 | - Session( | |
| 41 | - did: tokens.sub, | |
| 42 | - pdsUrl: pdsUrl.strip(chars = {'/'}), | |
| 43 | - authServerUrl: info.authServerUrl, | |
| 44 | - tokenEndpoint: info.tokenEndpoint, | |
| 45 | - accessToken: tokens.accessToken, | |
| 46 | - refreshToken: tokens.refreshToken, | |
| 47 | - dpopKey: info.dpopKey, | |
| 48 | - authNonce: info.dpopNonce, | |
| 49 | - clientId: clientId, | |
| 50 | - ) | |
| 51 | - | |
| 52 | -proc dpopUrl*(url: string): string = | |
| 53 | - ## The `htu` claim is the request URL without query or fragment (RFC 9449 | |
| 54 | - ## §4.2). Leaving the query on is a silent mismatch: the proof verifies | |
| 55 | - ## against a URL the server never compares it to, and every call 401s. | |
| 56 | - let u = parseUri(url) | |
| 57 | - var bare = u | |
| 58 | - bare.query = "" | |
| 59 | - bare.anchor = "" | |
| 60 | - $bare | |
| 61 | - | |
| 62 | -proc isNonceRetry(wwwAuth: string): bool = | |
| 63 | - ## WWW-Authenticate: DPoP error="use_dpop_nonce", error_description="..." | |
| 64 | - "use_dpop_nonce" in wwwAuth | |
| 65 | - | |
| 66 | -proc isExpiredToken(wwwAuth: string): bool = | |
| 67 | - "invalid_token" in wwwAuth | |
| 68 | - | |
| 69 | -proc parseXrpcError(body: string): (string, string) = | |
| 70 | - try: | |
| 71 | - let j = parseJson(body) | |
| 72 | - (j{"error"}.getStr, j{"message"}.getStr) | |
| 73 | - except CatchableError: | |
| 74 | - ("", "") | |
| 75 | - | |
| 76 | -proc raiseXrpc(status: int, body: string, what: string) = | |
| 77 | - let (err, msg) = parseXrpcError(body) | |
| 78 | - var e = newException(XrpcError, | |
| 79 | - &"{what} failed (HTTP {status})" & | |
| 80 | - (if err.len > 0: &": {err}" else: "") & | |
| 81 | - (if msg.len > 0: &" -- {msg}" else: "")) | |
| 82 | - e.status = status | |
| 83 | - e.xrpcError = err | |
| 84 | - raise e | |
| 85 | - | |
| 86 | -proc refreshSession*(sess: var Session, rng: var HmacDrbgContext) = | |
| 87 | - ## Exchange the refresh token for a new access token, reusing the session's | |
| 88 | - ## DPoP key -- the refresh token is bound to it just as the access token is. | |
| 89 | - if sess.refreshToken.len == 0: | |
| 90 | - raise newException(XrpcError, "session has no refresh token") | |
| 91 | - var info = AuthRequestData( | |
| 92 | - authServerUrl: sess.authServerUrl, | |
| 93 | - tokenEndpoint: sess.tokenEndpoint, | |
| 94 | - dpopKey: sess.dpopKey, | |
| 95 | - dpopNonce: sess.authNonce, | |
| 96 | - ) | |
| 97 | - let cfg = ClientConfig(clientId: sess.clientId) | |
| 98 | - let tokens = refresh(cfg, info, sess.refreshToken, rng) | |
| 99 | - sess.accessToken = tokens.accessToken | |
| 100 | - if tokens.refreshToken.len > 0: | |
| 101 | - sess.refreshToken = tokens.refreshToken | |
| 102 | - sess.authNonce = info.dpopNonce | |
| 103 | - | |
| 104 | -proc request*(sess: var Session, rng: var HmacDrbgContext, | |
| 105 | - httpMethod: HttpMethod, url: string, | |
| 106 | - body = "", contentType = "application/json"): Response = | |
| 107 | - ## One authenticated request, handling both retry conditions. | |
| 108 | - ## | |
| 109 | - ## Up to three attempts: the original, one for a nonce the PDS only reveals | |
| 110 | - ## by rejecting us, and one after refreshing an expired access token. | |
| 111 | - let client = newHttpClient(userAgent = UserAgent, timeout = HttpTimeoutMs) | |
| 112 | - defer: client.close() | |
| 113 | - | |
| 114 | - let htu = dpopUrl(url) | |
| 115 | - # HttpMethod stringifies straight to the uppercase token the `htm` claim | |
| 116 | - # wants ("GET", "POST"), so no trimming. | |
| 117 | - let htm = $httpMethod | |
| 118 | - var refreshed = false | |
| 119 | - var noncedOnce = false | |
| 120 | - | |
| 121 | - for attempt in 0 .. 2: | |
| 122 | - let proof = dpopProof(sess.dpopKey, rng, htm, htu, | |
| 123 | - nonce = sess.hostNonce, | |
| 124 | - accessToken = sess.accessToken, | |
| 125 | - issuer = sess.authServerUrl) | |
| 126 | - var headers = @{ | |
| 127 | - "Authorization": "DPoP " & sess.accessToken, # not Bearer | |
| 128 | - "DPoP": proof, | |
| 129 | - } | |
| 130 | - if body.len > 0: | |
| 131 | - headers.add ("Content-Type", contentType) | |
| 132 | - client.headers = newHttpHeaders(headers) | |
| 133 | - | |
| 134 | - result = client.request(url, httpMethod = httpMethod, body = body) | |
| 135 | - | |
| 136 | - # The PDS rotates nonces freely; adopt any it offers, success or not. | |
| 137 | - let supplied = result.headers.getOrDefault("DPoP-Nonce").string | |
| 138 | - if supplied.len > 0: | |
| 139 | - sess.hostNonce = supplied | |
| 140 | - | |
| 141 | - if result.code.int != 401: | |
| 142 | - return result | |
| 143 | - | |
| 144 | - let wwwAuth = result.headers.getOrDefault("WWW-Authenticate").string | |
| 145 | - if wwwAuth.len == 0: | |
| 146 | - return result | |
| 147 | - | |
| 148 | - if isNonceRetry(wwwAuth) and supplied.len > 0 and not noncedOnce: | |
| 149 | - noncedOnce = true | |
| 150 | - continue | |
| 151 | - | |
| 152 | - if isExpiredToken(wwwAuth) and not refreshed and sess.refreshToken.len > 0: | |
| 153 | - refreshed = true | |
| 154 | - refreshSession(sess, rng) | |
| 155 | - continue | |
| 156 | - | |
| 157 | - return result | |
| 158 | - | |
| 159 | - raise newException(XrpcError, "ran out of retries for " & url) | |
| 160 | - | |
| 161 | -proc query*(sess: var Session, rng: var HmacDrbgContext, nsid: string, | |
| 162 | - params: openArray[(string, string)] = []): JsonNode = | |
| 163 | - ## An XRPC query (GET), e.g. com.atproto.repo.listRecords. | |
| 164 | - var url = &"{sess.pdsUrl}/xrpc/{nsid}" | |
| 165 | - if params.len > 0: | |
| 166 | - url &= "?" & params.encodeQuery | |
| 167 | - let res = request(sess, rng, HttpGet, url) | |
| 168 | - if res.code.int != 200: | |
| 169 | - raiseXrpc(res.code.int, res.body, nsid) | |
| 170 | - try: | |
| 171 | - parseJson(res.body) | |
| 172 | - except JsonParsingError: | |
| 173 | - raise newException(XrpcError, nsid & " returned a non-JSON body") | |
| 174 | - | |
| 175 | -proc procedure*(sess: var Session, rng: var HmacDrbgContext, nsid: string, | |
| 176 | - body: JsonNode = nil): JsonNode = | |
| 177 | - ## An XRPC procedure (POST), e.g. com.atproto.repo.createRecord. | |
| 178 | - let url = &"{sess.pdsUrl}/xrpc/{nsid}" | |
| 179 | - let payload = if body == nil: "" else: $body | |
| 180 | - let res = request(sess, rng, HttpPost, url, body = payload) | |
| 181 | - if res.code.int notin [200, 201]: | |
| 182 | - raiseXrpc(res.code.int, res.body, nsid) | |
| 183 | - if res.body.strip.len == 0: | |
| 184 | - return newJObject() | |
| 185 | - try: | |
| 186 | - parseJson(res.body) | |
| 187 | - except JsonParsingError: | |
| 188 | - raise newException(XrpcError, nsid & " returned a non-JSON body") | |
| 189 | - | |
| 190 | -# --- unauthenticated helpers ---------------------------------------------- | |
| 191 | - | |
| 192 | -proc publicQuery*(host, nsid: string, | |
| 193 | - params: openArray[(string, string)] = []): JsonNode = | |
| 194 | - ## Many XRPC endpoints need no auth. Useful on its own, and it isolates | |
| 195 | - ## "can we speak XRPC at all" from "is our DPoP signing right". | |
| 196 | - var url = &"https://{hostOf(host)}/xrpc/{nsid}" | |
| 197 | - if params.len > 0: | |
| 198 | - url &= "?" & params.encodeQuery | |
| 199 | - let client = newHttpClient(userAgent = UserAgent, timeout = HttpTimeoutMs) | |
| 200 | - defer: client.close() | |
| 201 | - let res = client.get(url) | |
| 202 | - if res.code.int != 200: | |
| 203 | - raiseXrpc(res.code.int, res.body, nsid) | |
| 204 | - parseJson(res.body) | |
| deleted file mode 100644 | |||
| @@ -1,204 +0,0 @@ | |||
| 1 | -## Authenticated XRPC against a PDS, using a DPoP-bound OAuth access token. | ||
| 2 | -## | ||
| 3 | -## The important difference from the OAuth endpoints: a resource server | ||
| 4 | -## signals a stale nonce with **401** and a `WWW-Authenticate: DPoP | ||
| 5 | -## error="use_dpop_nonce"` header, where the auth server used 400 and a JSON | ||
| 6 | -## body. Same protocol, different shape, so the retry logic cannot be shared | ||
| 7 | -## by pattern-matching the status code alone. | ||
| 8 | -## | ||
| 9 | -## A session therefore tracks two nonces: one for the auth server (token | ||
| 10 | -## refresh) and one for the PDS. They are issued independently and are not | ||
| 11 | -## interchangeable. | ||
| 12 | - | ||
| 13 | -import std/[httpclient, json, strformat, strutils, uri] | ||
| 14 | -import bearssl/rand | ||
| 15 | -import ./dpop | ||
| 16 | -import ./identity | ||
| 17 | -import ./oauth | ||
| 18 | - | ||
| 19 | -type | ||
| 20 | - XrpcError* = object of CatchableError | ||
| 21 | - status*: int | ||
| 22 | - xrpcError*: string ## the `error` field of an XRPC error body | ||
| 23 | - | ||
| 24 | - Session* = object ## Everything needed to make an authenticated call. | ||
| 25 | - did*: string | ||
| 26 | - pdsUrl*: string | ||
| 27 | - authServerUrl*: string | ||
| 28 | - tokenEndpoint*: string | ||
| 29 | - accessToken*: string | ||
| 30 | - refreshToken*: string | ||
| 31 | - dpopKey*: P256Key | ||
| 32 | - hostNonce*: string ## nonce issued by the PDS | ||
| 33 | - authNonce*: string ## nonce issued by the auth server | ||
| 34 | - clientId*: string | ||
| 35 | - | ||
| 36 | -const HttpTimeoutMs = 30_000 | ||
| 37 | - | ||
| 38 | -proc sessionFromTokens*(info: AuthRequestData, tokens: TokenResponse, | ||
| 39 | - pdsUrl, clientId: string): Session = | ||
| 40 | - Session( | ||
| 41 | - did: tokens.sub, | ||
| 42 | - pdsUrl: pdsUrl.strip(chars = {'/'}), | ||
| 43 | - authServerUrl: info.authServerUrl, | ||
| 44 | - tokenEndpoint: info.tokenEndpoint, | ||
| 45 | - accessToken: tokens.accessToken, | ||
| 46 | - refreshToken: tokens.refreshToken, | ||
| 47 | - dpopKey: info.dpopKey, | ||
| 48 | - authNonce: info.dpopNonce, | ||
| 49 | - clientId: clientId, | ||
| 50 | - ) | ||
| 51 | - | ||
| 52 | -proc dpopUrl*(url: string): string = | ||
| 53 | - ## The `htu` claim is the request URL without query or fragment (RFC 9449 | ||
| 54 | - ## §4.2). Leaving the query on is a silent mismatch: the proof verifies | ||
| 55 | - ## against a URL the server never compares it to, and every call 401s. | ||
| 56 | - let u = parseUri(url) | ||
| 57 | - var bare = u | ||
| 58 | - bare.query = "" | ||
| 59 | - bare.anchor = "" | ||
| 60 | - $bare | ||
| 61 | - | ||
| 62 | -proc isNonceRetry(wwwAuth: string): bool = | ||
| 63 | - ## WWW-Authenticate: DPoP error="use_dpop_nonce", error_description="..." | ||
| 64 | - "use_dpop_nonce" in wwwAuth | ||
| 65 | - | ||
| 66 | -proc isExpiredToken(wwwAuth: string): bool = | ||
| 67 | - "invalid_token" in wwwAuth | ||
| 68 | - | ||
| 69 | -proc parseXrpcError(body: string): (string, string) = | ||
| 70 | - try: | ||
| 71 | - let j = parseJson(body) | ||
| 72 | - (j{"error"}.getStr, j{"message"}.getStr) | ||
| 73 | - except CatchableError: | ||
| 74 | - ("", "") | ||
| 75 | - | ||
| 76 | -proc raiseXrpc(status: int, body: string, what: string) = | ||
| 77 | - let (err, msg) = parseXrpcError(body) | ||
| 78 | - var e = newException(XrpcError, | ||
| 79 | - &"{what} failed (HTTP {status})" & | ||
| 80 | - (if err.len > 0: &": {err}" else: "") & | ||
| 81 | - (if msg.len > 0: &" -- {msg}" else: "")) | ||
| 82 | - e.status = status | ||
| 83 | - e.xrpcError = err | ||
| 84 | - raise e | ||
| 85 | - | ||
| 86 | -proc refreshSession*(sess: var Session, rng: var HmacDrbgContext) = | ||
| 87 | - ## Exchange the refresh token for a new access token, reusing the session's | ||
| 88 | - ## DPoP key -- the refresh token is bound to it just as the access token is. | ||
| 89 | - if sess.refreshToken.len == 0: | ||
| 90 | - raise newException(XrpcError, "session has no refresh token") | ||
| 91 | - var info = AuthRequestData( | ||
| 92 | - authServerUrl: sess.authServerUrl, | ||
| 93 | - tokenEndpoint: sess.tokenEndpoint, | ||
| 94 | - dpopKey: sess.dpopKey, | ||
| 95 | - dpopNonce: sess.authNonce, | ||
| 96 | - ) | ||
| 97 | - let cfg = ClientConfig(clientId: sess.clientId) | ||
| 98 | - let tokens = refresh(cfg, info, sess.refreshToken, rng) | ||
| 99 | - sess.accessToken = tokens.accessToken | ||
| 100 | - if tokens.refreshToken.len > 0: | ||
| 101 | - sess.refreshToken = tokens.refreshToken | ||
| 102 | - sess.authNonce = info.dpopNonce | ||
| 103 | - | ||
| 104 | -proc request*(sess: var Session, rng: var HmacDrbgContext, | ||
| 105 | - httpMethod: HttpMethod, url: string, | ||
| 106 | - body = "", contentType = "application/json"): Response = | ||
| 107 | - ## One authenticated request, handling both retry conditions. | ||
| 108 | - ## | ||
| 109 | - ## Up to three attempts: the original, one for a nonce the PDS only reveals | ||
| 110 | - ## by rejecting us, and one after refreshing an expired access token. | ||
| 111 | - let client = newHttpClient(userAgent = UserAgent, timeout = HttpTimeoutMs) | ||
| 112 | - defer: client.close() | ||
| 113 | - | ||
| 114 | - let htu = dpopUrl(url) | ||
| 115 | - # HttpMethod stringifies straight to the uppercase token the `htm` claim | ||
| 116 | - # wants ("GET", "POST"), so no trimming. | ||
| 117 | - let htm = $httpMethod | ||
| 118 | - var refreshed = false | ||
| 119 | - var noncedOnce = false | ||
| 120 | - | ||
| 121 | - for attempt in 0 .. 2: | ||
| 122 | - let proof = dpopProof(sess.dpopKey, rng, htm, htu, | ||
| 123 | - nonce = sess.hostNonce, | ||
| 124 | - accessToken = sess.accessToken, | ||
| 125 | - issuer = sess.authServerUrl) | ||
| 126 | - var headers = @{ | ||
| 127 | - "Authorization": "DPoP " & sess.accessToken, # not Bearer | ||
| 128 | - "DPoP": proof, | ||
| 129 | - } | ||
| 130 | - if body.len > 0: | ||
| 131 | - headers.add ("Content-Type", contentType) | ||
| 132 | - client.headers = newHttpHeaders(headers) | ||
| 133 | - | ||
| 134 | - result = client.request(url, httpMethod = httpMethod, body = body) | ||
| 135 | - | ||
| 136 | - # The PDS rotates nonces freely; adopt any it offers, success or not. | ||
| 137 | - let supplied = result.headers.getOrDefault("DPoP-Nonce").string | ||
| 138 | - if supplied.len > 0: | ||
| 139 | - sess.hostNonce = supplied | ||
| 140 | - | ||
| 141 | - if result.code.int != 401: | ||
| 142 | - return result | ||
| 143 | - | ||
| 144 | - let wwwAuth = result.headers.getOrDefault("WWW-Authenticate").string | ||
| 145 | - if wwwAuth.len == 0: | ||
| 146 | - return result | ||
| 147 | - | ||
| 148 | - if isNonceRetry(wwwAuth) and supplied.len > 0 and not noncedOnce: | ||
| 149 | - noncedOnce = true | ||
| 150 | - continue | ||
| 151 | - | ||
| 152 | - if isExpiredToken(wwwAuth) and not refreshed and sess.refreshToken.len > 0: | ||
| 153 | - refreshed = true | ||
| 154 | - refreshSession(sess, rng) | ||
| 155 | - continue | ||
| 156 | - | ||
| 157 | - return result | ||
| 158 | - | ||
| 159 | - raise newException(XrpcError, "ran out of retries for " & url) | ||
| 160 | - | ||
| 161 | -proc query*(sess: var Session, rng: var HmacDrbgContext, nsid: string, | ||
| 162 | - params: openArray[(string, string)] = []): JsonNode = | ||
| 163 | - ## An XRPC query (GET), e.g. com.atproto.repo.listRecords. | ||
| 164 | - var url = &"{sess.pdsUrl}/xrpc/{nsid}" | ||
| 165 | - if params.len > 0: | ||
| 166 | - url &= "?" & params.encodeQuery | ||
| 167 | - let res = request(sess, rng, HttpGet, url) | ||
| 168 | - if res.code.int != 200: | ||
| 169 | - raiseXrpc(res.code.int, res.body, nsid) | ||
| 170 | - try: | ||
| 171 | - parseJson(res.body) | ||
| 172 | - except JsonParsingError: | ||
| 173 | - raise newException(XrpcError, nsid & " returned a non-JSON body") | ||
| 174 | - | ||
| 175 | -proc procedure*(sess: var Session, rng: var HmacDrbgContext, nsid: string, | ||
| 176 | - body: JsonNode = nil): JsonNode = | ||
| 177 | - ## An XRPC procedure (POST), e.g. com.atproto.repo.createRecord. | ||
| 178 | - let url = &"{sess.pdsUrl}/xrpc/{nsid}" | ||
| 179 | - let payload = if body == nil: "" else: $body | ||
| 180 | - let res = request(sess, rng, HttpPost, url, body = payload) | ||
| 181 | - if res.code.int notin [200, 201]: | ||
| 182 | - raiseXrpc(res.code.int, res.body, nsid) | ||
| 183 | - if res.body.strip.len == 0: | ||
| 184 | - return newJObject() | ||
| 185 | - try: | ||
| 186 | - parseJson(res.body) | ||
| 187 | - except JsonParsingError: | ||
| 188 | - raise newException(XrpcError, nsid & " returned a non-JSON body") | ||
| 189 | - | ||
| 190 | -# --- unauthenticated helpers ---------------------------------------------- | ||
| 191 | - | ||
| 192 | -proc publicQuery*(host, nsid: string, | ||
| 193 | - params: openArray[(string, string)] = []): JsonNode = | ||
| 194 | - ## Many XRPC endpoints need no auth. Useful on its own, and it isolates | ||
| 195 | - ## "can we speak XRPC at all" from "is our DPoP signing right". | ||
| 196 | - var url = &"https://{hostOf(host)}/xrpc/{nsid}" | ||
| 197 | - if params.len > 0: | ||
| 198 | - url &= "?" & params.encodeQuery | ||
| 199 | - let client = newHttpClient(userAgent = UserAgent, timeout = HttpTimeoutMs) | ||
| 200 | - defer: client.close() | ||
| 201 | - let res = client.get(url) | ||
| 202 | - if res.code.int != 200: | ||
| 203 | - raiseXrpc(res.code.int, res.body, nsid) | ||
| 204 | - parseJson(res.body) | ||
deleted
spike/nim/cluster_probe.nim +0 -220 | deleted file mode 100644 | ||
| @@ -1,220 +0,0 @@ | ||
| 1 | -## Checks the similarity computations and scoring helpers. | |
| 2 | -## | |
| 3 | -## nim c -r -d:ssl cluster_probe.nim | |
| 4 | -## | |
| 5 | -## Recommendations are the one part of this system with no obviously correct | |
| 6 | -## answer, so the checks are about properties rather than values: that decay | |
| 7 | -## actually decays, that a pair is counted once, that normalisation does not | |
| 8 | -## produce NaN, that discovery is not just a mirror of who you already follow. | |
| 9 | - | |
| 10 | -import std/[math, options, os, random, sequtils, strformat, strutils, times] | |
| 11 | -import atproto/[articlestore, cluster, feedstore, gleandb, socialstore, sqlite] | |
| 12 | - | |
| 13 | -var failures = 0 | |
| 14 | - | |
| 15 | -proc report(name: string, ok: bool, detail = "") = | |
| 16 | - if not ok: inc failures | |
| 17 | - let label = if ok: "PASS" else: "FAIL" | |
| 18 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | |
| 19 | - else: echo &" {label} {name}" | |
| 20 | - | |
| 21 | -proc cleanup(base: string) = | |
| 22 | - for suffix in ["_users", "_articles", "_recs"]: | |
| 23 | - for ext in ["", "-wal", "-shm"]: | |
| 24 | - removeFile(base & suffix & ext) | |
| 25 | - | |
| 26 | -const | |
| 27 | - alice = "did:plc:alice" | |
| 28 | - bob = "did:plc:bob" | |
| 29 | - carol = "did:plc:carol" | |
| 30 | - | |
| 31 | -proc main() = | |
| 32 | - echo "Clustering probe" | |
| 33 | - echo "" | |
| 34 | - | |
| 35 | - # --- pure helpers ------------------------------------------------------- | |
| 36 | - block: | |
| 37 | - report("decay at zero days is 1", abs(decay(0.0) - 1.0) < 1e-12) | |
| 38 | - # ln(2)/0.023 is about 30 days, so a month-old event is worth half. | |
| 39 | - report("decay halves in about a month", | |
| 40 | - abs(decay(30.1) - 0.5) < 0.01, &"{decay(30.1):.4f}") | |
| 41 | - report("decay keeps shrinking", decay(365.0) < decay(30.0)) | |
| 42 | - report("decay never reaches zero", decay(3650.0) > 0.0, | |
| 43 | - &"{decay(3650.0):.3e}") | |
| 44 | - | |
| 45 | - block: | |
| 46 | - var s = @[0.0, 5.0, 10.0] | |
| 47 | - normalizeScores(s) | |
| 48 | - report("normalisation maps to 0..1", s == @[0.0, 0.5, 1.0], $s) | |
| 49 | - | |
| 50 | - # The case that would produce NaN: no spread to divide by. | |
| 51 | - var flat = @[3.0, 3.0, 3.0] | |
| 52 | - normalizeScores(flat) | |
| 53 | - report("an all-equal set is left alone", flat == @[3.0, 3.0, 3.0], $flat) | |
| 54 | - for v in flat: | |
| 55 | - report("and produces no NaN", not v.isNaN) | |
| 56 | - | |
| 57 | - var one = @[7.0] | |
| 58 | - normalizeScores(one) | |
| 59 | - report("a single score is left alone", one == @[7.0]) | |
| 60 | - | |
| 61 | - var empty: seq[float] = @[] | |
| 62 | - normalizeScores(empty) | |
| 63 | - report("an empty set does not crash", empty.len == 0) | |
| 64 | - | |
| 65 | - var negatives = @[-10.0, -5.0, 0.0] | |
| 66 | - normalizeScores(negatives) | |
| 67 | - report("negative scores normalise too", negatives == @[0.0, 0.5, 1.0], | |
| 68 | - $negatives) | |
| 69 | - | |
| 70 | - block: | |
| 71 | - var rng = initRand(42) | |
| 72 | - var pool: seq[PersonRecommendation] | |
| 73 | - for i in 0 ..< 10: | |
| 74 | - pool.add PersonRecommendation(did: &"did:plc:in{i}", isFollowed: true) | |
| 75 | - for i in 0 ..< 10: | |
| 76 | - pool.add PersonRecommendation(did: &"did:plc:out{i}", isFollowed: false) | |
| 77 | - | |
| 78 | - let sampled = samplePeople(pool, 3, rng) | |
| 79 | - let followed = sampled.countIt(it.isFollowed) | |
| 80 | - let strangers = sampled.len - followed | |
| 81 | - report("sampling caps each side at the half", sampled.len == 6, | |
| 82 | - $sampled.len) | |
| 83 | - # Ranking purely by similarity returns the network the reader already | |
| 84 | - # has; reserving half the slots is what makes this discovery. | |
| 85 | - report("half the slots go to people not followed", | |
| 86 | - followed == 3 and strangers == 3, | |
| 87 | - &"{followed} followed, {strangers} strangers") | |
| 88 | - | |
| 89 | - # Fewer than `half` on one side must not pad from the other. | |
| 90 | - var lopsided: seq[PersonRecommendation] | |
| 91 | - lopsided.add PersonRecommendation(did: "did:plc:only", isFollowed: true) | |
| 92 | - for i in 0 ..< 10: | |
| 93 | - lopsided.add PersonRecommendation(did: &"did:plc:s{i}", isFollowed: false) | |
| 94 | - let small = samplePeople(lopsided, 3, rng) | |
| 95 | - report("a thin side is not padded from the other", | |
| 96 | - small.countIt(it.isFollowed) == 1 and small.len == 4, $small.len) | |
| 97 | - | |
| 98 | - # Shuffled, so the same faces do not recur every visit. | |
| 99 | - var rngA = initRand(1) | |
| 100 | - var rngB = initRand(2) | |
| 101 | - let a = samplePeople(pool, 3, rngA).mapIt(it.did).join(",") | |
| 102 | - let b = samplePeople(pool, 3, rngB).mapIt(it.did).join(",") | |
| 103 | - report("sampling varies between runs", a != b) | |
| 104 | - | |
| 105 | - block: | |
| 106 | - var recs = @[ | |
| 107 | - FeedRecommendation(feedUrl: "c", score: 0.1), | |
| 108 | - FeedRecommendation(feedUrl: "a", score: 0.9), | |
| 109 | - FeedRecommendation(feedUrl: "b", score: 0.5), | |
| 110 | - ] | |
| 111 | - rankByScore(recs) | |
| 112 | - report("ranking puts the highest score first", | |
| 113 | - recs.mapIt(it.feedUrl) == @["a", "b", "c"], | |
| 114 | - recs.mapIt(it.feedUrl).join(",")) | |
| 115 | - | |
| 116 | - # --- similarity against a real database --------------------------------- | |
| 117 | - let base = getTempDir() / "glean_cluster_probe" | |
| 118 | - cleanup(base) | |
| 119 | - defer: cleanup(base) | |
| 120 | - | |
| 121 | - var g = gleandb.open(base) | |
| 122 | - defer: g.close() | |
| 123 | - g.migrate() | |
| 124 | - | |
| 125 | - for did in [alice, bob, carol]: | |
| 126 | - g.db.run("INSERT INTO users (did) VALUES (?)", p(did)) | |
| 127 | - | |
| 128 | - # alice and bob share two feeds; carol shares one with each. | |
| 129 | - for i in 1 .. 3: | |
| 130 | - g.upsertFeed(Feed(feedUrl: &"https://f{i}.test/feed", title: &"Feed {i}", | |
| 131 | - feedType: "rss")) | |
| 132 | - g.createSubscription(alice, "https://f1.test/feed", "", "", "", "") | |
| 133 | - g.createSubscription(alice, "https://f2.test/feed", "", "", "", "") | |
| 134 | - g.createSubscription(bob, "https://f1.test/feed", "", "", "", "") | |
| 135 | - g.createSubscription(bob, "https://f2.test/feed", "", "", "", "") | |
| 136 | - g.createSubscription(carol, "https://f1.test/feed", "", "", "", "") | |
| 137 | - g.createSubscription(carol, "https://f3.test/feed", "", "", "", "") | |
| 138 | - | |
| 139 | - block: | |
| 140 | - g.computeUserSimilarity() | |
| 141 | - let ab = g.userSimilarity(alice, bob) | |
| 142 | - let ac = g.userSimilarity(alice, carol) | |
| 143 | - # alice/bob share 2 of 2; alice/carol share 1 of 3. | |
| 144 | - report("identical subscribers score 1.0", abs(ab - 1.0) < 1e-9, &"{ab:.4f}") | |
| 145 | - report("partial overlap scores lower", | |
| 146 | - abs(ac - (1.0 / 3.0)) < 1e-9, &"{ac:.4f}") | |
| 147 | - report("similarity is symmetric on lookup", | |
| 148 | - abs(g.userSimilarity(bob, alice) - ab) < 1e-12) | |
| 149 | - report("an unrelated pair scores zero", | |
| 150 | - g.userSimilarity(alice, "did:plc:nobody") == 0.0) | |
| 151 | - | |
| 152 | - # One row per unordered pair, or every score would be counted twice | |
| 153 | - # downstream. | |
| 154 | - let rows = g.db.queryInt("SELECT count(*) FROM recs.user_similarity").get(0) | |
| 155 | - report("each pair is stored once", rows == 3, &"{rows} rows") | |
| 156 | - | |
| 157 | - block: | |
| 158 | - # Recomputing must replace rather than accumulate. | |
| 159 | - g.computeUserSimilarity() | |
| 160 | - let rows = g.db.queryInt("SELECT count(*) FROM recs.user_similarity").get(0) | |
| 161 | - report("recomputing replaces the previous generation", rows == 3, | |
| 162 | - &"{rows} rows") | |
| 163 | - | |
| 164 | - block: | |
| 165 | - g.computeFeedSimilarity() | |
| 166 | - let f12 = g.feedSimilarity("https://f1.test/feed", "https://f2.test/feed") | |
| 167 | - let f13 = g.feedSimilarity("https://f1.test/feed", "https://f3.test/feed") | |
| 168 | - # f1 and f2 share alice and bob; f1 and f3 share only carol. | |
| 169 | - report("feeds with more shared subscribers score higher", f12 > f13, | |
| 170 | - &"f1/f2={f12:.4f} f1/f3={f13:.4f}") | |
| 171 | - let reversed = g.feedSimilarity("https://f2.test/feed", "https://f1.test/feed") | |
| 172 | - report("feed similarity is symmetric on lookup", abs(reversed - f12) < 1e-12) | |
| 173 | - report("an unshared pair is absent", | |
| 174 | - g.feedSimilarity("https://f2.test/feed", "https://f3.test/feed") == 0.0) | |
| 175 | - | |
| 176 | - # --- decay in the query, not just in Nim -------------------------------- | |
| 177 | - block: | |
| 178 | - # Two pairs of people who liked the same articles, one recently and one | |
| 179 | - # long ago. The old overlap must weigh less -- that is the whole reason | |
| 180 | - # EXP is registered as a SQL function. | |
| 181 | - g.upsertFeed(Feed(feedUrl: "https://n.test/feed", title: "News")) | |
| 182 | - let fresh = now().utc | |
| 183 | - let stale = now().utc - initDuration(days = 400) | |
| 184 | - for (who, at, n) in [(alice, fresh, "r"), (bob, fresh, "r"), | |
| 185 | - (carol, stale, "s")]: | |
| 186 | - g.createLike(Like(uri: &"at://{who}/like/{n}", authorDid: who, | |
| 187 | - feedUrl: "https://n.test/feed", | |
| 188 | - articleUrl: &"https://n.test/{n}", | |
| 189 | - createdAt: some(at))) | |
| 190 | - # carol needs a partner on the stale article for a pair to exist. | |
| 191 | - g.createLike(Like(uri: "at://dave/like/s", authorDid: "did:plc:dave", | |
| 192 | - feedUrl: "https://n.test/feed", | |
| 193 | - articleUrl: "https://n.test/s", | |
| 194 | - createdAt: some(stale))) | |
| 195 | - | |
| 196 | - g.computeUserSimilarity() | |
| 197 | - proc overlapOf(a, b: string): float = | |
| 198 | - let (x, y) = if a < b: (a, b) else: (b, a) | |
| 199 | - g.db.queryFirst( | |
| 200 | - "SELECT common_likes FROM recs.user_similarity WHERE user_a = ? AND user_b = ?", | |
| 201 | - [p(x), p(y)], proc(r: Row): float = r.f(0)).get(-1.0) | |
| 202 | - | |
| 203 | - let recent = overlapOf(alice, bob) | |
| 204 | - let old = overlapOf(carol, "did:plc:dave") | |
| 205 | - # The check that was vacuous before: both sides were 0 because the Go | |
| 206 | - # query truncates a decayed sum that is always just under 1. | |
| 207 | - report("a recent shared like counts as one", recent == 1.0, &"{recent}") | |
| 208 | - report("a 400-day-old shared like decays to nothing", old == 0.0, &"{old}") | |
| 209 | - report("decay makes old overlap weigh less than new", recent > old, | |
| 210 | - &"recent={recent} old={old}") | |
| 211 | - | |
| 212 | - echo "" | |
| 213 | - if failures == 0: | |
| 214 | - echo "RESULT: similarity and scoring behave." | |
| 215 | - else: | |
| 216 | - echo &"RESULT: {failures} check(s) failed." | |
| 217 | - quit 1 | |
| 218 | - | |
| 219 | -when isMainModule: | |
| 220 | - main() | |
| deleted file mode 100644 | |||
| @@ -1,220 +0,0 @@ | |||
| 1 | -## Checks the similarity computations and scoring helpers. | ||
| 2 | -## | ||
| 3 | -## nim c -r -d:ssl cluster_probe.nim | ||
| 4 | -## | ||
| 5 | -## Recommendations are the one part of this system with no obviously correct | ||
| 6 | -## answer, so the checks are about properties rather than values: that decay | ||
| 7 | -## actually decays, that a pair is counted once, that normalisation does not | ||
| 8 | -## produce NaN, that discovery is not just a mirror of who you already follow. | ||
| 9 | - | ||
| 10 | -import std/[math, options, os, random, sequtils, strformat, strutils, times] | ||
| 11 | -import atproto/[articlestore, cluster, feedstore, gleandb, socialstore, sqlite] | ||
| 12 | - | ||
| 13 | -var failures = 0 | ||
| 14 | - | ||
| 15 | -proc report(name: string, ok: bool, detail = "") = | ||
| 16 | - if not ok: inc failures | ||
| 17 | - let label = if ok: "PASS" else: "FAIL" | ||
| 18 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | ||
| 19 | - else: echo &" {label} {name}" | ||
| 20 | - | ||
| 21 | -proc cleanup(base: string) = | ||
| 22 | - for suffix in ["_users", "_articles", "_recs"]: | ||
| 23 | - for ext in ["", "-wal", "-shm"]: | ||
| 24 | - removeFile(base & suffix & ext) | ||
| 25 | - | ||
| 26 | -const | ||
| 27 | - alice = "did:plc:alice" | ||
| 28 | - bob = "did:plc:bob" | ||
| 29 | - carol = "did:plc:carol" | ||
| 30 | - | ||
| 31 | -proc main() = | ||
| 32 | - echo "Clustering probe" | ||
| 33 | - echo "" | ||
| 34 | - | ||
| 35 | - # --- pure helpers ------------------------------------------------------- | ||
| 36 | - block: | ||
| 37 | - report("decay at zero days is 1", abs(decay(0.0) - 1.0) < 1e-12) | ||
| 38 | - # ln(2)/0.023 is about 30 days, so a month-old event is worth half. | ||
| 39 | - report("decay halves in about a month", | ||
| 40 | - abs(decay(30.1) - 0.5) < 0.01, &"{decay(30.1):.4f}") | ||
| 41 | - report("decay keeps shrinking", decay(365.0) < decay(30.0)) | ||
| 42 | - report("decay never reaches zero", decay(3650.0) > 0.0, | ||
| 43 | - &"{decay(3650.0):.3e}") | ||
| 44 | - | ||
| 45 | - block: | ||
| 46 | - var s = @[0.0, 5.0, 10.0] | ||
| 47 | - normalizeScores(s) | ||
| 48 | - report("normalisation maps to 0..1", s == @[0.0, 0.5, 1.0], $s) | ||
| 49 | - | ||
| 50 | - # The case that would produce NaN: no spread to divide by. | ||
| 51 | - var flat = @[3.0, 3.0, 3.0] | ||
| 52 | - normalizeScores(flat) | ||
| 53 | - report("an all-equal set is left alone", flat == @[3.0, 3.0, 3.0], $flat) | ||
| 54 | - for v in flat: | ||
| 55 | - report("and produces no NaN", not v.isNaN) | ||
| 56 | - | ||
| 57 | - var one = @[7.0] | ||
| 58 | - normalizeScores(one) | ||
| 59 | - report("a single score is left alone", one == @[7.0]) | ||
| 60 | - | ||
| 61 | - var empty: seq[float] = @[] | ||
| 62 | - normalizeScores(empty) | ||
| 63 | - report("an empty set does not crash", empty.len == 0) | ||
| 64 | - | ||
| 65 | - var negatives = @[-10.0, -5.0, 0.0] | ||
| 66 | - normalizeScores(negatives) | ||
| 67 | - report("negative scores normalise too", negatives == @[0.0, 0.5, 1.0], | ||
| 68 | - $negatives) | ||
| 69 | - | ||
| 70 | - block: | ||
| 71 | - var rng = initRand(42) | ||
| 72 | - var pool: seq[PersonRecommendation] | ||
| 73 | - for i in 0 ..< 10: | ||
| 74 | - pool.add PersonRecommendation(did: &"did:plc:in{i}", isFollowed: true) | ||
| 75 | - for i in 0 ..< 10: | ||
| 76 | - pool.add PersonRecommendation(did: &"did:plc:out{i}", isFollowed: false) | ||
| 77 | - | ||
| 78 | - let sampled = samplePeople(pool, 3, rng) | ||
| 79 | - let followed = sampled.countIt(it.isFollowed) | ||
| 80 | - let strangers = sampled.len - followed | ||
| 81 | - report("sampling caps each side at the half", sampled.len == 6, | ||
| 82 | - $sampled.len) | ||
| 83 | - # Ranking purely by similarity returns the network the reader already | ||
| 84 | - # has; reserving half the slots is what makes this discovery. | ||
| 85 | - report("half the slots go to people not followed", | ||
| 86 | - followed == 3 and strangers == 3, | ||
| 87 | - &"{followed} followed, {strangers} strangers") | ||
| 88 | - | ||
| 89 | - # Fewer than `half` on one side must not pad from the other. | ||
| 90 | - var lopsided: seq[PersonRecommendation] | ||
| 91 | - lopsided.add PersonRecommendation(did: "did:plc:only", isFollowed: true) | ||
| 92 | - for i in 0 ..< 10: | ||
| 93 | - lopsided.add PersonRecommendation(did: &"did:plc:s{i}", isFollowed: false) | ||
| 94 | - let small = samplePeople(lopsided, 3, rng) | ||
| 95 | - report("a thin side is not padded from the other", | ||
| 96 | - small.countIt(it.isFollowed) == 1 and small.len == 4, $small.len) | ||
| 97 | - | ||
| 98 | - # Shuffled, so the same faces do not recur every visit. | ||
| 99 | - var rngA = initRand(1) | ||
| 100 | - var rngB = initRand(2) | ||
| 101 | - let a = samplePeople(pool, 3, rngA).mapIt(it.did).join(",") | ||
| 102 | - let b = samplePeople(pool, 3, rngB).mapIt(it.did).join(",") | ||
| 103 | - report("sampling varies between runs", a != b) | ||
| 104 | - | ||
| 105 | - block: | ||
| 106 | - var recs = @[ | ||
| 107 | - FeedRecommendation(feedUrl: "c", score: 0.1), | ||
| 108 | - FeedRecommendation(feedUrl: "a", score: 0.9), | ||
| 109 | - FeedRecommendation(feedUrl: "b", score: 0.5), | ||
| 110 | - ] | ||
| 111 | - rankByScore(recs) | ||
| 112 | - report("ranking puts the highest score first", | ||
| 113 | - recs.mapIt(it.feedUrl) == @["a", "b", "c"], | ||
| 114 | - recs.mapIt(it.feedUrl).join(",")) | ||
| 115 | - | ||
| 116 | - # --- similarity against a real database --------------------------------- | ||
| 117 | - let base = getTempDir() / "glean_cluster_probe" | ||
| 118 | - cleanup(base) | ||
| 119 | - defer: cleanup(base) | ||
| 120 | - | ||
| 121 | - var g = gleandb.open(base) | ||
| 122 | - defer: g.close() | ||
| 123 | - g.migrate() | ||
| 124 | - | ||
| 125 | - for did in [alice, bob, carol]: | ||
| 126 | - g.db.run("INSERT INTO users (did) VALUES (?)", p(did)) | ||
| 127 | - | ||
| 128 | - # alice and bob share two feeds; carol shares one with each. | ||
| 129 | - for i in 1 .. 3: | ||
| 130 | - g.upsertFeed(Feed(feedUrl: &"https://f{i}.test/feed", title: &"Feed {i}", | ||
| 131 | - feedType: "rss")) | ||
| 132 | - g.createSubscription(alice, "https://f1.test/feed", "", "", "", "") | ||
| 133 | - g.createSubscription(alice, "https://f2.test/feed", "", "", "", "") | ||
| 134 | - g.createSubscription(bob, "https://f1.test/feed", "", "", "", "") | ||
| 135 | - g.createSubscription(bob, "https://f2.test/feed", "", "", "", "") | ||
| 136 | - g.createSubscription(carol, "https://f1.test/feed", "", "", "", "") | ||
| 137 | - g.createSubscription(carol, "https://f3.test/feed", "", "", "", "") | ||
| 138 | - | ||
| 139 | - block: | ||
| 140 | - g.computeUserSimilarity() | ||
| 141 | - let ab = g.userSimilarity(alice, bob) | ||
| 142 | - let ac = g.userSimilarity(alice, carol) | ||
| 143 | - # alice/bob share 2 of 2; alice/carol share 1 of 3. | ||
| 144 | - report("identical subscribers score 1.0", abs(ab - 1.0) < 1e-9, &"{ab:.4f}") | ||
| 145 | - report("partial overlap scores lower", | ||
| 146 | - abs(ac - (1.0 / 3.0)) < 1e-9, &"{ac:.4f}") | ||
| 147 | - report("similarity is symmetric on lookup", | ||
| 148 | - abs(g.userSimilarity(bob, alice) - ab) < 1e-12) | ||
| 149 | - report("an unrelated pair scores zero", | ||
| 150 | - g.userSimilarity(alice, "did:plc:nobody") == 0.0) | ||
| 151 | - | ||
| 152 | - # One row per unordered pair, or every score would be counted twice | ||
| 153 | - # downstream. | ||
| 154 | - let rows = g.db.queryInt("SELECT count(*) FROM recs.user_similarity").get(0) | ||
| 155 | - report("each pair is stored once", rows == 3, &"{rows} rows") | ||
| 156 | - | ||
| 157 | - block: | ||
| 158 | - # Recomputing must replace rather than accumulate. | ||
| 159 | - g.computeUserSimilarity() | ||
| 160 | - let rows = g.db.queryInt("SELECT count(*) FROM recs.user_similarity").get(0) | ||
| 161 | - report("recomputing replaces the previous generation", rows == 3, | ||
| 162 | - &"{rows} rows") | ||
| 163 | - | ||
| 164 | - block: | ||
| 165 | - g.computeFeedSimilarity() | ||
| 166 | - let f12 = g.feedSimilarity("https://f1.test/feed", "https://f2.test/feed") | ||
| 167 | - let f13 = g.feedSimilarity("https://f1.test/feed", "https://f3.test/feed") | ||
| 168 | - # f1 and f2 share alice and bob; f1 and f3 share only carol. | ||
| 169 | - report("feeds with more shared subscribers score higher", f12 > f13, | ||
| 170 | - &"f1/f2={f12:.4f} f1/f3={f13:.4f}") | ||
| 171 | - let reversed = g.feedSimilarity("https://f2.test/feed", "https://f1.test/feed") | ||
| 172 | - report("feed similarity is symmetric on lookup", abs(reversed - f12) < 1e-12) | ||
| 173 | - report("an unshared pair is absent", | ||
| 174 | - g.feedSimilarity("https://f2.test/feed", "https://f3.test/feed") == 0.0) | ||
| 175 | - | ||
| 176 | - # --- decay in the query, not just in Nim -------------------------------- | ||
| 177 | - block: | ||
| 178 | - # Two pairs of people who liked the same articles, one recently and one | ||
| 179 | - # long ago. The old overlap must weigh less -- that is the whole reason | ||
| 180 | - # EXP is registered as a SQL function. | ||
| 181 | - g.upsertFeed(Feed(feedUrl: "https://n.test/feed", title: "News")) | ||
| 182 | - let fresh = now().utc | ||
| 183 | - let stale = now().utc - initDuration(days = 400) | ||
| 184 | - for (who, at, n) in [(alice, fresh, "r"), (bob, fresh, "r"), | ||
| 185 | - (carol, stale, "s")]: | ||
| 186 | - g.createLike(Like(uri: &"at://{who}/like/{n}", authorDid: who, | ||
| 187 | - feedUrl: "https://n.test/feed", | ||
| 188 | - articleUrl: &"https://n.test/{n}", | ||
| 189 | - createdAt: some(at))) | ||
| 190 | - # carol needs a partner on the stale article for a pair to exist. | ||
| 191 | - g.createLike(Like(uri: "at://dave/like/s", authorDid: "did:plc:dave", | ||
| 192 | - feedUrl: "https://n.test/feed", | ||
| 193 | - articleUrl: "https://n.test/s", | ||
| 194 | - createdAt: some(stale))) | ||
| 195 | - | ||
| 196 | - g.computeUserSimilarity() | ||
| 197 | - proc overlapOf(a, b: string): float = | ||
| 198 | - let (x, y) = if a < b: (a, b) else: (b, a) | ||
| 199 | - g.db.queryFirst( | ||
| 200 | - "SELECT common_likes FROM recs.user_similarity WHERE user_a = ? AND user_b = ?", | ||
| 201 | - [p(x), p(y)], proc(r: Row): float = r.f(0)).get(-1.0) | ||
| 202 | - | ||
| 203 | - let recent = overlapOf(alice, bob) | ||
| 204 | - let old = overlapOf(carol, "did:plc:dave") | ||
| 205 | - # The check that was vacuous before: both sides were 0 because the Go | ||
| 206 | - # query truncates a decayed sum that is always just under 1. | ||
| 207 | - report("a recent shared like counts as one", recent == 1.0, &"{recent}") | ||
| 208 | - report("a 400-day-old shared like decays to nothing", old == 0.0, &"{old}") | ||
| 209 | - report("decay makes old overlap weigh less than new", recent > old, | ||
| 210 | - &"recent={recent} old={old}") | ||
| 211 | - | ||
| 212 | - echo "" | ||
| 213 | - if failures == 0: | ||
| 214 | - echo "RESULT: similarity and scoring behave." | ||
| 215 | - else: | ||
| 216 | - echo &"RESULT: {failures} check(s) failed." | ||
| 217 | - quit 1 | ||
| 218 | - | ||
| 219 | -when isMainModule: | ||
| 220 | - main() | ||
deleted
spike/nim/dpop_probe.nim +0 -23 | deleted file mode 100644 | ||
| @@ -1,23 +0,0 @@ | ||
| 1 | -## Cross-checks atproto/dpop.nim against an independent Go verifier. | |
| 2 | -## | |
| 3 | -## nim c -r dpop_probe.nim && ./dpop_probe | (cd verify && go run .) | |
| 4 | - | |
| 5 | -import std/json | |
| 6 | -import bearssl/rand | |
| 7 | -import atproto/dpop | |
| 8 | - | |
| 9 | -when isMainModule: | |
| 10 | - let rngRef = HmacDrbgContext.new() | |
| 11 | - doAssert rngRef != nil, "no system randomness" | |
| 12 | - var rng = rngRef[] | |
| 13 | - | |
| 14 | - let key = generateKey(rng) | |
| 15 | - let jwt = key.dpopProof(rng, "POST", "https://bsky.social/oauth/token", | |
| 16 | - nonce = "spike-nonce") | |
| 17 | - | |
| 18 | - # Emitted for verify/verify.go to check independently. | |
| 19 | - echo $(%*{ | |
| 20 | - "jwt": jwt, | |
| 21 | - "jwk": key.publicJwk, | |
| 22 | - "thumbprint": key.thumbprint, | |
| 23 | - }) | |
| deleted file mode 100644 | |||
| @@ -1,23 +0,0 @@ | |||
| 1 | -## Cross-checks atproto/dpop.nim against an independent Go verifier. | ||
| 2 | -## | ||
| 3 | -## nim c -r dpop_probe.nim && ./dpop_probe | (cd verify && go run .) | ||
| 4 | - | ||
| 5 | -import std/json | ||
| 6 | -import bearssl/rand | ||
| 7 | -import atproto/dpop | ||
| 8 | - | ||
| 9 | -when isMainModule: | ||
| 10 | - let rngRef = HmacDrbgContext.new() | ||
| 11 | - doAssert rngRef != nil, "no system randomness" | ||
| 12 | - var rng = rngRef[] | ||
| 13 | - | ||
| 14 | - let key = generateKey(rng) | ||
| 15 | - let jwt = key.dpopProof(rng, "POST", "https://bsky.social/oauth/token", | ||
| 16 | - nonce = "spike-nonce") | ||
| 17 | - | ||
| 18 | - # Emitted for verify/verify.go to check independently. | ||
| 19 | - echo $(%*{ | ||
| 20 | - "jwt": jwt, | ||
| 21 | - "jwk": key.publicJwk, | ||
| 22 | - "thumbprint": key.thumbprint, | ||
| 23 | - }) | ||
modified
spike/nim/feedfetcher_probe.nim +1 -1 | @@ -9,7 +9,7 @@ | ||
| 9 | 9 | ## against a real host on demand. |
| 10 | 10 | |
| 11 | 11 | import std/[strformat, strutils, tables] |
| 12 | -import atproto/[feedfetcher, feedparser] | |
| 12 | +import reader/[feedfetcher, feedparser] | |
| 13 | 13 | |
| 14 | 14 | var failures = 0 |
| 15 | 15 | |
| @@ -9,7 +9,7 @@ | |||
| 9 | ## against a real host on demand. | 9 | ## against a real host on demand. |
| 10 | 10 | ||
| 11 | import std/[strformat, strutils, tables] | 11 | import std/[strformat, strutils, tables] |
| 12 | -import atproto/[feedfetcher, feedparser] | 12 | +import reader/[feedfetcher, feedparser] |
| 13 | 13 | ||
| 14 | var failures = 0 | 14 | var failures = 0 |
| 15 | 15 | ||
modified
spike/nim/feedparser_probe.nim +1 -1 | @@ -8,7 +8,7 @@ | ||
| 8 | 8 | ## the wild are stranger than any fixture. |
| 9 | 9 | |
| 10 | 10 | import std/[httpclient, options, sequtils, strformat, strutils] |
| 11 | -import atproto/feedparser | |
| 11 | +import reader/feedparser | |
| 12 | 12 | |
| 13 | 13 | var failures = 0 |
| 14 | 14 | |
| @@ -8,7 +8,7 @@ | |||
| 8 | ## the wild are stranger than any fixture. | 8 | ## the wild are stranger than any fixture. |
| 9 | 9 | ||
| 10 | import std/[httpclient, options, sequtils, strformat, strutils] | 10 | import std/[httpclient, options, sequtils, strformat, strutils] |
| 11 | -import atproto/feedparser | 11 | +import reader/feedparser |
| 12 | 12 | ||
| 13 | var failures = 0 | 13 | var failures = 0 |
| 14 | 14 | ||
modified
spike/nim/glean.nim +1 -1 | @@ -12,7 +12,7 @@ | ||
| 12 | 12 | |
| 13 | 13 | import std/[asyncdispatch, asynchttpserver, httpclient, json, options, os, |
| 14 | 14 | strformat, strutils, uri] |
| 15 | -import atproto/[articlestore, feedfetcher, feedparser, feedstore, gleandb, | |
| 15 | +import reader/[articlestore, feedfetcher, feedparser, feedstore, gleandb, | |
| 16 | 16 | scraper, sqlite] |
| 17 | 17 | |
| 18 | 18 | const |
| @@ -12,7 +12,7 @@ | |||
| 12 | 12 | ||
| 13 | import std/[asyncdispatch, asynchttpserver, httpclient, json, options, os, | 13 | import std/[asyncdispatch, asynchttpserver, httpclient, json, options, os, |
| 14 | strformat, strutils, uri] | 14 | strformat, strutils, uri] |
| 15 | -import atproto/[articlestore, feedfetcher, feedparser, feedstore, gleandb, | 15 | +import reader/[articlestore, feedfetcher, feedparser, feedstore, gleandb, |
| 16 | scraper, sqlite] | 16 | scraper, sqlite] |
| 17 | 17 | ||
| 18 | const | 18 | const |
renamed
spike/nim/glean.nimble +2 -15 | similarity index 77% | ||
| rename from spike/nim/glean_spike.nimble | ||
| rename to spike/nim/glean.nimble | ||
| @@ -1,34 +1,21 @@ | ||
| 1 | 1 | version = "0.1.0" |
| 2 | 2 | author = "glean" |
| 3 | -description = "Nim port spike for glean: atproto, storage and the database layer" | |
| 3 | +description = "glean: a small RSS reader" | |
| 4 | 4 | license = "MIT" |
| 5 | 5 | srcDir = "." |
| 6 | 6 | |
| 7 | 7 | requires "nim >= 2.2.0" |
| 8 | -requires "bearssl" | |
| 9 | -requires "nimcrypto" | |
| 10 | -requires "chronos" | |
| 11 | -requires "websock" | |
| 12 | 8 | |
| 13 | 9 | # Probes that touch only this machine. Safe to run anywhere, including CI. |
| 14 | 10 | const offlineProbes = @[ |
| 15 | - "sqlite_probe", | |
| 16 | - "dpop_probe", | |
| 17 | - "store_probe", | |
| 18 | 11 | "gleandb_probe", |
| 19 | 12 | "stores_probe", |
| 20 | - "social_probe", | |
| 21 | - "cluster_probe", | |
| 22 | - "reconnect_probe", # uses a local websocket server, not the network | |
| 23 | 13 | ] |
| 24 | 14 | |
| 25 | 15 | # Probes that talk to real servers. Kept separate because a failure here can |
| 26 | 16 | # mean "bsky.social is having a bad day" rather than "the port is broken", and |
| 27 | 17 | # a test suite that cannot tell those apart stops being believed. |
| 28 | 18 | const networkProbes = @[ |
| 29 | - "oauth_probe", | |
| 30 | - "xrpc_probe", | |
| 31 | - "jetstream_probe", | |
| 32 | 19 | "feedparser_probe", # fixtures are offline, but it also fetches real feeds |
| 33 | 20 | "feedfetcher_probe", # retry policy is scripted; one check hits the network |
| 34 | 21 | "scraper_probe", # fixtures are offline; one check fetches a real article |
| @@ -58,5 +45,5 @@ task build, "Compile every module without running anything": | ||
| 58 | 45 | exec "nim c --hints:off -d:ssl -c " & probe & ".nim" |
| 59 | 46 | echo "\nEverything compiles." |
| 60 | 47 | |
| 61 | -task schema, "Regenerate atproto/schema.nim from the Go source": | |
| 48 | +task schema, "Regenerate reader/schema.nim from the Go source": | |
| 62 | 49 | exec "python3 tools/extract_schema.py" |
| similarity index 77% | |||
| rename from spike/nim/glean_spike.nimble | |||
| rename to spike/nim/glean.nimble | |||
| @@ -1,34 +1,21 @@ | |||
| 1 | version = "0.1.0" | 1 | version = "0.1.0" |
| 2 | author = "glean" | 2 | author = "glean" |
| 3 | -description = "Nim port spike for glean: atproto, storage and the database layer" | 3 | +description = "glean: a small RSS reader" |
| 4 | license = "MIT" | 4 | license = "MIT" |
| 5 | srcDir = "." | 5 | srcDir = "." |
| 6 | 6 | ||
| 7 | requires "nim >= 2.2.0" | 7 | requires "nim >= 2.2.0" |
| 8 | -requires "bearssl" | ||
| 9 | -requires "nimcrypto" | ||
| 10 | -requires "chronos" | ||
| 11 | -requires "websock" | ||
| 12 | 8 | ||
| 13 | # Probes that touch only this machine. Safe to run anywhere, including CI. | 9 | # Probes that touch only this machine. Safe to run anywhere, including CI. |
| 14 | const offlineProbes = @[ | 10 | const offlineProbes = @[ |
| 15 | - "sqlite_probe", | ||
| 16 | - "dpop_probe", | ||
| 17 | - "store_probe", | ||
| 18 | "gleandb_probe", | 11 | "gleandb_probe", |
| 19 | "stores_probe", | 12 | "stores_probe", |
| 20 | - "social_probe", | ||
| 21 | - "cluster_probe", | ||
| 22 | - "reconnect_probe", # uses a local websocket server, not the network | ||
| 23 | ] | 13 | ] |
| 24 | 14 | ||
| 25 | # Probes that talk to real servers. Kept separate because a failure here can | 15 | # Probes that talk to real servers. Kept separate because a failure here can |
| 26 | # mean "bsky.social is having a bad day" rather than "the port is broken", and | 16 | # mean "bsky.social is having a bad day" rather than "the port is broken", and |
| 27 | # a test suite that cannot tell those apart stops being believed. | 17 | # a test suite that cannot tell those apart stops being believed. |
| 28 | const networkProbes = @[ | 18 | const networkProbes = @[ |
| 29 | - "oauth_probe", | ||
| 30 | - "xrpc_probe", | ||
| 31 | - "jetstream_probe", | ||
| 32 | "feedparser_probe", # fixtures are offline, but it also fetches real feeds | 19 | "feedparser_probe", # fixtures are offline, but it also fetches real feeds |
| 33 | "feedfetcher_probe", # retry policy is scripted; one check hits the network | 20 | "feedfetcher_probe", # retry policy is scripted; one check hits the network |
| 34 | "scraper_probe", # fixtures are offline; one check fetches a real article | 21 | "scraper_probe", # fixtures are offline; one check fetches a real article |
| @@ -58,5 +45,5 @@ task build, "Compile every module without running anything": | |||
| 58 | exec "nim c --hints:off -d:ssl -c " & probe & ".nim" | 45 | exec "nim c --hints:off -d:ssl -c " & probe & ".nim" |
| 59 | echo "\nEverything compiles." | 46 | echo "\nEverything compiles." |
| 60 | 47 | ||
| 61 | -task schema, "Regenerate atproto/schema.nim from the Go source": | 48 | +task schema, "Regenerate reader/schema.nim from the Go source": |
| 62 | exec "python3 tools/extract_schema.py" | 49 | exec "python3 tools/extract_schema.py" |
modified
spike/nim/gleandb_probe.nim +14 -40 | @@ -9,7 +9,7 @@ | ||
| 9 | 9 | ## degrades or diverges later. |
| 10 | 10 | |
| 11 | 11 | import std/[options, os, strformat, strutils] |
| 12 | -import atproto/[gleandb, schema, sqlite] | |
| 12 | +import reader/[gleandb, schema, sqlite] | |
| 13 | 13 | |
| 14 | 14 | var failures = 0 |
| 15 | 15 | |
| @@ -20,7 +20,7 @@ proc report(name: string, ok: bool, detail = "") = | ||
| 20 | 20 | else: echo &" {label} {name}" |
| 21 | 21 | |
| 22 | 22 | proc cleanup(base: string) = |
| 23 | - for suffix in ["_users", "_articles", "_recs"]: | |
| 23 | + for suffix in ["_users", "_articles"]: | |
| 24 | 24 | for ext in ["", "-wal", "-shm"]: |
| 25 | 25 | removeFile(base & suffix & ext) |
| 26 | 26 | |
| @@ -35,43 +35,26 @@ proc main() = | ||
| 35 | 35 | var g = gleandb.open(base) |
| 36 | 36 | defer: g.close() |
| 37 | 37 | |
| 38 | - report("schema statements extracted from the Go source", | |
| 39 | - usersSchema.len + articlesSchema.len + recsSchema.len == 44, | |
| 40 | - &"{usersSchema.len} + {articlesSchema.len} + {recsSchema.len}") | |
| 38 | + report("the schema covers the reader's tables", readerSchema.len == 15, | |
| 39 | + &"{readerSchema.len} statements") | |
| 41 | 40 | |
| 42 | 41 | # --- the three databases ------------------------------------------------ |
| 43 | 42 | block: |
| 44 | 43 | let schemas = g.attachedSchemas() |
| 45 | - report("all three databases are attached", | |
| 46 | - "main" in schemas and "articles" in schemas and "recs" in schemas, | |
| 47 | - schemas.join(", ")) | |
| 44 | + report("both databases are attached", | |
| 45 | + "main" in schemas and "articles" in schemas, schemas.join(", ")) | |
| 48 | 46 | report("each is a separate file on disk", |
| 49 | - fileExists(base & "_users") and fileExists(base & "_articles") and | |
| 50 | - fileExists(base & "_recs")) | |
| 47 | + fileExists(base & "_users") and fileExists(base & "_articles")) | |
| 51 | 48 | |
| 52 | 49 | # --- pragmas ------------------------------------------------------------ |
| 53 | 50 | # WAL has to be set per schema: PRAGMA applies to one at a time, so setting |
| 54 | 51 | # it on main leaves the others journalling the slow way. |
| 55 | 52 | block: |
| 56 | - for name in ["", "articles", "recs"]: | |
| 53 | + for name in ["", "articles"]: | |
| 57 | 54 | let mode = g.journalMode(name).toLowerAscii |
| 58 | 55 | report(&"WAL is on for {(if name.len == 0: \"main\" else: name)}", |
| 59 | 56 | mode == "wal", mode) |
| 60 | 57 | |
| 61 | - # --- custom SQL functions ---------------------------------------------- | |
| 62 | - block: | |
| 63 | - let e = g.db.queryText("SELECT exp(1.0)") | |
| 64 | - report("exp() is callable from SQL", | |
| 65 | - e.isSome and e.get.startsWith("2.718"), e.get("")) | |
| 66 | - let l = g.db.queryText("SELECT log(2.718281828459045)") | |
| 67 | - report("log() is callable from SQL", | |
| 68 | - l.isSome and l.get.startsWith("1.0") or l.get("") == "1.0", l.get("")) | |
| 69 | - # log(0) is -inf; returning NULL keeps an ORDER BY sane. | |
| 70 | - report("log() of zero is NULL", | |
| 71 | - g.db.queryText("SELECT log(0)").isNone) | |
| 72 | - report("log() of a negative is NULL", | |
| 73 | - g.db.queryText("SELECT log(-5)").isNone) | |
| 74 | - | |
| 75 | 58 | # --- schema ------------------------------------------------------------- |
| 76 | 59 | g.migrate() |
| 77 | 60 | report("migrate() is idempotent", (g.migrate(); true)) |
| @@ -81,10 +64,12 @@ proc main() = | ||
| 81 | 64 | let n = g.db.queryInt( |
| 82 | 65 | &"SELECT count(*) FROM {schemaName}.sqlite_master WHERE type='table'") |
| 83 | 66 | n.get(0).int |
| 84 | - report("users tables created", tableCount("main") >= 7, $tableCount("main")) | |
| 85 | - report("articles tables created", tableCount("articles") >= 7, | |
| 86 | - $tableCount("articles")) | |
| 87 | - report("recs tables created", tableCount("recs") >= 7, $tableCount("recs")) | |
| 67 | + # main holds only the local user now; everything that grows lives in | |
| 68 | + # the articles database. | |
| 69 | + report("the users database holds just the user table", | |
| 70 | + tableCount("main") == 1, $tableCount("main")) | |
| 71 | + report("articles holds feeds, subscriptions, articles and read state", | |
| 72 | + tableCount("articles") >= 5, $tableCount("articles")) | |
| 88 | 73 | |
| 89 | 74 | # --- a real cross-database query --------------------------------------- |
| 90 | 75 | # The whole point of attaching rather than opening three connections. |
| @@ -139,17 +124,6 @@ proc main() = | ||
| 139 | 124 | if afterDelete.isNone: "row gone from the index" |
| 140 | 125 | else: "STILL INDEXED: " & afterDelete.get) |
| 141 | 126 | |
| 142 | - # --- vec0 --------------------------------------------------------------- | |
| 143 | - block: | |
| 144 | - g.createEmbeddingTables(dimension = 4) | |
| 145 | - report("embedding tables take their dimension from config", | |
| 146 | - g.db.queryInt( | |
| 147 | - "SELECT count(*) FROM recs.sqlite_master WHERE name LIKE '%embeddings%'" | |
| 148 | - ).get(0) > 0) | |
| 149 | - # Proves sqlite-vec is registered on this connection, not just linked. | |
| 150 | - let ver = g.db.queryText("SELECT vec_version()") | |
| 151 | - report("sqlite-vec is live on the glean connection", ver.isSome, ver.get("")) | |
| 152 | - | |
| 153 | 127 | echo "" |
| 154 | 128 | if failures == 0: |
| 155 | 129 | echo "RESULT: the database layer matches Glean's layout and behaviour." |
| @@ -9,7 +9,7 @@ | |||
| 9 | ## degrades or diverges later. | 9 | ## degrades or diverges later. |
| 10 | 10 | ||
| 11 | import std/[options, os, strformat, strutils] | 11 | import std/[options, os, strformat, strutils] |
| 12 | -import atproto/[gleandb, schema, sqlite] | 12 | +import reader/[gleandb, schema, sqlite] |
| 13 | 13 | ||
| 14 | var failures = 0 | 14 | var failures = 0 |
| 15 | 15 | ||
| @@ -20,7 +20,7 @@ proc report(name: string, ok: bool, detail = "") = | |||
| 20 | else: echo &" {label} {name}" | 20 | else: echo &" {label} {name}" |
| 21 | 21 | ||
| 22 | proc cleanup(base: string) = | 22 | proc cleanup(base: string) = |
| 23 | - for suffix in ["_users", "_articles", "_recs"]: | 23 | + for suffix in ["_users", "_articles"]: |
| 24 | for ext in ["", "-wal", "-shm"]: | 24 | for ext in ["", "-wal", "-shm"]: |
| 25 | removeFile(base & suffix & ext) | 25 | removeFile(base & suffix & ext) |
| 26 | 26 | ||
| @@ -35,43 +35,26 @@ proc main() = | |||
| 35 | var g = gleandb.open(base) | 35 | var g = gleandb.open(base) |
| 36 | defer: g.close() | 36 | defer: g.close() |
| 37 | 37 | ||
| 38 | - report("schema statements extracted from the Go source", | 38 | + report("the schema covers the reader's tables", readerSchema.len == 15, |
| 39 | - usersSchema.len + articlesSchema.len + recsSchema.len == 44, | 39 | + &"{readerSchema.len} statements") |
| 40 | - &"{usersSchema.len} + {articlesSchema.len} + {recsSchema.len}") | ||
| 41 | 40 | ||
| 42 | # --- the three databases ------------------------------------------------ | 41 | # --- the three databases ------------------------------------------------ |
| 43 | block: | 42 | block: |
| 44 | let schemas = g.attachedSchemas() | 43 | let schemas = g.attachedSchemas() |
| 45 | - report("all three databases are attached", | 44 | + report("both databases are attached", |
| 46 | - "main" in schemas and "articles" in schemas and "recs" in schemas, | 45 | + "main" in schemas and "articles" in schemas, schemas.join(", ")) |
| 47 | - schemas.join(", ")) | ||
| 48 | report("each is a separate file on disk", | 46 | report("each is a separate file on disk", |
| 49 | - fileExists(base & "_users") and fileExists(base & "_articles") and | 47 | + fileExists(base & "_users") and fileExists(base & "_articles")) |
| 50 | - fileExists(base & "_recs")) | ||
| 51 | 48 | ||
| 52 | # --- pragmas ------------------------------------------------------------ | 49 | # --- pragmas ------------------------------------------------------------ |
| 53 | # WAL has to be set per schema: PRAGMA applies to one at a time, so setting | 50 | # WAL has to be set per schema: PRAGMA applies to one at a time, so setting |
| 54 | # it on main leaves the others journalling the slow way. | 51 | # it on main leaves the others journalling the slow way. |
| 55 | block: | 52 | block: |
| 56 | - for name in ["", "articles", "recs"]: | 53 | + for name in ["", "articles"]: |
| 57 | let mode = g.journalMode(name).toLowerAscii | 54 | let mode = g.journalMode(name).toLowerAscii |
| 58 | report(&"WAL is on for {(if name.len == 0: \"main\" else: name)}", | 55 | report(&"WAL is on for {(if name.len == 0: \"main\" else: name)}", |
| 59 | mode == "wal", mode) | 56 | mode == "wal", mode) |
| 60 | 57 | ||
| 61 | - # --- custom SQL functions ---------------------------------------------- | ||
| 62 | - block: | ||
| 63 | - let e = g.db.queryText("SELECT exp(1.0)") | ||
| 64 | - report("exp() is callable from SQL", | ||
| 65 | - e.isSome and e.get.startsWith("2.718"), e.get("")) | ||
| 66 | - let l = g.db.queryText("SELECT log(2.718281828459045)") | ||
| 67 | - report("log() is callable from SQL", | ||
| 68 | - l.isSome and l.get.startsWith("1.0") or l.get("") == "1.0", l.get("")) | ||
| 69 | - # log(0) is -inf; returning NULL keeps an ORDER BY sane. | ||
| 70 | - report("log() of zero is NULL", | ||
| 71 | - g.db.queryText("SELECT log(0)").isNone) | ||
| 72 | - report("log() of a negative is NULL", | ||
| 73 | - g.db.queryText("SELECT log(-5)").isNone) | ||
| 74 | - | ||
| 75 | # --- schema ------------------------------------------------------------- | 58 | # --- schema ------------------------------------------------------------- |
| 76 | g.migrate() | 59 | g.migrate() |
| 77 | report("migrate() is idempotent", (g.migrate(); true)) | 60 | report("migrate() is idempotent", (g.migrate(); true)) |
| @@ -81,10 +64,12 @@ proc main() = | |||
| 81 | let n = g.db.queryInt( | 64 | let n = g.db.queryInt( |
| 82 | &"SELECT count(*) FROM {schemaName}.sqlite_master WHERE type='table'") | 65 | &"SELECT count(*) FROM {schemaName}.sqlite_master WHERE type='table'") |
| 83 | n.get(0).int | 66 | n.get(0).int |
| 84 | - report("users tables created", tableCount("main") >= 7, $tableCount("main")) | 67 | + # main holds only the local user now; everything that grows lives in |
| 85 | - report("articles tables created", tableCount("articles") >= 7, | 68 | + # the articles database. |
| 86 | - $tableCount("articles")) | 69 | + report("the users database holds just the user table", |
| 87 | - report("recs tables created", tableCount("recs") >= 7, $tableCount("recs")) | 70 | + tableCount("main") == 1, $tableCount("main")) |
| 71 | + report("articles holds feeds, subscriptions, articles and read state", | ||
| 72 | + tableCount("articles") >= 5, $tableCount("articles")) | ||
| 88 | 73 | ||
| 89 | # --- a real cross-database query --------------------------------------- | 74 | # --- a real cross-database query --------------------------------------- |
| 90 | # The whole point of attaching rather than opening three connections. | 75 | # The whole point of attaching rather than opening three connections. |
| @@ -139,17 +124,6 @@ proc main() = | |||
| 139 | if afterDelete.isNone: "row gone from the index" | 124 | if afterDelete.isNone: "row gone from the index" |
| 140 | else: "STILL INDEXED: " & afterDelete.get) | 125 | else: "STILL INDEXED: " & afterDelete.get) |
| 141 | 126 | ||
| 142 | - # --- vec0 --------------------------------------------------------------- | ||
| 143 | - block: | ||
| 144 | - g.createEmbeddingTables(dimension = 4) | ||
| 145 | - report("embedding tables take their dimension from config", | ||
| 146 | - g.db.queryInt( | ||
| 147 | - "SELECT count(*) FROM recs.sqlite_master WHERE name LIKE '%embeddings%'" | ||
| 148 | - ).get(0) > 0) | ||
| 149 | - # Proves sqlite-vec is registered on this connection, not just linked. | ||
| 150 | - let ver = g.db.queryText("SELECT vec_version()") | ||
| 151 | - report("sqlite-vec is live on the glean connection", ver.isSome, ver.get("")) | ||
| 152 | - | ||
| 153 | echo "" | 127 | echo "" |
| 154 | if failures == 0: | 128 | if failures == 0: |
| 155 | echo "RESULT: the database layer matches Glean's layout and behaviour." | 129 | echo "RESULT: the database layer matches Glean's layout and behaviour." |
deleted
spike/nim/jetstream_probe.nim +0 -150 | deleted file mode 100644 | ||
| @@ -1,150 +0,0 @@ | ||
| 1 | -## Reads the live ATProto firehose. | |
| 2 | -## | |
| 3 | -## nim c -r -d:ssl jetstream_probe.nim | |
| 4 | -## | |
| 5 | -## Jetstream is public, so unlike the OAuth and XRPC probes this one can check | |
| 6 | -## the real thing end to end. The point of interest is server-side filtering: | |
| 7 | -## an unfiltered subscription is the whole network, so a consumer that loses | |
| 8 | -## its query parameters still receives a healthy-looking torrent of events | |
| 9 | -## that are simply the wrong ones. | |
| 10 | - | |
| 11 | -import std/[json, sequtils, sets, strformat, strutils, tables] | |
| 12 | -import pkg/chronos | |
| 13 | -import atproto/jetstream | |
| 14 | - | |
| 15 | -var failures = 0 | |
| 16 | - | |
| 17 | -proc report(name: string, ok: bool, detail = "") = | |
| 18 | - if not ok: inc failures | |
| 19 | - let label = if ok: "PASS" else: "FAIL" | |
| 20 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | |
| 21 | - else: echo &" {label} {name}" | |
| 22 | - | |
| 23 | -proc main() {.async.} = | |
| 24 | - echo "Jetstream probe" | |
| 25 | - echo "" | |
| 26 | - | |
| 27 | - # --- URL construction --------------------------------------------------- | |
| 28 | - block: | |
| 29 | - let cfg = JetstreamConfig( | |
| 30 | - url: DefaultJetstream, | |
| 31 | - wantedCollections: @["app.bsky.feed.post", "app.bsky.feed.like"], | |
| 32 | - cursor: 123, | |
| 33 | - ) | |
| 34 | - let u = subscribeUrl(cfg) | |
| 35 | - report("adds /subscribe", "/subscribe" in u) | |
| 36 | - # Repeated keys, not one comma-joined value: Jetstream reads these as a | |
| 37 | - # multi-valued parameter and a joined string matches no collection. | |
| 38 | - report("repeats wantedCollections per value", | |
| 39 | - u.count("wantedCollections=") == 2, u.split('?')[^1]) | |
| 40 | - report("carries the cursor", "cursor=123" in u) | |
| 41 | - | |
| 42 | - block: | |
| 43 | - let cfg = JetstreamConfig(url: "wss://example.test/subscribe") | |
| 44 | - report("does not double up /subscribe", | |
| 45 | - subscribeUrl(cfg).count("/subscribe") == 1) | |
| 46 | - | |
| 47 | - # --- parsing ------------------------------------------------------------ | |
| 48 | - block: | |
| 49 | - let raw = """{"did":"did:plc:abc","time_us":1720000000000000, | |
| 50 | - "kind":"commit","commit":{"rev":"3k","operation":"create", | |
| 51 | - "collection":"app.bsky.feed.post","rkey":"xyz","cid":"bafy", | |
| 52 | - "record":{"text":"hi","$type":"app.bsky.feed.post"}}}""" | |
| 53 | - let ev = parseEvent(raw) | |
| 54 | - report("parses a commit event", ev.kind == ekCommit and ev.did == "did:plc:abc") | |
| 55 | - report("parses the operation", ev.commit.operation == coCreate) | |
| 56 | - report("parses the record", ev.commit.record{"text"}.getStr == "hi") | |
| 57 | - report("time_us survives as int64", ev.timeUs == 1720000000000000'i64, | |
| 58 | - $ev.timeUs) | |
| 59 | - | |
| 60 | - block: | |
| 61 | - # A delete carries no record; reading one must not crash a consumer. | |
| 62 | - let ev = parseEvent("""{"did":"did:plc:a","time_us":1,"kind":"commit", | |
| 63 | - "commit":{"operation":"delete","collection":"c","rkey":"r"}}""") | |
| 64 | - report("a delete has no record", | |
| 65 | - ev.commit.operation == coDelete and | |
| 66 | - (ev.commit.record == nil or ev.commit.record.kind == JNull)) | |
| 67 | - | |
| 68 | - # --- the live firehose -------------------------------------------------- | |
| 69 | - const wanted = "app.bsky.feed.post" | |
| 70 | - var events: seq[Event] | |
| 71 | - try: | |
| 72 | - let cfg = JetstreamConfig( | |
| 73 | - url: DefaultJetstream, | |
| 74 | - wantedCollections: @[wanted], | |
| 75 | - ) | |
| 76 | - events = await collectEvents(cfg, count = 25, timeout = 30.seconds) | |
| 77 | - except CatchableError as e: | |
| 78 | - report("connected to the live firehose", false, e.msg) | |
| 79 | - echo "" | |
| 80 | - echo "RESULT: could not reach Jetstream." | |
| 81 | - quit 1 | |
| 82 | - | |
| 83 | - report("received live events", events.len > 0, &"{events.len} events") | |
| 84 | - if events.len == 0: | |
| 85 | - echo "" | |
| 86 | - echo "RESULT: no events received." | |
| 87 | - quit 1 | |
| 88 | - | |
| 89 | - report("every event has a DID", events.allIt(it.did.startsWith("did:"))) | |
| 90 | - report("cursors advance", | |
| 91 | - events[^1].timeUs >= events[0].timeUs, | |
| 92 | - &"{events[0].timeUs} -> {events[^1].timeUs}") | |
| 93 | - | |
| 94 | - # The real check: server-side filtering actually applied. Without it this | |
| 95 | - # would be a mix of likes, follows, reposts and everything else. | |
| 96 | - var collections: CountTable[string] | |
| 97 | - for ev in events: | |
| 98 | - if ev.kind == ekCommit: | |
| 99 | - collections.inc ev.commit.collection | |
| 100 | - let others = toSeq(collections.keys).filterIt(it != wanted and it.len > 0) | |
| 101 | - report("filter applied server-side (only " & wanted & ")", | |
| 102 | - others.len == 0, | |
| 103 | - if others.len == 0: $collections.len & " collection" | |
| 104 | - else: "also saw " & others.join(", ")) | |
| 105 | - | |
| 106 | - let posts = events.filterIt( | |
| 107 | - it.kind == ekCommit and it.commit.collection == wanted and | |
| 108 | - it.commit.operation == coCreate and it.commit.record != nil) | |
| 109 | - report("create events carry a typed record", | |
| 110 | - posts.len == 0 or posts[0].commit.record{"$type"}.getStr == wanted, | |
| 111 | - if posts.len > 0: posts[0].commit.record{"$type"}.getStr else: "none seen") | |
| 112 | - | |
| 113 | - # --- cursor replay ------------------------------------------------------ | |
| 114 | - # This is how a consumer catches up after downtime, so "the cursor is | |
| 115 | - # accepted" is not enough -- it has to actually rewind. Replaying from an | |
| 116 | - # earlier cursor must return events at or after it, including ones already | |
| 117 | - # seen live. | |
| 118 | - block: | |
| 119 | - let rewindTo = events[0].timeUs | |
| 120 | - let seenLive = events.mapIt(it.timeUs).toHashSet | |
| 121 | - try: | |
| 122 | - let cfg = JetstreamConfig( | |
| 123 | - url: DefaultJetstream, | |
| 124 | - wantedCollections: @[wanted], | |
| 125 | - cursor: rewindTo, | |
| 126 | - ) | |
| 127 | - let replayed = await collectEvents(cfg, count = 25, timeout = 30.seconds) | |
| 128 | - report("replay from a cursor returns events", replayed.len > 0, | |
| 129 | - &"{replayed.len} events") | |
| 130 | - if replayed.len > 0: | |
| 131 | - report("replayed events start at or after the cursor", | |
| 132 | - replayed[0].timeUs >= rewindTo, | |
| 133 | - &"cursor {rewindTo} -> first {replayed[0].timeUs}") | |
| 134 | - # Proof it rewound rather than resuming live: the replay should | |
| 135 | - # re-deliver timestamps from the first batch. | |
| 136 | - let overlap = replayed.countIt(it.timeUs in seenLive) | |
| 137 | - report("replay re-delivers already-seen events (a real rewind)", | |
| 138 | - overlap > 0, &"{overlap} of {replayed.len} overlap") | |
| 139 | - except CatchableError as e: | |
| 140 | - report("replay from a cursor returns events", false, e.msg) | |
| 141 | - | |
| 142 | - echo "" | |
| 143 | - if failures == 0: | |
| 144 | - echo "RESULT: Jetstream works, with server-side filtering and replay confirmed." | |
| 145 | - else: | |
| 146 | - echo &"RESULT: {failures} check(s) failed." | |
| 147 | - quit 1 | |
| 148 | - | |
| 149 | -when isMainModule: | |
| 150 | - waitFor main() | |
| deleted file mode 100644 | |||
| @@ -1,150 +0,0 @@ | |||
| 1 | -## Reads the live ATProto firehose. | ||
| 2 | -## | ||
| 3 | -## nim c -r -d:ssl jetstream_probe.nim | ||
| 4 | -## | ||
| 5 | -## Jetstream is public, so unlike the OAuth and XRPC probes this one can check | ||
| 6 | -## the real thing end to end. The point of interest is server-side filtering: | ||
| 7 | -## an unfiltered subscription is the whole network, so a consumer that loses | ||
| 8 | -## its query parameters still receives a healthy-looking torrent of events | ||
| 9 | -## that are simply the wrong ones. | ||
| 10 | - | ||
| 11 | -import std/[json, sequtils, sets, strformat, strutils, tables] | ||
| 12 | -import pkg/chronos | ||
| 13 | -import atproto/jetstream | ||
| 14 | - | ||
| 15 | -var failures = 0 | ||
| 16 | - | ||
| 17 | -proc report(name: string, ok: bool, detail = "") = | ||
| 18 | - if not ok: inc failures | ||
| 19 | - let label = if ok: "PASS" else: "FAIL" | ||
| 20 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | ||
| 21 | - else: echo &" {label} {name}" | ||
| 22 | - | ||
| 23 | -proc main() {.async.} = | ||
| 24 | - echo "Jetstream probe" | ||
| 25 | - echo "" | ||
| 26 | - | ||
| 27 | - # --- URL construction --------------------------------------------------- | ||
| 28 | - block: | ||
| 29 | - let cfg = JetstreamConfig( | ||
| 30 | - url: DefaultJetstream, | ||
| 31 | - wantedCollections: @["app.bsky.feed.post", "app.bsky.feed.like"], | ||
| 32 | - cursor: 123, | ||
| 33 | - ) | ||
| 34 | - let u = subscribeUrl(cfg) | ||
| 35 | - report("adds /subscribe", "/subscribe" in u) | ||
| 36 | - # Repeated keys, not one comma-joined value: Jetstream reads these as a | ||
| 37 | - # multi-valued parameter and a joined string matches no collection. | ||
| 38 | - report("repeats wantedCollections per value", | ||
| 39 | - u.count("wantedCollections=") == 2, u.split('?')[^1]) | ||
| 40 | - report("carries the cursor", "cursor=123" in u) | ||
| 41 | - | ||
| 42 | - block: | ||
| 43 | - let cfg = JetstreamConfig(url: "wss://example.test/subscribe") | ||
| 44 | - report("does not double up /subscribe", | ||
| 45 | - subscribeUrl(cfg).count("/subscribe") == 1) | ||
| 46 | - | ||
| 47 | - # --- parsing ------------------------------------------------------------ | ||
| 48 | - block: | ||
| 49 | - let raw = """{"did":"did:plc:abc","time_us":1720000000000000, | ||
| 50 | - "kind":"commit","commit":{"rev":"3k","operation":"create", | ||
| 51 | - "collection":"app.bsky.feed.post","rkey":"xyz","cid":"bafy", | ||
| 52 | - "record":{"text":"hi","$type":"app.bsky.feed.post"}}}""" | ||
| 53 | - let ev = parseEvent(raw) | ||
| 54 | - report("parses a commit event", ev.kind == ekCommit and ev.did == "did:plc:abc") | ||
| 55 | - report("parses the operation", ev.commit.operation == coCreate) | ||
| 56 | - report("parses the record", ev.commit.record{"text"}.getStr == "hi") | ||
| 57 | - report("time_us survives as int64", ev.timeUs == 1720000000000000'i64, | ||
| 58 | - $ev.timeUs) | ||
| 59 | - | ||
| 60 | - block: | ||
| 61 | - # A delete carries no record; reading one must not crash a consumer. | ||
| 62 | - let ev = parseEvent("""{"did":"did:plc:a","time_us":1,"kind":"commit", | ||
| 63 | - "commit":{"operation":"delete","collection":"c","rkey":"r"}}""") | ||
| 64 | - report("a delete has no record", | ||
| 65 | - ev.commit.operation == coDelete and | ||
| 66 | - (ev.commit.record == nil or ev.commit.record.kind == JNull)) | ||
| 67 | - | ||
| 68 | - # --- the live firehose -------------------------------------------------- | ||
| 69 | - const wanted = "app.bsky.feed.post" | ||
| 70 | - var events: seq[Event] | ||
| 71 | - try: | ||
| 72 | - let cfg = JetstreamConfig( | ||
| 73 | - url: DefaultJetstream, | ||
| 74 | - wantedCollections: @[wanted], | ||
| 75 | - ) | ||
| 76 | - events = await collectEvents(cfg, count = 25, timeout = 30.seconds) | ||
| 77 | - except CatchableError as e: | ||
| 78 | - report("connected to the live firehose", false, e.msg) | ||
| 79 | - echo "" | ||
| 80 | - echo "RESULT: could not reach Jetstream." | ||
| 81 | - quit 1 | ||
| 82 | - | ||
| 83 | - report("received live events", events.len > 0, &"{events.len} events") | ||
| 84 | - if events.len == 0: | ||
| 85 | - echo "" | ||
| 86 | - echo "RESULT: no events received." | ||
| 87 | - quit 1 | ||
| 88 | - | ||
| 89 | - report("every event has a DID", events.allIt(it.did.startsWith("did:"))) | ||
| 90 | - report("cursors advance", | ||
| 91 | - events[^1].timeUs >= events[0].timeUs, | ||
| 92 | - &"{events[0].timeUs} -> {events[^1].timeUs}") | ||
| 93 | - | ||
| 94 | - # The real check: server-side filtering actually applied. Without it this | ||
| 95 | - # would be a mix of likes, follows, reposts and everything else. | ||
| 96 | - var collections: CountTable[string] | ||
| 97 | - for ev in events: | ||
| 98 | - if ev.kind == ekCommit: | ||
| 99 | - collections.inc ev.commit.collection | ||
| 100 | - let others = toSeq(collections.keys).filterIt(it != wanted and it.len > 0) | ||
| 101 | - report("filter applied server-side (only " & wanted & ")", | ||
| 102 | - others.len == 0, | ||
| 103 | - if others.len == 0: $collections.len & " collection" | ||
| 104 | - else: "also saw " & others.join(", ")) | ||
| 105 | - | ||
| 106 | - let posts = events.filterIt( | ||
| 107 | - it.kind == ekCommit and it.commit.collection == wanted and | ||
| 108 | - it.commit.operation == coCreate and it.commit.record != nil) | ||
| 109 | - report("create events carry a typed record", | ||
| 110 | - posts.len == 0 or posts[0].commit.record{"$type"}.getStr == wanted, | ||
| 111 | - if posts.len > 0: posts[0].commit.record{"$type"}.getStr else: "none seen") | ||
| 112 | - | ||
| 113 | - # --- cursor replay ------------------------------------------------------ | ||
| 114 | - # This is how a consumer catches up after downtime, so "the cursor is | ||
| 115 | - # accepted" is not enough -- it has to actually rewind. Replaying from an | ||
| 116 | - # earlier cursor must return events at or after it, including ones already | ||
| 117 | - # seen live. | ||
| 118 | - block: | ||
| 119 | - let rewindTo = events[0].timeUs | ||
| 120 | - let seenLive = events.mapIt(it.timeUs).toHashSet | ||
| 121 | - try: | ||
| 122 | - let cfg = JetstreamConfig( | ||
| 123 | - url: DefaultJetstream, | ||
| 124 | - wantedCollections: @[wanted], | ||
| 125 | - cursor: rewindTo, | ||
| 126 | - ) | ||
| 127 | - let replayed = await collectEvents(cfg, count = 25, timeout = 30.seconds) | ||
| 128 | - report("replay from a cursor returns events", replayed.len > 0, | ||
| 129 | - &"{replayed.len} events") | ||
| 130 | - if replayed.len > 0: | ||
| 131 | - report("replayed events start at or after the cursor", | ||
| 132 | - replayed[0].timeUs >= rewindTo, | ||
| 133 | - &"cursor {rewindTo} -> first {replayed[0].timeUs}") | ||
| 134 | - # Proof it rewound rather than resuming live: the replay should | ||
| 135 | - # re-deliver timestamps from the first batch. | ||
| 136 | - let overlap = replayed.countIt(it.timeUs in seenLive) | ||
| 137 | - report("replay re-delivers already-seen events (a real rewind)", | ||
| 138 | - overlap > 0, &"{overlap} of {replayed.len} overlap") | ||
| 139 | - except CatchableError as e: | ||
| 140 | - report("replay from a cursor returns events", false, e.msg) | ||
| 141 | - | ||
| 142 | - echo "" | ||
| 143 | - if failures == 0: | ||
| 144 | - echo "RESULT: Jetstream works, with server-side filtering and replay confirmed." | ||
| 145 | - else: | ||
| 146 | - echo &"RESULT: {failures} check(s) failed." | ||
| 147 | - quit 1 | ||
| 148 | - | ||
| 149 | -when isMainModule: | ||
| 150 | - waitFor main() | ||
modified
spike/nim/nim.cfg +0 -10 | @@ -1,12 +1,2 @@ | ||
| 1 | -# sqlite-vec is compiled into the binary rather than loaded at runtime, the | |
| 2 | -# same way the Go bindings do it. Without SQLITE_CORE the file builds as a | |
| 3 | -# loadable extension whose entry point reads its SQLite API pointer from the | |
| 4 | -# caller -- passing nil there segfaults instead of failing, so this define is | |
| 5 | -# load-bearing, not an optimisation. | |
| 6 | ---passC:"-DSQLITE_CORE" | |
| 7 | 1 | --passL:"-lsqlite3 -lm" |
| 8 | 2 | |
| 9 | -# websock pulls in chronicles, which will not compile unless a logging stream | |
| 10 | -# is configured. This spike has no use for its output, and leaving it on makes | |
| 11 | -# every consumer of websock inherit the requirement. | |
| 12 | ---define:"chronicles_enabled=off" | |
| @@ -1,12 +1,2 @@ | |||
| 1 | -# sqlite-vec is compiled into the binary rather than loaded at runtime, the | ||
| 2 | -# same way the Go bindings do it. Without SQLITE_CORE the file builds as a | ||
| 3 | -# loadable extension whose entry point reads its SQLite API pointer from the | ||
| 4 | -# caller -- passing nil there segfaults instead of failing, so this define is | ||
| 5 | -# load-bearing, not an optimisation. | ||
| 6 | ---passC:"-DSQLITE_CORE" | ||
| 7 | --passL:"-lsqlite3 -lm" | 1 | --passL:"-lsqlite3 -lm" |
| 8 | 2 | ||
| 9 | -# websock pulls in chronicles, which will not compile unless a logging stream | ||
| 10 | -# is configured. This spike has no use for its output, and leaving it on makes | ||
| 11 | -# every consumer of websock inherit the requirement. | ||
| 12 | ---define:"chronicles_enabled=off" | ||
deleted
spike/nim/oauth_probe.nim +0 -140 | deleted file mode 100644 | ||
| @@ -1,140 +0,0 @@ | ||
| 1 | -## Drives the OAuth flow against a real PDS, as far as it can go without a | |
| 2 | -## human at a browser. | |
| 3 | -## | |
| 4 | -## nim c -r -d:ssl oauth_probe.nim [handle] | |
| 5 | -## | |
| 6 | -## Everything here is unauthenticated and read-only except the PAR, which | |
| 7 | -## creates a short-lived pending authorization the server discards on its own. | |
| 8 | -## Getting a request_uri back is the meaningful result: it means the DPoP | |
| 9 | -## proof was accepted, the nonce retry worked, and PKCE was well-formed. | |
| 10 | - | |
| 11 | -import std/[httpclient, json, os, strformat, strutils, uri] | |
| 12 | -import bearssl/rand | |
| 13 | -import atproto/[dpop, identity, oauth] | |
| 14 | - | |
| 15 | -var failures = 0 | |
| 16 | - | |
| 17 | -proc report(name: string, ok: bool, detail = "") = | |
| 18 | - if not ok: inc failures | |
| 19 | - let label = if ok: "PASS" else: "FAIL" | |
| 20 | - if detail.len > 0: | |
| 21 | - echo &" {label} {name} -- {detail}" | |
| 22 | - else: | |
| 23 | - echo &" {label} {name}" | |
| 24 | - | |
| 25 | -when isMainModule: | |
| 26 | - let handle = if paramCount() >= 1: paramStr(1) else: "bsky.app" | |
| 27 | - echo "ATProto OAuth flow probe" | |
| 28 | - echo &" identifier: {handle}" | |
| 29 | - echo "" | |
| 30 | - | |
| 31 | - let rngRef = HmacDrbgContext.new() | |
| 32 | - doAssert rngRef != nil, "no system randomness" | |
| 33 | - var rng = rngRef[] | |
| 34 | - | |
| 35 | - # --- resolution --------------------------------------------------------- | |
| 36 | - var ident: Identity | |
| 37 | - try: | |
| 38 | - ident = lookup(handle) | |
| 39 | - report("handle resolves to a DID", ident.did.isDid, ident.did) | |
| 40 | - report("identity links to a PDS", ident.pdsEndpoint.len > 0, ident.pdsEndpoint) | |
| 41 | - except CatchableError as e: | |
| 42 | - report("handle resolves", false, e.msg) | |
| 43 | - quit 1 | |
| 44 | - | |
| 45 | - # --- discovery ---------------------------------------------------------- | |
| 46 | - var authServerUrl: string | |
| 47 | - try: | |
| 48 | - authServerUrl = resolveAuthServerUrl(ident.pdsEndpoint) | |
| 49 | - report("PDS advertises an auth server", authServerUrl.len > 0, authServerUrl) | |
| 50 | - except CatchableError as e: | |
| 51 | - report("PDS advertises an auth server", false, e.msg) | |
| 52 | - quit 1 | |
| 53 | - | |
| 54 | - var meta: AuthServerMetadata | |
| 55 | - try: | |
| 56 | - meta = resolveAuthServerMetadata(authServerUrl) | |
| 57 | - report("auth server metadata fetched", true, meta.issuer) | |
| 58 | - report("has a PAR endpoint", meta.parEndpoint.len > 0, meta.parEndpoint) | |
| 59 | - report("has a token endpoint", meta.tokenEndpoint.len > 0, meta.tokenEndpoint) | |
| 60 | - except CatchableError as e: | |
| 61 | - report("auth server metadata fetched", false, e.msg) | |
| 62 | - quit 1 | |
| 63 | - | |
| 64 | - # --- PKCE --------------------------------------------------------------- | |
| 65 | - block: | |
| 66 | - # RFC 7636 test vector: this verifier must produce this challenge. | |
| 67 | - const | |
| 68 | - verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" | |
| 69 | - want = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" | |
| 70 | - report("PKCE S256 matches RFC 7636 vector", | |
| 71 | - s256Challenge(verifier) == want, s256Challenge(verifier)) | |
| 72 | - | |
| 73 | - # --- the nonce dance ---------------------------------------------------- | |
| 74 | - # sendAuthRequest retries transparently, which would hide a server that | |
| 75 | - # never demanded a nonce. Prove the first unnonced attempt really is | |
| 76 | - # rejected, so the retry path is load-bearing rather than decorative. | |
| 77 | - let cfg0 = newLocalhostConfig("http://127.0.0.1:8080/callback") | |
| 78 | - block: | |
| 79 | - let | |
| 80 | - key = generateKey(rng) | |
| 81 | - proof = dpopProof(key, rng, "POST", meta.parEndpoint) | |
| 82 | - client = newHttpClient(userAgent = UserAgent, timeout = 15_000) | |
| 83 | - defer: client.close() | |
| 84 | - client.headers = newHttpHeaders({ | |
| 85 | - "Content-Type": "application/x-www-form-urlencoded", | |
| 86 | - "DPoP": proof, | |
| 87 | - }) | |
| 88 | - # The body must be fully valid: bsky.social validates parameters before it | |
| 89 | - # checks the nonce, so a stub body returns invalid_request and tells us | |
| 90 | - # nothing about the nonce requirement. | |
| 91 | - let verifier = secureRandomB64(48) | |
| 92 | - let body = @{ | |
| 93 | - "client_id": cfg0.clientId, | |
| 94 | - "state": secureRandomB64(16), | |
| 95 | - "redirect_uri": cfg0.callbackUrl, | |
| 96 | - "scope": scopeStr(cfg0.scopes), | |
| 97 | - "response_type": "code", | |
| 98 | - "code_challenge": s256Challenge(verifier), | |
| 99 | - "code_challenge_method": "S256", | |
| 100 | - }.encodeQuery | |
| 101 | - let res = client.post(meta.parEndpoint, body = body) | |
| 102 | - let code = try: parseJson(res.body){"error"}.getStr except CatchableError: "" | |
| 103 | - let nonce = res.headers.getOrDefault("DPoP-Nonce").string | |
| 104 | - report("first PAR without a nonce is rejected", | |
| 105 | - res.code.int == 400 and code == "use_dpop_nonce", &"HTTP {res.code.int} {code}") | |
| 106 | - report("rejection supplies a DPoP-Nonce to retry with", nonce.len > 0) | |
| 107 | - | |
| 108 | - # --- PAR ---------------------------------------------------------------- | |
| 109 | - # A localhost client needs no hosted metadata document, so the flow can be | |
| 110 | - # exercised without deploying anything. | |
| 111 | - let cfg = cfg0 | |
| 112 | - report("localhost client_id is well-formed", | |
| 113 | - cfg.clientId.startsWith("http://localhost?"), cfg.clientId) | |
| 114 | - | |
| 115 | - try: | |
| 116 | - let info = sendAuthRequest(cfg, meta, rng, loginHint = handle) | |
| 117 | - report("PAR accepted (DPoP proof + nonce retry + PKCE)", | |
| 118 | - info.requestUri.startsWith("urn:ietf:params:oauth:request_uri:"), | |
| 119 | - info.requestUri) | |
| 120 | - report("auth server issued a DPoP nonce", info.dpopNonce.len > 0, | |
| 121 | - info.dpopNonce[0 ..< min(12, info.dpopNonce.len)] & "...") | |
| 122 | - report("PKCE verifier retained for callback", info.pkceVerifier.len >= 43) | |
| 123 | - report("state is unguessable", info.state.len >= 21, &"{info.state.len} chars") | |
| 124 | - | |
| 125 | - let url = authorizeUrl(cfg, meta, info) | |
| 126 | - let q = parseUri(url).query | |
| 127 | - report("authorize URL carries request_uri", "request_uri=" in q) | |
| 128 | - report("authorize URL carries client_id", "client_id=" in q) | |
| 129 | - echo "" | |
| 130 | - echo " authorize URL:" | |
| 131 | - echo " ", url | |
| 132 | - except CatchableError as e: | |
| 133 | - report("PAR accepted", false, e.msg) | |
| 134 | - | |
| 135 | - echo "" | |
| 136 | - if failures == 0: | |
| 137 | - echo "RESULT: the OAuth flow works against a live PDS up to user consent." | |
| 138 | - else: | |
| 139 | - echo &"RESULT: {failures} check(s) failed." | |
| 140 | - quit 1 | |
| deleted file mode 100644 | |||
| @@ -1,140 +0,0 @@ | |||
| 1 | -## Drives the OAuth flow against a real PDS, as far as it can go without a | ||
| 2 | -## human at a browser. | ||
| 3 | -## | ||
| 4 | -## nim c -r -d:ssl oauth_probe.nim [handle] | ||
| 5 | -## | ||
| 6 | -## Everything here is unauthenticated and read-only except the PAR, which | ||
| 7 | -## creates a short-lived pending authorization the server discards on its own. | ||
| 8 | -## Getting a request_uri back is the meaningful result: it means the DPoP | ||
| 9 | -## proof was accepted, the nonce retry worked, and PKCE was well-formed. | ||
| 10 | - | ||
| 11 | -import std/[httpclient, json, os, strformat, strutils, uri] | ||
| 12 | -import bearssl/rand | ||
| 13 | -import atproto/[dpop, identity, oauth] | ||
| 14 | - | ||
| 15 | -var failures = 0 | ||
| 16 | - | ||
| 17 | -proc report(name: string, ok: bool, detail = "") = | ||
| 18 | - if not ok: inc failures | ||
| 19 | - let label = if ok: "PASS" else: "FAIL" | ||
| 20 | - if detail.len > 0: | ||
| 21 | - echo &" {label} {name} -- {detail}" | ||
| 22 | - else: | ||
| 23 | - echo &" {label} {name}" | ||
| 24 | - | ||
| 25 | -when isMainModule: | ||
| 26 | - let handle = if paramCount() >= 1: paramStr(1) else: "bsky.app" | ||
| 27 | - echo "ATProto OAuth flow probe" | ||
| 28 | - echo &" identifier: {handle}" | ||
| 29 | - echo "" | ||
| 30 | - | ||
| 31 | - let rngRef = HmacDrbgContext.new() | ||
| 32 | - doAssert rngRef != nil, "no system randomness" | ||
| 33 | - var rng = rngRef[] | ||
| 34 | - | ||
| 35 | - # --- resolution --------------------------------------------------------- | ||
| 36 | - var ident: Identity | ||
| 37 | - try: | ||
| 38 | - ident = lookup(handle) | ||
| 39 | - report("handle resolves to a DID", ident.did.isDid, ident.did) | ||
| 40 | - report("identity links to a PDS", ident.pdsEndpoint.len > 0, ident.pdsEndpoint) | ||
| 41 | - except CatchableError as e: | ||
| 42 | - report("handle resolves", false, e.msg) | ||
| 43 | - quit 1 | ||
| 44 | - | ||
| 45 | - # --- discovery ---------------------------------------------------------- | ||
| 46 | - var authServerUrl: string | ||
| 47 | - try: | ||
| 48 | - authServerUrl = resolveAuthServerUrl(ident.pdsEndpoint) | ||
| 49 | - report("PDS advertises an auth server", authServerUrl.len > 0, authServerUrl) | ||
| 50 | - except CatchableError as e: | ||
| 51 | - report("PDS advertises an auth server", false, e.msg) | ||
| 52 | - quit 1 | ||
| 53 | - | ||
| 54 | - var meta: AuthServerMetadata | ||
| 55 | - try: | ||
| 56 | - meta = resolveAuthServerMetadata(authServerUrl) | ||
| 57 | - report("auth server metadata fetched", true, meta.issuer) | ||
| 58 | - report("has a PAR endpoint", meta.parEndpoint.len > 0, meta.parEndpoint) | ||
| 59 | - report("has a token endpoint", meta.tokenEndpoint.len > 0, meta.tokenEndpoint) | ||
| 60 | - except CatchableError as e: | ||
| 61 | - report("auth server metadata fetched", false, e.msg) | ||
| 62 | - quit 1 | ||
| 63 | - | ||
| 64 | - # --- PKCE --------------------------------------------------------------- | ||
| 65 | - block: | ||
| 66 | - # RFC 7636 test vector: this verifier must produce this challenge. | ||
| 67 | - const | ||
| 68 | - verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" | ||
| 69 | - want = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" | ||
| 70 | - report("PKCE S256 matches RFC 7636 vector", | ||
| 71 | - s256Challenge(verifier) == want, s256Challenge(verifier)) | ||
| 72 | - | ||
| 73 | - # --- the nonce dance ---------------------------------------------------- | ||
| 74 | - # sendAuthRequest retries transparently, which would hide a server that | ||
| 75 | - # never demanded a nonce. Prove the first unnonced attempt really is | ||
| 76 | - # rejected, so the retry path is load-bearing rather than decorative. | ||
| 77 | - let cfg0 = newLocalhostConfig("http://127.0.0.1:8080/callback") | ||
| 78 | - block: | ||
| 79 | - let | ||
| 80 | - key = generateKey(rng) | ||
| 81 | - proof = dpopProof(key, rng, "POST", meta.parEndpoint) | ||
| 82 | - client = newHttpClient(userAgent = UserAgent, timeout = 15_000) | ||
| 83 | - defer: client.close() | ||
| 84 | - client.headers = newHttpHeaders({ | ||
| 85 | - "Content-Type": "application/x-www-form-urlencoded", | ||
| 86 | - "DPoP": proof, | ||
| 87 | - }) | ||
| 88 | - # The body must be fully valid: bsky.social validates parameters before it | ||
| 89 | - # checks the nonce, so a stub body returns invalid_request and tells us | ||
| 90 | - # nothing about the nonce requirement. | ||
| 91 | - let verifier = secureRandomB64(48) | ||
| 92 | - let body = @{ | ||
| 93 | - "client_id": cfg0.clientId, | ||
| 94 | - "state": secureRandomB64(16), | ||
| 95 | - "redirect_uri": cfg0.callbackUrl, | ||
| 96 | - "scope": scopeStr(cfg0.scopes), | ||
| 97 | - "response_type": "code", | ||
| 98 | - "code_challenge": s256Challenge(verifier), | ||
| 99 | - "code_challenge_method": "S256", | ||
| 100 | - }.encodeQuery | ||
| 101 | - let res = client.post(meta.parEndpoint, body = body) | ||
| 102 | - let code = try: parseJson(res.body){"error"}.getStr except CatchableError: "" | ||
| 103 | - let nonce = res.headers.getOrDefault("DPoP-Nonce").string | ||
| 104 | - report("first PAR without a nonce is rejected", | ||
| 105 | - res.code.int == 400 and code == "use_dpop_nonce", &"HTTP {res.code.int} {code}") | ||
| 106 | - report("rejection supplies a DPoP-Nonce to retry with", nonce.len > 0) | ||
| 107 | - | ||
| 108 | - # --- PAR ---------------------------------------------------------------- | ||
| 109 | - # A localhost client needs no hosted metadata document, so the flow can be | ||
| 110 | - # exercised without deploying anything. | ||
| 111 | - let cfg = cfg0 | ||
| 112 | - report("localhost client_id is well-formed", | ||
| 113 | - cfg.clientId.startsWith("http://localhost?"), cfg.clientId) | ||
| 114 | - | ||
| 115 | - try: | ||
| 116 | - let info = sendAuthRequest(cfg, meta, rng, loginHint = handle) | ||
| 117 | - report("PAR accepted (DPoP proof + nonce retry + PKCE)", | ||
| 118 | - info.requestUri.startsWith("urn:ietf:params:oauth:request_uri:"), | ||
| 119 | - info.requestUri) | ||
| 120 | - report("auth server issued a DPoP nonce", info.dpopNonce.len > 0, | ||
| 121 | - info.dpopNonce[0 ..< min(12, info.dpopNonce.len)] & "...") | ||
| 122 | - report("PKCE verifier retained for callback", info.pkceVerifier.len >= 43) | ||
| 123 | - report("state is unguessable", info.state.len >= 21, &"{info.state.len} chars") | ||
| 124 | - | ||
| 125 | - let url = authorizeUrl(cfg, meta, info) | ||
| 126 | - let q = parseUri(url).query | ||
| 127 | - report("authorize URL carries request_uri", "request_uri=" in q) | ||
| 128 | - report("authorize URL carries client_id", "client_id=" in q) | ||
| 129 | - echo "" | ||
| 130 | - echo " authorize URL:" | ||
| 131 | - echo " ", url | ||
| 132 | - except CatchableError as e: | ||
| 133 | - report("PAR accepted", false, e.msg) | ||
| 134 | - | ||
| 135 | - echo "" | ||
| 136 | - if failures == 0: | ||
| 137 | - echo "RESULT: the OAuth flow works against a live PDS up to user consent." | ||
| 138 | - else: | ||
| 139 | - echo &"RESULT: {failures} check(s) failed." | ||
| 140 | - quit 1 | ||
renamed
spike/nim/reader/articlestore.nim +5 -12 | similarity index 95% | ||
| rename from spike/nim/atproto/articlestore.nim | ||
| rename to spike/nim/reader/articlestore.nim | ||
| @@ -26,8 +26,6 @@ type | ||
| 26 | 26 | published*: Option[DateTime] |
| 27 | 27 | updated*: Option[DateTime] |
| 28 | 28 | isRead*: bool |
| 29 | - likeCount*: int | |
| 30 | - hasLiked*: bool | |
| 31 | 29 | |
| 32 | 30 | ArticleFilter* = enum |
| 33 | 31 | afAll = "all" |
| @@ -64,8 +62,6 @@ proc readArticleRow*(r: Row): Article = | ||
| 64 | 62 | published: parseSqliteTime(r.str(11)), |
| 65 | 63 | updated: parseSqliteTime(r.str(12)), |
| 66 | 64 | isRead: r.b(13), |
| 67 | - likeCount: r.i(14), | |
| 68 | - hasLiked: r.b(15), | |
| 69 | 65 | ) |
| 70 | 66 | |
| 71 | 67 | ## The column list is shared so every listing returns the same shape and |
| @@ -75,10 +71,7 @@ const articleColumns = """ | ||
| 75 | 71 | a.id, a.feed_url, COALESCE(f.title, a.feed_url), f.favicon_url, |
| 76 | 72 | a.guid, a.title, a.url, a.author, a.summary, a.content, a.full_content, |
| 77 | 73 | a.published, a.updated, |
| 78 | - COALESCE(r.is_read, 0), | |
| 79 | - (SELECT count(*) FROM articles.likes l WHERE l.article_url = a.url), | |
| 80 | - EXISTS(SELECT 1 FROM articles.likes l | |
| 81 | - WHERE l.article_url = a.url AND l.author_did = ?)""" | |
| 74 | + COALESCE(r.is_read, 0)""" | |
| 82 | 75 | |
| 83 | 76 | const articleFrom = """ |
| 84 | 77 | FROM articles.articles a |
| @@ -87,11 +80,11 @@ const articleFrom = """ | ||
| 87 | 80 | |
| 88 | 81 | proc getArticle*(g: GleanDb, userDid: string, id: int64): Option[Article] = |
| 89 | 82 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.id = ?", |
| 90 | - [p(userDid), p(userDid), p(id)], readArticleRow) | |
| 83 | + [p(userDid), p(id)], readArticleRow) | |
| 91 | 84 | |
| 92 | 85 | proc getArticleByUrl*(g: GleanDb, userDid, url: string): Option[Article] = |
| 93 | 86 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.url = ?", |
| 94 | - [p(userDid), p(userDid), p(url)], readArticleRow) | |
| 87 | + [p(userDid), p(url)], readArticleRow) | |
| 95 | 88 | |
| 96 | 89 | proc listArticles*(g: GleanDb, userDid: string, filter = afAll, |
| 97 | 90 | feedUrl = "", category = "", sortOldest = false, |
| @@ -101,7 +94,7 @@ proc listArticles*(g: GleanDb, userDid: string, filter = afAll, | ||
| 101 | 94 | JOIN articles.subscriptions s |
| 102 | 95 | ON s.feed_url = a.feed_url AND s.user_did = ? |
| 103 | 96 | WHERE 1=1""" |
| 104 | - var params = @[p(userDid), p(userDid), p(userDid)] | |
| 97 | + var params = @[p(userDid), p(userDid)] | |
| 105 | 98 | |
| 106 | 99 | case filter |
| 107 | 100 | of afUnread: sql &= " AND COALESCE(r.is_read, 0) = 0" |
| @@ -147,7 +140,7 @@ proc searchArticles*(g: GleanDb, userDid, query: string, | ||
| 147 | 140 | WHERE articles_fts MATCH ?) m ON m.rowid = a.id |
| 148 | 141 | ORDER BY m.rank |
| 149 | 142 | LIMIT ? OFFSET ?""", |
| 150 | - [p(userDid), p(userDid), p(userDid), p(query), | |
| 143 | + [p(userDid), p(userDid), p(query), | |
| 151 | 144 | p(limit.int64), p(offset.int64)], readArticleRow) |
| 152 | 145 | |
| 153 | 146 | proc batchUpsertArticles*(g: GleanDb, articles: seq[NewArticle], |
| similarity index 95% | |||
| rename from spike/nim/atproto/articlestore.nim | |||
| rename to spike/nim/reader/articlestore.nim | |||
| @@ -26,8 +26,6 @@ type | |||
| 26 | published*: Option[DateTime] | 26 | published*: Option[DateTime] |
| 27 | updated*: Option[DateTime] | 27 | updated*: Option[DateTime] |
| 28 | isRead*: bool | 28 | isRead*: bool |
| 29 | - likeCount*: int | ||
| 30 | - hasLiked*: bool | ||
| 31 | 29 | ||
| 32 | ArticleFilter* = enum | 30 | ArticleFilter* = enum |
| 33 | afAll = "all" | 31 | afAll = "all" |
| @@ -64,8 +62,6 @@ proc readArticleRow*(r: Row): Article = | |||
| 64 | published: parseSqliteTime(r.str(11)), | 62 | published: parseSqliteTime(r.str(11)), |
| 65 | updated: parseSqliteTime(r.str(12)), | 63 | updated: parseSqliteTime(r.str(12)), |
| 66 | isRead: r.b(13), | 64 | isRead: r.b(13), |
| 67 | - likeCount: r.i(14), | ||
| 68 | - hasLiked: r.b(15), | ||
| 69 | ) | 65 | ) |
| 70 | 66 | ||
| 71 | ## The column list is shared so every listing returns the same shape and | 67 | ## The column list is shared so every listing returns the same shape and |
| @@ -75,10 +71,7 @@ const articleColumns = """ | |||
| 75 | a.id, a.feed_url, COALESCE(f.title, a.feed_url), f.favicon_url, | 71 | a.id, a.feed_url, COALESCE(f.title, a.feed_url), f.favicon_url, |
| 76 | a.guid, a.title, a.url, a.author, a.summary, a.content, a.full_content, | 72 | a.guid, a.title, a.url, a.author, a.summary, a.content, a.full_content, |
| 77 | a.published, a.updated, | 73 | a.published, a.updated, |
| 78 | - COALESCE(r.is_read, 0), | 74 | + COALESCE(r.is_read, 0)""" |
| 79 | - (SELECT count(*) FROM articles.likes l WHERE l.article_url = a.url), | ||
| 80 | - EXISTS(SELECT 1 FROM articles.likes l | ||
| 81 | - WHERE l.article_url = a.url AND l.author_did = ?)""" | ||
| 82 | 75 | ||
| 83 | const articleFrom = """ | 76 | const articleFrom = """ |
| 84 | FROM articles.articles a | 77 | FROM articles.articles a |
| @@ -87,11 +80,11 @@ const articleFrom = """ | |||
| 87 | 80 | ||
| 88 | proc getArticle*(g: GleanDb, userDid: string, id: int64): Option[Article] = | 81 | proc getArticle*(g: GleanDb, userDid: string, id: int64): Option[Article] = |
| 89 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.id = ?", | 82 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.id = ?", |
| 90 | - [p(userDid), p(userDid), p(id)], readArticleRow) | 83 | + [p(userDid), p(id)], readArticleRow) |
| 91 | 84 | ||
| 92 | proc getArticleByUrl*(g: GleanDb, userDid, url: string): Option[Article] = | 85 | proc getArticleByUrl*(g: GleanDb, userDid, url: string): Option[Article] = |
| 93 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.url = ?", | 86 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.url = ?", |
| 94 | - [p(userDid), p(userDid), p(url)], readArticleRow) | 87 | + [p(userDid), p(url)], readArticleRow) |
| 95 | 88 | ||
| 96 | proc listArticles*(g: GleanDb, userDid: string, filter = afAll, | 89 | proc listArticles*(g: GleanDb, userDid: string, filter = afAll, |
| 97 | feedUrl = "", category = "", sortOldest = false, | 90 | feedUrl = "", category = "", sortOldest = false, |
| @@ -101,7 +94,7 @@ proc listArticles*(g: GleanDb, userDid: string, filter = afAll, | |||
| 101 | JOIN articles.subscriptions s | 94 | JOIN articles.subscriptions s |
| 102 | ON s.feed_url = a.feed_url AND s.user_did = ? | 95 | ON s.feed_url = a.feed_url AND s.user_did = ? |
| 103 | WHERE 1=1""" | 96 | WHERE 1=1""" |
| 104 | - var params = @[p(userDid), p(userDid), p(userDid)] | 97 | + var params = @[p(userDid), p(userDid)] |
| 105 | 98 | ||
| 106 | case filter | 99 | case filter |
| 107 | of afUnread: sql &= " AND COALESCE(r.is_read, 0) = 0" | 100 | of afUnread: sql &= " AND COALESCE(r.is_read, 0) = 0" |
| @@ -147,7 +140,7 @@ proc searchArticles*(g: GleanDb, userDid, query: string, | |||
| 147 | WHERE articles_fts MATCH ?) m ON m.rowid = a.id | 140 | WHERE articles_fts MATCH ?) m ON m.rowid = a.id |
| 148 | ORDER BY m.rank | 141 | ORDER BY m.rank |
| 149 | LIMIT ? OFFSET ?""", | 142 | LIMIT ? OFFSET ?""", |
| 150 | - [p(userDid), p(userDid), p(userDid), p(query), | 143 | + [p(userDid), p(userDid), p(query), |
| 151 | p(limit.int64), p(offset.int64)], readArticleRow) | 144 | p(limit.int64), p(offset.int64)], readArticleRow) |
| 152 | 145 | ||
| 153 | proc batchUpsertArticles*(g: GleanDb, articles: seq[NewArticle], | 146 | proc batchUpsertArticles*(g: GleanDb, articles: seq[NewArticle], |
renamed
spike/nim/reader/feedfetcher.nim +0 -0 | similarity index 100% | ||
| rename from spike/nim/atproto/feedfetcher.nim | ||
| rename to spike/nim/reader/feedfetcher.nim |
| similarity index 100% | |||
| rename from spike/nim/atproto/feedfetcher.nim | |||
| rename to spike/nim/reader/feedfetcher.nim |
renamed
spike/nim/reader/feedparser.nim +0 -0 | similarity index 100% | ||
| rename from spike/nim/atproto/feedparser.nim | ||
| rename to spike/nim/reader/feedparser.nim |
| similarity index 100% | |||
| rename from spike/nim/atproto/feedparser.nim | |||
| rename to spike/nim/reader/feedparser.nim |
renamed
spike/nim/reader/feedstore.nim +0 -0 | similarity index 100% | ||
| rename from spike/nim/atproto/feedstore.nim | ||
| rename to spike/nim/reader/feedstore.nim |
| similarity index 100% | |||
| rename from spike/nim/atproto/feedstore.nim | |||
| rename to spike/nim/reader/feedstore.nim |
added
spike/nim/reader/gleandb.nim +86 -0 | new file mode 100644 | ||
| @@ -0,0 +1,86 @@ | ||
| 1 | +## The database: two SQLite files behind one connection. | |
| 2 | +## | |
| 3 | +## `main` holds the local user, `articles` the feeds and their contents. They | |
| 4 | +## are attached together so a query can join across them, while each still | |
| 5 | +## checkpoints and vacuums on its own -- which matters because the article | |
| 6 | +## store is the only one that grows without bound. | |
| 7 | +## | |
| 8 | +## The pragmas are copied from internal/db/db.go. They are not decoration: | |
| 9 | +## WAL is what lets readers run during a write, and the per-database repeats | |
| 10 | +## matter because PRAGMA applies to one schema at a time -- setting WAL on | |
| 11 | +## `main` leaves `articles` journalling the slow way. | |
| 12 | + | |
| 13 | +import std/[options, strformat, strutils] | |
| 14 | +import ./schema | |
| 15 | +import ./sqlite | |
| 16 | + | |
| 17 | +type | |
| 18 | + GleanDb* = object | |
| 19 | + db*: Db | |
| 20 | + basePath*: string | |
| 21 | + | |
| 22 | +const | |
| 23 | + MainPragmas = [ | |
| 24 | + "journal_mode=WAL", | |
| 25 | + "wal_autocheckpoint=1000", | |
| 26 | + "busy_timeout=30000", | |
| 27 | + "synchronous=NORMAL", | |
| 28 | + "temp_store=FILE", | |
| 29 | + "mmap_size=268435456", | |
| 30 | + "auto_vacuum=INCREMENTAL", | |
| 31 | + ] | |
| 32 | + AttachedPragmas = [ | |
| 33 | + "journal_mode=WAL", | |
| 34 | + "wal_autocheckpoint=1000", | |
| 35 | + "busy_timeout=30000", | |
| 36 | + "synchronous=NORMAL", | |
| 37 | + "temp_store=FILE", | |
| 38 | + "mmap_size=268435456", | |
| 39 | + ] | |
| 40 | + | |
| 41 | +proc applyPragmas(db: Db, schemaName: string, pragmas: openArray[string]) = | |
| 42 | + for pragma in pragmas: | |
| 43 | + # A pragma that will not take is not worth failing a boot over -- some are | |
| 44 | + # advisory, and mmap_size in particular is refused on filesystems that | |
| 45 | + # cannot map. The ones that matter are asserted after opening instead. | |
| 46 | + try: | |
| 47 | + if schemaName.len == 0: | |
| 48 | + db.exec &"PRAGMA {pragma}" | |
| 49 | + else: | |
| 50 | + db.exec &"PRAGMA {schemaName}.{pragma}" | |
| 51 | + except SqliteError: | |
| 52 | + discard | |
| 53 | + | |
| 54 | +proc open*(basePath: string): GleanDb = | |
| 55 | + ## Open `<basePath>_users` and attach `_articles`. | |
| 56 | + let | |
| 57 | + usersPath = basePath & "_users" | |
| 58 | + articlesPath = basePath & "_articles" | |
| 59 | + | |
| 60 | + var db = sqlite.open(usersPath) | |
| 61 | + applyPragmas(db, "", MainPragmas) | |
| 62 | + | |
| 63 | + # Quoted and single-quote-escaped: a path is not necessarily tame, and this | |
| 64 | + # one can come from configuration. | |
| 65 | + for (name, path) in [("articles", articlesPath)]: | |
| 66 | + db.exec &"ATTACH DATABASE '{path.replace(\"'\", \"''\")}' AS {name}" | |
| 67 | + applyPragmas(db, name, AttachedPragmas) | |
| 68 | + | |
| 69 | + GleanDb(db: db, basePath: basePath) | |
| 70 | + | |
| 71 | +proc migrate*(g: GleanDb) = | |
| 72 | + ## Apply the schema. Every statement is IF NOT EXISTS, so this runs on every | |
| 73 | + ## boot rather than being gated on a version. | |
| 74 | + for stmt in readerSchema: g.db.exec stmt | |
| 75 | + | |
| 76 | +proc close*(g: var GleanDb) = | |
| 77 | + g.db.close() | |
| 78 | + | |
| 79 | +proc journalMode*(g: GleanDb, schemaName = ""): string = | |
| 80 | + let sql = if schemaName.len == 0: "PRAGMA journal_mode" | |
| 81 | + else: &"PRAGMA {schemaName}.journal_mode" | |
| 82 | + g.db.queryText(sql).get("") | |
| 83 | + | |
| 84 | +proc attachedSchemas*(g: GleanDb): seq[string] = | |
| 85 | + for row in g.db.rows("PRAGMA database_list"): | |
| 86 | + result.add row[1] | |
| new file mode 100644 | |||
| @@ -0,0 +1,86 @@ | |||
| 1 | +## The database: two SQLite files behind one connection. | ||
| 2 | +## | ||
| 3 | +## `main` holds the local user, `articles` the feeds and their contents. They | ||
| 4 | +## are attached together so a query can join across them, while each still | ||
| 5 | +## checkpoints and vacuums on its own -- which matters because the article | ||
| 6 | +## store is the only one that grows without bound. | ||
| 7 | +## | ||
| 8 | +## The pragmas are copied from internal/db/db.go. They are not decoration: | ||
| 9 | +## WAL is what lets readers run during a write, and the per-database repeats | ||
| 10 | +## matter because PRAGMA applies to one schema at a time -- setting WAL on | ||
| 11 | +## `main` leaves `articles` journalling the slow way. | ||
| 12 | + | ||
| 13 | +import std/[options, strformat, strutils] | ||
| 14 | +import ./schema | ||
| 15 | +import ./sqlite | ||
| 16 | + | ||
| 17 | +type | ||
| 18 | + GleanDb* = object | ||
| 19 | + db*: Db | ||
| 20 | + basePath*: string | ||
| 21 | + | ||
| 22 | +const | ||
| 23 | + MainPragmas = [ | ||
| 24 | + "journal_mode=WAL", | ||
| 25 | + "wal_autocheckpoint=1000", | ||
| 26 | + "busy_timeout=30000", | ||
| 27 | + "synchronous=NORMAL", | ||
| 28 | + "temp_store=FILE", | ||
| 29 | + "mmap_size=268435456", | ||
| 30 | + "auto_vacuum=INCREMENTAL", | ||
| 31 | + ] | ||
| 32 | + AttachedPragmas = [ | ||
| 33 | + "journal_mode=WAL", | ||
| 34 | + "wal_autocheckpoint=1000", | ||
| 35 | + "busy_timeout=30000", | ||
| 36 | + "synchronous=NORMAL", | ||
| 37 | + "temp_store=FILE", | ||
| 38 | + "mmap_size=268435456", | ||
| 39 | + ] | ||
| 40 | + | ||
| 41 | +proc applyPragmas(db: Db, schemaName: string, pragmas: openArray[string]) = | ||
| 42 | + for pragma in pragmas: | ||
| 43 | + # A pragma that will not take is not worth failing a boot over -- some are | ||
| 44 | + # advisory, and mmap_size in particular is refused on filesystems that | ||
| 45 | + # cannot map. The ones that matter are asserted after opening instead. | ||
| 46 | + try: | ||
| 47 | + if schemaName.len == 0: | ||
| 48 | + db.exec &"PRAGMA {pragma}" | ||
| 49 | + else: | ||
| 50 | + db.exec &"PRAGMA {schemaName}.{pragma}" | ||
| 51 | + except SqliteError: | ||
| 52 | + discard | ||
| 53 | + | ||
| 54 | +proc open*(basePath: string): GleanDb = | ||
| 55 | + ## Open `<basePath>_users` and attach `_articles`. | ||
| 56 | + let | ||
| 57 | + usersPath = basePath & "_users" | ||
| 58 | + articlesPath = basePath & "_articles" | ||
| 59 | + | ||
| 60 | + var db = sqlite.open(usersPath) | ||
| 61 | + applyPragmas(db, "", MainPragmas) | ||
| 62 | + | ||
| 63 | + # Quoted and single-quote-escaped: a path is not necessarily tame, and this | ||
| 64 | + # one can come from configuration. | ||
| 65 | + for (name, path) in [("articles", articlesPath)]: | ||
| 66 | + db.exec &"ATTACH DATABASE '{path.replace(\"'\", \"''\")}' AS {name}" | ||
| 67 | + applyPragmas(db, name, AttachedPragmas) | ||
| 68 | + | ||
| 69 | + GleanDb(db: db, basePath: basePath) | ||
| 70 | + | ||
| 71 | +proc migrate*(g: GleanDb) = | ||
| 72 | + ## Apply the schema. Every statement is IF NOT EXISTS, so this runs on every | ||
| 73 | + ## boot rather than being gated on a version. | ||
| 74 | + for stmt in readerSchema: g.db.exec stmt | ||
| 75 | + | ||
| 76 | +proc close*(g: var GleanDb) = | ||
| 77 | + g.db.close() | ||
| 78 | + | ||
| 79 | +proc journalMode*(g: GleanDb, schemaName = ""): string = | ||
| 80 | + let sql = if schemaName.len == 0: "PRAGMA journal_mode" | ||
| 81 | + else: &"PRAGMA {schemaName}.journal_mode" | ||
| 82 | + g.db.queryText(sql).get("") | ||
| 83 | + | ||
| 84 | +proc attachedSchemas*(g: GleanDb): seq[string] = | ||
| 85 | + for row in g.db.rows("PRAGMA database_list"): | ||
| 86 | + result.add row[1] | ||
added
spike/nim/reader/schema.nim +97 -0 | new file mode 100644 | ||
| @@ -0,0 +1,97 @@ | ||
| 1 | +## The reader's schema. | |
| 2 | +## | |
| 3 | +## Two databases: `main` holds the single local user, `articles` holds feeds | |
| 4 | +## and their contents. They are separate files so the article store -- the | |
| 5 | +## only one that grows without bound -- can be vacuumed or discarded on its | |
| 6 | +## own. | |
| 7 | +## | |
| 8 | +## Originally generated from a Go server's schema and then trimmed to what a | |
| 9 | +## single-user reader needs: no accounts, no OAuth, no social records, no | |
| 10 | +## derived recommendation tables. What is left is feeds, subscriptions, | |
| 11 | +## articles, read state and the full-text index. It is maintained by hand | |
| 12 | +## from here. | |
| 13 | +## | |
| 14 | +## `read_state_history` is not redundant with `read_state`. Articles are | |
| 15 | +## purged on a retention window and can return from the feed later under a | |
| 16 | +## new surrogate id; the history is keyed on (feed_url, guid), which survives | |
| 17 | +## that, so a re-ingested article does not come back unread. | |
| 18 | + | |
| 19 | +const readerSchema* = [ | |
| 20 | + """CREATE TABLE IF NOT EXISTS users ( | |
| 21 | + did TEXT PRIMARY KEY, | |
| 22 | + indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 23 | + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 24 | + follows_dirty BOOLEAN NOT NULL DEFAULT 1 | |
| 25 | + )""", | |
| 26 | + """CREATE TABLE IF NOT EXISTS articles.feeds ( | |
| 27 | + feed_url TEXT PRIMARY KEY, | |
| 28 | + title TEXT, | |
| 29 | + site_url TEXT, | |
| 30 | + description TEXT, | |
| 31 | + feed_type TEXT CHECK(feed_type IN ('rss', 'atom', 'json', 'atproto')), | |
| 32 | + last_fetched_at DATETIME, | |
| 33 | + last_error TEXT, | |
| 34 | + subscriber_count INTEGER NOT NULL DEFAULT 0, | |
| 35 | + consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0, | |
| 36 | + error_count INTEGER NOT NULL DEFAULT 0, | |
| 37 | + favicon_url TEXT | |
| 38 | + )""", | |
| 39 | + """CREATE TABLE IF NOT EXISTS articles.subscriptions ( | |
| 40 | + id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 41 | + user_did TEXT NOT NULL, | |
| 42 | + feed_url TEXT NOT NULL, | |
| 43 | + title TEXT, | |
| 44 | + category TEXT, | |
| 45 | + added_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 46 | + uri TEXT, | |
| 47 | + cid TEXT, | |
| 48 | + UNIQUE(user_did, feed_url) | |
| 49 | + )""", | |
| 50 | + """CREATE TABLE IF NOT EXISTS articles.articles ( | |
| 51 | + id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 52 | + feed_url TEXT NOT NULL, | |
| 53 | + guid TEXT NOT NULL, | |
| 54 | + title TEXT NOT NULL DEFAULT '', | |
| 55 | + url TEXT, | |
| 56 | + author TEXT, | |
| 57 | + summary TEXT, | |
| 58 | + content TEXT, | |
| 59 | + full_content TEXT, | |
| 60 | + published DATETIME, | |
| 61 | + updated DATETIME, | |
| 62 | + fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 63 | + language TEXT NOT NULL DEFAULT '', | |
| 64 | + UNIQUE(feed_url, guid) | |
| 65 | + )""", | |
| 66 | + """CREATE TABLE IF NOT EXISTS articles.read_state ( | |
| 67 | + user_did TEXT NOT NULL, | |
| 68 | + article_id INTEGER NOT NULL, | |
| 69 | + is_read BOOLEAN NOT NULL DEFAULT 0, | |
| 70 | + read_at DATETIME, | |
| 71 | + PRIMARY KEY (user_did, article_id) | |
| 72 | + )""", | |
| 73 | + """CREATE TABLE IF NOT EXISTS articles.read_state_history ( | |
| 74 | + user_did TEXT NOT NULL, | |
| 75 | + feed_url TEXT NOT NULL, | |
| 76 | + guid TEXT NOT NULL, | |
| 77 | + is_read BOOLEAN NOT NULL DEFAULT 0, | |
| 78 | + read_at DATETIME, | |
| 79 | + PRIMARY KEY (user_did, feed_url, guid) | |
| 80 | + )""", | |
| 81 | + """CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed_user ON subscriptions(feed_url, user_did)""", | |
| 82 | + """CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_uri ON subscriptions(uri)""", | |
| 83 | + """CREATE INDEX IF NOT EXISTS articles.idx_articles_url ON articles(url)""", | |
| 84 | + """CREATE INDEX IF NOT EXISTS articles.idx_read_state_unread ON read_state(user_did, is_read) WHERE is_read = 0""", | |
| 85 | + """CREATE INDEX IF NOT EXISTS articles.idx_articles_language ON articles(language)""", | |
| 86 | + """CREATE VIRTUAL TABLE IF NOT EXISTS articles.articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)""", | |
| 87 | + """CREATE TRIGGER IF NOT EXISTS articles.articles_ai AFTER INSERT ON articles BEGIN | |
| 88 | + INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); | |
| 89 | + END""", | |
| 90 | + """CREATE TRIGGER IF NOT EXISTS articles.articles_ad AFTER DELETE ON articles BEGIN | |
| 91 | + INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); | |
| 92 | + END""", | |
| 93 | + """CREATE TRIGGER IF NOT EXISTS articles.articles_au AFTER UPDATE ON articles BEGIN | |
| 94 | + INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); | |
| 95 | + INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); | |
| 96 | + END""", | |
| 97 | +] | |
| new file mode 100644 | |||
| @@ -0,0 +1,97 @@ | |||
| 1 | +## The reader's schema. | ||
| 2 | +## | ||
| 3 | +## Two databases: `main` holds the single local user, `articles` holds feeds | ||
| 4 | +## and their contents. They are separate files so the article store -- the | ||
| 5 | +## only one that grows without bound -- can be vacuumed or discarded on its | ||
| 6 | +## own. | ||
| 7 | +## | ||
| 8 | +## Originally generated from a Go server's schema and then trimmed to what a | ||
| 9 | +## single-user reader needs: no accounts, no OAuth, no social records, no | ||
| 10 | +## derived recommendation tables. What is left is feeds, subscriptions, | ||
| 11 | +## articles, read state and the full-text index. It is maintained by hand | ||
| 12 | +## from here. | ||
| 13 | +## | ||
| 14 | +## `read_state_history` is not redundant with `read_state`. Articles are | ||
| 15 | +## purged on a retention window and can return from the feed later under a | ||
| 16 | +## new surrogate id; the history is keyed on (feed_url, guid), which survives | ||
| 17 | +## that, so a re-ingested article does not come back unread. | ||
| 18 | + | ||
| 19 | +const readerSchema* = [ | ||
| 20 | + """CREATE TABLE IF NOT EXISTS users ( | ||
| 21 | + did TEXT PRIMARY KEY, | ||
| 22 | + indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 23 | + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 24 | + follows_dirty BOOLEAN NOT NULL DEFAULT 1 | ||
| 25 | + )""", | ||
| 26 | + """CREATE TABLE IF NOT EXISTS articles.feeds ( | ||
| 27 | + feed_url TEXT PRIMARY KEY, | ||
| 28 | + title TEXT, | ||
| 29 | + site_url TEXT, | ||
| 30 | + description TEXT, | ||
| 31 | + feed_type TEXT CHECK(feed_type IN ('rss', 'atom', 'json', 'atproto')), | ||
| 32 | + last_fetched_at DATETIME, | ||
| 33 | + last_error TEXT, | ||
| 34 | + subscriber_count INTEGER NOT NULL DEFAULT 0, | ||
| 35 | + consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0, | ||
| 36 | + error_count INTEGER NOT NULL DEFAULT 0, | ||
| 37 | + favicon_url TEXT | ||
| 38 | + )""", | ||
| 39 | + """CREATE TABLE IF NOT EXISTS articles.subscriptions ( | ||
| 40 | + id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 41 | + user_did TEXT NOT NULL, | ||
| 42 | + feed_url TEXT NOT NULL, | ||
| 43 | + title TEXT, | ||
| 44 | + category TEXT, | ||
| 45 | + added_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 46 | + uri TEXT, | ||
| 47 | + cid TEXT, | ||
| 48 | + UNIQUE(user_did, feed_url) | ||
| 49 | + )""", | ||
| 50 | + """CREATE TABLE IF NOT EXISTS articles.articles ( | ||
| 51 | + id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 52 | + feed_url TEXT NOT NULL, | ||
| 53 | + guid TEXT NOT NULL, | ||
| 54 | + title TEXT NOT NULL DEFAULT '', | ||
| 55 | + url TEXT, | ||
| 56 | + author TEXT, | ||
| 57 | + summary TEXT, | ||
| 58 | + content TEXT, | ||
| 59 | + full_content TEXT, | ||
| 60 | + published DATETIME, | ||
| 61 | + updated DATETIME, | ||
| 62 | + fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 63 | + language TEXT NOT NULL DEFAULT '', | ||
| 64 | + UNIQUE(feed_url, guid) | ||
| 65 | + )""", | ||
| 66 | + """CREATE TABLE IF NOT EXISTS articles.read_state ( | ||
| 67 | + user_did TEXT NOT NULL, | ||
| 68 | + article_id INTEGER NOT NULL, | ||
| 69 | + is_read BOOLEAN NOT NULL DEFAULT 0, | ||
| 70 | + read_at DATETIME, | ||
| 71 | + PRIMARY KEY (user_did, article_id) | ||
| 72 | + )""", | ||
| 73 | + """CREATE TABLE IF NOT EXISTS articles.read_state_history ( | ||
| 74 | + user_did TEXT NOT NULL, | ||
| 75 | + feed_url TEXT NOT NULL, | ||
| 76 | + guid TEXT NOT NULL, | ||
| 77 | + is_read BOOLEAN NOT NULL DEFAULT 0, | ||
| 78 | + read_at DATETIME, | ||
| 79 | + PRIMARY KEY (user_did, feed_url, guid) | ||
| 80 | + )""", | ||
| 81 | + """CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed_user ON subscriptions(feed_url, user_did)""", | ||
| 82 | + """CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_uri ON subscriptions(uri)""", | ||
| 83 | + """CREATE INDEX IF NOT EXISTS articles.idx_articles_url ON articles(url)""", | ||
| 84 | + """CREATE INDEX IF NOT EXISTS articles.idx_read_state_unread ON read_state(user_did, is_read) WHERE is_read = 0""", | ||
| 85 | + """CREATE INDEX IF NOT EXISTS articles.idx_articles_language ON articles(language)""", | ||
| 86 | + """CREATE VIRTUAL TABLE IF NOT EXISTS articles.articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)""", | ||
| 87 | + """CREATE TRIGGER IF NOT EXISTS articles.articles_ai AFTER INSERT ON articles BEGIN | ||
| 88 | + INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); | ||
| 89 | + END""", | ||
| 90 | + """CREATE TRIGGER IF NOT EXISTS articles.articles_ad AFTER DELETE ON articles BEGIN | ||
| 91 | + INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); | ||
| 92 | + END""", | ||
| 93 | + """CREATE TRIGGER IF NOT EXISTS articles.articles_au AFTER UPDATE ON articles BEGIN | ||
| 94 | + INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); | ||
| 95 | + INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); | ||
| 96 | + END""", | ||
| 97 | +] | ||
renamed
spike/nim/reader/scraper.nim +0 -0 | similarity index 100% | ||
| rename from spike/nim/atproto/scraper.nim | ||
| rename to spike/nim/reader/scraper.nim |
| similarity index 100% | |||
| rename from spike/nim/atproto/scraper.nim | |||
| rename to spike/nim/reader/scraper.nim |
renamed
spike/nim/reader/sqlite.nim +5 -14 | similarity index 94% | ||
| rename from spike/nim/atproto/sqlite.nim | ||
| rename to spike/nim/reader/sqlite.nim | ||
| @@ -1,11 +1,8 @@ | ||
| 1 | -## A small SQLite wrapper, with sqlite-vec compiled in. | |
| 1 | +## A small SQLite wrapper. | |
| 2 | 2 | ## |
| 3 | -## Deliberately thin: enough to prepare, bind and step, not an ORM. The | |
| 4 | -## sqlite-vec extension is linked rather than loaded at runtime -- see | |
| 5 | -## nim.cfg, where the SQLITE_CORE define lives, and note that getting that | |
| 6 | -## wrong segfaults rather than erroring. | |
| 7 | - | |
| 8 | -{.compile: "../vendor/sqlite-vec.c".} | |
| 3 | +## Deliberately thin: enough to prepare, bind and step, not an ORM. FTS5 is | |
| 4 | +## used for article search and ships with the system library; nothing here | |
| 5 | +## needs an extension. | |
| 9 | 6 | |
| 10 | 7 | import std/options |
| 11 | 8 | |
| @@ -51,8 +48,6 @@ proc sqlite3_changes(db: pointer): cint | ||
| 51 | 48 | proc sqlite3_clear_bindings(s: pointer): cint |
| 52 | 49 | {.pop.} |
| 53 | 50 | |
| 54 | -proc sqlite3_vec_init(db: pointer, errMsg: ptr cstring, | |
| 55 | - api: pointer): cint {.importc, cdecl.} | |
| 56 | 51 | |
| 57 | 52 | const SQLITE_TRANSIENT = cast[pointer](-1) |
| 58 | 53 | |
| @@ -62,7 +57,7 @@ proc check(db: pointer, rc: cint, what: string) = | ||
| 62 | 57 | if rc notin [SQLITE_OK.cint, SQLITE_ROW.cint, SQLITE_DONE.cint]: |
| 63 | 58 | raise newException(SqliteError, what & ": " & $sqlite3_errmsg(db)) |
| 64 | 59 | |
| 65 | -proc open*(path: string, withVec = true): Db = | |
| 60 | +proc open*(path: string): Db = | |
| 66 | 61 | ## Open (or create) a database. |
| 67 | 62 | ## |
| 68 | 63 | ## WAL is set here rather than left to callers: it is what makes a reader |
| @@ -75,10 +70,6 @@ proc open*(path: string, withVec = true): Db = | ||
| 75 | 70 | raise newException(SqliteError, "opening " & path & ": " & msg) |
| 76 | 71 | result = Db(handle: handle) |
| 77 | 72 | |
| 78 | - if withVec: | |
| 79 | - let rc = sqlite3_vec_init(handle, nil, nil) | |
| 80 | - if rc != SQLITE_OK: | |
| 81 | - raise newException(SqliteError, "registering sqlite-vec failed") | |
| 82 | 73 | |
| 83 | 74 | discard sqlite3_exec(handle, "PRAGMA journal_mode=WAL", nil, nil, nil) |
| 84 | 75 | discard sqlite3_exec(handle, "PRAGMA synchronous=NORMAL", nil, nil, nil) |
| similarity index 94% | |||
| rename from spike/nim/atproto/sqlite.nim | |||
| rename to spike/nim/reader/sqlite.nim | |||
| @@ -1,11 +1,8 @@ | |||
| 1 | -## A small SQLite wrapper, with sqlite-vec compiled in. | 1 | +## A small SQLite wrapper. |
| 2 | ## | 2 | ## |
| 3 | -## Deliberately thin: enough to prepare, bind and step, not an ORM. The | 3 | +## Deliberately thin: enough to prepare, bind and step, not an ORM. FTS5 is |
| 4 | -## sqlite-vec extension is linked rather than loaded at runtime -- see | 4 | +## used for article search and ships with the system library; nothing here |
| 5 | -## nim.cfg, where the SQLITE_CORE define lives, and note that getting that | 5 | +## needs an extension. |
| 6 | -## wrong segfaults rather than erroring. | ||
| 7 | - | ||
| 8 | -{.compile: "../vendor/sqlite-vec.c".} | ||
| 9 | 6 | ||
| 10 | import std/options | 7 | import std/options |
| 11 | 8 | ||
| @@ -51,8 +48,6 @@ proc sqlite3_changes(db: pointer): cint | |||
| 51 | proc sqlite3_clear_bindings(s: pointer): cint | 48 | proc sqlite3_clear_bindings(s: pointer): cint |
| 52 | {.pop.} | 49 | {.pop.} |
| 53 | 50 | ||
| 54 | -proc sqlite3_vec_init(db: pointer, errMsg: ptr cstring, | ||
| 55 | - api: pointer): cint {.importc, cdecl.} | ||
| 56 | 51 | ||
| 57 | const SQLITE_TRANSIENT = cast[pointer](-1) | 52 | const SQLITE_TRANSIENT = cast[pointer](-1) |
| 58 | 53 | ||
| @@ -62,7 +57,7 @@ proc check(db: pointer, rc: cint, what: string) = | |||
| 62 | if rc notin [SQLITE_OK.cint, SQLITE_ROW.cint, SQLITE_DONE.cint]: | 57 | if rc notin [SQLITE_OK.cint, SQLITE_ROW.cint, SQLITE_DONE.cint]: |
| 63 | raise newException(SqliteError, what & ": " & $sqlite3_errmsg(db)) | 58 | raise newException(SqliteError, what & ": " & $sqlite3_errmsg(db)) |
| 64 | 59 | ||
| 65 | -proc open*(path: string, withVec = true): Db = | 60 | +proc open*(path: string): Db = |
| 66 | ## Open (or create) a database. | 61 | ## Open (or create) a database. |
| 67 | ## | 62 | ## |
| 68 | ## WAL is set here rather than left to callers: it is what makes a reader | 63 | ## WAL is set here rather than left to callers: it is what makes a reader |
| @@ -75,10 +70,6 @@ proc open*(path: string, withVec = true): Db = | |||
| 75 | raise newException(SqliteError, "opening " & path & ": " & msg) | 70 | raise newException(SqliteError, "opening " & path & ": " & msg) |
| 76 | result = Db(handle: handle) | 71 | result = Db(handle: handle) |
| 77 | 72 | ||
| 78 | - if withVec: | ||
| 79 | - let rc = sqlite3_vec_init(handle, nil, nil) | ||
| 80 | - if rc != SQLITE_OK: | ||
| 81 | - raise newException(SqliteError, "registering sqlite-vec failed") | ||
| 82 | 73 | ||
| 83 | discard sqlite3_exec(handle, "PRAGMA journal_mode=WAL", nil, nil, nil) | 74 | discard sqlite3_exec(handle, "PRAGMA journal_mode=WAL", nil, nil, nil) |
| 84 | discard sqlite3_exec(handle, "PRAGMA synchronous=NORMAL", nil, nil, nil) | 75 | discard sqlite3_exec(handle, "PRAGMA synchronous=NORMAL", nil, nil, nil) |
deleted
spike/nim/readme-reader.md +0 -56 | deleted file mode 100644 | ||
| @@ -1,56 +0,0 @@ | ||
| 1 | -# glean — a small RSS reader | |
| 2 | - | |
| 3 | -One binary, one SQLite database, no accounts. | |
| 4 | - | |
| 5 | -``` | |
| 6 | -nim c -d:ssl glean.nim | |
| 7 | -./glean | |
| 8 | -``` | |
| 9 | - | |
| 10 | -Listens on `127.0.0.1:8080` and stores its database under | |
| 11 | -`~/.local/share/glean/`. Override with `GLEAN_PORT` and `GLEAN_DB`. | |
| 12 | - | |
| 13 | -## API | |
| 14 | - | |
| 15 | -``` | |
| 16 | -GET /feeds subscriptions, with unread counts | |
| 17 | -POST /feeds url=… subscribe and fetch immediately | |
| 18 | -DELETE /feeds?url=… unsubscribe | |
| 19 | - | |
| 20 | -GET /articles unread, newest first | |
| 21 | -GET /articles?status=all|read …or everything, or just the read ones | |
| 22 | -GET /articles?feed=… …scoped to one feed | |
| 23 | -GET /articles?q=… full-text search (FTS5) | |
| 24 | -GET /articles/:id one article | |
| 25 | - | |
| 26 | -POST /read id=… mark read (undo=1 to reverse) | |
| 27 | -POST /read-all [feed=…] mark a feed, or everything, read | |
| 28 | -POST /refresh fetch every subscribed feed | |
| 29 | -POST /fetch-content id=… scrape the full article text | |
| 30 | - | |
| 31 | -GET /unread unread count | |
| 32 | -``` | |
| 33 | - | |
| 34 | -Parameters are read from the query string or a form body, whichever the | |
| 35 | -caller used. | |
| 36 | - | |
| 37 | -## What it does not do | |
| 38 | - | |
| 39 | -No accounts, no sessions, no CSRF, no ATProto. It runs on your machine and | |
| 40 | -reads your feeds, and that assumption is what keeps it a few hundred lines | |
| 41 | -rather than a few thousand. | |
| 42 | - | |
| 43 | -The one visible consequence: **do not expose it to a network.** It binds | |
| 44 | -loopback deliberately. Anything that can reach the port can read and change | |
| 45 | -everything, because there is nobody to distinguish from anybody else. | |
| 46 | - | |
| 47 | -## What it is built on | |
| 48 | - | |
| 49 | -The modules under `atproto/` — fetching with a sane retry policy, parsing | |
| 50 | -RSS/RDF/Atom/JSON Feed, SQLite with FTS5, and a readability-style extractor | |
| 51 | -that sanitises what it returns. Those came out of a larger port and carry | |
| 52 | -their own tests (`nimble test`); this file is a few hundred lines of routing | |
| 53 | -on top. | |
| 54 | - | |
| 55 | -Storage keeps a single hard-coded user, because the schema came from a | |
| 56 | -multi-user server and it was cheaper to satisfy that than to rewrite it. | |
| deleted file mode 100644 | |||
| @@ -1,56 +0,0 @@ | |||
| 1 | -# glean — a small RSS reader | ||
| 2 | - | ||
| 3 | -One binary, one SQLite database, no accounts. | ||
| 4 | - | ||
| 5 | -``` | ||
| 6 | -nim c -d:ssl glean.nim | ||
| 7 | -./glean | ||
| 8 | -``` | ||
| 9 | - | ||
| 10 | -Listens on `127.0.0.1:8080` and stores its database under | ||
| 11 | -`~/.local/share/glean/`. Override with `GLEAN_PORT` and `GLEAN_DB`. | ||
| 12 | - | ||
| 13 | -## API | ||
| 14 | - | ||
| 15 | -``` | ||
| 16 | -GET /feeds subscriptions, with unread counts | ||
| 17 | -POST /feeds url=… subscribe and fetch immediately | ||
| 18 | -DELETE /feeds?url=… unsubscribe | ||
| 19 | - | ||
| 20 | -GET /articles unread, newest first | ||
| 21 | -GET /articles?status=all|read …or everything, or just the read ones | ||
| 22 | -GET /articles?feed=… …scoped to one feed | ||
| 23 | -GET /articles?q=… full-text search (FTS5) | ||
| 24 | -GET /articles/:id one article | ||
| 25 | - | ||
| 26 | -POST /read id=… mark read (undo=1 to reverse) | ||
| 27 | -POST /read-all [feed=…] mark a feed, or everything, read | ||
| 28 | -POST /refresh fetch every subscribed feed | ||
| 29 | -POST /fetch-content id=… scrape the full article text | ||
| 30 | - | ||
| 31 | -GET /unread unread count | ||
| 32 | -``` | ||
| 33 | - | ||
| 34 | -Parameters are read from the query string or a form body, whichever the | ||
| 35 | -caller used. | ||
| 36 | - | ||
| 37 | -## What it does not do | ||
| 38 | - | ||
| 39 | -No accounts, no sessions, no CSRF, no ATProto. It runs on your machine and | ||
| 40 | -reads your feeds, and that assumption is what keeps it a few hundred lines | ||
| 41 | -rather than a few thousand. | ||
| 42 | - | ||
| 43 | -The one visible consequence: **do not expose it to a network.** It binds | ||
| 44 | -loopback deliberately. Anything that can reach the port can read and change | ||
| 45 | -everything, because there is nobody to distinguish from anybody else. | ||
| 46 | - | ||
| 47 | -## What it is built on | ||
| 48 | - | ||
| 49 | -The modules under `atproto/` — fetching with a sane retry policy, parsing | ||
| 50 | -RSS/RDF/Atom/JSON Feed, SQLite with FTS5, and a readability-style extractor | ||
| 51 | -that sanitises what it returns. Those came out of a larger port and carry | ||
| 52 | -their own tests (`nimble test`); this file is a few hundred lines of routing | ||
| 53 | -on top. | ||
| 54 | - | ||
| 55 | -Storage keeps a single hard-coded user, because the schema came from a | ||
| 56 | -multi-user server and it was cheaper to satisfy that than to rewrite it. | ||
modified
spike/nim/readme.md +54 -556 | @@ -1,577 +1,75 @@ | ||
| 1 | -# Nim port spike | |
| 1 | +# glean — a small RSS reader | |
| 2 | 2 | |
| 3 | -Two questions had to be answered before committing to a Nim rewrite of the Go | |
| 4 | -backend. Neither is about volume -- 14k lines is a lot of typing, not a lot of | |
| 5 | -risk. The risk is that Go has libraries Nim does not, and two of them sit under | |
| 6 | -everything. | |
| 7 | - | |
| 8 | -Run both: | |
| 3 | +One binary, one SQLite database, no accounts. | |
| 9 | 4 | |
| 10 | 5 | ``` |
| 11 | -cd spike/nim | |
| 12 | -nimble test # everything that needs no network | |
| 13 | -nimble testnet # the probes that talk to real servers | |
| 14 | -nimble testall # both | |
| 15 | -nimble schema # regenerate atproto/schema.nim from the Go source | |
| 6 | +nim c -d:ssl glean.nim | |
| 7 | +./glean | |
| 16 | 8 | ``` |
| 17 | 9 | |
| 18 | -Reusable code lives in `atproto/`; the `*_probe.nim` files are the checks. | |
| 19 | -They are split by whether they touch the network: a failure against | |
| 20 | -`bsky.social` can mean that server is having a bad day rather than that the | |
| 21 | -port is broken, and a suite that cannot tell those apart stops being | |
| 22 | -believed. Individual probes still run on their own: | |
| 10 | +Listens on `127.0.0.1:8080` and stores its database under | |
| 11 | +`~/.local/share/glean/`. Override with `GLEAN_PORT` and `GLEAN_DB`. | |
| 12 | + | |
| 13 | +## API | |
| 23 | 14 | |
| 24 | 15 | ``` |
| 25 | -nim c -r -d:ssl <name>_probe.nim | |
| 16 | +GET /feeds subscriptions, with unread counts | |
| 17 | +POST /feeds url=… subscribe and fetch immediately | |
| 18 | +DELETE /feeds?url=… unsubscribe | |
| 19 | + | |
| 20 | +GET /articles unread, newest first | |
| 21 | +GET /articles?status=all|read …or everything, or just the read ones | |
| 22 | +GET /articles?feed=… …scoped to one feed | |
| 23 | +GET /articles?q=… full-text search (FTS5) | |
| 24 | +GET /articles/:id one article | |
| 25 | + | |
| 26 | +POST /read id=… mark read (undo=1 to reverse) | |
| 27 | +POST /read-all [feed=…] mark a feed, or everything, read | |
| 28 | +POST /refresh fetch every subscribed feed | |
| 29 | +POST /fetch-content id=… scrape the full article text | |
| 30 | + | |
| 31 | +GET /unread unread count | |
| 26 | 32 | ``` |
| 27 | 33 | |
| 28 | -## 1. sqlite-vec + FTS5 — works | |
| 29 | - | |
| 30 | -Glean needs FTS5 for article search and sqlite-vec for embedding similarity. | |
| 31 | -Both are C-level concerns, so the question was never "does Nim have a SQLite | |
| 32 | -library" but whether the same extension links and registers the same way. | |
| 33 | - | |
| 34 | -`sqlite_probe.nim` compiles the same `sqlite-vec.c` the Go bindings vendor, | |
| 35 | -calls `sqlite3_vec_init` directly, and exercises `vec_version()`, an FTS5 | |
| 36 | -`MATCH`, a `vec0` virtual table, float32 blob binding, and a KNN query. | |
| 37 | -All pass; KNN returns the expected nearest neighbour. | |
| 38 | - | |
| 39 | -**One trap, and it is a nasty one.** sqlite-vec must be compiled with | |
| 40 | -`-DSQLITE_CORE`. Without it the file builds as a *loadable extension* whose | |
| 41 | -entry point takes its SQLite API pointer from the caller — pass `nil` there and | |
| 42 | -it segfaults inside `sqlite3_vec_init` rather than returning an error. Nim's | |
| 43 | -`{.compile: (file, flags).}` tuple form did not reliably apply the define, so | |
| 44 | -it lives in `nim.cfg` where it cannot be forgotten. | |
| 45 | - | |
| 46 | -## 2. ES256 / DPoP — works | |
| 47 | - | |
| 48 | -This was the real risk. Glean gets OAuth from `bluesky-social/indigo`; Nim has | |
| 49 | -no ATProto library, so a port has to build the DPoP layer itself, and | |
| 50 | -everything downstream of sign-in depends on it. | |
| 51 | - | |
| 52 | -`dpop_probe.nim` generates a P-256 key, builds the public JWK, computes an | |
| 53 | -RFC 7638 thumbprint, and signs a DPoP proof JWT (RFC 9449). | |
| 54 | - | |
| 55 | -It uses **bearssl** rather than OpenSSL for one specific reason: JOSE requires | |
| 56 | -the signature as raw `R||S`, 64 bytes, and most crypto libraries hand you DER. | |
| 57 | -`br_ecdsa_sign_raw` emits `R||S` directly, so there is no DER-unwrapping step | |
| 58 | -to get subtly wrong. | |
| 59 | - | |
| 60 | -Self-verification would only prove internal consistency, so the token goes to | |
| 61 | -`verify/verify.go`, which re-implements the verifier from Go's standard | |
| 62 | -library — the same stack the current server runs on. It checks the header | |
| 63 | -claims, that the signature is 64 raw bytes and not DER-wrapped, that the | |
| 64 | -signature verifies against the JWK, that a tampered payload is *rejected*, and | |
| 65 | -that the thumbprint matches when recomputed independently. | |
| 66 | - | |
| 67 | -19/19 checks pass, and the whole cycle passes across 30 independently generated | |
| 68 | -keypairs — worth confirming, because ECDSA values with leading zeros are a | |
| 69 | -classic source of intermittent length bugs in `R||S` encoding. | |
| 70 | - | |
| 71 | -## 3. The OAuth flow — works against a live PDS | |
| 72 | - | |
| 73 | -`atproto/identity.nim` and `atproto/oauth.nim` implement resolution and the | |
| 74 | -client flow, ported from the shape of indigo's `atproto/auth/oauth`. | |
| 75 | -`oauth_probe.nim` drives it end to end against real servers: | |
| 76 | - | |
| 77 | - handle -> DID -> PDS -> auth server -> metadata -> PAR -> authorize URL | |
| 78 | - | |
| 79 | -Against `bsky.social` all 15 checks pass, including a **real PAR that the | |
| 80 | -production auth server accepts** and returns a `request_uri` for. That single | |
| 81 | -result exercises the DPoP signature, the nonce retry and PKCE at once — a | |
| 82 | -server-side validation no amount of local testing substitutes for. | |
| 83 | - | |
| 84 | -Two things worth recording: | |
| 85 | - | |
| 86 | -**The nonce retry is load-bearing, and the probe proves it.** A PDS rejects the | |
| 87 | -first DPoP-signed request of a session with `400 use_dpop_nonce` and supplies a | |
| 88 | -`DPoP-Nonce` header to retry with. Since `sendAuthRequest` retries | |
| 89 | -transparently, the probe separately fires an unnonced request and asserts the | |
| 90 | -rejection — otherwise a server that never demanded a nonce would look identical | |
| 91 | -to a working retry. | |
| 92 | - | |
| 93 | -**Order of validation caught me out.** That check first sent a stub body and got | |
| 94 | -`invalid_request`, not `use_dpop_nonce`: bsky.social validates request | |
| 95 | -parameters *before* the nonce. The body has to be fully valid to observe the | |
| 96 | -nonce requirement at all. | |
| 97 | - | |
| 98 | -The flow stops at the authorization URL, which is correct — the next step is a | |
| 99 | -human granting consent in a browser. `exchangeCode` and `refresh` are | |
| 100 | -implemented against that callback but are consequently untested. | |
| 101 | - | |
| 102 | -## 4. XRPC signing — proof accepted by a real PDS | |
| 103 | - | |
| 104 | -`atproto/xrpc.nim` makes authenticated calls with a DPoP-bound access token: | |
| 105 | -`Authorization: DPoP <token>` (not Bearer), a per-request proof, and the retry | |
| 106 | -logic for both failure modes. | |
| 107 | - | |
| 108 | -Resource-server proofs are **not** the same as auth-server proofs, even when | |
| 109 | -one host fills both roles. A PDS request carries `ath` (base64url SHA-256 of | |
| 110 | -the access token, which is what binds the proof to that one credential) and | |
| 111 | -`iss`; the PAR and token requests that mint the token carry neither. The nonce | |
| 112 | -handshake also differs: a PDS answers **401** with `WWW-Authenticate: DPoP | |
| 113 | -error="use_dpop_nonce"`, where the auth server used **400** and a JSON body. | |
| 114 | -A session therefore tracks two independent nonces. | |
| 115 | - | |
| 116 | -`xrpc_probe.nim` cannot get a consented token, so it signs a request with a | |
| 117 | -deliberately invalid one and reads what a real PDS says. Against | |
| 118 | -`bsky.social`'s PDS: | |
| 119 | - | |
| 120 | - WWW-Authenticate: DPoP algs="...", error="invalid_token", | |
| 121 | - error_description="Malformed token" | |
| 122 | - | |
| 123 | -That is the informative result. `invalid_token` rather than | |
| 124 | -`invalid_dpop_proof` means the PDS parsed and accepted the proof — signature, | |
| 125 | -claims, `ath`, `htu`, `htm` — and rejected only the credential. The challenge | |
| 126 | -scheme coming back as `DPoP` confirms the Authorization scheme too, and the | |
| 127 | -response carried a host nonce. | |
| 128 | - | |
| 129 | -`ath` is pinned against a value computed with `openssl dgst` rather than | |
| 130 | -checked for self-consistency, since a self-consistent hash would pass any | |
| 131 | -internal assertion and still be refused by every server. | |
| 132 | - | |
| 133 | -Two bugs the probe caught, both of which would have failed every authenticated | |
| 134 | -call in ways that are hard to read from the outside: | |
| 135 | - | |
| 136 | -- `htm` was built as `($httpMethod)[4..^1]`, assuming the enum stringified to | |
| 137 | - `HttpGet`. It stringifies to `GET`, so this was a range error, not a | |
| 138 | - mis-signed claim — loud, but only once something ran. | |
| 139 | -- `hostOf` returned an empty host for a scheme-less input like | |
| 140 | - `public.api.bsky.app`, because `parseUri` files that under `path`. URLs came | |
| 141 | - out as `https:///xrpc/...`. | |
| 142 | - | |
| 143 | -A third mistake was in the probe rather than the code: the first version | |
| 144 | -pointed at `com.atproto.repo.listRecords`, which is **public**. The PDS served | |
| 145 | -it with a 200 while ignoring the bogus credential entirely — a passing test | |
| 146 | -that tested nothing. `com.atproto.server.getSession` has to evaluate the | |
| 147 | -credential. | |
| 148 | - | |
| 149 | -## 5. Jetstream — works, verified against the live firehose | |
| 150 | - | |
| 151 | -`atproto/jetstream.nim` consumes the ATProto firehose as JSON over a | |
| 152 | -websocket, using `websock` (already vendored via chronos). Jetstream is public | |
| 153 | -and unauthenticated, so unlike the OAuth and XRPC probes this one checks the | |
| 154 | -real thing end to end: 17/17 against the live network. | |
| 155 | - | |
| 156 | -Two capabilities that matter to Glean are confirmed rather than assumed: | |
| 157 | - | |
| 158 | -**Server-side filtering.** The probe subscribes to one collection and asserts | |
| 159 | -that nothing else arrives. This is worth checking explicitly because the | |
| 160 | -failure mode is invisible: an unfiltered subscription is the entire network, | |
| 161 | -so a consumer that loses its query parameters still receives a healthy-looking | |
| 162 | -torrent of events that happen to be the wrong ones. | |
| 163 | - | |
| 164 | -**Cursor replay.** Rewinding to an earlier `time_us` must actually rewind, not | |
| 165 | -quietly resume live — that is how a consumer catches up after downtime. The | |
| 166 | -probe replays from a cursor it saw and asserts the events *overlap* what it | |
| 167 | -already received (25 of 25). Accepting the parameter proves nothing on its | |
| 168 | -own. | |
| 169 | - | |
| 170 | -Two traps: | |
| 171 | - | |
| 172 | -- **websock's `Uri` overload silently drops the query string.** It forwards | |
| 173 | - only `uri.path`, so every Jetstream filter would vanish and the consumer | |
| 174 | - would subscribe to the whole firehose while looking fine. `connect` uses the | |
| 175 | - host/path form and assembles path-plus-query itself. | |
| 176 | -- Jetstream wants `wantedCollections` **repeated per value**, not one | |
| 177 | - comma-joined string. A joined value matches no collection at all. | |
| 178 | - | |
| 179 | -`websock` pulls in chronicles, which does not compile unless a logging stream | |
| 180 | -is configured and resolves it in the *importing* module's scope — hence the | |
| 181 | -`import chronicles` in `jetstream.nim` that nothing appears to use, and the | |
| 182 | -`chronicles_enabled=off` in `nim.cfg`. | |
| 183 | - | |
| 184 | -## 6. Reconnect and backoff — works | |
| 185 | - | |
| 186 | -`atproto/jetstream_run.nim` keeps a subscription alive. Three concerns, all | |
| 187 | -about not losing events rather than about uptime: | |
| 188 | - | |
| 189 | -- **Resume from a rewound cursor.** Cursors are persisted in batches, so the | |
| 190 | - last one written always trails the last one seen. Reconnecting rewinds past | |
| 191 | - that gap and accepts re-delivery: duplicates are cheap when handling is | |
| 192 | - idempotent, a hole in the record is not. | |
| 193 | -- **Rotate connections on a timer.** Server-side filters are fixed for the | |
| 194 | - life of a connection, so a subscription that never reconnects never learns | |
| 195 | - about newly-known DIDs. Glean's Go consumer rotates every 15 minutes for | |
| 196 | - this reason, not for robustness. | |
| 197 | -- **Back off with jitter.** The Go consumer waits a flat 5s, which is fine | |
| 198 | - against one healthy server and unkind to one that is down. | |
| 199 | - | |
| 200 | -A healthy server proves nothing here, so `reconnect_probe.nim` runs a local | |
| 201 | -websocket server that hangs up after three frames. Over two seconds the | |
| 202 | -supervisor made 12 connections and received 36 events across them. | |
| 203 | - | |
| 204 | -The probe checks that the cursor **reaches the server**, not merely that it | |
| 205 | -advances locally: a consumer that tracks a cursor it never sends looks | |
| 206 | -identical from the inside and silently loses the gap on every reconnect. 11 | |
| 207 | -of 12 connections carried one, and the first correctly did not — it starts | |
| 208 | -live. | |
| 209 | - | |
| 210 | -One bug worth recording. `delayFor` short-circuited `attempt <= 0` straight | |
| 211 | -to the initial delay, skipping jitter on the first retry. That is precisely | |
| 212 | -the moment jitter exists for: one server restart drops every consumer at | |
| 213 | -once, and un-jittered first retries bring them all back simultaneously. | |
| 214 | - | |
| 215 | -## 7. Session persistence — works | |
| 216 | - | |
| 217 | -`atproto/sqlite.nim` is a thin wrapper over the library the storage spike | |
| 218 | -already proved (sqlite-vec linked in, WAL on). `atproto/store.nim` keeps the | |
| 219 | -three things a restart must not lose, in the same tables the Go backend uses: | |
| 220 | -`oauth_auth_requests`, `oauth_sessions`, `jetstream_cursor`. Rows hold JSON, | |
| 221 | -as on the Go side, so adding a session field does not mean a migration. | |
| 222 | - | |
| 223 | -`atproto/cursor_store.nim` bridges the store and the supervisor, batching | |
| 224 | -writes: the cursor advances on every event, often hundreds a second, and a | |
| 225 | -synchronous write per event would make the database the bottleneck for a | |
| 226 | -firehose. The stored cursor therefore trails the live one — which is exactly | |
| 227 | -the gap the supervisor's rewind exists to cover. | |
| 228 | - | |
| 229 | -The check that earns its keep is the DPoP key. A missing row is loud; a key | |
| 230 | -that round-trips *almost* correctly is not — every request after a restart | |
| 231 | -would fail authentication with nothing pointing at storage. So the reloaded | |
| 232 | -key is checked by **signing with it and verifying against the original public | |
| 233 | -point**, with a second key as a negative control. Comparing fields, or | |
| 234 | -re-signing and comparing bytes, would prove neither thing: ECDSA is | |
| 235 | -randomised. | |
| 236 | - | |
| 237 | -Two other properties worth pinning: | |
| 238 | - | |
| 239 | -- Taking a pending auth request **consumes** it. An authorization code is | |
| 240 | - single-use, and leaving the row behind invites a replayed callback. | |
| 241 | -- Cursors never move backwards. A late event that rewound the stored cursor | |
| 242 | - would rewind the subscription on the next reconnect. | |
| 243 | - | |
| 244 | -The public half of the key is stored rather than re-derived on load. Deriving | |
| 245 | -it needs a curve operation, and a stored key that cannot be read back without | |
| 246 | -one is a restart that silently drops every session. | |
| 247 | - | |
| 248 | -**These rows are key material.** A session holds the DPoP private key and the | |
| 249 | -refresh token; together they are the account. The database file wants the | |
| 250 | -protection of a password store, not of a cache. | |
| 251 | - | |
| 252 | -## 8. The database layer — foundation done | |
| 253 | - | |
| 254 | -`atproto/gleandb.nim` opens Glean's storage the way the Go backend does: | |
| 255 | -`<base>_users` as `main`, with `_articles` and `_recs` **attached**, so a | |
| 256 | -query can join across them while each file still checkpoints and vacuums | |
| 257 | -alone — and so the derived recommendation tables can be dropped and rebuilt | |
| 258 | -without touching anything a user wrote. The suffixes match, so a database | |
| 259 | -written by either implementation is readable by the other. | |
| 260 | - | |
| 261 | -Pragmas are repeated per schema rather than set once, which is the detail | |
| 262 | -worth knowing: `PRAGMA` applies to one database at a time, so setting WAL on | |
| 263 | -`main` leaves `articles` journalling the slow way. The probe asserts WAL on | |
| 264 | -all three rather than trusting the call. | |
| 265 | - | |
| 266 | -`exp()` and `log()` are registered as SQL functions, as the Go connection | |
| 267 | -does, and they are load-bearing: the clustering SQL calls `EXP` and `LOG` in | |
| 268 | -seven places for time decay and popularity normalisation. SQLite only ships | |
| 269 | -maths functions when built with `SQLITE_ENABLE_MATH_FUNCTIONS`, so without | |
| 270 | -these registrations every recommendation query fails at runtime rather than | |
| 271 | -at startup. | |
| 272 | - | |
| 273 | -`log()` returns NULL for zero and negatives where Go's `math.Log` returns | |
| 274 | -`-inf` and NaN. The call sites all pass `1 + count`, so the argument is never | |
| 275 | -below 1 and the two agree in practice -- but it is a divergence, noted here | |
| 276 | -rather than left to be found. | |
| 277 | - | |
| 278 | -**The schema is extracted, not retyped.** `tools/extract_schema.py` pulls all | |
| 279 | -44 DDL statements out of `internal/db/db.go` into `atproto/schema.nim`. | |
| 280 | -Hand-transcribing them would drift, and a column differing by a default or a | |
| 281 | -CHECK constraint surfaces months later as a violation nobody can place. | |
| 282 | - | |
| 283 | -The probe leans on things that fail quietly: an ATTACH that did not happen, a | |
| 284 | -pragma that did not take, FTS5 triggers that never fire. That last one is the | |
| 285 | -sharpest — the index is maintained by triggers, so if they were missed, | |
| 286 | -search returns nothing while every other query looks healthy. Insert, update | |
| 287 | -and delete are each checked against the index. | |
| 288 | - | |
| 289 | -### Scope | |
| 34 | +Parameters are read from the query string or a form body, whichever the | |
| 35 | +caller used. | |
| 290 | 36 | |
| 291 | -This is the foundation, not the whole layer. Glean's `internal/db` is ~2,800 | |
| 292 | -lines of non-test code; what is ported here is the connection, the schema and | |
| 293 | -the primitives, plus a cross-database join, FTS5 and vec0 proven end to end. | |
| 37 | +## What it does not do | |
| 294 | 38 | |
| 295 | -The remaining query surface — `article.go`, `feed.go`, `social.go`, | |
| 296 | -`retention.go` and friends — is mechanical against this base, and large. It | |
| 297 | -is the first part of the port where the work is volume rather than risk. | |
| 39 | +No accounts, no sessions, no CSRF, no ATProto. It runs on your machine and | |
| 40 | +reads your feeds, and that assumption is what keeps it a few hundred lines | |
| 41 | +rather than a few thousand. | |
| 298 | 42 | |
| 299 | -## 9. Article and feed queries | |
| 43 | +The one visible consequence: **do not expose it to a network.** It binds | |
| 44 | +loopback deliberately. Anything that can reach the port can read and change | |
| 45 | +everything, because there is nobody to distinguish from anybody else. | |
| 300 | 46 | |
| 301 | -`atproto/feedstore.nim` and `atproto/articlestore.nim` port the core of | |
| 302 | -`internal/db/feed.go` and `internal/db/article.go`, with `sqlite.nim` grown to | |
| 303 | -carry them: typed nullable row access, transactions, and prepared statements | |
| 304 | -that can be stepped many times. | |
| 47 | +## What it is built on | |
| 305 | 48 | |
| 306 | -Read state is the shape everything else follows. It is per-user and lives in | |
| 307 | -its own table, so a listing is always a LEFT JOIN and "unread" means *no row* | |
| 308 | -— hence `COALESCE(r.is_read, 0)` throughout rather than a column on the | |
| 309 | -article. | |
| 49 | +Six modules under `reader/`: | |
| 310 | 50 | |
| 311 | -Two behaviours carried over deliberately, both about not resurfacing things a | |
| 312 | -reader has dealt with: | |
| 51 | +| | | | |
| 52 | +|---|---| | |
| 53 | +| `feedfetcher` | HTTP with a retry policy that retries only 429 and 5xx | | |
| 54 | +| `feedparser` | RSS 2.0, RDF, Atom and JSON Feed | | |
| 55 | +| `scraper` | readability-style extraction, output sanitised | | |
| 56 | +| `sqlite` | a thin wrapper: prepare, bind, step, transactions | | |
| 57 | +| `gleandb` | two attached databases and the schema | | |
| 58 | +| `feedstore` / `articlestore` | the queries | | |
| 313 | 59 | |
| 314 | -- Articles already older than the retention window are **not ingested**. | |
| 315 | - Writing them only to purge them churns the database. | |
| 316 | -- After ingest, read state is **restored from history**, because a purged | |
| 317 | - article that reappears gets a new surrogate id and would otherwise come | |
| 318 | - back unread. | |
| 60 | +Each carries its own checks: | |
| 319 | 61 | |
| 320 | -The probe targets the ways these can be quietly wrong: a mark-all-read whose | |
| 321 | -scope clears the account instead of one feed, a subscriber count that drifts, | |
| 322 | -a listing that leaks another user's feeds, a NULL date sorting to the top. | |
| 323 | - | |
| 324 | -### Four bugs the probe caught | |
| 325 | - | |
| 326 | -All four were wrong assumptions about the schema or about SQLite, and none | |
| 327 | -would have raised at compile time: | |
| 328 | - | |
| 329 | -- `likes` keys on **`author_did`**, not `user_did` — a like is an ATProto | |
| 330 | - record with an author, not a user-scoped row. | |
| 331 | -- FTS5 requires the **bare table name** on the left of `MATCH`. Neither an | |
| 332 | - alias nor a schema-qualified name is accepted, so the match moved into a | |
| 333 | - subquery, which also carries `rank` out for ordering. | |
| 334 | -- The shared feed column list used bare names, which work until a query joins | |
| 335 | - `subscriptions` — which also has `feed_url` — and then fail as ambiguous. | |
| 336 | - Now qualified with the table alias. | |
| 337 | -- The probe's own first draft assumed `users` had a `handle` column. It does | |
| 338 | - not; handles are resolved from atproto rather than stored. | |
| 339 | - | |
| 340 | -### Scope | |
| 341 | - | |
| 342 | -Roughly the core of the two files by usage: feeds, subscriptions, listing, | |
| 343 | -filtering, read state, counts, search, retention, dead feeds. Not ported: | |
| 344 | -`BatchReconcileSubscriptions`, OPML-adjacent helpers, | |
| 345 | -`GetNextArticleID`'s navigation logic, and the several list variants the | |
| 346 | -recommendation engine uses. They are mechanical against these patterns. | |
| 347 | - | |
| 348 | -## 10. Likes, annotations, follows and trending | |
| 349 | - | |
| 350 | -`atproto/socialstore.nim` ports `internal/db/social.go`, completing the | |
| 351 | -database layer. | |
| 352 | - | |
| 353 | -Likes and annotations are ATProto records first and rows second. They are | |
| 354 | -keyed by their `at://` URI because that is the identity the PDS owns, and | |
| 355 | -each can arrive twice -- once from the firehose and once from a PDS sync -- | |
| 356 | -so every write is idempotent by construction rather than by checking first. | |
| 357 | -An edit keeps the URI and changes the content, which is why annotations | |
| 358 | -upsert rather than insert. | |
| 359 | - | |
| 360 | -Two details worth keeping: | |
| 361 | - | |
| 362 | -- The language filter always keeps `language = ''`. That means *not yet | |
| 363 | - classified*, not *no language*; excluding it would make new articles vanish | |
| 364 | - for anyone with a filter set and reappear minutes later once classified. | |
| 365 | -- Future-dated articles sort last in trending. A feed publishing with a | |
| 366 | - scheduled timestamp would otherwise pin itself to the top indefinitely. | |
| 367 | - | |
| 368 | -The probe weights toward the destructive path. `deleteOrphanedLikes` and its | |
| 369 | -annotation twin delete local rows the PDS no longer has, so the checks assert | |
| 370 | -both halves: the dropped rows go, and *other* users' rows do not. The empty | |
| 371 | -active set is called out in the code, because "you unliked everything" and | |
| 372 | -"the fetch failed" are indistinguishable from inside the store — the caller | |
| 373 | -has to not get there. | |
| 374 | - | |
| 375 | -## 11. Feed parsing | |
| 376 | - | |
| 377 | -`atproto/feedparser.nim` ports `internal/feed/parser.go`: RSS 2.0, RDF | |
| 378 | -(RSS 1.0), Atom and JSON Feed. | |
| 379 | - | |
| 380 | -The interesting part of a feed parser is not the happy path, it is the | |
| 381 | -fallbacks, because feeds in the wild disagree with the spec constantly. A | |
| 382 | -missing `guid` falls back to the link — it is what dedupes an article across | |
| 383 | -fetches, so an item without one would be re-inserted forever. `rel="self"` | |
| 384 | -loses to `rel="alternate"`, or a feed whose first link is itself would point | |
| 385 | -every article back at the feed. An unparseable date leaves the article | |
| 386 | -undated rather than dropping it. Atom's `updated` stands in for a missing | |
| 387 | -`published`, since Atom requires only the former. | |
| 388 | - | |
| 389 | -Namespaces are matched by local name rather than resolved. Go's decoder maps | |
| 390 | -`content:encoded` to its namespace URI; Nim's keeps the literal prefix, and | |
| 391 | -feeds are inconsistent enough about prefixes that suffix matching is both | |
| 392 | -simpler and more forgiving. | |
| 393 | - | |
| 394 | -### Two bugs, one of them quiet and severe | |
| 395 | - | |
| 396 | -**`xmltree.innerText` silently skips CDATA.** RSS ships article bodies inside | |
| 397 | -CDATA *precisely because* they contain HTML, so relying on `innerText` loses | |
| 398 | -the content of most real feeds while every surrounding field parses | |
| 399 | -perfectly — a parser that looks like it works and returns empty articles. | |
| 400 | -The parser now walks the tree itself, including `xnCData`. | |
| 401 | - | |
| 402 | -**Nim's `zzz` requires `-05:00`; `ZZZ` accepts `-0500`.** Feeds use both, so | |
| 403 | -both formats are in the list. Without it, every RSS feed using a numeric | |
| 404 | -offset — which is most of them — parsed as undated. | |
| 405 | - | |
| 406 | -Neither would have failed a compile, and neither is visible without a | |
| 407 | -fixture that contains the case. | |
| 408 | - | |
| 409 | -### Beyond fixtures | |
| 410 | - | |
| 411 | -Fixtures only contain what their author thought of, so the probe also fetches | |
| 412 | -four real feeds across two formats (Rust Blog, nim-lang, LWN, Hacker News) | |
| 413 | -and asserts every item has a guid and a title. All four parse, 65 items | |
| 414 | -between them, all dated and all carrying text. | |
| 415 | - | |
| 416 | -## 12. Fetching feeds | |
| 417 | - | |
| 418 | -`atproto/feedfetcher.nim` ports `internal/feed/fetcher.go`. Retries exist for | |
| 419 | -servers that are briefly unwell -- 429 and 5xx -- and for nothing else. A 404 | |
| 420 | -will still be a 404 in a second, and bytes that do not parse will not parse | |
| 421 | -differently on a second read. | |
| 422 | - | |
| 423 | -The retry policy is the substance, so the probe drives it with a scripted | |
| 424 | -transport: 503 twice then success, a 404 that must not be retried, a 429 | |
| 425 | -naming its own delay. None of those are reproducible against a real host on | |
| 426 | -demand. | |
| 427 | - | |
| 428 | -### A bug in the Go fetcher | |
| 429 | - | |
| 430 | -The Go retry policy does not do what it reads as doing. `executeRequest` | |
| 431 | -returns its `*http.Response` **only on success**, so in `Fetch` the guard | |
| 432 | - | |
| 433 | -```go | |
| 434 | -if resp != nil && !httpclient.IsRetryable(resp.StatusCode) { | |
| 62 | +``` | |
| 63 | +nimble test # no network needed | |
| 64 | +nimble testnet # parses and scrapes real sites | |
| 435 | 65 | ``` |
| 436 | 66 | |
| 437 | -can never be true when `err != nil` -- the response is always nil on the | |
| 438 | -error path. Three things follow: | |
| 439 | - | |
| 440 | -- Every failure retries the full four attempts regardless of status, so a | |
| 441 | - permanently-404 feed costs four requests and ~7s of backoff on every | |
| 442 | - refresh cycle. | |
| 443 | -- `retryBackoff`'s `Retry-After` handling is unreachable for the same reason: | |
| 444 | - `lastResp` is only non-nil once the function has already returned, so a | |
| 445 | - rate-limited server's own delay is never honoured. | |
| 446 | -- The HTTP status is never checked at all, so an error page is handed to the | |
| 447 | - parser and fails there instead of failing as an HTTP error. | |
| 448 | - | |
| 449 | -This port checks the status, honours `Retry-After` (capped, so a server | |
| 450 | -asking for an hour cannot hold a worker for one), and retries only what is | |
| 451 | -worth retrying. That is a behaviour change rather than a translation, which | |
| 452 | -is why it is written down both here and at the top of the module. | |
| 453 | - | |
| 454 | -Worth fixing on the Go side too; it is a handful of lines. | |
| 455 | - | |
| 456 | -## 13. The scraper | |
| 457 | - | |
| 458 | -`atproto/scraper.nim` ports `internal/scraper/scraper.go`. Feeds routinely | |
| 459 | -ship a first paragraph and a "read more" link, so the reader view needs the | |
| 460 | -page itself: find the node most likely to be the article, strip the furniture | |
| 461 | -around it, and re-render a whitelisted subset of HTML. Output stays HTML | |
| 462 | -rather than text, because paragraphs, headings, lists and code blocks are | |
| 463 | -most of what makes a long article readable. | |
| 464 | - | |
| 465 | -### Sanitisation is the real test | |
| 466 | - | |
| 467 | -This renders HTML fetched from an arbitrary site into a reader's client, so | |
| 468 | -the probe weights toward what must *not* survive: `script` and `style` | |
| 469 | -content, `iframe`, event handlers, unknown data attributes, `javascript:` | |
| 470 | -URLs. Attributes are whitelisted rather than blacklisted, so an attribute | |
| 471 | -nobody anticipated is dropped by default instead of needing to be added to a | |
| 472 | -list. A page that renders correctly proves nothing about what was stripped, | |
| 473 | -so each of those is asserted separately. | |
| 474 | - | |
| 475 | -Links carry `rel="noopener noreferrer"`, and a link whose target is not | |
| 476 | -http(s) is flattened to its text -- the words survive without the anchor. | |
| 477 | - | |
| 478 | -Two smaller details worth keeping: | |
| 479 | - | |
| 480 | -- A lazy-loaded image resolves through `src` → `data-src` → `data-lazy-src` | |
| 481 | - to the first *reachable* URL. Rendering the placeholder gives a broken | |
| 482 | - image where the picture should be. | |
| 483 | -- Removal walks children back to front. Deleting by index shifts every | |
| 484 | - sibling after it, so a forward walk skips one after each removal -- the | |
| 485 | - probe has four consecutive `<nav>` elements specifically to catch that. | |
| 486 | - | |
| 487 | -The extractor refuses rather than returning something thin. A caller handed a | |
| 488 | -nav menu cannot tell it from a short article; an error lets it fall back to | |
| 489 | -the feed's own summary. | |
| 490 | - | |
| 491 | -The live check takes its URL from the Rust blog's feed rather than | |
| 492 | -hard-coding one, after a hard-coded guess 404'd -- a probe that rots when | |
| 493 | -someone reorganises their permalinks is worse than no probe. | |
| 494 | - | |
| 495 | -## 14. Similarity and scoring | |
| 496 | - | |
| 497 | -`atproto/cluster.nim` ports the similarity computations from | |
| 498 | -`internal/cluster/jaccard.go` and the pure helpers from `scoring.go`. | |
| 499 | - | |
| 500 | -Both similarities are precomputed on a schedule and staged into a temp table | |
| 501 | -before being swapped in, so the expensive pass holds no lock on the live | |
| 502 | -table and readers keep seeing the previous generation rather than an empty | |
| 503 | -one. | |
| 504 | - | |
| 505 | -Feed similarity is time-decayed, `exp(-0.023 * days)` -- a half-life of about | |
| 506 | -a month. Without it a long-lived account's earliest subscriptions dominate | |
| 507 | -its recommendations forever. User similarity over *subscriptions* is | |
| 508 | -deliberately undecayed, because a subscription is a standing choice rather | |
| 509 | -than an event; shared *likes* are events, so those decay on both sides. | |
| 510 | - | |
| 511 | -Recommendations have no obviously correct answer, so the probe checks | |
| 512 | -properties rather than values: that decay decays, that each pair is stored | |
| 513 | -once, that recomputing replaces rather than accumulates, that normalisation | |
| 514 | -of an all-equal set does not produce NaN, and that `samplePeople` reserves | |
| 515 | -half its slots for people the reader does *not* follow -- ranking purely by | |
| 516 | -similarity returns the network they already have, which is not discovery. | |
| 517 | - | |
| 518 | -### A bug inherited from the Go implementation | |
| 519 | - | |
| 520 | -`common_likes` is computed as `CAST(SUM(decay * decay) AS INTEGER)`. Each | |
| 521 | -decay factor is at most 1, so their product is always just under it: a pair | |
| 522 | -sharing one recent like sums to 0.9999 and **truncates to 0**. Two shared | |
| 523 | -likes store 1. The number is shown to readers as the reason for a | |
| 524 | -recommendation -- "N shared likes" -- so a real overlap reads as none. | |
| 525 | - | |
| 526 | -This port uses `ROUND` instead. Worth fixing on the Go side too. | |
| 527 | - | |
| 528 | -The probe caught it only because the assertion looked *vacuous*: both the | |
| 529 | -recent and the stale pair returned 0, so "recent >= old" passed while | |
| 530 | -proving nothing. A test that passes for the wrong reason is worse than one | |
| 531 | -that fails. | |
| 532 | - | |
| 533 | -## What this does and does not prove | |
| 534 | - | |
| 535 | -Proven: the cryptography, the storage layer, and the OAuth flow up to user | |
| 536 | -consent all work from Nim, in the formats ATProto and real servers accept. | |
| 537 | - | |
| 538 | -Not proven, and still ahead: | |
| 539 | - | |
| 540 | -- **Token exchange and refresh** — written, but needs a browser consent to | |
| 541 | - exercise. This is the one place a real end-to-end test still has to happen, | |
| 542 | - and it also covers the `invalid_token` refresh-and-retry branch of | |
| 543 | - `xrpc.request`, which is currently unexercised. | |
| 544 | - | |
| 545 | -### Not needed: DAG-CBOR, CAR and the MST | |
| 546 | - | |
| 547 | -Earlier notes listed these as remaining work. That was wrong, and worth | |
| 548 | -correcting because they are by far the largest format risk left in ATProto. | |
| 549 | - | |
| 550 | -Glean never parses them. `internal/atproto/sync.go` opens by saying so: it | |
| 551 | -reconciles with `com.atproto.repo.listRecords` rather than | |
| 552 | -`com.atproto.sync.getRepo`, because it only syncs known users, Jetstream | |
| 553 | -covers real-time events, and every reconcile is idempotent. Reads go through | |
| 554 | -`listRecords`/`getRecord` and writes through | |
| 555 | -`createRecord`/`putRecord`/`deleteRecord` — all JSON. | |
| 556 | - | |
| 557 | -The imports confirm it. Glean uses four indigo packages — `atclient`, | |
| 558 | -`auth/oauth`, `identity`, `syntax` — and none of `repo`, `mst`, `cbor` or | |
| 559 | -`car`. | |
| 560 | - | |
| 561 | -A Nim port inherits that choice for free. Implementing DAG-CBOR, CIDs, CAR | |
| 562 | -framing and Merkle Search Tree traversal would be real work in service of a | |
| 563 | -code path this application does not have. | |
| 564 | - | |
| 565 | -The honest read: none of the three things that could have sunk the port did. | |
| 566 | -What is left is protocol plumbing against well-specified formats — large, but | |
| 567 | -ordinary. | |
| 67 | +Storage keeps a single hard-coded user. The schema began life in a | |
| 68 | +multi-user server, and satisfying that one column was cheaper than | |
| 69 | +rewriting every query around it. | |
| 568 | 70 | |
| 569 | -## Caveats in this code | |
| 71 | +## History | |
| 570 | 72 | |
| 571 | -- `identity.nim` shells out to `dig` for the DNS leg of handle resolution, | |
| 572 | - because Nim's stdlib has no resolver. A missing `dig` degrades to the | |
| 573 | - HTTP `.well-known` fallback rather than failing, but a real port should bind | |
| 574 | - a resolver instead of depending on a binary. | |
| 575 | -- Only public and localhost clients are supported. Confidential clients | |
| 576 | - (`private_key_jwt` client assertions) are not implemented; Glean uses a | |
| 577 | - public client today. | |
| 73 | +This started as a port of a much larger reader — one with accounts, ATProto | |
| 74 | +sync, social recommendations and a clustering engine. That half has been | |
| 75 | +deleted. What remains is the part that reads feeds. | |
| @@ -1,577 +1,75 @@ | |||
| 1 | -# Nim port spike | 1 | +# glean — a small RSS reader |
| 2 | 2 | ||
| 3 | -Two questions had to be answered before committing to a Nim rewrite of the Go | 3 | +One binary, one SQLite database, no accounts. |
| 4 | -backend. Neither is about volume -- 14k lines is a lot of typing, not a lot of | ||
| 5 | -risk. The risk is that Go has libraries Nim does not, and two of them sit under | ||
| 6 | -everything. | ||
| 7 | - | ||
| 8 | -Run both: | ||
| 9 | 4 | ||
| 10 | ``` | 5 | ``` |
| 11 | -cd spike/nim | 6 | +nim c -d:ssl glean.nim |
| 12 | -nimble test # everything that needs no network | 7 | +./glean |
| 13 | -nimble testnet # the probes that talk to real servers | ||
| 14 | -nimble testall # both | ||
| 15 | -nimble schema # regenerate atproto/schema.nim from the Go source | ||
| 16 | ``` | 8 | ``` |
| 17 | 9 | ||
| 18 | -Reusable code lives in `atproto/`; the `*_probe.nim` files are the checks. | 10 | +Listens on `127.0.0.1:8080` and stores its database under |
| 19 | -They are split by whether they touch the network: a failure against | 11 | +`~/.local/share/glean/`. Override with `GLEAN_PORT` and `GLEAN_DB`. |
| 20 | -`bsky.social` can mean that server is having a bad day rather than that the | 12 | + |
| 21 | -port is broken, and a suite that cannot tell those apart stops being | 13 | +## API |
| 22 | -believed. Individual probes still run on their own: | ||
| 23 | 14 | ||
| 24 | ``` | 15 | ``` |
| 25 | -nim c -r -d:ssl <name>_probe.nim | 16 | +GET /feeds subscriptions, with unread counts |
| 17 | +POST /feeds url=… subscribe and fetch immediately | ||
| 18 | +DELETE /feeds?url=… unsubscribe | ||
| 19 | + | ||
| 20 | +GET /articles unread, newest first | ||
| 21 | +GET /articles?status=all|read …or everything, or just the read ones | ||
| 22 | +GET /articles?feed=… …scoped to one feed | ||
| 23 | +GET /articles?q=… full-text search (FTS5) | ||
| 24 | +GET /articles/:id one article | ||
| 25 | + | ||
| 26 | +POST /read id=… mark read (undo=1 to reverse) | ||
| 27 | +POST /read-all [feed=…] mark a feed, or everything, read | ||
| 28 | +POST /refresh fetch every subscribed feed | ||
| 29 | +POST /fetch-content id=… scrape the full article text | ||
| 30 | + | ||
| 31 | +GET /unread unread count | ||
| 26 | ``` | 32 | ``` |
| 27 | 33 | ||
| 28 | -## 1. sqlite-vec + FTS5 — works | 34 | +Parameters are read from the query string or a form body, whichever the |
| 29 | - | 35 | +caller used. |
| 30 | -Glean needs FTS5 for article search and sqlite-vec for embedding similarity. | ||
| 31 | -Both are C-level concerns, so the question was never "does Nim have a SQLite | ||
| 32 | -library" but whether the same extension links and registers the same way. | ||
| 33 | - | ||
| 34 | -`sqlite_probe.nim` compiles the same `sqlite-vec.c` the Go bindings vendor, | ||
| 35 | -calls `sqlite3_vec_init` directly, and exercises `vec_version()`, an FTS5 | ||
| 36 | -`MATCH`, a `vec0` virtual table, float32 blob binding, and a KNN query. | ||
| 37 | -All pass; KNN returns the expected nearest neighbour. | ||
| 38 | - | ||
| 39 | -**One trap, and it is a nasty one.** sqlite-vec must be compiled with | ||
| 40 | -`-DSQLITE_CORE`. Without it the file builds as a *loadable extension* whose | ||
| 41 | -entry point takes its SQLite API pointer from the caller — pass `nil` there and | ||
| 42 | -it segfaults inside `sqlite3_vec_init` rather than returning an error. Nim's | ||
| 43 | -`{.compile: (file, flags).}` tuple form did not reliably apply the define, so | ||
| 44 | -it lives in `nim.cfg` where it cannot be forgotten. | ||
| 45 | - | ||
| 46 | -## 2. ES256 / DPoP — works | ||
| 47 | - | ||
| 48 | -This was the real risk. Glean gets OAuth from `bluesky-social/indigo`; Nim has | ||
| 49 | -no ATProto library, so a port has to build the DPoP layer itself, and | ||
| 50 | -everything downstream of sign-in depends on it. | ||
| 51 | - | ||
| 52 | -`dpop_probe.nim` generates a P-256 key, builds the public JWK, computes an | ||
| 53 | -RFC 7638 thumbprint, and signs a DPoP proof JWT (RFC 9449). | ||
| 54 | - | ||
| 55 | -It uses **bearssl** rather than OpenSSL for one specific reason: JOSE requires | ||
| 56 | -the signature as raw `R||S`, 64 bytes, and most crypto libraries hand you DER. | ||
| 57 | -`br_ecdsa_sign_raw` emits `R||S` directly, so there is no DER-unwrapping step | ||
| 58 | -to get subtly wrong. | ||
| 59 | - | ||
| 60 | -Self-verification would only prove internal consistency, so the token goes to | ||
| 61 | -`verify/verify.go`, which re-implements the verifier from Go's standard | ||
| 62 | -library — the same stack the current server runs on. It checks the header | ||
| 63 | -claims, that the signature is 64 raw bytes and not DER-wrapped, that the | ||
| 64 | -signature verifies against the JWK, that a tampered payload is *rejected*, and | ||
| 65 | -that the thumbprint matches when recomputed independently. | ||
| 66 | - | ||
| 67 | -19/19 checks pass, and the whole cycle passes across 30 independently generated | ||
| 68 | -keypairs — worth confirming, because ECDSA values with leading zeros are a | ||
| 69 | -classic source of intermittent length bugs in `R||S` encoding. | ||
| 70 | - | ||
| 71 | -## 3. The OAuth flow — works against a live PDS | ||
| 72 | - | ||
| 73 | -`atproto/identity.nim` and `atproto/oauth.nim` implement resolution and the | ||
| 74 | -client flow, ported from the shape of indigo's `atproto/auth/oauth`. | ||
| 75 | -`oauth_probe.nim` drives it end to end against real servers: | ||
| 76 | - | ||
| 77 | - handle -> DID -> PDS -> auth server -> metadata -> PAR -> authorize URL | ||
| 78 | - | ||
| 79 | -Against `bsky.social` all 15 checks pass, including a **real PAR that the | ||
| 80 | -production auth server accepts** and returns a `request_uri` for. That single | ||
| 81 | -result exercises the DPoP signature, the nonce retry and PKCE at once — a | ||
| 82 | -server-side validation no amount of local testing substitutes for. | ||
| 83 | - | ||
| 84 | -Two things worth recording: | ||
| 85 | - | ||
| 86 | -**The nonce retry is load-bearing, and the probe proves it.** A PDS rejects the | ||
| 87 | -first DPoP-signed request of a session with `400 use_dpop_nonce` and supplies a | ||
| 88 | -`DPoP-Nonce` header to retry with. Since `sendAuthRequest` retries | ||
| 89 | -transparently, the probe separately fires an unnonced request and asserts the | ||
| 90 | -rejection — otherwise a server that never demanded a nonce would look identical | ||
| 91 | -to a working retry. | ||
| 92 | - | ||
| 93 | -**Order of validation caught me out.** That check first sent a stub body and got | ||
| 94 | -`invalid_request`, not `use_dpop_nonce`: bsky.social validates request | ||
| 95 | -parameters *before* the nonce. The body has to be fully valid to observe the | ||
| 96 | -nonce requirement at all. | ||
| 97 | - | ||
| 98 | -The flow stops at the authorization URL, which is correct — the next step is a | ||
| 99 | -human granting consent in a browser. `exchangeCode` and `refresh` are | ||
| 100 | -implemented against that callback but are consequently untested. | ||
| 101 | - | ||
| 102 | -## 4. XRPC signing — proof accepted by a real PDS | ||
| 103 | - | ||
| 104 | -`atproto/xrpc.nim` makes authenticated calls with a DPoP-bound access token: | ||
| 105 | -`Authorization: DPoP <token>` (not Bearer), a per-request proof, and the retry | ||
| 106 | -logic for both failure modes. | ||
| 107 | - | ||
| 108 | -Resource-server proofs are **not** the same as auth-server proofs, even when | ||
| 109 | -one host fills both roles. A PDS request carries `ath` (base64url SHA-256 of | ||
| 110 | -the access token, which is what binds the proof to that one credential) and | ||
| 111 | -`iss`; the PAR and token requests that mint the token carry neither. The nonce | ||
| 112 | -handshake also differs: a PDS answers **401** with `WWW-Authenticate: DPoP | ||
| 113 | -error="use_dpop_nonce"`, where the auth server used **400** and a JSON body. | ||
| 114 | -A session therefore tracks two independent nonces. | ||
| 115 | - | ||
| 116 | -`xrpc_probe.nim` cannot get a consented token, so it signs a request with a | ||
| 117 | -deliberately invalid one and reads what a real PDS says. Against | ||
| 118 | -`bsky.social`'s PDS: | ||
| 119 | - | ||
| 120 | - WWW-Authenticate: DPoP algs="...", error="invalid_token", | ||
| 121 | - error_description="Malformed token" | ||
| 122 | - | ||
| 123 | -That is the informative result. `invalid_token` rather than | ||
| 124 | -`invalid_dpop_proof` means the PDS parsed and accepted the proof — signature, | ||
| 125 | -claims, `ath`, `htu`, `htm` — and rejected only the credential. The challenge | ||
| 126 | -scheme coming back as `DPoP` confirms the Authorization scheme too, and the | ||
| 127 | -response carried a host nonce. | ||
| 128 | - | ||
| 129 | -`ath` is pinned against a value computed with `openssl dgst` rather than | ||
| 130 | -checked for self-consistency, since a self-consistent hash would pass any | ||
| 131 | -internal assertion and still be refused by every server. | ||
| 132 | - | ||
| 133 | -Two bugs the probe caught, both of which would have failed every authenticated | ||
| 134 | -call in ways that are hard to read from the outside: | ||
| 135 | - | ||
| 136 | -- `htm` was built as `($httpMethod)[4..^1]`, assuming the enum stringified to | ||
| 137 | - `HttpGet`. It stringifies to `GET`, so this was a range error, not a | ||
| 138 | - mis-signed claim — loud, but only once something ran. | ||
| 139 | -- `hostOf` returned an empty host for a scheme-less input like | ||
| 140 | - `public.api.bsky.app`, because `parseUri` files that under `path`. URLs came | ||
| 141 | - out as `https:///xrpc/...`. | ||
| 142 | - | ||
| 143 | -A third mistake was in the probe rather than the code: the first version | ||
| 144 | -pointed at `com.atproto.repo.listRecords`, which is **public**. The PDS served | ||
| 145 | -it with a 200 while ignoring the bogus credential entirely — a passing test | ||
| 146 | -that tested nothing. `com.atproto.server.getSession` has to evaluate the | ||
| 147 | -credential. | ||
| 148 | - | ||
| 149 | -## 5. Jetstream — works, verified against the live firehose | ||
| 150 | - | ||
| 151 | -`atproto/jetstream.nim` consumes the ATProto firehose as JSON over a | ||
| 152 | -websocket, using `websock` (already vendored via chronos). Jetstream is public | ||
| 153 | -and unauthenticated, so unlike the OAuth and XRPC probes this one checks the | ||
| 154 | -real thing end to end: 17/17 against the live network. | ||
| 155 | - | ||
| 156 | -Two capabilities that matter to Glean are confirmed rather than assumed: | ||
| 157 | - | ||
| 158 | -**Server-side filtering.** The probe subscribes to one collection and asserts | ||
| 159 | -that nothing else arrives. This is worth checking explicitly because the | ||
| 160 | -failure mode is invisible: an unfiltered subscription is the entire network, | ||
| 161 | -so a consumer that loses its query parameters still receives a healthy-looking | ||
| 162 | -torrent of events that happen to be the wrong ones. | ||
| 163 | - | ||
| 164 | -**Cursor replay.** Rewinding to an earlier `time_us` must actually rewind, not | ||
| 165 | -quietly resume live — that is how a consumer catches up after downtime. The | ||
| 166 | -probe replays from a cursor it saw and asserts the events *overlap* what it | ||
| 167 | -already received (25 of 25). Accepting the parameter proves nothing on its | ||
| 168 | -own. | ||
| 169 | - | ||
| 170 | -Two traps: | ||
| 171 | - | ||
| 172 | -- **websock's `Uri` overload silently drops the query string.** It forwards | ||
| 173 | - only `uri.path`, so every Jetstream filter would vanish and the consumer | ||
| 174 | - would subscribe to the whole firehose while looking fine. `connect` uses the | ||
| 175 | - host/path form and assembles path-plus-query itself. | ||
| 176 | -- Jetstream wants `wantedCollections` **repeated per value**, not one | ||
| 177 | - comma-joined string. A joined value matches no collection at all. | ||
| 178 | - | ||
| 179 | -`websock` pulls in chronicles, which does not compile unless a logging stream | ||
| 180 | -is configured and resolves it in the *importing* module's scope — hence the | ||
| 181 | -`import chronicles` in `jetstream.nim` that nothing appears to use, and the | ||
| 182 | -`chronicles_enabled=off` in `nim.cfg`. | ||
| 183 | - | ||
| 184 | -## 6. Reconnect and backoff — works | ||
| 185 | - | ||
| 186 | -`atproto/jetstream_run.nim` keeps a subscription alive. Three concerns, all | ||
| 187 | -about not losing events rather than about uptime: | ||
| 188 | - | ||
| 189 | -- **Resume from a rewound cursor.** Cursors are persisted in batches, so the | ||
| 190 | - last one written always trails the last one seen. Reconnecting rewinds past | ||
| 191 | - that gap and accepts re-delivery: duplicates are cheap when handling is | ||
| 192 | - idempotent, a hole in the record is not. | ||
| 193 | -- **Rotate connections on a timer.** Server-side filters are fixed for the | ||
| 194 | - life of a connection, so a subscription that never reconnects never learns | ||
| 195 | - about newly-known DIDs. Glean's Go consumer rotates every 15 minutes for | ||
| 196 | - this reason, not for robustness. | ||
| 197 | -- **Back off with jitter.** The Go consumer waits a flat 5s, which is fine | ||
| 198 | - against one healthy server and unkind to one that is down. | ||
| 199 | - | ||
| 200 | -A healthy server proves nothing here, so `reconnect_probe.nim` runs a local | ||
| 201 | -websocket server that hangs up after three frames. Over two seconds the | ||
| 202 | -supervisor made 12 connections and received 36 events across them. | ||
| 203 | - | ||
| 204 | -The probe checks that the cursor **reaches the server**, not merely that it | ||
| 205 | -advances locally: a consumer that tracks a cursor it never sends looks | ||
| 206 | -identical from the inside and silently loses the gap on every reconnect. 11 | ||
| 207 | -of 12 connections carried one, and the first correctly did not — it starts | ||
| 208 | -live. | ||
| 209 | - | ||
| 210 | -One bug worth recording. `delayFor` short-circuited `attempt <= 0` straight | ||
| 211 | -to the initial delay, skipping jitter on the first retry. That is precisely | ||
| 212 | -the moment jitter exists for: one server restart drops every consumer at | ||
| 213 | -once, and un-jittered first retries bring them all back simultaneously. | ||
| 214 | - | ||
| 215 | -## 7. Session persistence — works | ||
| 216 | - | ||
| 217 | -`atproto/sqlite.nim` is a thin wrapper over the library the storage spike | ||
| 218 | -already proved (sqlite-vec linked in, WAL on). `atproto/store.nim` keeps the | ||
| 219 | -three things a restart must not lose, in the same tables the Go backend uses: | ||
| 220 | -`oauth_auth_requests`, `oauth_sessions`, `jetstream_cursor`. Rows hold JSON, | ||
| 221 | -as on the Go side, so adding a session field does not mean a migration. | ||
| 222 | - | ||
| 223 | -`atproto/cursor_store.nim` bridges the store and the supervisor, batching | ||
| 224 | -writes: the cursor advances on every event, often hundreds a second, and a | ||
| 225 | -synchronous write per event would make the database the bottleneck for a | ||
| 226 | -firehose. The stored cursor therefore trails the live one — which is exactly | ||
| 227 | -the gap the supervisor's rewind exists to cover. | ||
| 228 | - | ||
| 229 | -The check that earns its keep is the DPoP key. A missing row is loud; a key | ||
| 230 | -that round-trips *almost* correctly is not — every request after a restart | ||
| 231 | -would fail authentication with nothing pointing at storage. So the reloaded | ||
| 232 | -key is checked by **signing with it and verifying against the original public | ||
| 233 | -point**, with a second key as a negative control. Comparing fields, or | ||
| 234 | -re-signing and comparing bytes, would prove neither thing: ECDSA is | ||
| 235 | -randomised. | ||
| 236 | - | ||
| 237 | -Two other properties worth pinning: | ||
| 238 | - | ||
| 239 | -- Taking a pending auth request **consumes** it. An authorization code is | ||
| 240 | - single-use, and leaving the row behind invites a replayed callback. | ||
| 241 | -- Cursors never move backwards. A late event that rewound the stored cursor | ||
| 242 | - would rewind the subscription on the next reconnect. | ||
| 243 | - | ||
| 244 | -The public half of the key is stored rather than re-derived on load. Deriving | ||
| 245 | -it needs a curve operation, and a stored key that cannot be read back without | ||
| 246 | -one is a restart that silently drops every session. | ||
| 247 | - | ||
| 248 | -**These rows are key material.** A session holds the DPoP private key and the | ||
| 249 | -refresh token; together they are the account. The database file wants the | ||
| 250 | -protection of a password store, not of a cache. | ||
| 251 | - | ||
| 252 | -## 8. The database layer — foundation done | ||
| 253 | - | ||
| 254 | -`atproto/gleandb.nim` opens Glean's storage the way the Go backend does: | ||
| 255 | -`<base>_users` as `main`, with `_articles` and `_recs` **attached**, so a | ||
| 256 | -query can join across them while each file still checkpoints and vacuums | ||
| 257 | -alone — and so the derived recommendation tables can be dropped and rebuilt | ||
| 258 | -without touching anything a user wrote. The suffixes match, so a database | ||
| 259 | -written by either implementation is readable by the other. | ||
| 260 | - | ||
| 261 | -Pragmas are repeated per schema rather than set once, which is the detail | ||
| 262 | -worth knowing: `PRAGMA` applies to one database at a time, so setting WAL on | ||
| 263 | -`main` leaves `articles` journalling the slow way. The probe asserts WAL on | ||
| 264 | -all three rather than trusting the call. | ||
| 265 | - | ||
| 266 | -`exp()` and `log()` are registered as SQL functions, as the Go connection | ||
| 267 | -does, and they are load-bearing: the clustering SQL calls `EXP` and `LOG` in | ||
| 268 | -seven places for time decay and popularity normalisation. SQLite only ships | ||
| 269 | -maths functions when built with `SQLITE_ENABLE_MATH_FUNCTIONS`, so without | ||
| 270 | -these registrations every recommendation query fails at runtime rather than | ||
| 271 | -at startup. | ||
| 272 | - | ||
| 273 | -`log()` returns NULL for zero and negatives where Go's `math.Log` returns | ||
| 274 | -`-inf` and NaN. The call sites all pass `1 + count`, so the argument is never | ||
| 275 | -below 1 and the two agree in practice -- but it is a divergence, noted here | ||
| 276 | -rather than left to be found. | ||
| 277 | - | ||
| 278 | -**The schema is extracted, not retyped.** `tools/extract_schema.py` pulls all | ||
| 279 | -44 DDL statements out of `internal/db/db.go` into `atproto/schema.nim`. | ||
| 280 | -Hand-transcribing them would drift, and a column differing by a default or a | ||
| 281 | -CHECK constraint surfaces months later as a violation nobody can place. | ||
| 282 | - | ||
| 283 | -The probe leans on things that fail quietly: an ATTACH that did not happen, a | ||
| 284 | -pragma that did not take, FTS5 triggers that never fire. That last one is the | ||
| 285 | -sharpest — the index is maintained by triggers, so if they were missed, | ||
| 286 | -search returns nothing while every other query looks healthy. Insert, update | ||
| 287 | -and delete are each checked against the index. | ||
| 288 | - | ||
| 289 | -### Scope | ||
| 290 | 36 | ||
| 291 | -This is the foundation, not the whole layer. Glean's `internal/db` is ~2,800 | 37 | +## What it does not do |
| 292 | -lines of non-test code; what is ported here is the connection, the schema and | ||
| 293 | -the primitives, plus a cross-database join, FTS5 and vec0 proven end to end. | ||
| 294 | 38 | ||
| 295 | -The remaining query surface — `article.go`, `feed.go`, `social.go`, | 39 | +No accounts, no sessions, no CSRF, no ATProto. It runs on your machine and |
| 296 | -`retention.go` and friends — is mechanical against this base, and large. It | 40 | +reads your feeds, and that assumption is what keeps it a few hundred lines |
| 297 | -is the first part of the port where the work is volume rather than risk. | 41 | +rather than a few thousand. |
| 298 | 42 | ||
| 299 | -## 9. Article and feed queries | 43 | +The one visible consequence: **do not expose it to a network.** It binds |
| 44 | +loopback deliberately. Anything that can reach the port can read and change | ||
| 45 | +everything, because there is nobody to distinguish from anybody else. | ||
| 300 | 46 | ||
| 301 | -`atproto/feedstore.nim` and `atproto/articlestore.nim` port the core of | 47 | +## What it is built on |
| 302 | -`internal/db/feed.go` and `internal/db/article.go`, with `sqlite.nim` grown to | ||
| 303 | -carry them: typed nullable row access, transactions, and prepared statements | ||
| 304 | -that can be stepped many times. | ||
| 305 | 48 | ||
| 306 | -Read state is the shape everything else follows. It is per-user and lives in | 49 | +Six modules under `reader/`: |
| 307 | -its own table, so a listing is always a LEFT JOIN and "unread" means *no row* | ||
| 308 | -— hence `COALESCE(r.is_read, 0)` throughout rather than a column on the | ||
| 309 | -article. | ||
| 310 | 50 | ||
| 311 | -Two behaviours carried over deliberately, both about not resurfacing things a | 51 | +| | | |
| 312 | -reader has dealt with: | 52 | +|---|---| |
| 53 | +| `feedfetcher` | HTTP with a retry policy that retries only 429 and 5xx | | ||
| 54 | +| `feedparser` | RSS 2.0, RDF, Atom and JSON Feed | | ||
| 55 | +| `scraper` | readability-style extraction, output sanitised | | ||
| 56 | +| `sqlite` | a thin wrapper: prepare, bind, step, transactions | | ||
| 57 | +| `gleandb` | two attached databases and the schema | | ||
| 58 | +| `feedstore` / `articlestore` | the queries | | ||
| 313 | 59 | ||
| 314 | -- Articles already older than the retention window are **not ingested**. | 60 | +Each carries its own checks: |
| 315 | - Writing them only to purge them churns the database. | ||
| 316 | -- After ingest, read state is **restored from history**, because a purged | ||
| 317 | - article that reappears gets a new surrogate id and would otherwise come | ||
| 318 | - back unread. | ||
| 319 | 61 | ||
| 320 | -The probe targets the ways these can be quietly wrong: a mark-all-read whose | 62 | +``` |
| 321 | -scope clears the account instead of one feed, a subscriber count that drifts, | 63 | +nimble test # no network needed |
| 322 | -a listing that leaks another user's feeds, a NULL date sorting to the top. | 64 | +nimble testnet # parses and scrapes real sites |
| 323 | - | ||
| 324 | -### Four bugs the probe caught | ||
| 325 | - | ||
| 326 | -All four were wrong assumptions about the schema or about SQLite, and none | ||
| 327 | -would have raised at compile time: | ||
| 328 | - | ||
| 329 | -- `likes` keys on **`author_did`**, not `user_did` — a like is an ATProto | ||
| 330 | - record with an author, not a user-scoped row. | ||
| 331 | -- FTS5 requires the **bare table name** on the left of `MATCH`. Neither an | ||
| 332 | - alias nor a schema-qualified name is accepted, so the match moved into a | ||
| 333 | - subquery, which also carries `rank` out for ordering. | ||
| 334 | -- The shared feed column list used bare names, which work until a query joins | ||
| 335 | - `subscriptions` — which also has `feed_url` — and then fail as ambiguous. | ||
| 336 | - Now qualified with the table alias. | ||
| 337 | -- The probe's own first draft assumed `users` had a `handle` column. It does | ||
| 338 | - not; handles are resolved from atproto rather than stored. | ||
| 339 | - | ||
| 340 | -### Scope | ||
| 341 | - | ||
| 342 | -Roughly the core of the two files by usage: feeds, subscriptions, listing, | ||
| 343 | -filtering, read state, counts, search, retention, dead feeds. Not ported: | ||
| 344 | -`BatchReconcileSubscriptions`, OPML-adjacent helpers, | ||
| 345 | -`GetNextArticleID`'s navigation logic, and the several list variants the | ||
| 346 | -recommendation engine uses. They are mechanical against these patterns. | ||
| 347 | - | ||
| 348 | -## 10. Likes, annotations, follows and trending | ||
| 349 | - | ||
| 350 | -`atproto/socialstore.nim` ports `internal/db/social.go`, completing the | ||
| 351 | -database layer. | ||
| 352 | - | ||
| 353 | -Likes and annotations are ATProto records first and rows second. They are | ||
| 354 | -keyed by their `at://` URI because that is the identity the PDS owns, and | ||
| 355 | -each can arrive twice -- once from the firehose and once from a PDS sync -- | ||
| 356 | -so every write is idempotent by construction rather than by checking first. | ||
| 357 | -An edit keeps the URI and changes the content, which is why annotations | ||
| 358 | -upsert rather than insert. | ||
| 359 | - | ||
| 360 | -Two details worth keeping: | ||
| 361 | - | ||
| 362 | -- The language filter always keeps `language = ''`. That means *not yet | ||
| 363 | - classified*, not *no language*; excluding it would make new articles vanish | ||
| 364 | - for anyone with a filter set and reappear minutes later once classified. | ||
| 365 | -- Future-dated articles sort last in trending. A feed publishing with a | ||
| 366 | - scheduled timestamp would otherwise pin itself to the top indefinitely. | ||
| 367 | - | ||
| 368 | -The probe weights toward the destructive path. `deleteOrphanedLikes` and its | ||
| 369 | -annotation twin delete local rows the PDS no longer has, so the checks assert | ||
| 370 | -both halves: the dropped rows go, and *other* users' rows do not. The empty | ||
| 371 | -active set is called out in the code, because "you unliked everything" and | ||
| 372 | -"the fetch failed" are indistinguishable from inside the store — the caller | ||
| 373 | -has to not get there. | ||
| 374 | - | ||
| 375 | -## 11. Feed parsing | ||
| 376 | - | ||
| 377 | -`atproto/feedparser.nim` ports `internal/feed/parser.go`: RSS 2.0, RDF | ||
| 378 | -(RSS 1.0), Atom and JSON Feed. | ||
| 379 | - | ||
| 380 | -The interesting part of a feed parser is not the happy path, it is the | ||
| 381 | -fallbacks, because feeds in the wild disagree with the spec constantly. A | ||
| 382 | -missing `guid` falls back to the link — it is what dedupes an article across | ||
| 383 | -fetches, so an item without one would be re-inserted forever. `rel="self"` | ||
| 384 | -loses to `rel="alternate"`, or a feed whose first link is itself would point | ||
| 385 | -every article back at the feed. An unparseable date leaves the article | ||
| 386 | -undated rather than dropping it. Atom's `updated` stands in for a missing | ||
| 387 | -`published`, since Atom requires only the former. | ||
| 388 | - | ||
| 389 | -Namespaces are matched by local name rather than resolved. Go's decoder maps | ||
| 390 | -`content:encoded` to its namespace URI; Nim's keeps the literal prefix, and | ||
| 391 | -feeds are inconsistent enough about prefixes that suffix matching is both | ||
| 392 | -simpler and more forgiving. | ||
| 393 | - | ||
| 394 | -### Two bugs, one of them quiet and severe | ||
| 395 | - | ||
| 396 | -**`xmltree.innerText` silently skips CDATA.** RSS ships article bodies inside | ||
| 397 | -CDATA *precisely because* they contain HTML, so relying on `innerText` loses | ||
| 398 | -the content of most real feeds while every surrounding field parses | ||
| 399 | -perfectly — a parser that looks like it works and returns empty articles. | ||
| 400 | -The parser now walks the tree itself, including `xnCData`. | ||
| 401 | - | ||
| 402 | -**Nim's `zzz` requires `-05:00`; `ZZZ` accepts `-0500`.** Feeds use both, so | ||
| 403 | -both formats are in the list. Without it, every RSS feed using a numeric | ||
| 404 | -offset — which is most of them — parsed as undated. | ||
| 405 | - | ||
| 406 | -Neither would have failed a compile, and neither is visible without a | ||
| 407 | -fixture that contains the case. | ||
| 408 | - | ||
| 409 | -### Beyond fixtures | ||
| 410 | - | ||
| 411 | -Fixtures only contain what their author thought of, so the probe also fetches | ||
| 412 | -four real feeds across two formats (Rust Blog, nim-lang, LWN, Hacker News) | ||
| 413 | -and asserts every item has a guid and a title. All four parse, 65 items | ||
| 414 | -between them, all dated and all carrying text. | ||
| 415 | - | ||
| 416 | -## 12. Fetching feeds | ||
| 417 | - | ||
| 418 | -`atproto/feedfetcher.nim` ports `internal/feed/fetcher.go`. Retries exist for | ||
| 419 | -servers that are briefly unwell -- 429 and 5xx -- and for nothing else. A 404 | ||
| 420 | -will still be a 404 in a second, and bytes that do not parse will not parse | ||
| 421 | -differently on a second read. | ||
| 422 | - | ||
| 423 | -The retry policy is the substance, so the probe drives it with a scripted | ||
| 424 | -transport: 503 twice then success, a 404 that must not be retried, a 429 | ||
| 425 | -naming its own delay. None of those are reproducible against a real host on | ||
| 426 | -demand. | ||
| 427 | - | ||
| 428 | -### A bug in the Go fetcher | ||
| 429 | - | ||
| 430 | -The Go retry policy does not do what it reads as doing. `executeRequest` | ||
| 431 | -returns its `*http.Response` **only on success**, so in `Fetch` the guard | ||
| 432 | - | ||
| 433 | -```go | ||
| 434 | -if resp != nil && !httpclient.IsRetryable(resp.StatusCode) { | ||
| 435 | ``` | 65 | ``` |
| 436 | 66 | ||
| 437 | -can never be true when `err != nil` -- the response is always nil on the | 67 | +Storage keeps a single hard-coded user. The schema began life in a |
| 438 | -error path. Three things follow: | 68 | +multi-user server, and satisfying that one column was cheaper than |
| 439 | - | 69 | +rewriting every query around it. |
| 440 | -- Every failure retries the full four attempts regardless of status, so a | ||
| 441 | - permanently-404 feed costs four requests and ~7s of backoff on every | ||
| 442 | - refresh cycle. | ||
| 443 | -- `retryBackoff`'s `Retry-After` handling is unreachable for the same reason: | ||
| 444 | - `lastResp` is only non-nil once the function has already returned, so a | ||
| 445 | - rate-limited server's own delay is never honoured. | ||
| 446 | -- The HTTP status is never checked at all, so an error page is handed to the | ||
| 447 | - parser and fails there instead of failing as an HTTP error. | ||
| 448 | - | ||
| 449 | -This port checks the status, honours `Retry-After` (capped, so a server | ||
| 450 | -asking for an hour cannot hold a worker for one), and retries only what is | ||
| 451 | -worth retrying. That is a behaviour change rather than a translation, which | ||
| 452 | -is why it is written down both here and at the top of the module. | ||
| 453 | - | ||
| 454 | -Worth fixing on the Go side too; it is a handful of lines. | ||
| 455 | - | ||
| 456 | -## 13. The scraper | ||
| 457 | - | ||
| 458 | -`atproto/scraper.nim` ports `internal/scraper/scraper.go`. Feeds routinely | ||
| 459 | -ship a first paragraph and a "read more" link, so the reader view needs the | ||
| 460 | -page itself: find the node most likely to be the article, strip the furniture | ||
| 461 | -around it, and re-render a whitelisted subset of HTML. Output stays HTML | ||
| 462 | -rather than text, because paragraphs, headings, lists and code blocks are | ||
| 463 | -most of what makes a long article readable. | ||
| 464 | - | ||
| 465 | -### Sanitisation is the real test | ||
| 466 | - | ||
| 467 | -This renders HTML fetched from an arbitrary site into a reader's client, so | ||
| 468 | -the probe weights toward what must *not* survive: `script` and `style` | ||
| 469 | -content, `iframe`, event handlers, unknown data attributes, `javascript:` | ||
| 470 | -URLs. Attributes are whitelisted rather than blacklisted, so an attribute | ||
| 471 | -nobody anticipated is dropped by default instead of needing to be added to a | ||
| 472 | -list. A page that renders correctly proves nothing about what was stripped, | ||
| 473 | -so each of those is asserted separately. | ||
| 474 | - | ||
| 475 | -Links carry `rel="noopener noreferrer"`, and a link whose target is not | ||
| 476 | -http(s) is flattened to its text -- the words survive without the anchor. | ||
| 477 | - | ||
| 478 | -Two smaller details worth keeping: | ||
| 479 | - | ||
| 480 | -- A lazy-loaded image resolves through `src` → `data-src` → `data-lazy-src` | ||
| 481 | - to the first *reachable* URL. Rendering the placeholder gives a broken | ||
| 482 | - image where the picture should be. | ||
| 483 | -- Removal walks children back to front. Deleting by index shifts every | ||
| 484 | - sibling after it, so a forward walk skips one after each removal -- the | ||
| 485 | - probe has four consecutive `<nav>` elements specifically to catch that. | ||
| 486 | - | ||
| 487 | -The extractor refuses rather than returning something thin. A caller handed a | ||
| 488 | -nav menu cannot tell it from a short article; an error lets it fall back to | ||
| 489 | -the feed's own summary. | ||
| 490 | - | ||
| 491 | -The live check takes its URL from the Rust blog's feed rather than | ||
| 492 | -hard-coding one, after a hard-coded guess 404'd -- a probe that rots when | ||
| 493 | -someone reorganises their permalinks is worse than no probe. | ||
| 494 | - | ||
| 495 | -## 14. Similarity and scoring | ||
| 496 | - | ||
| 497 | -`atproto/cluster.nim` ports the similarity computations from | ||
| 498 | -`internal/cluster/jaccard.go` and the pure helpers from `scoring.go`. | ||
| 499 | - | ||
| 500 | -Both similarities are precomputed on a schedule and staged into a temp table | ||
| 501 | -before being swapped in, so the expensive pass holds no lock on the live | ||
| 502 | -table and readers keep seeing the previous generation rather than an empty | ||
| 503 | -one. | ||
| 504 | - | ||
| 505 | -Feed similarity is time-decayed, `exp(-0.023 * days)` -- a half-life of about | ||
| 506 | -a month. Without it a long-lived account's earliest subscriptions dominate | ||
| 507 | -its recommendations forever. User similarity over *subscriptions* is | ||
| 508 | -deliberately undecayed, because a subscription is a standing choice rather | ||
| 509 | -than an event; shared *likes* are events, so those decay on both sides. | ||
| 510 | - | ||
| 511 | -Recommendations have no obviously correct answer, so the probe checks | ||
| 512 | -properties rather than values: that decay decays, that each pair is stored | ||
| 513 | -once, that recomputing replaces rather than accumulates, that normalisation | ||
| 514 | -of an all-equal set does not produce NaN, and that `samplePeople` reserves | ||
| 515 | -half its slots for people the reader does *not* follow -- ranking purely by | ||
| 516 | -similarity returns the network they already have, which is not discovery. | ||
| 517 | - | ||
| 518 | -### A bug inherited from the Go implementation | ||
| 519 | - | ||
| 520 | -`common_likes` is computed as `CAST(SUM(decay * decay) AS INTEGER)`. Each | ||
| 521 | -decay factor is at most 1, so their product is always just under it: a pair | ||
| 522 | -sharing one recent like sums to 0.9999 and **truncates to 0**. Two shared | ||
| 523 | -likes store 1. The number is shown to readers as the reason for a | ||
| 524 | -recommendation -- "N shared likes" -- so a real overlap reads as none. | ||
| 525 | - | ||
| 526 | -This port uses `ROUND` instead. Worth fixing on the Go side too. | ||
| 527 | - | ||
| 528 | -The probe caught it only because the assertion looked *vacuous*: both the | ||
| 529 | -recent and the stale pair returned 0, so "recent >= old" passed while | ||
| 530 | -proving nothing. A test that passes for the wrong reason is worse than one | ||
| 531 | -that fails. | ||
| 532 | - | ||
| 533 | -## What this does and does not prove | ||
| 534 | - | ||
| 535 | -Proven: the cryptography, the storage layer, and the OAuth flow up to user | ||
| 536 | -consent all work from Nim, in the formats ATProto and real servers accept. | ||
| 537 | - | ||
| 538 | -Not proven, and still ahead: | ||
| 539 | - | ||
| 540 | -- **Token exchange and refresh** — written, but needs a browser consent to | ||
| 541 | - exercise. This is the one place a real end-to-end test still has to happen, | ||
| 542 | - and it also covers the `invalid_token` refresh-and-retry branch of | ||
| 543 | - `xrpc.request`, which is currently unexercised. | ||
| 544 | - | ||
| 545 | -### Not needed: DAG-CBOR, CAR and the MST | ||
| 546 | - | ||
| 547 | -Earlier notes listed these as remaining work. That was wrong, and worth | ||
| 548 | -correcting because they are by far the largest format risk left in ATProto. | ||
| 549 | - | ||
| 550 | -Glean never parses them. `internal/atproto/sync.go` opens by saying so: it | ||
| 551 | -reconciles with `com.atproto.repo.listRecords` rather than | ||
| 552 | -`com.atproto.sync.getRepo`, because it only syncs known users, Jetstream | ||
| 553 | -covers real-time events, and every reconcile is idempotent. Reads go through | ||
| 554 | -`listRecords`/`getRecord` and writes through | ||
| 555 | -`createRecord`/`putRecord`/`deleteRecord` — all JSON. | ||
| 556 | - | ||
| 557 | -The imports confirm it. Glean uses four indigo packages — `atclient`, | ||
| 558 | -`auth/oauth`, `identity`, `syntax` — and none of `repo`, `mst`, `cbor` or | ||
| 559 | -`car`. | ||
| 560 | - | ||
| 561 | -A Nim port inherits that choice for free. Implementing DAG-CBOR, CIDs, CAR | ||
| 562 | -framing and Merkle Search Tree traversal would be real work in service of a | ||
| 563 | -code path this application does not have. | ||
| 564 | - | ||
| 565 | -The honest read: none of the three things that could have sunk the port did. | ||
| 566 | -What is left is protocol plumbing against well-specified formats — large, but | ||
| 567 | -ordinary. | ||
| 568 | 70 | ||
| 569 | -## Caveats in this code | 71 | +## History |
| 570 | 72 | ||
| 571 | -- `identity.nim` shells out to `dig` for the DNS leg of handle resolution, | 73 | +This started as a port of a much larger reader — one with accounts, ATProto |
| 572 | - because Nim's stdlib has no resolver. A missing `dig` degrades to the | 74 | +sync, social recommendations and a clustering engine. That half has been |
| 573 | - HTTP `.well-known` fallback rather than failing, but a real port should bind | 75 | +deleted. What remains is the part that reads feeds. |
| 574 | - a resolver instead of depending on a binary. | ||
| 575 | -- Only public and localhost clients are supported. Confidential clients | ||
| 576 | - (`private_key_jwt` client assertions) are not implemented; Glean uses a | ||
| 577 | - public client today. | ||
deleted
spike/nim/reconnect_probe.nim +0 -182 | deleted file mode 100644 | ||
| @@ -1,182 +0,0 @@ | ||
| 1 | -## Checks the reconnect/backoff supervisor. | |
| 2 | -## | |
| 3 | -## nim c -r -d:ssl reconnect_probe.nim | |
| 4 | -## | |
| 5 | -## The backoff curve and cursor arithmetic are pure and tested directly. The | |
| 6 | -## reconnect behaviour itself is tested against a local websocket server that | |
| 7 | -## is deliberately hostile -- it drops every connection after a few frames -- | |
| 8 | -## because a consumer that "works" against a healthy server proves nothing | |
| 9 | -## about the case this module exists for. | |
| 10 | - | |
| 11 | -import std/[json, sequtils, strformat, strutils] | |
| 12 | -import pkg/chronicles | |
| 13 | -import pkg/chronos | |
| 14 | -import pkg/websock/websock | |
| 15 | -import atproto/[jetstream, jetstream_run] | |
| 16 | - | |
| 17 | -var failures = 0 | |
| 18 | - | |
| 19 | -proc report(name: string, ok: bool, detail = "") = | |
| 20 | - if not ok: inc failures | |
| 21 | - let label = if ok: "PASS" else: "FAIL" | |
| 22 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | |
| 23 | - else: echo &" {label} {name}" | |
| 24 | - | |
| 25 | -# --- a server that hangs up ------------------------------------------------ | |
| 26 | - | |
| 27 | -type FlakyServer = ref object | |
| 28 | - http: HttpServer | |
| 29 | - connections*: int | |
| 30 | - framesPerConn*: int | |
| 31 | - cursorsSeen*: seq[string] ## the cursor query param of each connection | |
| 32 | - | |
| 33 | -proc start(framesPerConn: int): FlakyServer = | |
| 34 | - let srv = FlakyServer(framesPerConn: framesPerConn) | |
| 35 | - | |
| 36 | - proc handle(request: HttpRequest) {.async, gcsafe.} = | |
| 37 | - srv.connections.inc | |
| 38 | - srv.cursorsSeen.add request.uri.query | |
| 39 | - let server = WSServer.new(protos = ["", "jetstream"]) | |
| 40 | - let ws = await server.handleRequest(request) | |
| 41 | - # Send a few well-formed events, then drop the connection mid-stream. | |
| 42 | - for i in 0 ..< srv.framesPerConn: | |
| 43 | - let ev = %*{ | |
| 44 | - "did": "did:plc:probe", | |
| 45 | - "time_us": 1_700_000_000_000_000'i64 + srv.connections * 1000 + i, | |
| 46 | - "kind": "commit", | |
| 47 | - "commit": {"rev": "r", "operation": "create", | |
| 48 | - "collection": "app.test.item", "rkey": $i, | |
| 49 | - "record": {"n": i}}, | |
| 50 | - } | |
| 51 | - await ws.send($ev) | |
| 52 | - await ws.stream.closeWait() | |
| 53 | - | |
| 54 | - srv.http = HttpServer.create(initTAddress("127.0.0.1:0"), handle) | |
| 55 | - srv.http.start() | |
| 56 | - srv | |
| 57 | - | |
| 58 | -proc port(s: FlakyServer): int = s.http.localAddress.port.int | |
| 59 | - | |
| 60 | -# --- checks ---------------------------------------------------------------- | |
| 61 | - | |
| 62 | -proc main() {.async.} = | |
| 63 | - echo "Jetstream reconnect probe" | |
| 64 | - echo "" | |
| 65 | - | |
| 66 | - # --- backoff curve ------------------------------------------------------ | |
| 67 | - block: | |
| 68 | - let b = BackoffConfig(initial: 1.seconds, maxDelay: 30.seconds, | |
| 69 | - factor: 2.0, jitter: 0.0) | |
| 70 | - report("first retry uses the initial delay", | |
| 71 | - delayFor(b, 0) == 1.seconds, $delayFor(b, 0).milliseconds & "ms") | |
| 72 | - report("the first retry is jittered too (herd control)", | |
| 73 | - delayFor(BackoffConfig(initial: 1.seconds, maxDelay: 10.seconds, | |
| 74 | - factor: 2.0, jitter: 0.5), | |
| 75 | - 0, rand01 = 1.0) == 500.milliseconds) | |
| 76 | - report("delay grows geometrically", | |
| 77 | - delayFor(b, 1) == 2.seconds and delayFor(b, 2) == 4.seconds, | |
| 78 | - &"{delayFor(b, 1).milliseconds}ms, {delayFor(b, 2).milliseconds}ms") | |
| 79 | - report("delay is capped", | |
| 80 | - delayFor(b, 20) == 30.seconds, $delayFor(b, 20).milliseconds & "ms") | |
| 81 | - | |
| 82 | - block: | |
| 83 | - # Jitter must reduce, never exceed, so the cap stays a real ceiling. | |
| 84 | - let b = BackoffConfig(initial: 1.seconds, maxDelay: 10.seconds, | |
| 85 | - factor: 2.0, jitter: 0.5) | |
| 86 | - let atMax = delayFor(b, 0, rand01 = 0.0) | |
| 87 | - let atMin = delayFor(b, 0, rand01 = 1.0) | |
| 88 | - report("jitter only shortens the delay", | |
| 89 | - atMax == 1.seconds and atMin == 500.milliseconds, | |
| 90 | - &"{atMin.milliseconds}..{atMax.milliseconds}ms") | |
| 91 | - report("a jittered capped delay never exceeds the cap", | |
| 92 | - delayFor(b, 20, rand01 = 0.0) <= 10.seconds) | |
| 93 | - | |
| 94 | - # --- cursor rewind ------------------------------------------------------ | |
| 95 | - block: | |
| 96 | - let store = CursorStore(cursor: 1_700_000_000_000_000'i64) | |
| 97 | - let c = rewoundCursor(store, 5.seconds) | |
| 98 | - report("resume rewinds past the unflushed window", | |
| 99 | - c == 1_700_000_000_000_000'i64 - 5_000_000, | |
| 100 | - &"{store.cursor} -> {c}") | |
| 101 | - | |
| 102 | - # Starting live must stay live; rewinding zero would ask for 1970. | |
| 103 | - report("no cursor stays live", rewoundCursor(CursorStore(), 5.seconds) == 0) | |
| 104 | - | |
| 105 | - let young = CursorStore(cursor: 1000) | |
| 106 | - report("rewind clamps at zero", rewoundCursor(young, 5.seconds) == 0) | |
| 107 | - | |
| 108 | - # --- reconnect against a server that hangs up --------------------------- | |
| 109 | - let server = start(framesPerConn = 3) | |
| 110 | - var received: seq[Event] | |
| 111 | - | |
| 112 | - proc onEvent(ev: Event) {.gcsafe, raises: [].} = | |
| 113 | - received.add ev | |
| 114 | - | |
| 115 | - var cfg = newSupervisorConfig(JetstreamConfig( | |
| 116 | - url: &"ws://127.0.0.1:{server.port}", | |
| 117 | - wantedCollections: @["app.test.item"], | |
| 118 | - )) | |
| 119 | - # Tight timings so the probe finishes; rotation off, so every reconnect | |
| 120 | - # here is a genuine recovery from the server hanging up. | |
| 121 | - cfg.backoff = BackoffConfig(initial: 50.milliseconds, | |
| 122 | - maxDelay: 200.milliseconds, | |
| 123 | - factor: 2.0, jitter: 0.1) | |
| 124 | - cfg.rotateEvery = ZeroDuration | |
| 125 | - cfg.rewind = 0.seconds | |
| 126 | - | |
| 127 | - let sup = newSupervisor(cfg, onEvent) | |
| 128 | - let runner = sup.run() | |
| 129 | - await sleepAsync(2.seconds) | |
| 130 | - sup.stop() | |
| 131 | - await runner.cancelAndWait() | |
| 132 | - | |
| 133 | - report("survived the server hanging up", server.connections > 1, | |
| 134 | - &"{server.connections} connections") | |
| 135 | - report("kept receiving across reconnects", received.len > 3, | |
| 136 | - &"{received.len} events over {server.connections} connections") | |
| 137 | - report("events arrived from more than one connection", | |
| 138 | - received.len >= server.connections, | |
| 139 | - &"{received.len} events") | |
| 140 | - | |
| 141 | - # The cursor must advance across connections, or a restart would replay | |
| 142 | - # from the beginning forever. | |
| 143 | - report("cursor advanced to the newest event seen", | |
| 144 | - sup.store.load() == received[^1].timeUs, | |
| 145 | - &"{sup.store.load()}") | |
| 146 | - | |
| 147 | - # And it must actually reach the server. Tracking a cursor that is never | |
| 148 | - # sent looks identical from the client side and loses every event in the | |
| 149 | - # gap on each reconnect. | |
| 150 | - block: | |
| 151 | - let withCursor = server.cursorsSeen.filterIt("cursor=" in it) | |
| 152 | - report("reconnects send a cursor", | |
| 153 | - withCursor.len > 0, | |
| 154 | - &"{withCursor.len} of {server.cursorsSeen.len} connections") | |
| 155 | - report("the first connection has no cursor (starts live)", | |
| 156 | - server.cursorsSeen.len > 0 and "cursor=" notin server.cursorsSeen[0]) | |
| 157 | - if withCursor.len >= 2: | |
| 158 | - proc cursorOf(q: string): int64 = | |
| 159 | - for part in q.split('&'): | |
| 160 | - if part.startsWith("cursor="): | |
| 161 | - return try: parseBiggestInt(part[7 .. ^1]).int64 | |
| 162 | - except ValueError: 0 | |
| 163 | - 0 | |
| 164 | - report("the cursor advances between reconnects", | |
| 165 | - cursorOf(withCursor[^1]) > cursorOf(withCursor[0]), | |
| 166 | - &"{cursorOf(withCursor[0])} -> {cursorOf(withCursor[^1])}") | |
| 167 | - | |
| 168 | - # Stop accepting before closing: a connection still being torn down by the | |
| 169 | - # supervisor would otherwise keep closeWait pending indefinitely. | |
| 170 | - server.http.stop() | |
| 171 | - if not await server.http.closeWait().withTimeout(5.seconds): | |
| 172 | - report("server shut down cleanly", false, "closeWait timed out") | |
| 173 | - | |
| 174 | - echo "" | |
| 175 | - if failures == 0: | |
| 176 | - echo "RESULT: reconnect, backoff and cursor resume all work." | |
| 177 | - else: | |
| 178 | - echo &"RESULT: {failures} check(s) failed." | |
| 179 | - quit 1 | |
| 180 | - | |
| 181 | -when isMainModule: | |
| 182 | - waitFor main() | |
| deleted file mode 100644 | |||
| @@ -1,182 +0,0 @@ | |||
| 1 | -## Checks the reconnect/backoff supervisor. | ||
| 2 | -## | ||
| 3 | -## nim c -r -d:ssl reconnect_probe.nim | ||
| 4 | -## | ||
| 5 | -## The backoff curve and cursor arithmetic are pure and tested directly. The | ||
| 6 | -## reconnect behaviour itself is tested against a local websocket server that | ||
| 7 | -## is deliberately hostile -- it drops every connection after a few frames -- | ||
| 8 | -## because a consumer that "works" against a healthy server proves nothing | ||
| 9 | -## about the case this module exists for. | ||
| 10 | - | ||
| 11 | -import std/[json, sequtils, strformat, strutils] | ||
| 12 | -import pkg/chronicles | ||
| 13 | -import pkg/chronos | ||
| 14 | -import pkg/websock/websock | ||
| 15 | -import atproto/[jetstream, jetstream_run] | ||
| 16 | - | ||
| 17 | -var failures = 0 | ||
| 18 | - | ||
| 19 | -proc report(name: string, ok: bool, detail = "") = | ||
| 20 | - if not ok: inc failures | ||
| 21 | - let label = if ok: "PASS" else: "FAIL" | ||
| 22 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | ||
| 23 | - else: echo &" {label} {name}" | ||
| 24 | - | ||
| 25 | -# --- a server that hangs up ------------------------------------------------ | ||
| 26 | - | ||
| 27 | -type FlakyServer = ref object | ||
| 28 | - http: HttpServer | ||
| 29 | - connections*: int | ||
| 30 | - framesPerConn*: int | ||
| 31 | - cursorsSeen*: seq[string] ## the cursor query param of each connection | ||
| 32 | - | ||
| 33 | -proc start(framesPerConn: int): FlakyServer = | ||
| 34 | - let srv = FlakyServer(framesPerConn: framesPerConn) | ||
| 35 | - | ||
| 36 | - proc handle(request: HttpRequest) {.async, gcsafe.} = | ||
| 37 | - srv.connections.inc | ||
| 38 | - srv.cursorsSeen.add request.uri.query | ||
| 39 | - let server = WSServer.new(protos = ["", "jetstream"]) | ||
| 40 | - let ws = await server.handleRequest(request) | ||
| 41 | - # Send a few well-formed events, then drop the connection mid-stream. | ||
| 42 | - for i in 0 ..< srv.framesPerConn: | ||
| 43 | - let ev = %*{ | ||
| 44 | - "did": "did:plc:probe", | ||
| 45 | - "time_us": 1_700_000_000_000_000'i64 + srv.connections * 1000 + i, | ||
| 46 | - "kind": "commit", | ||
| 47 | - "commit": {"rev": "r", "operation": "create", | ||
| 48 | - "collection": "app.test.item", "rkey": $i, | ||
| 49 | - "record": {"n": i}}, | ||
| 50 | - } | ||
| 51 | - await ws.send($ev) | ||
| 52 | - await ws.stream.closeWait() | ||
| 53 | - | ||
| 54 | - srv.http = HttpServer.create(initTAddress("127.0.0.1:0"), handle) | ||
| 55 | - srv.http.start() | ||
| 56 | - srv | ||
| 57 | - | ||
| 58 | -proc port(s: FlakyServer): int = s.http.localAddress.port.int | ||
| 59 | - | ||
| 60 | -# --- checks ---------------------------------------------------------------- | ||
| 61 | - | ||
| 62 | -proc main() {.async.} = | ||
| 63 | - echo "Jetstream reconnect probe" | ||
| 64 | - echo "" | ||
| 65 | - | ||
| 66 | - # --- backoff curve ------------------------------------------------------ | ||
| 67 | - block: | ||
| 68 | - let b = BackoffConfig(initial: 1.seconds, maxDelay: 30.seconds, | ||
| 69 | - factor: 2.0, jitter: 0.0) | ||
| 70 | - report("first retry uses the initial delay", | ||
| 71 | - delayFor(b, 0) == 1.seconds, $delayFor(b, 0).milliseconds & "ms") | ||
| 72 | - report("the first retry is jittered too (herd control)", | ||
| 73 | - delayFor(BackoffConfig(initial: 1.seconds, maxDelay: 10.seconds, | ||
| 74 | - factor: 2.0, jitter: 0.5), | ||
| 75 | - 0, rand01 = 1.0) == 500.milliseconds) | ||
| 76 | - report("delay grows geometrically", | ||
| 77 | - delayFor(b, 1) == 2.seconds and delayFor(b, 2) == 4.seconds, | ||
| 78 | - &"{delayFor(b, 1).milliseconds}ms, {delayFor(b, 2).milliseconds}ms") | ||
| 79 | - report("delay is capped", | ||
| 80 | - delayFor(b, 20) == 30.seconds, $delayFor(b, 20).milliseconds & "ms") | ||
| 81 | - | ||
| 82 | - block: | ||
| 83 | - # Jitter must reduce, never exceed, so the cap stays a real ceiling. | ||
| 84 | - let b = BackoffConfig(initial: 1.seconds, maxDelay: 10.seconds, | ||
| 85 | - factor: 2.0, jitter: 0.5) | ||
| 86 | - let atMax = delayFor(b, 0, rand01 = 0.0) | ||
| 87 | - let atMin = delayFor(b, 0, rand01 = 1.0) | ||
| 88 | - report("jitter only shortens the delay", | ||
| 89 | - atMax == 1.seconds and atMin == 500.milliseconds, | ||
| 90 | - &"{atMin.milliseconds}..{atMax.milliseconds}ms") | ||
| 91 | - report("a jittered capped delay never exceeds the cap", | ||
| 92 | - delayFor(b, 20, rand01 = 0.0) <= 10.seconds) | ||
| 93 | - | ||
| 94 | - # --- cursor rewind ------------------------------------------------------ | ||
| 95 | - block: | ||
| 96 | - let store = CursorStore(cursor: 1_700_000_000_000_000'i64) | ||
| 97 | - let c = rewoundCursor(store, 5.seconds) | ||
| 98 | - report("resume rewinds past the unflushed window", | ||
| 99 | - c == 1_700_000_000_000_000'i64 - 5_000_000, | ||
| 100 | - &"{store.cursor} -> {c}") | ||
| 101 | - | ||
| 102 | - # Starting live must stay live; rewinding zero would ask for 1970. | ||
| 103 | - report("no cursor stays live", rewoundCursor(CursorStore(), 5.seconds) == 0) | ||
| 104 | - | ||
| 105 | - let young = CursorStore(cursor: 1000) | ||
| 106 | - report("rewind clamps at zero", rewoundCursor(young, 5.seconds) == 0) | ||
| 107 | - | ||
| 108 | - # --- reconnect against a server that hangs up --------------------------- | ||
| 109 | - let server = start(framesPerConn = 3) | ||
| 110 | - var received: seq[Event] | ||
| 111 | - | ||
| 112 | - proc onEvent(ev: Event) {.gcsafe, raises: [].} = | ||
| 113 | - received.add ev | ||
| 114 | - | ||
| 115 | - var cfg = newSupervisorConfig(JetstreamConfig( | ||
| 116 | - url: &"ws://127.0.0.1:{server.port}", | ||
| 117 | - wantedCollections: @["app.test.item"], | ||
| 118 | - )) | ||
| 119 | - # Tight timings so the probe finishes; rotation off, so every reconnect | ||
| 120 | - # here is a genuine recovery from the server hanging up. | ||
| 121 | - cfg.backoff = BackoffConfig(initial: 50.milliseconds, | ||
| 122 | - maxDelay: 200.milliseconds, | ||
| 123 | - factor: 2.0, jitter: 0.1) | ||
| 124 | - cfg.rotateEvery = ZeroDuration | ||
| 125 | - cfg.rewind = 0.seconds | ||
| 126 | - | ||
| 127 | - let sup = newSupervisor(cfg, onEvent) | ||
| 128 | - let runner = sup.run() | ||
| 129 | - await sleepAsync(2.seconds) | ||
| 130 | - sup.stop() | ||
| 131 | - await runner.cancelAndWait() | ||
| 132 | - | ||
| 133 | - report("survived the server hanging up", server.connections > 1, | ||
| 134 | - &"{server.connections} connections") | ||
| 135 | - report("kept receiving across reconnects", received.len > 3, | ||
| 136 | - &"{received.len} events over {server.connections} connections") | ||
| 137 | - report("events arrived from more than one connection", | ||
| 138 | - received.len >= server.connections, | ||
| 139 | - &"{received.len} events") | ||
| 140 | - | ||
| 141 | - # The cursor must advance across connections, or a restart would replay | ||
| 142 | - # from the beginning forever. | ||
| 143 | - report("cursor advanced to the newest event seen", | ||
| 144 | - sup.store.load() == received[^1].timeUs, | ||
| 145 | - &"{sup.store.load()}") | ||
| 146 | - | ||
| 147 | - # And it must actually reach the server. Tracking a cursor that is never | ||
| 148 | - # sent looks identical from the client side and loses every event in the | ||
| 149 | - # gap on each reconnect. | ||
| 150 | - block: | ||
| 151 | - let withCursor = server.cursorsSeen.filterIt("cursor=" in it) | ||
| 152 | - report("reconnects send a cursor", | ||
| 153 | - withCursor.len > 0, | ||
| 154 | - &"{withCursor.len} of {server.cursorsSeen.len} connections") | ||
| 155 | - report("the first connection has no cursor (starts live)", | ||
| 156 | - server.cursorsSeen.len > 0 and "cursor=" notin server.cursorsSeen[0]) | ||
| 157 | - if withCursor.len >= 2: | ||
| 158 | - proc cursorOf(q: string): int64 = | ||
| 159 | - for part in q.split('&'): | ||
| 160 | - if part.startsWith("cursor="): | ||
| 161 | - return try: parseBiggestInt(part[7 .. ^1]).int64 | ||
| 162 | - except ValueError: 0 | ||
| 163 | - 0 | ||
| 164 | - report("the cursor advances between reconnects", | ||
| 165 | - cursorOf(withCursor[^1]) > cursorOf(withCursor[0]), | ||
| 166 | - &"{cursorOf(withCursor[0])} -> {cursorOf(withCursor[^1])}") | ||
| 167 | - | ||
| 168 | - # Stop accepting before closing: a connection still being torn down by the | ||
| 169 | - # supervisor would otherwise keep closeWait pending indefinitely. | ||
| 170 | - server.http.stop() | ||
| 171 | - if not await server.http.closeWait().withTimeout(5.seconds): | ||
| 172 | - report("server shut down cleanly", false, "closeWait timed out") | ||
| 173 | - | ||
| 174 | - echo "" | ||
| 175 | - if failures == 0: | ||
| 176 | - echo "RESULT: reconnect, backoff and cursor resume all work." | ||
| 177 | - else: | ||
| 178 | - echo &"RESULT: {failures} check(s) failed." | ||
| 179 | - quit 1 | ||
| 180 | - | ||
| 181 | -when isMainModule: | ||
| 182 | - waitFor main() | ||
modified
spike/nim/scraper_probe.nim +1 -1 | @@ -9,7 +9,7 @@ | ||
| 9 | 9 | ## page that renders correctly proves nothing about what was stripped. |
| 10 | 10 | |
| 11 | 11 | import std/[htmlparser, httpclient, options, strformat, strutils, xmltree] |
| 12 | -import atproto/[feedparser, scraper] | |
| 12 | +import reader/[feedparser, scraper] | |
| 13 | 13 | |
| 14 | 14 | var failures = 0 |
| 15 | 15 | |
| @@ -9,7 +9,7 @@ | |||
| 9 | ## page that renders correctly proves nothing about what was stripped. | 9 | ## page that renders correctly proves nothing about what was stripped. |
| 10 | 10 | ||
| 11 | import std/[htmlparser, httpclient, options, strformat, strutils, xmltree] | 11 | import std/[htmlparser, httpclient, options, strformat, strutils, xmltree] |
| 12 | -import atproto/[feedparser, scraper] | 12 | +import reader/[feedparser, scraper] |
| 13 | 13 | ||
| 14 | var failures = 0 | 14 | var failures = 0 |
| 15 | 15 | ||
deleted
spike/nim/social_probe.nim +0 -216 | deleted file mode 100644 | ||
| @@ -1,216 +0,0 @@ | ||
| 1 | -## Exercises likes, annotations, follows and trending. | |
| 2 | -## | |
| 3 | -## nim c -r -d:ssl social_probe.nim | |
| 4 | -## | |
| 5 | -## Weighted towards the destructive and the social-graph paths: orphan | |
| 6 | -## reconciliation deletes rows, and trending decides what a reader sees, so | |
| 7 | -## both are worth more than "the insert worked". | |
| 8 | - | |
| 9 | -import std/[options, os, strformat, strutils, times] | |
| 10 | -import atproto/[articlestore, feedstore, gleandb, socialstore, sqlite] | |
| 11 | - | |
| 12 | -var failures = 0 | |
| 13 | - | |
| 14 | -proc report(name: string, ok: bool, detail = "") = | |
| 15 | - if not ok: inc failures | |
| 16 | - let label = if ok: "PASS" else: "FAIL" | |
| 17 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | |
| 18 | - else: echo &" {label} {name}" | |
| 19 | - | |
| 20 | -proc cleanup(base: string) = | |
| 21 | - for suffix in ["_users", "_articles", "_recs"]: | |
| 22 | - for ext in ["", "-wal", "-shm"]: | |
| 23 | - removeFile(base & suffix & ext) | |
| 24 | - | |
| 25 | -const | |
| 26 | - alice = "did:plc:alice" | |
| 27 | - bob = "did:plc:bob" | |
| 28 | - carol = "did:plc:carol" | |
| 29 | - feedA = "https://a.test/feed" | |
| 30 | - urlA1 = "https://a.test/1" | |
| 31 | - urlA2 = "https://a.test/2" | |
| 32 | - | |
| 33 | -proc main() = | |
| 34 | - echo "Social store probe" | |
| 35 | - echo "" | |
| 36 | - | |
| 37 | - let base = getTempDir() / "glean_social_probe" | |
| 38 | - cleanup(base) | |
| 39 | - defer: cleanup(base) | |
| 40 | - | |
| 41 | - var g = gleandb.open(base) | |
| 42 | - defer: g.close() | |
| 43 | - g.migrate() | |
| 44 | - | |
| 45 | - for did in [alice, bob, carol]: | |
| 46 | - g.db.run("INSERT INTO users (did) VALUES (?)", p(did)) | |
| 47 | - g.upsertFeed(Feed(feedUrl: feedA, title: "Feed A", feedType: "rss")) | |
| 48 | - for did in [alice, bob, carol]: | |
| 49 | - g.createSubscription(did, feedA, "", "", "", "") | |
| 50 | - | |
| 51 | - let t = now().utc | |
| 52 | - g.batchUpsertArticles(@[ | |
| 53 | - NewArticle(feedUrl: feedA, guid: "a1", title: "First", url: urlA1, | |
| 54 | - published: some(t - initDuration(hours = 2))), | |
| 55 | - NewArticle(feedUrl: feedA, guid: "a2", title: "Second", url: urlA2, | |
| 56 | - published: some(t - initDuration(hours = 1))), | |
| 57 | - ]) | |
| 58 | - | |
| 59 | - # --- likes -------------------------------------------------------------- | |
| 60 | - block: | |
| 61 | - g.createLike(Like(uri: "at://alice/like/1", authorDid: alice, | |
| 62 | - feedUrl: feedA, articleUrl: urlA1, createdAt: some(t))) | |
| 63 | - report("a like round-trips", g.hasLiked(alice, feedA, urlA1)) | |
| 64 | - report("a like is per author", not g.hasLiked(bob, feedA, urlA1)) | |
| 65 | - | |
| 66 | - # The same record arrives from Jetstream and from a PDS sync. | |
| 67 | - g.createLike(Like(uri: "at://alice/like/1", authorDid: alice, | |
| 68 | - feedUrl: feedA, articleUrl: urlA1, createdAt: some(t))) | |
| 69 | - report("re-delivering a like does not duplicate it", | |
| 70 | - g.getLikeCount(feedA, urlA1) == 1, $g.getLikeCount(feedA, urlA1)) | |
| 71 | - | |
| 72 | - g.batchCreateLikes(@[ | |
| 73 | - Like(uri: "at://bob/like/1", authorDid: bob, feedUrl: feedA, | |
| 74 | - articleUrl: urlA1, createdAt: some(t)), | |
| 75 | - Like(uri: "at://carol/like/1", authorDid: carol, feedUrl: feedA, | |
| 76 | - articleUrl: urlA1, createdAt: some(t)), | |
| 77 | - Like(uri: "at://bob/like/2", authorDid: bob, feedUrl: feedA, | |
| 78 | - articleUrl: urlA2, createdAt: some(t)), | |
| 79 | - ]) | |
| 80 | - report("batch insert works", g.getLikeCount(feedA, urlA1) == 3, | |
| 81 | - $g.getLikeCount(feedA, urlA1)) | |
| 82 | - report("likes are listed per author", | |
| 83 | - g.listLikes(bob).len == 2, $g.listLikes(bob).len) | |
| 84 | - report("likes can be scoped to a feed", | |
| 85 | - g.listLikes(bob, feedUrl = feedA).len == 2) | |
| 86 | - | |
| 87 | - g.deleteLikeByUserArticle(alice, feedA, urlA1) | |
| 88 | - report("unliking removes only that author's like", | |
| 89 | - g.getLikeCount(feedA, urlA1) == 2 and not g.hasLiked(alice, feedA, urlA1), | |
| 90 | - $g.getLikeCount(feedA, urlA1)) | |
| 91 | - | |
| 92 | - # --- annotations -------------------------------------------------------- | |
| 93 | - block: | |
| 94 | - g.createAnnotation(Annotation( | |
| 95 | - uri: "at://alice/ann/1", authorDid: alice, feedUrl: feedA, | |
| 96 | - articleUrl: urlA1, quote: "a quote", note: "a note", | |
| 97 | - tags: @["nim", "systems"], rating: some(4), createdAt: some(t))) | |
| 98 | - let listed = g.listAnnotations(articleUrl = urlA1) | |
| 99 | - report("an annotation round-trips", listed.len == 1, $listed.len) | |
| 100 | - if listed.len > 0: | |
| 101 | - report("tags round-trip as a list", listed[0].tags == @["nim", "systems"], | |
| 102 | - listed[0].tags.join("|")) | |
| 103 | - report("rating round-trips", listed[0].rating == some(4)) | |
| 104 | - | |
| 105 | - # An edit keeps the URI and changes the content. | |
| 106 | - g.createAnnotation(Annotation( | |
| 107 | - uri: "at://alice/ann/1", authorDid: alice, feedUrl: feedA, | |
| 108 | - articleUrl: urlA1, note: "edited", tags: @["nim"], createdAt: some(t))) | |
| 109 | - let edited = g.listAnnotations(articleUrl = urlA1) | |
| 110 | - report("re-delivering an annotation updates rather than duplicates", | |
| 111 | - edited.len == 1 and edited[0].note == "edited", | |
| 112 | - &"{edited.len} rows, note={edited[0].note}") | |
| 113 | - report("a cleared rating round-trips as none", edited[0].rating.isNone) | |
| 114 | - | |
| 115 | - report("annotationExists finds it", g.annotationExists("at://alice/ann/1")) | |
| 116 | - report("annotationExists rejects an unknown uri", | |
| 117 | - not g.annotationExists("at://nobody/ann/9")) | |
| 118 | - | |
| 119 | - g.batchCreateAnnotations(@[ | |
| 120 | - Annotation(uri: "at://bob/ann/1", authorDid: bob, feedUrl: feedA, | |
| 121 | - articleUrl: urlA1, note: "bob's", createdAt: some(t))]) | |
| 122 | - report("annotations are listed per author", | |
| 123 | - g.listAnnotations(authorDid = bob).len == 1) | |
| 124 | - | |
| 125 | - # --- follows ------------------------------------------------------------ | |
| 126 | - block: | |
| 127 | - g.upsertFollow(alice, bob, "at://alice/follow/1", "cid1") | |
| 128 | - g.upsertFollow(alice, bob, "at://alice/follow/1", "cid1") | |
| 129 | - report("following is idempotent", g.listFollows(alice) == @[bob], | |
| 130 | - g.listFollows(alice).join(",")) | |
| 131 | - g.upsertFollow(alice, carol, "", "") | |
| 132 | - report("follows accumulate", g.listFollows(alice).len == 2) | |
| 133 | - g.deleteFollow(alice, carol) | |
| 134 | - report("unfollowing removes one edge", g.listFollows(alice) == @[bob]) | |
| 135 | - | |
| 136 | - # --- trending ----------------------------------------------------------- | |
| 137 | - block: | |
| 138 | - let since = formatSqliteTime(t - initDuration(days = 1)) | |
| 139 | - let global = g.listTrendingArticles(alice, since) | |
| 140 | - report("global trending ranks by likes", | |
| 141 | - global.len == 2 and global[0].url == urlA1, | |
| 142 | - if global.len > 0: &"top={global[0].url} likes={global[0].likeCount}" | |
| 143 | - else: "empty") | |
| 144 | - report("trending counts annotations too", | |
| 145 | - global.len > 0 and global[0].annotationCount == 2, | |
| 146 | - if global.len > 0: $global[0].annotationCount else: "-") | |
| 147 | - report("hasLiked is per viewer", | |
| 148 | - not global[0].hasLiked, "alice unliked hers earlier") | |
| 149 | - | |
| 150 | - let viewedByBob = g.listTrendingArticles(bob, since) | |
| 151 | - report("hasLiked is true for a viewer who liked it", | |
| 152 | - viewedByBob[0].hasLiked) | |
| 153 | - | |
| 154 | - # Alice follows only bob, so carol's like should not pull an article in | |
| 155 | - # that bob did not also like. | |
| 156 | - let scoped = g.listTrendingArticlesForUser(alice, since) | |
| 157 | - report("graph-scoped trending only counts followed authors", | |
| 158 | - scoped.len > 0, $scoped.len) | |
| 159 | - let scopedTop = scoped[0] | |
| 160 | - report("a followed author's likes are counted", | |
| 161 | - scopedTop.likeCount >= 1, $scopedTop.likeCount) | |
| 162 | - | |
| 163 | - # An author nobody follows contributes nothing to a graph-scoped view. | |
| 164 | - let carolOnly = g.listTrendingArticlesForUser(carol, since) | |
| 165 | - report("a reader with no follows still sees their own likes", | |
| 166 | - carolOnly.len == 1, $carolOnly.len) | |
| 167 | - | |
| 168 | - # --- language filter ---------------------------------------------------- | |
| 169 | - block: | |
| 170 | - let (sql, params) = langFilter(@["en", "fr"], "ar.") | |
| 171 | - report("language filter builds placeholders", | |
| 172 | - sql.count("?") == 2 and params.len == 2, sql.strip()) | |
| 173 | - # Unclassified articles must survive the filter, or new items vanish for | |
| 174 | - # a few minutes and then reappear. | |
| 175 | - report("language filter keeps unclassified articles", | |
| 176 | - "language = ''" in sql) | |
| 177 | - report("an empty language list adds no filter", | |
| 178 | - langFilter(@[], "ar.")[0].len == 0) | |
| 179 | - | |
| 180 | - # --- liked articles ----------------------------------------------------- | |
| 181 | - block: | |
| 182 | - let liked = g.listLikedArticles(bob) | |
| 183 | - report("liked articles are listed newest first", liked.len == 2, $liked.len) | |
| 184 | - report("liked articles carry article fields", | |
| 185 | - liked[0].title.len > 0 and liked[0].feedTitle == "Feed A", | |
| 186 | - liked[0].title) | |
| 187 | - | |
| 188 | - # --- reconciliation ----------------------------------------------------- | |
| 189 | - # The destructive path: local rows the PDS no longer has must go, and | |
| 190 | - # everyone else's must not. | |
| 191 | - block: | |
| 192 | - report("bob starts with 2 likes", g.listLikes(bob).len == 2) | |
| 193 | - g.deleteOrphanedLikes(bob, @["at://bob/like/1"]) | |
| 194 | - report("reconciling deletes likes the PDS dropped", | |
| 195 | - g.listLikes(bob).len == 1, $g.listLikes(bob).len) | |
| 196 | - report("reconciling leaves other users alone", | |
| 197 | - g.listLikes(carol).len == 1, $g.listLikes(carol).len) | |
| 198 | - | |
| 199 | - g.deleteOrphanedAnnotations(alice, @["at://alice/ann/1"]) | |
| 200 | - report("reconciling keeps annotations the PDS still has", | |
| 201 | - g.listAnnotations(authorDid = alice).len == 1) | |
| 202 | - g.deleteOrphanedAnnotations(alice, @[]) | |
| 203 | - report("an empty active set clears that author's annotations", | |
| 204 | - g.listAnnotations(authorDid = alice).len == 0) | |
| 205 | - report("and leaves another author's annotations", | |
| 206 | - g.listAnnotations(authorDid = bob).len == 1) | |
| 207 | - | |
| 208 | - echo "" | |
| 209 | - if failures == 0: | |
| 210 | - echo "RESULT: likes, annotations, follows and trending behave." | |
| 211 | - else: | |
| 212 | - echo &"RESULT: {failures} check(s) failed." | |
| 213 | - quit 1 | |
| 214 | - | |
| 215 | -when isMainModule: | |
| 216 | - main() | |
| deleted file mode 100644 | |||
| @@ -1,216 +0,0 @@ | |||
| 1 | -## Exercises likes, annotations, follows and trending. | ||
| 2 | -## | ||
| 3 | -## nim c -r -d:ssl social_probe.nim | ||
| 4 | -## | ||
| 5 | -## Weighted towards the destructive and the social-graph paths: orphan | ||
| 6 | -## reconciliation deletes rows, and trending decides what a reader sees, so | ||
| 7 | -## both are worth more than "the insert worked". | ||
| 8 | - | ||
| 9 | -import std/[options, os, strformat, strutils, times] | ||
| 10 | -import atproto/[articlestore, feedstore, gleandb, socialstore, sqlite] | ||
| 11 | - | ||
| 12 | -var failures = 0 | ||
| 13 | - | ||
| 14 | -proc report(name: string, ok: bool, detail = "") = | ||
| 15 | - if not ok: inc failures | ||
| 16 | - let label = if ok: "PASS" else: "FAIL" | ||
| 17 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | ||
| 18 | - else: echo &" {label} {name}" | ||
| 19 | - | ||
| 20 | -proc cleanup(base: string) = | ||
| 21 | - for suffix in ["_users", "_articles", "_recs"]: | ||
| 22 | - for ext in ["", "-wal", "-shm"]: | ||
| 23 | - removeFile(base & suffix & ext) | ||
| 24 | - | ||
| 25 | -const | ||
| 26 | - alice = "did:plc:alice" | ||
| 27 | - bob = "did:plc:bob" | ||
| 28 | - carol = "did:plc:carol" | ||
| 29 | - feedA = "https://a.test/feed" | ||
| 30 | - urlA1 = "https://a.test/1" | ||
| 31 | - urlA2 = "https://a.test/2" | ||
| 32 | - | ||
| 33 | -proc main() = | ||
| 34 | - echo "Social store probe" | ||
| 35 | - echo "" | ||
| 36 | - | ||
| 37 | - let base = getTempDir() / "glean_social_probe" | ||
| 38 | - cleanup(base) | ||
| 39 | - defer: cleanup(base) | ||
| 40 | - | ||
| 41 | - var g = gleandb.open(base) | ||
| 42 | - defer: g.close() | ||
| 43 | - g.migrate() | ||
| 44 | - | ||
| 45 | - for did in [alice, bob, carol]: | ||
| 46 | - g.db.run("INSERT INTO users (did) VALUES (?)", p(did)) | ||
| 47 | - g.upsertFeed(Feed(feedUrl: feedA, title: "Feed A", feedType: "rss")) | ||
| 48 | - for did in [alice, bob, carol]: | ||
| 49 | - g.createSubscription(did, feedA, "", "", "", "") | ||
| 50 | - | ||
| 51 | - let t = now().utc | ||
| 52 | - g.batchUpsertArticles(@[ | ||
| 53 | - NewArticle(feedUrl: feedA, guid: "a1", title: "First", url: urlA1, | ||
| 54 | - published: some(t - initDuration(hours = 2))), | ||
| 55 | - NewArticle(feedUrl: feedA, guid: "a2", title: "Second", url: urlA2, | ||
| 56 | - published: some(t - initDuration(hours = 1))), | ||
| 57 | - ]) | ||
| 58 | - | ||
| 59 | - # --- likes -------------------------------------------------------------- | ||
| 60 | - block: | ||
| 61 | - g.createLike(Like(uri: "at://alice/like/1", authorDid: alice, | ||
| 62 | - feedUrl: feedA, articleUrl: urlA1, createdAt: some(t))) | ||
| 63 | - report("a like round-trips", g.hasLiked(alice, feedA, urlA1)) | ||
| 64 | - report("a like is per author", not g.hasLiked(bob, feedA, urlA1)) | ||
| 65 | - | ||
| 66 | - # The same record arrives from Jetstream and from a PDS sync. | ||
| 67 | - g.createLike(Like(uri: "at://alice/like/1", authorDid: alice, | ||
| 68 | - feedUrl: feedA, articleUrl: urlA1, createdAt: some(t))) | ||
| 69 | - report("re-delivering a like does not duplicate it", | ||
| 70 | - g.getLikeCount(feedA, urlA1) == 1, $g.getLikeCount(feedA, urlA1)) | ||
| 71 | - | ||
| 72 | - g.batchCreateLikes(@[ | ||
| 73 | - Like(uri: "at://bob/like/1", authorDid: bob, feedUrl: feedA, | ||
| 74 | - articleUrl: urlA1, createdAt: some(t)), | ||
| 75 | - Like(uri: "at://carol/like/1", authorDid: carol, feedUrl: feedA, | ||
| 76 | - articleUrl: urlA1, createdAt: some(t)), | ||
| 77 | - Like(uri: "at://bob/like/2", authorDid: bob, feedUrl: feedA, | ||
| 78 | - articleUrl: urlA2, createdAt: some(t)), | ||
| 79 | - ]) | ||
| 80 | - report("batch insert works", g.getLikeCount(feedA, urlA1) == 3, | ||
| 81 | - $g.getLikeCount(feedA, urlA1)) | ||
| 82 | - report("likes are listed per author", | ||
| 83 | - g.listLikes(bob).len == 2, $g.listLikes(bob).len) | ||
| 84 | - report("likes can be scoped to a feed", | ||
| 85 | - g.listLikes(bob, feedUrl = feedA).len == 2) | ||
| 86 | - | ||
| 87 | - g.deleteLikeByUserArticle(alice, feedA, urlA1) | ||
| 88 | - report("unliking removes only that author's like", | ||
| 89 | - g.getLikeCount(feedA, urlA1) == 2 and not g.hasLiked(alice, feedA, urlA1), | ||
| 90 | - $g.getLikeCount(feedA, urlA1)) | ||
| 91 | - | ||
| 92 | - # --- annotations -------------------------------------------------------- | ||
| 93 | - block: | ||
| 94 | - g.createAnnotation(Annotation( | ||
| 95 | - uri: "at://alice/ann/1", authorDid: alice, feedUrl: feedA, | ||
| 96 | - articleUrl: urlA1, quote: "a quote", note: "a note", | ||
| 97 | - tags: @["nim", "systems"], rating: some(4), createdAt: some(t))) | ||
| 98 | - let listed = g.listAnnotations(articleUrl = urlA1) | ||
| 99 | - report("an annotation round-trips", listed.len == 1, $listed.len) | ||
| 100 | - if listed.len > 0: | ||
| 101 | - report("tags round-trip as a list", listed[0].tags == @["nim", "systems"], | ||
| 102 | - listed[0].tags.join("|")) | ||
| 103 | - report("rating round-trips", listed[0].rating == some(4)) | ||
| 104 | - | ||
| 105 | - # An edit keeps the URI and changes the content. | ||
| 106 | - g.createAnnotation(Annotation( | ||
| 107 | - uri: "at://alice/ann/1", authorDid: alice, feedUrl: feedA, | ||
| 108 | - articleUrl: urlA1, note: "edited", tags: @["nim"], createdAt: some(t))) | ||
| 109 | - let edited = g.listAnnotations(articleUrl = urlA1) | ||
| 110 | - report("re-delivering an annotation updates rather than duplicates", | ||
| 111 | - edited.len == 1 and edited[0].note == "edited", | ||
| 112 | - &"{edited.len} rows, note={edited[0].note}") | ||
| 113 | - report("a cleared rating round-trips as none", edited[0].rating.isNone) | ||
| 114 | - | ||
| 115 | - report("annotationExists finds it", g.annotationExists("at://alice/ann/1")) | ||
| 116 | - report("annotationExists rejects an unknown uri", | ||
| 117 | - not g.annotationExists("at://nobody/ann/9")) | ||
| 118 | - | ||
| 119 | - g.batchCreateAnnotations(@[ | ||
| 120 | - Annotation(uri: "at://bob/ann/1", authorDid: bob, feedUrl: feedA, | ||
| 121 | - articleUrl: urlA1, note: "bob's", createdAt: some(t))]) | ||
| 122 | - report("annotations are listed per author", | ||
| 123 | - g.listAnnotations(authorDid = bob).len == 1) | ||
| 124 | - | ||
| 125 | - # --- follows ------------------------------------------------------------ | ||
| 126 | - block: | ||
| 127 | - g.upsertFollow(alice, bob, "at://alice/follow/1", "cid1") | ||
| 128 | - g.upsertFollow(alice, bob, "at://alice/follow/1", "cid1") | ||
| 129 | - report("following is idempotent", g.listFollows(alice) == @[bob], | ||
| 130 | - g.listFollows(alice).join(",")) | ||
| 131 | - g.upsertFollow(alice, carol, "", "") | ||
| 132 | - report("follows accumulate", g.listFollows(alice).len == 2) | ||
| 133 | - g.deleteFollow(alice, carol) | ||
| 134 | - report("unfollowing removes one edge", g.listFollows(alice) == @[bob]) | ||
| 135 | - | ||
| 136 | - # --- trending ----------------------------------------------------------- | ||
| 137 | - block: | ||
| 138 | - let since = formatSqliteTime(t - initDuration(days = 1)) | ||
| 139 | - let global = g.listTrendingArticles(alice, since) | ||
| 140 | - report("global trending ranks by likes", | ||
| 141 | - global.len == 2 and global[0].url == urlA1, | ||
| 142 | - if global.len > 0: &"top={global[0].url} likes={global[0].likeCount}" | ||
| 143 | - else: "empty") | ||
| 144 | - report("trending counts annotations too", | ||
| 145 | - global.len > 0 and global[0].annotationCount == 2, | ||
| 146 | - if global.len > 0: $global[0].annotationCount else: "-") | ||
| 147 | - report("hasLiked is per viewer", | ||
| 148 | - not global[0].hasLiked, "alice unliked hers earlier") | ||
| 149 | - | ||
| 150 | - let viewedByBob = g.listTrendingArticles(bob, since) | ||
| 151 | - report("hasLiked is true for a viewer who liked it", | ||
| 152 | - viewedByBob[0].hasLiked) | ||
| 153 | - | ||
| 154 | - # Alice follows only bob, so carol's like should not pull an article in | ||
| 155 | - # that bob did not also like. | ||
| 156 | - let scoped = g.listTrendingArticlesForUser(alice, since) | ||
| 157 | - report("graph-scoped trending only counts followed authors", | ||
| 158 | - scoped.len > 0, $scoped.len) | ||
| 159 | - let scopedTop = scoped[0] | ||
| 160 | - report("a followed author's likes are counted", | ||
| 161 | - scopedTop.likeCount >= 1, $scopedTop.likeCount) | ||
| 162 | - | ||
| 163 | - # An author nobody follows contributes nothing to a graph-scoped view. | ||
| 164 | - let carolOnly = g.listTrendingArticlesForUser(carol, since) | ||
| 165 | - report("a reader with no follows still sees their own likes", | ||
| 166 | - carolOnly.len == 1, $carolOnly.len) | ||
| 167 | - | ||
| 168 | - # --- language filter ---------------------------------------------------- | ||
| 169 | - block: | ||
| 170 | - let (sql, params) = langFilter(@["en", "fr"], "ar.") | ||
| 171 | - report("language filter builds placeholders", | ||
| 172 | - sql.count("?") == 2 and params.len == 2, sql.strip()) | ||
| 173 | - # Unclassified articles must survive the filter, or new items vanish for | ||
| 174 | - # a few minutes and then reappear. | ||
| 175 | - report("language filter keeps unclassified articles", | ||
| 176 | - "language = ''" in sql) | ||
| 177 | - report("an empty language list adds no filter", | ||
| 178 | - langFilter(@[], "ar.")[0].len == 0) | ||
| 179 | - | ||
| 180 | - # --- liked articles ----------------------------------------------------- | ||
| 181 | - block: | ||
| 182 | - let liked = g.listLikedArticles(bob) | ||
| 183 | - report("liked articles are listed newest first", liked.len == 2, $liked.len) | ||
| 184 | - report("liked articles carry article fields", | ||
| 185 | - liked[0].title.len > 0 and liked[0].feedTitle == "Feed A", | ||
| 186 | - liked[0].title) | ||
| 187 | - | ||
| 188 | - # --- reconciliation ----------------------------------------------------- | ||
| 189 | - # The destructive path: local rows the PDS no longer has must go, and | ||
| 190 | - # everyone else's must not. | ||
| 191 | - block: | ||
| 192 | - report("bob starts with 2 likes", g.listLikes(bob).len == 2) | ||
| 193 | - g.deleteOrphanedLikes(bob, @["at://bob/like/1"]) | ||
| 194 | - report("reconciling deletes likes the PDS dropped", | ||
| 195 | - g.listLikes(bob).len == 1, $g.listLikes(bob).len) | ||
| 196 | - report("reconciling leaves other users alone", | ||
| 197 | - g.listLikes(carol).len == 1, $g.listLikes(carol).len) | ||
| 198 | - | ||
| 199 | - g.deleteOrphanedAnnotations(alice, @["at://alice/ann/1"]) | ||
| 200 | - report("reconciling keeps annotations the PDS still has", | ||
| 201 | - g.listAnnotations(authorDid = alice).len == 1) | ||
| 202 | - g.deleteOrphanedAnnotations(alice, @[]) | ||
| 203 | - report("an empty active set clears that author's annotations", | ||
| 204 | - g.listAnnotations(authorDid = alice).len == 0) | ||
| 205 | - report("and leaves another author's annotations", | ||
| 206 | - g.listAnnotations(authorDid = bob).len == 1) | ||
| 207 | - | ||
| 208 | - echo "" | ||
| 209 | - if failures == 0: | ||
| 210 | - echo "RESULT: likes, annotations, follows and trending behave." | ||
| 211 | - else: | ||
| 212 | - echo &"RESULT: {failures} check(s) failed." | ||
| 213 | - quit 1 | ||
| 214 | - | ||
| 215 | -when isMainModule: | ||
| 216 | - main() | ||
deleted
spike/nim/sqlite_probe.nim +0 -151 | deleted file mode 100644 | ||
| @@ -1,151 +0,0 @@ | ||
| 1 | -## Spike: can Nim drive the two SQLite features Glean depends on? | |
| 2 | -## | |
| 3 | -## Glean's Go backend needs FTS5 (full-text search over articles) and | |
| 4 | -## sqlite-vec (embedding similarity for recommendations). Both are C-level | |
| 5 | -## concerns, so the question for a Nim port is not "does Nim have a SQLite | |
| 6 | -## library" but whether the same extension can be linked and registered the | |
| 7 | -## same way. The Go bindings compile sqlite-vec.c with -DSQLITE_CORE and call | |
| 8 | -## sqlite3_vec_init directly; this does exactly that from Nim. | |
| 9 | - | |
| 10 | -{.compile: "vendor/sqlite-vec.c".} # flags live in nim.cfg | |
| 11 | - | |
| 12 | -import std/[strformat, strutils, sequtils] | |
| 13 | - | |
| 14 | -type | |
| 15 | - Sqlite3 = distinct pointer | |
| 16 | - Stmt = distinct pointer | |
| 17 | - | |
| 18 | -const | |
| 19 | - SQLITE_OK = 0 | |
| 20 | - SQLITE_ROW = 100 | |
| 21 | - SQLITE_DONE = 101 | |
| 22 | - | |
| 23 | -{.push importc, cdecl.} | |
| 24 | -proc sqlite3_open(filename: cstring, db: var Sqlite3): cint | |
| 25 | -proc sqlite3_close(db: Sqlite3): cint | |
| 26 | -proc sqlite3_errmsg(db: Sqlite3): cstring | |
| 27 | -proc sqlite3_exec(db: Sqlite3, sql: cstring, cb, arg: pointer, | |
| 28 | - errmsg: ptr cstring): cint | |
| 29 | -proc sqlite3_prepare_v2(db: Sqlite3, sql: cstring, nByte: cint, | |
| 30 | - stmt: var Stmt, tail: ptr cstring): cint | |
| 31 | -proc sqlite3_step(s: Stmt): cint | |
| 32 | -proc sqlite3_finalize(s: Stmt): cint | |
| 33 | -proc sqlite3_column_text(s: Stmt, col: cint): cstring | |
| 34 | -proc sqlite3_column_double(s: Stmt, col: cint): cdouble | |
| 35 | -proc sqlite3_column_int64(s: Stmt, col: cint): int64 | |
| 36 | -proc sqlite3_bind_blob(s: Stmt, col: cint, data: pointer, n: cint, | |
| 37 | - destructor: pointer): cint | |
| 38 | -proc sqlite3_libversion(): cstring | |
| 39 | -{.pop.} | |
| 40 | - | |
| 41 | -# Provided by the compiled sqlite-vec.c. With SQLITE_CORE it is an ordinary | |
| 42 | -# function rather than a loadable-extension entry point. | |
| 43 | -proc sqlite3_vec_init(db: Sqlite3, errMsg: ptr cstring, | |
| 44 | - api: pointer): cint {.importc, cdecl.} | |
| 45 | - | |
| 46 | -const SQLITE_TRANSIENT = cast[pointer](-1) | |
| 47 | - | |
| 48 | -var failures = 0 | |
| 49 | - | |
| 50 | -proc check(db: Sqlite3, rc: cint, what: string) = | |
| 51 | - if rc notin [SQLITE_OK, SQLITE_ROW, SQLITE_DONE]: | |
| 52 | - raise newException(IOError, &"{what}: {sqlite3_errmsg(db)} (rc={rc})") | |
| 53 | - | |
| 54 | -proc exec(db: Sqlite3, sql: string) = | |
| 55 | - check(db, sqlite3_exec(db, sql.cstring, nil, nil, nil), "exec " & sql[0..min(40, sql.high)]) | |
| 56 | - | |
| 57 | -proc report(name: string, ok: bool, detail = "") = | |
| 58 | - if ok: | |
| 59 | - echo &" PASS {name}" & (if detail.len > 0: " -- " & detail else: "") | |
| 60 | - else: | |
| 61 | - inc failures | |
| 62 | - echo &" FAIL {name}" & (if detail.len > 0: " -- " & detail else: "") | |
| 63 | - | |
| 64 | -## Embeddings are bound as raw little-endian float32 blobs, the same wire | |
| 65 | -## format sqlite-vec expects from every binding. | |
| 66 | -proc toBlob(v: seq[float32]): string = | |
| 67 | - result = newString(v.len * 4) | |
| 68 | - if v.len > 0: | |
| 69 | - copyMem(result[0].addr, v[0].unsafeAddr, result.len) | |
| 70 | - | |
| 71 | -proc main() = | |
| 72 | - echo "sqlite-vec / FTS5 spike" | |
| 73 | - echo " sqlite ", sqlite3_libversion() | |
| 74 | - | |
| 75 | - var db: Sqlite3 | |
| 76 | - check(db, sqlite3_open(":memory:", db), "open") | |
| 77 | - defer: discard sqlite3_close(db) | |
| 78 | - | |
| 79 | - # --- sqlite-vec registration --- | |
| 80 | - block: | |
| 81 | - let rc = sqlite3_vec_init(db, nil, nil) | |
| 82 | - report("sqlite3_vec_init registers", rc == SQLITE_OK, &"rc={rc}") | |
| 83 | - | |
| 84 | - block: | |
| 85 | - var s: Stmt | |
| 86 | - check(db, sqlite3_prepare_v2(db, "SELECT vec_version()", -1, s, nil), "prepare vec_version") | |
| 87 | - let ok = sqlite3_step(s) == SQLITE_ROW | |
| 88 | - let ver = if ok: $sqlite3_column_text(s, 0) else: "" | |
| 89 | - discard sqlite3_finalize(s) | |
| 90 | - report("vec_version() callable", ok, ver) | |
| 91 | - | |
| 92 | - # --- FTS5, as used for article search --- | |
| 93 | - block: | |
| 94 | - db.exec "CREATE VIRTUAL TABLE docs USING fts5(title, body)" | |
| 95 | - db.exec "INSERT INTO docs VALUES ('Nim at scale', 'systems programming with a python face')" | |
| 96 | - db.exec "INSERT INTO docs VALUES ('Go concurrency', 'goroutines and channels')" | |
| 97 | - var s: Stmt | |
| 98 | - check(db, sqlite3_prepare_v2(db, | |
| 99 | - "SELECT title FROM docs WHERE docs MATCH 'systems' ORDER BY rank", -1, s, nil), "prepare fts") | |
| 100 | - let ok = sqlite3_step(s) == SQLITE_ROW | |
| 101 | - let title = if ok: $sqlite3_column_text(s, 0) else: "" | |
| 102 | - discard sqlite3_finalize(s) | |
| 103 | - report("FTS5 match + rank", ok and title == "Nim at scale", title) | |
| 104 | - | |
| 105 | - # --- vec0 virtual table: the recommendation path --- | |
| 106 | - block: | |
| 107 | - db.exec "CREATE VIRTUAL TABLE embeddings USING vec0(id INTEGER PRIMARY KEY, v float[4])" | |
| 108 | - report("vec0 table created", true) | |
| 109 | - | |
| 110 | - # Three vectors; the query is nearest to id 2. | |
| 111 | - let rows = { | |
| 112 | - 1'i64: @[1.0'f32, 0.0, 0.0, 0.0], | |
| 113 | - 2'i64: @[0.0'f32, 1.0, 0.0, 0.0], | |
| 114 | - 3'i64: @[0.0'f32, 0.0, 1.0, 0.0], | |
| 115 | - } | |
| 116 | - for (id, vec) in rows.items: | |
| 117 | - var s: Stmt | |
| 118 | - let sql = &"INSERT INTO embeddings(id, v) VALUES ({id}, ?)" | |
| 119 | - check(db, sqlite3_prepare_v2(db, sql.cstring, -1, s, nil), "prepare insert") | |
| 120 | - let blob = vec.toBlob | |
| 121 | - check(db, sqlite3_bind_blob(s, 1, blob[0].unsafeAddr, blob.len.cint, | |
| 122 | - SQLITE_TRANSIENT), "bind blob") | |
| 123 | - check(db, sqlite3_step(s), "insert vec") | |
| 124 | - discard sqlite3_finalize(s) | |
| 125 | - report("float32 blobs bound", true, &"{rows.len} rows") | |
| 126 | - | |
| 127 | - block: | |
| 128 | - let query = @[0.05'f32, 0.95, 0.0, 0.0] | |
| 129 | - var s: Stmt | |
| 130 | - check(db, sqlite3_prepare_v2(db, | |
| 131 | - "SELECT id, distance FROM embeddings WHERE v MATCH ? AND k = 2 ORDER BY distance", | |
| 132 | - -1, s, nil), "prepare knn") | |
| 133 | - let blob = query.toBlob | |
| 134 | - check(db, sqlite3_bind_blob(s, 1, blob[0].unsafeAddr, blob.len.cint, | |
| 135 | - SQLITE_TRANSIENT), "bind query") | |
| 136 | - var got: seq[(int64, float)] | |
| 137 | - while sqlite3_step(s) == SQLITE_ROW: | |
| 138 | - got.add (sqlite3_column_int64(s, 0), sqlite3_column_double(s, 1).float) | |
| 139 | - discard sqlite3_finalize(s) | |
| 140 | - let nearest = if got.len > 0: got[0][0] else: -1 | |
| 141 | - report("KNN returns nearest first", got.len == 2 and nearest == 2, | |
| 142 | - &"k=2 -> {got.mapIt($it[0]).join(\",\")}") | |
| 143 | - | |
| 144 | - echo "" | |
| 145 | - if failures == 0: | |
| 146 | - echo "RESULT: sqlite-vec + FTS5 work from Nim." | |
| 147 | - else: | |
| 148 | - echo &"RESULT: {failures} check(s) failed." | |
| 149 | - quit 1 | |
| 150 | - | |
| 151 | -main() | |
| deleted file mode 100644 | |||
| @@ -1,151 +0,0 @@ | |||
| 1 | -## Spike: can Nim drive the two SQLite features Glean depends on? | ||
| 2 | -## | ||
| 3 | -## Glean's Go backend needs FTS5 (full-text search over articles) and | ||
| 4 | -## sqlite-vec (embedding similarity for recommendations). Both are C-level | ||
| 5 | -## concerns, so the question for a Nim port is not "does Nim have a SQLite | ||
| 6 | -## library" but whether the same extension can be linked and registered the | ||
| 7 | -## same way. The Go bindings compile sqlite-vec.c with -DSQLITE_CORE and call | ||
| 8 | -## sqlite3_vec_init directly; this does exactly that from Nim. | ||
| 9 | - | ||
| 10 | -{.compile: "vendor/sqlite-vec.c".} # flags live in nim.cfg | ||
| 11 | - | ||
| 12 | -import std/[strformat, strutils, sequtils] | ||
| 13 | - | ||
| 14 | -type | ||
| 15 | - Sqlite3 = distinct pointer | ||
| 16 | - Stmt = distinct pointer | ||
| 17 | - | ||
| 18 | -const | ||
| 19 | - SQLITE_OK = 0 | ||
| 20 | - SQLITE_ROW = 100 | ||
| 21 | - SQLITE_DONE = 101 | ||
| 22 | - | ||
| 23 | -{.push importc, cdecl.} | ||
| 24 | -proc sqlite3_open(filename: cstring, db: var Sqlite3): cint | ||
| 25 | -proc sqlite3_close(db: Sqlite3): cint | ||
| 26 | -proc sqlite3_errmsg(db: Sqlite3): cstring | ||
| 27 | -proc sqlite3_exec(db: Sqlite3, sql: cstring, cb, arg: pointer, | ||
| 28 | - errmsg: ptr cstring): cint | ||
| 29 | -proc sqlite3_prepare_v2(db: Sqlite3, sql: cstring, nByte: cint, | ||
| 30 | - stmt: var Stmt, tail: ptr cstring): cint | ||
| 31 | -proc sqlite3_step(s: Stmt): cint | ||
| 32 | -proc sqlite3_finalize(s: Stmt): cint | ||
| 33 | -proc sqlite3_column_text(s: Stmt, col: cint): cstring | ||
| 34 | -proc sqlite3_column_double(s: Stmt, col: cint): cdouble | ||
| 35 | -proc sqlite3_column_int64(s: Stmt, col: cint): int64 | ||
| 36 | -proc sqlite3_bind_blob(s: Stmt, col: cint, data: pointer, n: cint, | ||
| 37 | - destructor: pointer): cint | ||
| 38 | -proc sqlite3_libversion(): cstring | ||
| 39 | -{.pop.} | ||
| 40 | - | ||
| 41 | -# Provided by the compiled sqlite-vec.c. With SQLITE_CORE it is an ordinary | ||
| 42 | -# function rather than a loadable-extension entry point. | ||
| 43 | -proc sqlite3_vec_init(db: Sqlite3, errMsg: ptr cstring, | ||
| 44 | - api: pointer): cint {.importc, cdecl.} | ||
| 45 | - | ||
| 46 | -const SQLITE_TRANSIENT = cast[pointer](-1) | ||
| 47 | - | ||
| 48 | -var failures = 0 | ||
| 49 | - | ||
| 50 | -proc check(db: Sqlite3, rc: cint, what: string) = | ||
| 51 | - if rc notin [SQLITE_OK, SQLITE_ROW, SQLITE_DONE]: | ||
| 52 | - raise newException(IOError, &"{what}: {sqlite3_errmsg(db)} (rc={rc})") | ||
| 53 | - | ||
| 54 | -proc exec(db: Sqlite3, sql: string) = | ||
| 55 | - check(db, sqlite3_exec(db, sql.cstring, nil, nil, nil), "exec " & sql[0..min(40, sql.high)]) | ||
| 56 | - | ||
| 57 | -proc report(name: string, ok: bool, detail = "") = | ||
| 58 | - if ok: | ||
| 59 | - echo &" PASS {name}" & (if detail.len > 0: " -- " & detail else: "") | ||
| 60 | - else: | ||
| 61 | - inc failures | ||
| 62 | - echo &" FAIL {name}" & (if detail.len > 0: " -- " & detail else: "") | ||
| 63 | - | ||
| 64 | -## Embeddings are bound as raw little-endian float32 blobs, the same wire | ||
| 65 | -## format sqlite-vec expects from every binding. | ||
| 66 | -proc toBlob(v: seq[float32]): string = | ||
| 67 | - result = newString(v.len * 4) | ||
| 68 | - if v.len > 0: | ||
| 69 | - copyMem(result[0].addr, v[0].unsafeAddr, result.len) | ||
| 70 | - | ||
| 71 | -proc main() = | ||
| 72 | - echo "sqlite-vec / FTS5 spike" | ||
| 73 | - echo " sqlite ", sqlite3_libversion() | ||
| 74 | - | ||
| 75 | - var db: Sqlite3 | ||
| 76 | - check(db, sqlite3_open(":memory:", db), "open") | ||
| 77 | - defer: discard sqlite3_close(db) | ||
| 78 | - | ||
| 79 | - # --- sqlite-vec registration --- | ||
| 80 | - block: | ||
| 81 | - let rc = sqlite3_vec_init(db, nil, nil) | ||
| 82 | - report("sqlite3_vec_init registers", rc == SQLITE_OK, &"rc={rc}") | ||
| 83 | - | ||
| 84 | - block: | ||
| 85 | - var s: Stmt | ||
| 86 | - check(db, sqlite3_prepare_v2(db, "SELECT vec_version()", -1, s, nil), "prepare vec_version") | ||
| 87 | - let ok = sqlite3_step(s) == SQLITE_ROW | ||
| 88 | - let ver = if ok: $sqlite3_column_text(s, 0) else: "" | ||
| 89 | - discard sqlite3_finalize(s) | ||
| 90 | - report("vec_version() callable", ok, ver) | ||
| 91 | - | ||
| 92 | - # --- FTS5, as used for article search --- | ||
| 93 | - block: | ||
| 94 | - db.exec "CREATE VIRTUAL TABLE docs USING fts5(title, body)" | ||
| 95 | - db.exec "INSERT INTO docs VALUES ('Nim at scale', 'systems programming with a python face')" | ||
| 96 | - db.exec "INSERT INTO docs VALUES ('Go concurrency', 'goroutines and channels')" | ||
| 97 | - var s: Stmt | ||
| 98 | - check(db, sqlite3_prepare_v2(db, | ||
| 99 | - "SELECT title FROM docs WHERE docs MATCH 'systems' ORDER BY rank", -1, s, nil), "prepare fts") | ||
| 100 | - let ok = sqlite3_step(s) == SQLITE_ROW | ||
| 101 | - let title = if ok: $sqlite3_column_text(s, 0) else: "" | ||
| 102 | - discard sqlite3_finalize(s) | ||
| 103 | - report("FTS5 match + rank", ok and title == "Nim at scale", title) | ||
| 104 | - | ||
| 105 | - # --- vec0 virtual table: the recommendation path --- | ||
| 106 | - block: | ||
| 107 | - db.exec "CREATE VIRTUAL TABLE embeddings USING vec0(id INTEGER PRIMARY KEY, v float[4])" | ||
| 108 | - report("vec0 table created", true) | ||
| 109 | - | ||
| 110 | - # Three vectors; the query is nearest to id 2. | ||
| 111 | - let rows = { | ||
| 112 | - 1'i64: @[1.0'f32, 0.0, 0.0, 0.0], | ||
| 113 | - 2'i64: @[0.0'f32, 1.0, 0.0, 0.0], | ||
| 114 | - 3'i64: @[0.0'f32, 0.0, 1.0, 0.0], | ||
| 115 | - } | ||
| 116 | - for (id, vec) in rows.items: | ||
| 117 | - var s: Stmt | ||
| 118 | - let sql = &"INSERT INTO embeddings(id, v) VALUES ({id}, ?)" | ||
| 119 | - check(db, sqlite3_prepare_v2(db, sql.cstring, -1, s, nil), "prepare insert") | ||
| 120 | - let blob = vec.toBlob | ||
| 121 | - check(db, sqlite3_bind_blob(s, 1, blob[0].unsafeAddr, blob.len.cint, | ||
| 122 | - SQLITE_TRANSIENT), "bind blob") | ||
| 123 | - check(db, sqlite3_step(s), "insert vec") | ||
| 124 | - discard sqlite3_finalize(s) | ||
| 125 | - report("float32 blobs bound", true, &"{rows.len} rows") | ||
| 126 | - | ||
| 127 | - block: | ||
| 128 | - let query = @[0.05'f32, 0.95, 0.0, 0.0] | ||
| 129 | - var s: Stmt | ||
| 130 | - check(db, sqlite3_prepare_v2(db, | ||
| 131 | - "SELECT id, distance FROM embeddings WHERE v MATCH ? AND k = 2 ORDER BY distance", | ||
| 132 | - -1, s, nil), "prepare knn") | ||
| 133 | - let blob = query.toBlob | ||
| 134 | - check(db, sqlite3_bind_blob(s, 1, blob[0].unsafeAddr, blob.len.cint, | ||
| 135 | - SQLITE_TRANSIENT), "bind query") | ||
| 136 | - var got: seq[(int64, float)] | ||
| 137 | - while sqlite3_step(s) == SQLITE_ROW: | ||
| 138 | - got.add (sqlite3_column_int64(s, 0), sqlite3_column_double(s, 1).float) | ||
| 139 | - discard sqlite3_finalize(s) | ||
| 140 | - let nearest = if got.len > 0: got[0][0] else: -1 | ||
| 141 | - report("KNN returns nearest first", got.len == 2 and nearest == 2, | ||
| 142 | - &"k=2 -> {got.mapIt($it[0]).join(\",\")}") | ||
| 143 | - | ||
| 144 | - echo "" | ||
| 145 | - if failures == 0: | ||
| 146 | - echo "RESULT: sqlite-vec + FTS5 work from Nim." | ||
| 147 | - else: | ||
| 148 | - echo &"RESULT: {failures} check(s) failed." | ||
| 149 | - quit 1 | ||
| 150 | - | ||
| 151 | -main() | ||
deleted
spike/nim/store_probe.nim +0 -185 | deleted file mode 100644 | ||
| @@ -1,185 +0,0 @@ | ||
| 1 | -## Checks that a session survives a restart intact. | |
| 2 | -## | |
| 3 | -## nim c -r -d:ssl store_probe.nim | |
| 4 | -## | |
| 5 | -## The interesting failure is not "the row is missing" -- that is loud. It is | |
| 6 | -## a DPoP key that round-trips *almost* correctly: every request after a | |
| 7 | -## restart then fails authentication with no clue that storage is at fault. | |
| 8 | -## So the key is checked by signing with the reloaded copy and verifying | |
| 9 | -## against the original public point, not by comparing fields. | |
| 10 | - | |
| 11 | -import std/[json, options, os, strformat, strutils] | |
| 12 | -import pkg/chronos | |
| 13 | -import bearssl/rand | |
| 14 | -import atproto/[cursor_store, dpop, jetstream_run, oauth, sqlite, store] | |
| 15 | - | |
| 16 | -var failures = 0 | |
| 17 | - | |
| 18 | -proc report(name: string, ok: bool, detail = "") = | |
| 19 | - if not ok: inc failures | |
| 20 | - let label = if ok: "PASS" else: "FAIL" | |
| 21 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | |
| 22 | - else: echo &" {label} {name}" | |
| 23 | - | |
| 24 | -proc main() = | |
| 25 | - echo "Session persistence probe" | |
| 26 | - echo "" | |
| 27 | - | |
| 28 | - let rngRef = HmacDrbgContext.new() | |
| 29 | - doAssert rngRef != nil | |
| 30 | - var rng = rngRef[] | |
| 31 | - | |
| 32 | - let path = getTempDir() / "glean_store_probe.db" | |
| 33 | - removeFile(path) | |
| 34 | - removeFile(path & "-wal") | |
| 35 | - removeFile(path & "-shm") | |
| 36 | - defer: | |
| 37 | - removeFile(path) | |
| 38 | - removeFile(path & "-wal") | |
| 39 | - removeFile(path & "-shm") | |
| 40 | - | |
| 41 | - let key = generateKey(rng) | |
| 42 | - let originalThumb = key.thumbprint | |
| 43 | - | |
| 44 | - # --- write, then close the database entirely --------------------------- | |
| 45 | - block: | |
| 46 | - var db = sqlite.open(path) | |
| 47 | - migrate(db) | |
| 48 | - report("schema applies", true, "sqlite " & libVersion()) | |
| 49 | - | |
| 50 | - saveSession(db, SessionRecord( | |
| 51 | - accountDid: "did:plc:probe", | |
| 52 | - sessionId: "sess-1", | |
| 53 | - pdsUrl: "https://pds.example", | |
| 54 | - authServerUrl: "https://bsky.social", | |
| 55 | - tokenEndpoint: "https://bsky.social/oauth/token", | |
| 56 | - accessToken: "access-abc", | |
| 57 | - refreshToken: "refresh-xyz", | |
| 58 | - dpopKey: key, | |
| 59 | - hostNonce: "host-n", | |
| 60 | - authNonce: "auth-n", | |
| 61 | - scopes: @["atproto", "transition:generic"], | |
| 62 | - createdAt: 1_700_000_000, | |
| 63 | - )) | |
| 64 | - saveCursor(db, 1_700_000_000_000_000'i64) | |
| 65 | - saveAuthRequest(db, AuthRequestData( | |
| 66 | - state: "state-1", | |
| 67 | - authServerUrl: "https://bsky.social", | |
| 68 | - scopes: @["atproto"], | |
| 69 | - pkceVerifier: "verifier-1", | |
| 70 | - requestUri: "urn:ietf:params:oauth:request_uri:req-1", | |
| 71 | - tokenEndpoint: "https://bsky.social/oauth/token", | |
| 72 | - dpopNonce: "n-1", | |
| 73 | - dpopKey: key, | |
| 74 | - accountDid: "did:plc:probe", | |
| 75 | - )) | |
| 76 | - db.close() | |
| 77 | - | |
| 78 | - # --- reopen, as a restart would ---------------------------------------- | |
| 79 | - var db = sqlite.open(path) | |
| 80 | - defer: db.close() | |
| 81 | - migrate(db) # idempotent, as it is on every boot | |
| 82 | - | |
| 83 | - block: | |
| 84 | - let got = getSession(db, "did:plc:probe", "sess-1") | |
| 85 | - report("session survives a restart", got.isSome) | |
| 86 | - if got.isSome: | |
| 87 | - let s = got.get | |
| 88 | - report("tokens round-trip", | |
| 89 | - s.accessToken == "access-abc" and s.refreshToken == "refresh-xyz") | |
| 90 | - report("nonces round-trip", | |
| 91 | - s.hostNonce == "host-n" and s.authNonce == "auth-n") | |
| 92 | - report("scopes round-trip", s.scopes == @["atproto", "transition:generic"]) | |
| 93 | - | |
| 94 | - # The check that matters. | |
| 95 | - report("reloaded key has the same JWK thumbprint", | |
| 96 | - s.dpopKey.thumbprint == originalThumb, s.dpopKey.thumbprint) | |
| 97 | - | |
| 98 | - const msg = "eyJhbGciOiJFUzI1NiJ9.eyJodG0iOiJHRVQifQ" | |
| 99 | - let sig = s.dpopKey.signEs256(rng, msg) | |
| 100 | - report("reloaded key still signs verifiably", | |
| 101 | - key.verifyEs256(msg, sig), | |
| 102 | - "signed with the reloaded key, verified against the original") | |
| 103 | - | |
| 104 | - # Negative control: a verifier that accepts anything proves nothing. | |
| 105 | - let other = generateKey(rng) | |
| 106 | - report("a different key does not verify", | |
| 107 | - not other.verifyEs256(msg, sig)) | |
| 108 | - | |
| 109 | - block: | |
| 110 | - report("unknown session reads as none", | |
| 111 | - getSession(db, "did:plc:probe", "nope").isNone) | |
| 112 | - report("sessions are listed per account", | |
| 113 | - listSessions(db, "did:plc:probe") == @["sess-1"]) | |
| 114 | - report("accounts are listed", listAccounts(db) == @["did:plc:probe"]) | |
| 115 | - | |
| 116 | - block: | |
| 117 | - report("cursor survives a restart", | |
| 118 | - loadCursor(db) == 1_700_000_000_000_000'i64, $loadCursor(db)) | |
| 119 | - | |
| 120 | - # --- auth requests are single-use -------------------------------------- | |
| 121 | - block: | |
| 122 | - let first = takeAuthRequest(db, "state-1") | |
| 123 | - report("pending auth request survives a restart", first.isSome) | |
| 124 | - if first.isSome: | |
| 125 | - report("PKCE verifier round-trips", first.get.pkceVerifier == "verifier-1") | |
| 126 | - report("auth request key round-trips", | |
| 127 | - first.get.dpopKey.thumbprint == originalThumb) | |
| 128 | - # An authorization code is single-use; leaving the row behind invites a | |
| 129 | - # replayed callback. | |
| 130 | - report("taking an auth request consumes it", | |
| 131 | - takeAuthRequest(db, "state-1").isNone) | |
| 132 | - | |
| 133 | - # --- batched cursor store ---------------------------------------------- | |
| 134 | - block: | |
| 135 | - let cs = newDbCursorStore(db, flushEvery = 3600) # never auto-flush here | |
| 136 | - report("cursor store loads the persisted value", | |
| 137 | - cs.load() == 1_700_000_000_000_000'i64) | |
| 138 | - | |
| 139 | - cs.save(1_700_000_000_500_000'i64) | |
| 140 | - # Batching means the write is deferred, not lost. | |
| 141 | - report("a fresh cursor is not written immediately", | |
| 142 | - loadCursor(db) == 1_700_000_000_000_000'i64) | |
| 143 | - cs.close() | |
| 144 | - report("closing flushes the pending cursor", | |
| 145 | - loadCursor(db) == 1_700_000_000_500_000'i64, $loadCursor(db)) | |
| 146 | - | |
| 147 | - # Cursors must never move backwards, or a late event would rewind the | |
| 148 | - # subscription on the next reconnect. | |
| 149 | - let cs2 = newDbCursorStore(db, flushEvery = 0) | |
| 150 | - cs2.save(1'i64) | |
| 151 | - cs2.close() | |
| 152 | - report("an older cursor does not overwrite a newer one", | |
| 153 | - loadCursor(db) == 1_700_000_000_500_000'i64, $loadCursor(db)) | |
| 154 | - | |
| 155 | - # --- the seam with the supervisor -------------------------------------- | |
| 156 | - # jetstream_run takes a CursorStore base reference, so this only works if | |
| 157 | - # the overrides dispatch dynamically. A silent fall-through to the base | |
| 158 | - # method would read the in-memory field instead of the database and look | |
| 159 | - # entirely healthy while resuming from the wrong place. | |
| 160 | - block: | |
| 161 | - let cs = newDbCursorStore(db, flushEvery = 3600) | |
| 162 | - let asBase: CursorStore = cs | |
| 163 | - report("DbCursorStore dispatches through the CursorStore base", | |
| 164 | - asBase.load() == loadCursor(db), $asBase.load()) | |
| 165 | - | |
| 166 | - let rewound = rewoundCursor(asBase, 5.seconds) | |
| 167 | - report("the supervisor rewinds the persisted cursor", | |
| 168 | - rewound == loadCursor(db) - 5_000_000, $rewound) | |
| 169 | - | |
| 170 | - # --- deletion ---------------------------------------------------------- | |
| 171 | - block: | |
| 172 | - deleteSession(db, "did:plc:probe", "sess-1") | |
| 173 | - report("sessions can be deleted", | |
| 174 | - getSession(db, "did:plc:probe", "sess-1").isNone and | |
| 175 | - listAccounts(db).len == 0) | |
| 176 | - | |
| 177 | - echo "" | |
| 178 | - if failures == 0: | |
| 179 | - echo "RESULT: sessions, keys and cursors survive a restart." | |
| 180 | - else: | |
| 181 | - echo &"RESULT: {failures} check(s) failed." | |
| 182 | - quit 1 | |
| 183 | - | |
| 184 | -when isMainModule: | |
| 185 | - main() | |
| deleted file mode 100644 | |||
| @@ -1,185 +0,0 @@ | |||
| 1 | -## Checks that a session survives a restart intact. | ||
| 2 | -## | ||
| 3 | -## nim c -r -d:ssl store_probe.nim | ||
| 4 | -## | ||
| 5 | -## The interesting failure is not "the row is missing" -- that is loud. It is | ||
| 6 | -## a DPoP key that round-trips *almost* correctly: every request after a | ||
| 7 | -## restart then fails authentication with no clue that storage is at fault. | ||
| 8 | -## So the key is checked by signing with the reloaded copy and verifying | ||
| 9 | -## against the original public point, not by comparing fields. | ||
| 10 | - | ||
| 11 | -import std/[json, options, os, strformat, strutils] | ||
| 12 | -import pkg/chronos | ||
| 13 | -import bearssl/rand | ||
| 14 | -import atproto/[cursor_store, dpop, jetstream_run, oauth, sqlite, store] | ||
| 15 | - | ||
| 16 | -var failures = 0 | ||
| 17 | - | ||
| 18 | -proc report(name: string, ok: bool, detail = "") = | ||
| 19 | - if not ok: inc failures | ||
| 20 | - let label = if ok: "PASS" else: "FAIL" | ||
| 21 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | ||
| 22 | - else: echo &" {label} {name}" | ||
| 23 | - | ||
| 24 | -proc main() = | ||
| 25 | - echo "Session persistence probe" | ||
| 26 | - echo "" | ||
| 27 | - | ||
| 28 | - let rngRef = HmacDrbgContext.new() | ||
| 29 | - doAssert rngRef != nil | ||
| 30 | - var rng = rngRef[] | ||
| 31 | - | ||
| 32 | - let path = getTempDir() / "glean_store_probe.db" | ||
| 33 | - removeFile(path) | ||
| 34 | - removeFile(path & "-wal") | ||
| 35 | - removeFile(path & "-shm") | ||
| 36 | - defer: | ||
| 37 | - removeFile(path) | ||
| 38 | - removeFile(path & "-wal") | ||
| 39 | - removeFile(path & "-shm") | ||
| 40 | - | ||
| 41 | - let key = generateKey(rng) | ||
| 42 | - let originalThumb = key.thumbprint | ||
| 43 | - | ||
| 44 | - # --- write, then close the database entirely --------------------------- | ||
| 45 | - block: | ||
| 46 | - var db = sqlite.open(path) | ||
| 47 | - migrate(db) | ||
| 48 | - report("schema applies", true, "sqlite " & libVersion()) | ||
| 49 | - | ||
| 50 | - saveSession(db, SessionRecord( | ||
| 51 | - accountDid: "did:plc:probe", | ||
| 52 | - sessionId: "sess-1", | ||
| 53 | - pdsUrl: "https://pds.example", | ||
| 54 | - authServerUrl: "https://bsky.social", | ||
| 55 | - tokenEndpoint: "https://bsky.social/oauth/token", | ||
| 56 | - accessToken: "access-abc", | ||
| 57 | - refreshToken: "refresh-xyz", | ||
| 58 | - dpopKey: key, | ||
| 59 | - hostNonce: "host-n", | ||
| 60 | - authNonce: "auth-n", | ||
| 61 | - scopes: @["atproto", "transition:generic"], | ||
| 62 | - createdAt: 1_700_000_000, | ||
| 63 | - )) | ||
| 64 | - saveCursor(db, 1_700_000_000_000_000'i64) | ||
| 65 | - saveAuthRequest(db, AuthRequestData( | ||
| 66 | - state: "state-1", | ||
| 67 | - authServerUrl: "https://bsky.social", | ||
| 68 | - scopes: @["atproto"], | ||
| 69 | - pkceVerifier: "verifier-1", | ||
| 70 | - requestUri: "urn:ietf:params:oauth:request_uri:req-1", | ||
| 71 | - tokenEndpoint: "https://bsky.social/oauth/token", | ||
| 72 | - dpopNonce: "n-1", | ||
| 73 | - dpopKey: key, | ||
| 74 | - accountDid: "did:plc:probe", | ||
| 75 | - )) | ||
| 76 | - db.close() | ||
| 77 | - | ||
| 78 | - # --- reopen, as a restart would ---------------------------------------- | ||
| 79 | - var db = sqlite.open(path) | ||
| 80 | - defer: db.close() | ||
| 81 | - migrate(db) # idempotent, as it is on every boot | ||
| 82 | - | ||
| 83 | - block: | ||
| 84 | - let got = getSession(db, "did:plc:probe", "sess-1") | ||
| 85 | - report("session survives a restart", got.isSome) | ||
| 86 | - if got.isSome: | ||
| 87 | - let s = got.get | ||
| 88 | - report("tokens round-trip", | ||
| 89 | - s.accessToken == "access-abc" and s.refreshToken == "refresh-xyz") | ||
| 90 | - report("nonces round-trip", | ||
| 91 | - s.hostNonce == "host-n" and s.authNonce == "auth-n") | ||
| 92 | - report("scopes round-trip", s.scopes == @["atproto", "transition:generic"]) | ||
| 93 | - | ||
| 94 | - # The check that matters. | ||
| 95 | - report("reloaded key has the same JWK thumbprint", | ||
| 96 | - s.dpopKey.thumbprint == originalThumb, s.dpopKey.thumbprint) | ||
| 97 | - | ||
| 98 | - const msg = "eyJhbGciOiJFUzI1NiJ9.eyJodG0iOiJHRVQifQ" | ||
| 99 | - let sig = s.dpopKey.signEs256(rng, msg) | ||
| 100 | - report("reloaded key still signs verifiably", | ||
| 101 | - key.verifyEs256(msg, sig), | ||
| 102 | - "signed with the reloaded key, verified against the original") | ||
| 103 | - | ||
| 104 | - # Negative control: a verifier that accepts anything proves nothing. | ||
| 105 | - let other = generateKey(rng) | ||
| 106 | - report("a different key does not verify", | ||
| 107 | - not other.verifyEs256(msg, sig)) | ||
| 108 | - | ||
| 109 | - block: | ||
| 110 | - report("unknown session reads as none", | ||
| 111 | - getSession(db, "did:plc:probe", "nope").isNone) | ||
| 112 | - report("sessions are listed per account", | ||
| 113 | - listSessions(db, "did:plc:probe") == @["sess-1"]) | ||
| 114 | - report("accounts are listed", listAccounts(db) == @["did:plc:probe"]) | ||
| 115 | - | ||
| 116 | - block: | ||
| 117 | - report("cursor survives a restart", | ||
| 118 | - loadCursor(db) == 1_700_000_000_000_000'i64, $loadCursor(db)) | ||
| 119 | - | ||
| 120 | - # --- auth requests are single-use -------------------------------------- | ||
| 121 | - block: | ||
| 122 | - let first = takeAuthRequest(db, "state-1") | ||
| 123 | - report("pending auth request survives a restart", first.isSome) | ||
| 124 | - if first.isSome: | ||
| 125 | - report("PKCE verifier round-trips", first.get.pkceVerifier == "verifier-1") | ||
| 126 | - report("auth request key round-trips", | ||
| 127 | - first.get.dpopKey.thumbprint == originalThumb) | ||
| 128 | - # An authorization code is single-use; leaving the row behind invites a | ||
| 129 | - # replayed callback. | ||
| 130 | - report("taking an auth request consumes it", | ||
| 131 | - takeAuthRequest(db, "state-1").isNone) | ||
| 132 | - | ||
| 133 | - # --- batched cursor store ---------------------------------------------- | ||
| 134 | - block: | ||
| 135 | - let cs = newDbCursorStore(db, flushEvery = 3600) # never auto-flush here | ||
| 136 | - report("cursor store loads the persisted value", | ||
| 137 | - cs.load() == 1_700_000_000_000_000'i64) | ||
| 138 | - | ||
| 139 | - cs.save(1_700_000_000_500_000'i64) | ||
| 140 | - # Batching means the write is deferred, not lost. | ||
| 141 | - report("a fresh cursor is not written immediately", | ||
| 142 | - loadCursor(db) == 1_700_000_000_000_000'i64) | ||
| 143 | - cs.close() | ||
| 144 | - report("closing flushes the pending cursor", | ||
| 145 | - loadCursor(db) == 1_700_000_000_500_000'i64, $loadCursor(db)) | ||
| 146 | - | ||
| 147 | - # Cursors must never move backwards, or a late event would rewind the | ||
| 148 | - # subscription on the next reconnect. | ||
| 149 | - let cs2 = newDbCursorStore(db, flushEvery = 0) | ||
| 150 | - cs2.save(1'i64) | ||
| 151 | - cs2.close() | ||
| 152 | - report("an older cursor does not overwrite a newer one", | ||
| 153 | - loadCursor(db) == 1_700_000_000_500_000'i64, $loadCursor(db)) | ||
| 154 | - | ||
| 155 | - # --- the seam with the supervisor -------------------------------------- | ||
| 156 | - # jetstream_run takes a CursorStore base reference, so this only works if | ||
| 157 | - # the overrides dispatch dynamically. A silent fall-through to the base | ||
| 158 | - # method would read the in-memory field instead of the database and look | ||
| 159 | - # entirely healthy while resuming from the wrong place. | ||
| 160 | - block: | ||
| 161 | - let cs = newDbCursorStore(db, flushEvery = 3600) | ||
| 162 | - let asBase: CursorStore = cs | ||
| 163 | - report("DbCursorStore dispatches through the CursorStore base", | ||
| 164 | - asBase.load() == loadCursor(db), $asBase.load()) | ||
| 165 | - | ||
| 166 | - let rewound = rewoundCursor(asBase, 5.seconds) | ||
| 167 | - report("the supervisor rewinds the persisted cursor", | ||
| 168 | - rewound == loadCursor(db) - 5_000_000, $rewound) | ||
| 169 | - | ||
| 170 | - # --- deletion ---------------------------------------------------------- | ||
| 171 | - block: | ||
| 172 | - deleteSession(db, "did:plc:probe", "sess-1") | ||
| 173 | - report("sessions can be deleted", | ||
| 174 | - getSession(db, "did:plc:probe", "sess-1").isNone and | ||
| 175 | - listAccounts(db).len == 0) | ||
| 176 | - | ||
| 177 | - echo "" | ||
| 178 | - if failures == 0: | ||
| 179 | - echo "RESULT: sessions, keys and cursors survive a restart." | ||
| 180 | - else: | ||
| 181 | - echo &"RESULT: {failures} check(s) failed." | ||
| 182 | - quit 1 | ||
| 183 | - | ||
| 184 | -when isMainModule: | ||
| 185 | - main() | ||
modified
spike/nim/stores_probe.nim +1 -1 | @@ -7,7 +7,7 @@ | ||
| 7 | 7 | ## returns other people's feeds, a NULL that sorts to the top. |
| 8 | 8 | |
| 9 | 9 | import std/[options, os, strformat, strutils, times] |
| 10 | -import atproto/[articlestore, feedstore, gleandb, sqlite] | |
| 10 | +import reader/[articlestore, feedstore, gleandb, sqlite] | |
| 11 | 11 | |
| 12 | 12 | var failures = 0 |
| 13 | 13 | |
| @@ -7,7 +7,7 @@ | |||
| 7 | ## returns other people's feeds, a NULL that sorts to the top. | 7 | ## returns other people's feeds, a NULL that sorts to the top. |
| 8 | 8 | ||
| 9 | import std/[options, os, strformat, strutils, times] | 9 | import std/[options, os, strformat, strutils, times] |
| 10 | -import atproto/[articlestore, feedstore, gleandb, sqlite] | 10 | +import reader/[articlestore, feedstore, gleandb, sqlite] |
| 11 | 11 | ||
| 12 | var failures = 0 | 12 | var failures = 0 |
| 13 | 13 | ||
deleted
spike/nim/tools/extract_schema.py +0 -72 | deleted file mode 100755 | ||
| @@ -1,72 +0,0 @@ | ||
| 1 | -#!/usr/bin/env python3 | |
| 2 | -"""Regenerate atproto/schema.nim from the Go schema. | |
| 3 | - | |
| 4 | -The DDL lives in internal/db/db.go as three []string literals. Retyping 44 | |
| 5 | -statements into Nim would drift -- a differing default or CHECK constraint | |
| 6 | -surfaces months later as a violation nobody can place -- so they are copied | |
| 7 | -mechanically instead. | |
| 8 | - | |
| 9 | - python3 tools/extract_schema.py | |
| 10 | -""" | |
| 11 | -import pathlib | |
| 12 | -import re | |
| 13 | -import sys | |
| 14 | - | |
| 15 | -GO = pathlib.Path(__file__).resolve().parents[3] / "internal" / "db" / "db.go" | |
| 16 | -OUT = pathlib.Path(__file__).resolve().parents[1] / "atproto" / "schema.nim" | |
| 17 | - | |
| 18 | -HEADER = '''## Glean's schema, in the three databases it uses. | |
| 19 | -## | |
| 20 | -## Extracted mechanically from internal/db/db.go rather than retyped: 44 DDL | |
| 21 | -## statements transcribed by hand would drift from the Go definition, and a | |
| 22 | -## column that differs by a default or a CHECK is the kind of thing that only | |
| 23 | -## shows up as a constraint violation months later. | |
| 24 | -## | |
| 25 | -## Regenerate with tools/extract_schema.py after changing the Go schema. | |
| 26 | - | |
| 27 | -''' | |
| 28 | - | |
| 29 | - | |
| 30 | -def groups(src): | |
| 31 | - """Locate each `var <name>Schema = []string{ ... }` block by line.""" | |
| 32 | - lines = src.splitlines() | |
| 33 | - found = [] | |
| 34 | - for i, line in enumerate(lines): | |
| 35 | - m = re.match(r"var (\w+Schema) = \[\]string\{", line) | |
| 36 | - if not m: | |
| 37 | - continue | |
| 38 | - for j in range(i + 1, len(lines)): | |
| 39 | - if lines[j] == "}": | |
| 40 | - found.append((m.group(1), i + 1, j)) | |
| 41 | - break | |
| 42 | - return lines, found | |
| 43 | - | |
| 44 | - | |
| 45 | -def main(): | |
| 46 | - if not GO.exists(): | |
| 47 | - sys.exit(f"cannot find {GO}") | |
| 48 | - src = GO.read_text() | |
| 49 | - lines, found = groups(src) | |
| 50 | - if not found: | |
| 51 | - sys.exit("no schema blocks found; did db.go change shape?") | |
| 52 | - | |
| 53 | - out = [HEADER] | |
| 54 | - total = 0 | |
| 55 | - for name, start, end in found: | |
| 56 | - body = "\n".join(lines[start:end]) | |
| 57 | - stmts = re.findall(r"`([^`]*)`", body) | |
| 58 | - total += len(stmts) | |
| 59 | - out.append(f"const {name}* = [\n") | |
| 60 | - for st in stmts: | |
| 61 | - if '"""' in st: | |
| 62 | - sys.exit(f"statement contains a triple quote: {st[:60]}") | |
| 63 | - out.append(f' """{st}""",\n') | |
| 64 | - out.append("]\n\n") | |
| 65 | - print(f"{name}: {len(stmts)} statements") | |
| 66 | - | |
| 67 | - OUT.write_text("".join(out)) | |
| 68 | - print(f"wrote {OUT} ({total} statements)") | |
| 69 | - | |
| 70 | - | |
| 71 | -if __name__ == "__main__": | |
| 72 | - main() | |
| deleted file mode 100755 | |||
| @@ -1,72 +0,0 @@ | |||
| 1 | -#!/usr/bin/env python3 | ||
| 2 | -"""Regenerate atproto/schema.nim from the Go schema. | ||
| 3 | - | ||
| 4 | -The DDL lives in internal/db/db.go as three []string literals. Retyping 44 | ||
| 5 | -statements into Nim would drift -- a differing default or CHECK constraint | ||
| 6 | -surfaces months later as a violation nobody can place -- so they are copied | ||
| 7 | -mechanically instead. | ||
| 8 | - | ||
| 9 | - python3 tools/extract_schema.py | ||
| 10 | -""" | ||
| 11 | -import pathlib | ||
| 12 | -import re | ||
| 13 | -import sys | ||
| 14 | - | ||
| 15 | -GO = pathlib.Path(__file__).resolve().parents[3] / "internal" / "db" / "db.go" | ||
| 16 | -OUT = pathlib.Path(__file__).resolve().parents[1] / "atproto" / "schema.nim" | ||
| 17 | - | ||
| 18 | -HEADER = '''## Glean's schema, in the three databases it uses. | ||
| 19 | -## | ||
| 20 | -## Extracted mechanically from internal/db/db.go rather than retyped: 44 DDL | ||
| 21 | -## statements transcribed by hand would drift from the Go definition, and a | ||
| 22 | -## column that differs by a default or a CHECK is the kind of thing that only | ||
| 23 | -## shows up as a constraint violation months later. | ||
| 24 | -## | ||
| 25 | -## Regenerate with tools/extract_schema.py after changing the Go schema. | ||
| 26 | - | ||
| 27 | -''' | ||
| 28 | - | ||
| 29 | - | ||
| 30 | -def groups(src): | ||
| 31 | - """Locate each `var <name>Schema = []string{ ... }` block by line.""" | ||
| 32 | - lines = src.splitlines() | ||
| 33 | - found = [] | ||
| 34 | - for i, line in enumerate(lines): | ||
| 35 | - m = re.match(r"var (\w+Schema) = \[\]string\{", line) | ||
| 36 | - if not m: | ||
| 37 | - continue | ||
| 38 | - for j in range(i + 1, len(lines)): | ||
| 39 | - if lines[j] == "}": | ||
| 40 | - found.append((m.group(1), i + 1, j)) | ||
| 41 | - break | ||
| 42 | - return lines, found | ||
| 43 | - | ||
| 44 | - | ||
| 45 | -def main(): | ||
| 46 | - if not GO.exists(): | ||
| 47 | - sys.exit(f"cannot find {GO}") | ||
| 48 | - src = GO.read_text() | ||
| 49 | - lines, found = groups(src) | ||
| 50 | - if not found: | ||
| 51 | - sys.exit("no schema blocks found; did db.go change shape?") | ||
| 52 | - | ||
| 53 | - out = [HEADER] | ||
| 54 | - total = 0 | ||
| 55 | - for name, start, end in found: | ||
| 56 | - body = "\n".join(lines[start:end]) | ||
| 57 | - stmts = re.findall(r"`([^`]*)`", body) | ||
| 58 | - total += len(stmts) | ||
| 59 | - out.append(f"const {name}* = [\n") | ||
| 60 | - for st in stmts: | ||
| 61 | - if '"""' in st: | ||
| 62 | - sys.exit(f"statement contains a triple quote: {st[:60]}") | ||
| 63 | - out.append(f' """{st}""",\n') | ||
| 64 | - out.append("]\n\n") | ||
| 65 | - print(f"{name}: {len(stmts)} statements") | ||
| 66 | - | ||
| 67 | - OUT.write_text("".join(out)) | ||
| 68 | - print(f"wrote {OUT} ({total} statements)") | ||
| 69 | - | ||
| 70 | - | ||
| 71 | -if __name__ == "__main__": | ||
| 72 | - main() | ||
deleted
spike/nim/vendor/sqlite-vec.c +0 -9749 | deleted file mode 100644 | ||
| @@ -1,9749 +0,0 @@ | ||
| 1 | -#include "sqlite-vec.h" | |
| 2 | - | |
| 3 | -#include <assert.h> | |
| 4 | -#include <errno.h> | |
| 5 | -#include <float.h> | |
| 6 | -#include <inttypes.h> | |
| 7 | -#include <limits.h> | |
| 8 | -#include <math.h> | |
| 9 | -#include <stdbool.h> | |
| 10 | -#include <stdint.h> | |
| 11 | -#include <stdlib.h> | |
| 12 | -#include <string.h> | |
| 13 | - | |
| 14 | -#ifndef SQLITE_VEC_OMIT_FS | |
| 15 | -#include <stdio.h> | |
| 16 | -#endif | |
| 17 | - | |
| 18 | -#ifndef SQLITE_CORE | |
| 19 | -#include "sqlite3ext.h" | |
| 20 | -SQLITE_EXTENSION_INIT1 | |
| 21 | -#else | |
| 22 | -#include "sqlite3.h" | |
| 23 | -#endif | |
| 24 | - | |
| 25 | -#ifndef UINT32_TYPE | |
| 26 | -#ifdef HAVE_UINT32_T | |
| 27 | -#define UINT32_TYPE uint32_t | |
| 28 | -#else | |
| 29 | -#define UINT32_TYPE unsigned int | |
| 30 | -#endif | |
| 31 | -#endif | |
| 32 | -#ifndef UINT16_TYPE | |
| 33 | -#ifdef HAVE_UINT16_T | |
| 34 | -#define UINT16_TYPE uint16_t | |
| 35 | -#else | |
| 36 | -#define UINT16_TYPE unsigned short int | |
| 37 | -#endif | |
| 38 | -#endif | |
| 39 | -#ifndef INT16_TYPE | |
| 40 | -#ifdef HAVE_INT16_T | |
| 41 | -#define INT16_TYPE int16_t | |
| 42 | -#else | |
| 43 | -#define INT16_TYPE short int | |
| 44 | -#endif | |
| 45 | -#endif | |
| 46 | -#ifndef UINT8_TYPE | |
| 47 | -#ifdef HAVE_UINT8_T | |
| 48 | -#define UINT8_TYPE uint8_t | |
| 49 | -#else | |
| 50 | -#define UINT8_TYPE unsigned char | |
| 51 | -#endif | |
| 52 | -#endif | |
| 53 | -#ifndef INT8_TYPE | |
| 54 | -#ifdef HAVE_INT8_T | |
| 55 | -#define INT8_TYPE int8_t | |
| 56 | -#else | |
| 57 | -#define INT8_TYPE signed char | |
| 58 | -#endif | |
| 59 | -#endif | |
| 60 | -#ifndef LONGDOUBLE_TYPE | |
| 61 | -#define LONGDOUBLE_TYPE long double | |
| 62 | -#endif | |
| 63 | - | |
| 64 | -#ifndef _WIN32 | |
| 65 | -#ifndef __EMSCRIPTEN__ | |
| 66 | -#ifndef __COSMOPOLITAN__ | |
| 67 | -#ifndef __wasi__ | |
| 68 | -typedef u_int8_t uint8_t; | |
| 69 | -typedef u_int16_t uint16_t; | |
| 70 | -typedef u_int64_t uint64_t; | |
| 71 | -#endif | |
| 72 | -#endif | |
| 73 | -#endif | |
| 74 | -#endif | |
| 75 | - | |
| 76 | -typedef int8_t i8; | |
| 77 | -typedef uint8_t u8; | |
| 78 | -typedef int16_t i16; | |
| 79 | -typedef int32_t i32; | |
| 80 | -typedef sqlite3_int64 i64; | |
| 81 | -typedef uint32_t u32; | |
| 82 | -typedef uint64_t u64; | |
| 83 | -typedef float f32; | |
| 84 | -typedef size_t usize; | |
| 85 | - | |
| 86 | -#ifndef UNUSED_PARAMETER | |
| 87 | -#define UNUSED_PARAMETER(X) (void)(X) | |
| 88 | -#endif | |
| 89 | - | |
| 90 | -// sqlite3_vtab_in() was added in SQLite version 3.38 (2022-02-22) | |
| 91 | -// https://www.sqlite.org/changes.html#version_3_38_0 | |
| 92 | -#if SQLITE_VERSION_NUMBER >= 3038000 | |
| 93 | -#define COMPILER_SUPPORTS_VTAB_IN 1 | |
| 94 | -#endif | |
| 95 | - | |
| 96 | -#ifndef SQLITE_SUBTYPE | |
| 97 | -#define SQLITE_SUBTYPE 0x000100000 | |
| 98 | -#endif | |
| 99 | - | |
| 100 | -#ifndef SQLITE_RESULT_SUBTYPE | |
| 101 | -#define SQLITE_RESULT_SUBTYPE 0x001000000 | |
| 102 | -#endif | |
| 103 | - | |
| 104 | -#ifndef SQLITE_INDEX_CONSTRAINT_LIMIT | |
| 105 | -#define SQLITE_INDEX_CONSTRAINT_LIMIT 73 | |
| 106 | -#endif | |
| 107 | - | |
| 108 | -#ifndef SQLITE_INDEX_CONSTRAINT_OFFSET | |
| 109 | -#define SQLITE_INDEX_CONSTRAINT_OFFSET 74 | |
| 110 | -#endif | |
| 111 | - | |
| 112 | -#define countof(x) (sizeof(x) / sizeof((x)[0])) | |
| 113 | -#define min(a, b) (((a) <= (b)) ? (a) : (b)) | |
| 114 | - | |
| 115 | -enum VectorElementType { | |
| 116 | - // clang-format off | |
| 117 | - SQLITE_VEC_ELEMENT_TYPE_FLOAT32 = 223 + 0, | |
| 118 | - SQLITE_VEC_ELEMENT_TYPE_BIT = 223 + 1, | |
| 119 | - SQLITE_VEC_ELEMENT_TYPE_INT8 = 223 + 2, | |
| 120 | - // clang-format on | |
| 121 | -}; | |
| 122 | - | |
| 123 | -#ifdef SQLITE_VEC_ENABLE_AVX | |
| 124 | -#include <immintrin.h> | |
| 125 | -#define PORTABLE_ALIGN32 __attribute__((aligned(32))) | |
| 126 | -#define PORTABLE_ALIGN64 __attribute__((aligned(64))) | |
| 127 | - | |
| 128 | -static f32 l2_sqr_float_avx(const void *pVect1v, const void *pVect2v, | |
| 129 | - const void *qty_ptr) { | |
| 130 | - f32 *pVect1 = (f32 *)pVect1v; | |
| 131 | - f32 *pVect2 = (f32 *)pVect2v; | |
| 132 | - size_t qty = *((size_t *)qty_ptr); | |
| 133 | - f32 PORTABLE_ALIGN32 TmpRes[8]; | |
| 134 | - size_t qty16 = qty >> 4; | |
| 135 | - | |
| 136 | - const f32 *pEnd1 = pVect1 + (qty16 << 4); | |
| 137 | - | |
| 138 | - __m256 diff, v1, v2; | |
| 139 | - __m256 sum = _mm256_set1_ps(0); | |
| 140 | - | |
| 141 | - while (pVect1 < pEnd1) { | |
| 142 | - v1 = _mm256_loadu_ps(pVect1); | |
| 143 | - pVect1 += 8; | |
| 144 | - v2 = _mm256_loadu_ps(pVect2); | |
| 145 | - pVect2 += 8; | |
| 146 | - diff = _mm256_sub_ps(v1, v2); | |
| 147 | - sum = _mm256_add_ps(sum, _mm256_mul_ps(diff, diff)); | |
| 148 | - | |
| 149 | - v1 = _mm256_loadu_ps(pVect1); | |
| 150 | - pVect1 += 8; | |
| 151 | - v2 = _mm256_loadu_ps(pVect2); | |
| 152 | - pVect2 += 8; | |
| 153 | - diff = _mm256_sub_ps(v1, v2); | |
| 154 | - sum = _mm256_add_ps(sum, _mm256_mul_ps(diff, diff)); | |
| 155 | - } | |
| 156 | - | |
| 157 | - _mm256_store_ps(TmpRes, sum); | |
| 158 | - return sqrt(TmpRes[0] + TmpRes[1] + TmpRes[2] + TmpRes[3] + TmpRes[4] + | |
| 159 | - TmpRes[5] + TmpRes[6] + TmpRes[7]); | |
| 160 | -} | |
| 161 | -#endif | |
| 162 | - | |
| 163 | -#ifdef SQLITE_VEC_ENABLE_NEON | |
| 164 | -#include <arm_neon.h> | |
| 165 | - | |
| 166 | -#define PORTABLE_ALIGN32 __attribute__((aligned(32))) | |
| 167 | - | |
| 168 | -// thx https://github.com/nmslib/hnswlib/pull/299/files | |
| 169 | -static f32 l2_sqr_float_neon(const void *pVect1v, const void *pVect2v, | |
| 170 | - const void *qty_ptr) { | |
| 171 | - f32 *pVect1 = (f32 *)pVect1v; | |
| 172 | - f32 *pVect2 = (f32 *)pVect2v; | |
| 173 | - size_t qty = *((size_t *)qty_ptr); | |
| 174 | - size_t qty16 = qty >> 4; | |
| 175 | - | |
| 176 | - const f32 *pEnd1 = pVect1 + (qty16 << 4); | |
| 177 | - | |
| 178 | - float32x4_t diff, v1, v2; | |
| 179 | - float32x4_t sum0 = vdupq_n_f32(0); | |
| 180 | - float32x4_t sum1 = vdupq_n_f32(0); | |
| 181 | - float32x4_t sum2 = vdupq_n_f32(0); | |
| 182 | - float32x4_t sum3 = vdupq_n_f32(0); | |
| 183 | - | |
| 184 | - while (pVect1 < pEnd1) { | |
| 185 | - v1 = vld1q_f32(pVect1); | |
| 186 | - pVect1 += 4; | |
| 187 | - v2 = vld1q_f32(pVect2); | |
| 188 | - pVect2 += 4; | |
| 189 | - diff = vsubq_f32(v1, v2); | |
| 190 | - sum0 = vfmaq_f32(sum0, diff, diff); | |
| 191 | - | |
| 192 | - v1 = vld1q_f32(pVect1); | |
| 193 | - pVect1 += 4; | |
| 194 | - v2 = vld1q_f32(pVect2); | |
| 195 | - pVect2 += 4; | |
| 196 | - diff = vsubq_f32(v1, v2); | |
| 197 | - sum1 = vfmaq_f32(sum1, diff, diff); | |
| 198 | - | |
| 199 | - v1 = vld1q_f32(pVect1); | |
| 200 | - pVect1 += 4; | |
| 201 | - v2 = vld1q_f32(pVect2); | |
| 202 | - pVect2 += 4; | |
| 203 | - diff = vsubq_f32(v1, v2); | |
| 204 | - sum2 = vfmaq_f32(sum2, diff, diff); | |
| 205 | - | |
| 206 | - v1 = vld1q_f32(pVect1); | |
| 207 | - pVect1 += 4; | |
| 208 | - v2 = vld1q_f32(pVect2); | |
| 209 | - pVect2 += 4; | |
| 210 | - diff = vsubq_f32(v1, v2); | |
| 211 | - sum3 = vfmaq_f32(sum3, diff, diff); | |
| 212 | - } | |
| 213 | - | |
| 214 | - f32 sum_scalar = | |
| 215 | - vaddvq_f32(vaddq_f32(vaddq_f32(sum0, sum1), vaddq_f32(sum2, sum3))); | |
| 216 | - const f32 *pEnd2 = pVect1 + (qty - (qty16 << 4)); | |
| 217 | - while (pVect1 < pEnd2) { | |
| 218 | - f32 diff = *pVect1 - *pVect2; | |
| 219 | - sum_scalar += diff * diff; | |
| 220 | - pVect1++; | |
| 221 | - pVect2++; | |
| 222 | - } | |
| 223 | - | |
| 224 | - return sqrt(sum_scalar); | |
| 225 | -} | |
| 226 | - | |
| 227 | -static f32 l2_sqr_int8_neon(const void *pVect1v, const void *pVect2v, | |
| 228 | - const void *qty_ptr) { | |
| 229 | - i8 *pVect1 = (i8 *)pVect1v; | |
| 230 | - i8 *pVect2 = (i8 *)pVect2v; | |
| 231 | - size_t qty = *((size_t *)qty_ptr); | |
| 232 | - | |
| 233 | - const i8 *pEnd1 = pVect1 + qty; | |
| 234 | - i32 sum_scalar = 0; | |
| 235 | - | |
| 236 | - while (pVect1 < pEnd1 - 7) { | |
| 237 | - // loading 8 at a time | |
| 238 | - int8x8_t v1 = vld1_s8(pVect1); | |
| 239 | - int8x8_t v2 = vld1_s8(pVect2); | |
| 240 | - pVect1 += 8; | |
| 241 | - pVect2 += 8; | |
| 242 | - | |
| 243 | - // widen to protect against overflow | |
| 244 | - int16x8_t v1_wide = vmovl_s8(v1); | |
| 245 | - int16x8_t v2_wide = vmovl_s8(v2); | |
| 246 | - | |
| 247 | - int16x8_t diff = vsubq_s16(v1_wide, v2_wide); | |
| 248 | - int16x8_t squared_diff = vmulq_s16(diff, diff); | |
| 249 | - int32x4_t sum = vpaddlq_s16(squared_diff); | |
| 250 | - | |
| 251 | - sum_scalar += vgetq_lane_s32(sum, 0) + vgetq_lane_s32(sum, 1) + | |
| 252 | - vgetq_lane_s32(sum, 2) + vgetq_lane_s32(sum, 3); | |
| 253 | - } | |
| 254 | - | |
| 255 | - // handle leftovers | |
| 256 | - while (pVect1 < pEnd1) { | |
| 257 | - i16 diff = (i16)*pVect1 - (i16)*pVect2; | |
| 258 | - sum_scalar += diff * diff; | |
| 259 | - pVect1++; | |
| 260 | - pVect2++; | |
| 261 | - } | |
| 262 | - | |
| 263 | - return sqrtf(sum_scalar); | |
| 264 | -} | |
| 265 | - | |
| 266 | -static i32 l1_int8_neon(const void *pVect1v, const void *pVect2v, | |
| 267 | - const void *qty_ptr) { | |
| 268 | - i8 *pVect1 = (i8 *)pVect1v; | |
| 269 | - i8 *pVect2 = (i8 *)pVect2v; | |
| 270 | - size_t qty = *((size_t *)qty_ptr); | |
| 271 | - | |
| 272 | - const int8_t *pEnd1 = pVect1 + qty; | |
| 273 | - | |
| 274 | - int32x4_t acc1 = vdupq_n_s32(0); | |
| 275 | - int32x4_t acc2 = vdupq_n_s32(0); | |
| 276 | - int32x4_t acc3 = vdupq_n_s32(0); | |
| 277 | - int32x4_t acc4 = vdupq_n_s32(0); | |
| 278 | - | |
| 279 | - while (pVect1 < pEnd1 - 63) { | |
| 280 | - int8x16_t v1 = vld1q_s8(pVect1); | |
| 281 | - int8x16_t v2 = vld1q_s8(pVect2); | |
| 282 | - int8x16_t diff1 = vabdq_s8(v1, v2); | |
| 283 | - acc1 = vaddq_s32(acc1, vpaddlq_u16(vpaddlq_u8(diff1))); | |
| 284 | - | |
| 285 | - v1 = vld1q_s8(pVect1 + 16); | |
| 286 | - v2 = vld1q_s8(pVect2 + 16); | |
| 287 | - int8x16_t diff2 = vabdq_s8(v1, v2); | |
| 288 | - acc2 = vaddq_s32(acc2, vpaddlq_u16(vpaddlq_u8(diff2))); | |
| 289 | - | |
| 290 | - v1 = vld1q_s8(pVect1 + 32); | |
| 291 | - v2 = vld1q_s8(pVect2 + 32); | |
| 292 | - int8x16_t diff3 = vabdq_s8(v1, v2); | |
| 293 | - acc3 = vaddq_s32(acc3, vpaddlq_u16(vpaddlq_u8(diff3))); | |
| 294 | - | |
| 295 | - v1 = vld1q_s8(pVect1 + 48); | |
| 296 | - v2 = vld1q_s8(pVect2 + 48); | |
| 297 | - int8x16_t diff4 = vabdq_s8(v1, v2); | |
| 298 | - acc4 = vaddq_s32(acc4, vpaddlq_u16(vpaddlq_u8(diff4))); | |
| 299 | - | |
| 300 | - pVect1 += 64; | |
| 301 | - pVect2 += 64; | |
| 302 | - } | |
| 303 | - | |
| 304 | - while (pVect1 < pEnd1 - 15) { | |
| 305 | - int8x16_t v1 = vld1q_s8(pVect1); | |
| 306 | - int8x16_t v2 = vld1q_s8(pVect2); | |
| 307 | - int8x16_t diff = vabdq_s8(v1, v2); | |
| 308 | - acc1 = vaddq_s32(acc1, vpaddlq_u16(vpaddlq_u8(diff))); | |
| 309 | - pVect1 += 16; | |
| 310 | - pVect2 += 16; | |
| 311 | - } | |
| 312 | - | |
| 313 | - int32x4_t acc = vaddq_s32(vaddq_s32(acc1, acc2), vaddq_s32(acc3, acc4)); | |
| 314 | - | |
| 315 | - int32_t sum = 0; | |
| 316 | - while (pVect1 < pEnd1) { | |
| 317 | - int32_t diff = abs((int32_t)*pVect1 - (int32_t)*pVect2); | |
| 318 | - sum += diff; | |
| 319 | - pVect1++; | |
| 320 | - pVect2++; | |
| 321 | - } | |
| 322 | - | |
| 323 | - return vaddvq_s32(acc) + sum; | |
| 324 | -} | |
| 325 | - | |
| 326 | -static double l1_f32_neon(const void *pVect1v, const void *pVect2v, | |
| 327 | - const void *qty_ptr) { | |
| 328 | - f32 *pVect1 = (f32 *)pVect1v; | |
| 329 | - f32 *pVect2 = (f32 *)pVect2v; | |
| 330 | - size_t qty = *((size_t *)qty_ptr); | |
| 331 | - | |
| 332 | - const f32 *pEnd1 = pVect1 + qty; | |
| 333 | - float64x2_t acc = vdupq_n_f64(0); | |
| 334 | - | |
| 335 | - while (pVect1 < pEnd1 - 3) { | |
| 336 | - float32x4_t v1 = vld1q_f32(pVect1); | |
| 337 | - float32x4_t v2 = vld1q_f32(pVect2); | |
| 338 | - pVect1 += 4; | |
| 339 | - pVect2 += 4; | |
| 340 | - | |
| 341 | - // f32x4 -> f64x2 pad for overflow | |
| 342 | - float64x2_t low_diff = vabdq_f64(vcvt_f64_f32(vget_low_f32(v1)), | |
| 343 | - vcvt_f64_f32(vget_low_f32(v2))); | |
| 344 | - float64x2_t high_diff = | |
| 345 | - vabdq_f64(vcvt_high_f64_f32(v1), vcvt_high_f64_f32(v2)); | |
| 346 | - | |
| 347 | - acc = vaddq_f64(acc, vaddq_f64(low_diff, high_diff)); | |
| 348 | - } | |
| 349 | - | |
| 350 | - double sum = 0; | |
| 351 | - while (pVect1 < pEnd1) { | |
| 352 | - sum += fabs((double)*pVect1 - (double)*pVect2); | |
| 353 | - pVect1++; | |
| 354 | - pVect2++; | |
| 355 | - } | |
| 356 | - | |
| 357 | - return vaddvq_f64(acc) + sum; | |
| 358 | -} | |
| 359 | -#endif | |
| 360 | - | |
| 361 | -static f32 l2_sqr_float(const void *pVect1v, const void *pVect2v, | |
| 362 | - const void *qty_ptr) { | |
| 363 | - f32 *pVect1 = (f32 *)pVect1v; | |
| 364 | - f32 *pVect2 = (f32 *)pVect2v; | |
| 365 | - size_t qty = *((size_t *)qty_ptr); | |
| 366 | - | |
| 367 | - f32 res = 0; | |
| 368 | - for (size_t i = 0; i < qty; i++) { | |
| 369 | - f32 t = *pVect1 - *pVect2; | |
| 370 | - pVect1++; | |
| 371 | - pVect2++; | |
| 372 | - res += t * t; | |
| 373 | - } | |
| 374 | - return sqrt(res); | |
| 375 | -} | |
| 376 | - | |
| 377 | -static f32 l2_sqr_int8(const void *pA, const void *pB, const void *pD) { | |
| 378 | - i8 *a = (i8 *)pA; | |
| 379 | - i8 *b = (i8 *)pB; | |
| 380 | - size_t d = *((size_t *)pD); | |
| 381 | - | |
| 382 | - f32 res = 0; | |
| 383 | - for (size_t i = 0; i < d; i++) { | |
| 384 | - f32 t = *a - *b; | |
| 385 | - a++; | |
| 386 | - b++; | |
| 387 | - res += t * t; | |
| 388 | - } | |
| 389 | - return sqrt(res); | |
| 390 | -} | |
| 391 | - | |
| 392 | -static f32 distance_l2_sqr_float(const void *a, const void *b, const void *d) { | |
| 393 | -#ifdef SQLITE_VEC_ENABLE_NEON | |
| 394 | - if ((*(const size_t *)d) > 16) { | |
| 395 | - return l2_sqr_float_neon(a, b, d); | |
| 396 | - } | |
| 397 | -#endif | |
| 398 | -#ifdef SQLITE_VEC_ENABLE_AVX | |
| 399 | - if (((*(const size_t *)d) % 16 == 0)) { | |
| 400 | - return l2_sqr_float_avx(a, b, d); | |
| 401 | - } | |
| 402 | -#endif | |
| 403 | - return l2_sqr_float(a, b, d); | |
| 404 | -} | |
| 405 | - | |
| 406 | -static f32 distance_l2_sqr_int8(const void *a, const void *b, const void *d) { | |
| 407 | -#ifdef SQLITE_VEC_ENABLE_NEON | |
| 408 | - if ((*(const size_t *)d) > 7) { | |
| 409 | - return l2_sqr_int8_neon(a, b, d); | |
| 410 | - } | |
| 411 | -#endif | |
| 412 | - return l2_sqr_int8(a, b, d); | |
| 413 | -} | |
| 414 | - | |
| 415 | -static i32 l1_int8(const void *pA, const void *pB, const void *pD) { | |
| 416 | - i8 *a = (i8 *)pA; | |
| 417 | - i8 *b = (i8 *)pB; | |
| 418 | - size_t d = *((size_t *)pD); | |
| 419 | - | |
| 420 | - i32 res = 0; | |
| 421 | - for (size_t i = 0; i < d; i++) { | |
| 422 | - res += abs(*a - *b); | |
| 423 | - a++; | |
| 424 | - b++; | |
| 425 | - } | |
| 426 | - | |
| 427 | - return res; | |
| 428 | -} | |
| 429 | - | |
| 430 | -static i32 distance_l1_int8(const void *a, const void *b, const void *d) { | |
| 431 | -#ifdef SQLITE_VEC_ENABLE_NEON | |
| 432 | - if ((*(const size_t *)d) > 15) { | |
| 433 | - return l1_int8_neon(a, b, d); | |
| 434 | - } | |
| 435 | -#endif | |
| 436 | - return l1_int8(a, b, d); | |
| 437 | -} | |
| 438 | - | |
| 439 | -static double l1_f32(const void *pA, const void *pB, const void *pD) { | |
| 440 | - f32 *a = (f32 *)pA; | |
| 441 | - f32 *b = (f32 *)pB; | |
| 442 | - size_t d = *((size_t *)pD); | |
| 443 | - | |
| 444 | - double res = 0; | |
| 445 | - for (size_t i = 0; i < d; i++) { | |
| 446 | - res += fabs((double)*a - (double)*b); | |
| 447 | - a++; | |
| 448 | - b++; | |
| 449 | - } | |
| 450 | - | |
| 451 | - return res; | |
| 452 | -} | |
| 453 | - | |
| 454 | -static double distance_l1_f32(const void *a, const void *b, const void *d) { | |
| 455 | -#ifdef SQLITE_VEC_ENABLE_NEON | |
| 456 | - if ((*(const size_t *)d) > 3) { | |
| 457 | - return l1_f32_neon(a, b, d); | |
| 458 | - } | |
| 459 | -#endif | |
| 460 | - return l1_f32(a, b, d); | |
| 461 | -} | |
| 462 | - | |
| 463 | -static f32 distance_cosine_float(const void *pVect1v, const void *pVect2v, | |
| 464 | - const void *qty_ptr) { | |
| 465 | - f32 *pVect1 = (f32 *)pVect1v; | |
| 466 | - f32 *pVect2 = (f32 *)pVect2v; | |
| 467 | - size_t qty = *((size_t *)qty_ptr); | |
| 468 | - | |
| 469 | - f32 dot = 0; | |
| 470 | - f32 aMag = 0; | |
| 471 | - f32 bMag = 0; | |
| 472 | - for (size_t i = 0; i < qty; i++) { | |
| 473 | - dot += *pVect1 * *pVect2; | |
| 474 | - aMag += *pVect1 * *pVect1; | |
| 475 | - bMag += *pVect2 * *pVect2; | |
| 476 | - pVect1++; | |
| 477 | - pVect2++; | |
| 478 | - } | |
| 479 | - return 1 - (dot / (sqrt(aMag) * sqrt(bMag))); | |
| 480 | -} | |
| 481 | -static f32 distance_cosine_int8(const void *pA, const void *pB, | |
| 482 | - const void *pD) { | |
| 483 | - i8 *a = (i8 *)pA; | |
| 484 | - i8 *b = (i8 *)pB; | |
| 485 | - size_t d = *((size_t *)pD); | |
| 486 | - | |
| 487 | - f32 dot = 0; | |
| 488 | - f32 aMag = 0; | |
| 489 | - f32 bMag = 0; | |
| 490 | - for (size_t i = 0; i < d; i++) { | |
| 491 | - dot += *a * *b; | |
| 492 | - aMag += *a * *a; | |
| 493 | - bMag += *b * *b; | |
| 494 | - a++; | |
| 495 | - b++; | |
| 496 | - } | |
| 497 | - return 1 - (dot / (sqrt(aMag) * sqrt(bMag))); | |
| 498 | -} | |
| 499 | - | |
| 500 | -// https://github.com/facebookresearch/faiss/blob/77e2e79cd0a680adc343b9840dd865da724c579e/faiss/utils/hamming_distance/common.h#L34 | |
| 501 | -static u8 hamdist_table[256] = { | |
| 502 | - 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, | |
| 503 | - 2, 3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, | |
| 504 | - 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, | |
| 505 | - 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, | |
| 506 | - 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, | |
| 507 | - 4, 5, 5, 6, 5, 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, | |
| 508 | - 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, | |
| 509 | - 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, | |
| 510 | - 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, | |
| 511 | - 4, 5, 5, 6, 5, 6, 6, 7, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, | |
| 512 | - 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8}; | |
| 513 | - | |
| 514 | -static f32 distance_hamming_u8(u8 *a, u8 *b, size_t n) { | |
| 515 | - int same = 0; | |
| 516 | - for (unsigned long i = 0; i < n; i++) { | |
| 517 | - same += hamdist_table[a[i] ^ b[i]]; | |
| 518 | - } | |
| 519 | - return (f32)same; | |
| 520 | -} | |
| 521 | - | |
| 522 | -#ifdef _MSC_VER | |
| 523 | -#if !defined(__clang__) && (defined(_M_ARM) || defined(_M_ARM64)) | |
| 524 | -// From | |
| 525 | -// https://github.com/ngtcp2/ngtcp2/blob/b64f1e77b5e0d880b93d31f474147fae4a1d17cc/lib/ngtcp2_ringbuf.c, | |
| 526 | -// line 34-43 | |
| 527 | -static unsigned int __builtin_popcountl(unsigned int x) { | |
| 528 | - unsigned int c = 0; | |
| 529 | - for (; x; ++c) { | |
| 530 | - x &= x - 1; | |
| 531 | - } | |
| 532 | - return c; | |
| 533 | -} | |
| 534 | -#else | |
| 535 | -#include <intrin.h> | |
| 536 | -#define __builtin_popcountl __popcnt64 | |
| 537 | -#endif | |
| 538 | -#endif | |
| 539 | - | |
| 540 | -static f32 distance_hamming_u64(u64 *a, u64 *b, size_t n) { | |
| 541 | - int same = 0; | |
| 542 | - for (unsigned long i = 0; i < n; i++) { | |
| 543 | - same += __builtin_popcountl(a[i] ^ b[i]); | |
| 544 | - } | |
| 545 | - return (f32)same; | |
| 546 | -} | |
| 547 | - | |
| 548 | -/** | |
| 549 | - * @brief Calculate the hamming distance between two bitvectors. | |
| 550 | - * | |
| 551 | - * @param a - first bitvector, MUST have d dimensions | |
| 552 | - * @param b - second bitvector, MUST have d dimensions | |
| 553 | - * @param d - pointer to size_t, MUST be divisible by CHAR_BIT | |
| 554 | - * @return f32 | |
| 555 | - */ | |
| 556 | -static f32 distance_hamming(const void *a, const void *b, const void *d) { | |
| 557 | - size_t dimensions = *((size_t *)d); | |
| 558 | - | |
| 559 | - if ((dimensions % 64) == 0) { | |
| 560 | - return distance_hamming_u64((u64 *)a, (u64 *)b, dimensions / 8 / CHAR_BIT); | |
| 561 | - } | |
| 562 | - return distance_hamming_u8((u8 *)a, (u8 *)b, dimensions / CHAR_BIT); | |
| 563 | -} | |
| 564 | - | |
| 565 | -// from SQLite source: | |
| 566 | -// https://github.com/sqlite/sqlite/blob/a509a90958ddb234d1785ed7801880ccb18b497e/src/json.c#L153 | |
| 567 | -static const char vecJsonIsSpaceX[] = { | |
| 568 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 569 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 570 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 571 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 572 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 573 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 574 | - | |
| 575 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 576 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 577 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 578 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 579 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 580 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
| 581 | -}; | |
| 582 | - | |
| 583 | -#define vecJsonIsspace(x) (vecJsonIsSpaceX[(unsigned char)x]) | |
| 584 | - | |
| 585 | -typedef void (*vector_cleanup)(void *p); | |
| 586 | - | |
| 587 | -void vector_cleanup_noop(void *_) { UNUSED_PARAMETER(_); } | |
| 588 | - | |
| 589 | -#define JSON_SUBTYPE 74 | |
| 590 | - | |
| 591 | -void vtab_set_error(sqlite3_vtab *pVTab, const char *zFormat, ...) { | |
| 592 | - va_list args; | |
| 593 | - sqlite3_free(pVTab->zErrMsg); | |
| 594 | - va_start(args, zFormat); | |
| 595 | - pVTab->zErrMsg = sqlite3_vmprintf(zFormat, args); | |
| 596 | - va_end(args); | |
| 597 | -} | |
| 598 | -struct Array { | |
| 599 | - size_t element_size; | |
| 600 | - size_t length; | |
| 601 | - size_t capacity; | |
| 602 | - void *z; | |
| 603 | -}; | |
| 604 | - | |
| 605 | -/** | |
| 606 | - * @brief Initial an array with the given element size and capacity. | |
| 607 | - * | |
| 608 | - * @param array | |
| 609 | - * @param element_size | |
| 610 | - * @param init_capacity | |
| 611 | - * @return SQLITE_OK on success, error code on failure. Only error is | |
| 612 | - * SQLITE_NOMEM | |
| 613 | - */ | |
| 614 | -int array_init(struct Array *array, size_t element_size, size_t init_capacity) { | |
| 615 | - int sz = element_size * init_capacity; | |
| 616 | - void *z = sqlite3_malloc(sz); | |
| 617 | - if (!z) { | |
| 618 | - return SQLITE_NOMEM; | |
| 619 | - } | |
| 620 | - memset(z, 0, sz); | |
| 621 | - | |
| 622 | - array->element_size = element_size; | |
| 623 | - array->length = 0; | |
| 624 | - array->capacity = init_capacity; | |
| 625 | - array->z = z; | |
| 626 | - return SQLITE_OK; | |
| 627 | -} | |
| 628 | - | |
| 629 | -int array_append(struct Array *array, const void *element) { | |
| 630 | - if (array->length == array->capacity) { | |
| 631 | - size_t new_capacity = array->capacity * 2 + 100; | |
| 632 | - void *z = sqlite3_realloc64(array->z, array->element_size * new_capacity); | |
| 633 | - if (z) { | |
| 634 | - array->capacity = new_capacity; | |
| 635 | - array->z = z; | |
| 636 | - } else { | |
| 637 | - return SQLITE_NOMEM; | |
| 638 | - } | |
| 639 | - } | |
| 640 | - memcpy(&((unsigned char *)array->z)[array->length * array->element_size], | |
| 641 | - element, array->element_size); | |
| 642 | - array->length++; | |
| 643 | - return SQLITE_OK; | |
| 644 | -} | |
| 645 | - | |
| 646 | -void array_cleanup(struct Array *array) { | |
| 647 | - if (!array) | |
| 648 | - return; | |
| 649 | - array->element_size = 0; | |
| 650 | - array->length = 0; | |
| 651 | - array->capacity = 0; | |
| 652 | - sqlite3_free(array->z); | |
| 653 | - array->z = NULL; | |
| 654 | -} | |
| 655 | - | |
| 656 | -char *vector_subtype_name(int subtype) { | |
| 657 | - switch (subtype) { | |
| 658 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: | |
| 659 | - return "float32"; | |
| 660 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | |
| 661 | - return "int8"; | |
| 662 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: | |
| 663 | - return "bit"; | |
| 664 | - } | |
| 665 | - return ""; | |
| 666 | -} | |
| 667 | -char *type_name(int type) { | |
| 668 | - switch (type) { | |
| 669 | - case SQLITE_INTEGER: | |
| 670 | - return "INTEGER"; | |
| 671 | - case SQLITE_BLOB: | |
| 672 | - return "BLOB"; | |
| 673 | - case SQLITE_TEXT: | |
| 674 | - return "TEXT"; | |
| 675 | - case SQLITE_FLOAT: | |
| 676 | - return "FLOAT"; | |
| 677 | - case SQLITE_NULL: | |
| 678 | - return "NULL"; | |
| 679 | - } | |
| 680 | - return ""; | |
| 681 | -} | |
| 682 | - | |
| 683 | -typedef void (*fvec_cleanup)(f32 *vector); | |
| 684 | - | |
| 685 | -void fvec_cleanup_noop(f32 *_) { UNUSED_PARAMETER(_); } | |
| 686 | - | |
| 687 | -static int fvec_from_value(sqlite3_value *value, f32 **vector, | |
| 688 | - size_t *dimensions, fvec_cleanup *cleanup, | |
| 689 | - char **pzErr) { | |
| 690 | - int value_type = sqlite3_value_type(value); | |
| 691 | - | |
| 692 | - if (value_type == SQLITE_BLOB) { | |
| 693 | - const void *blob = sqlite3_value_blob(value); | |
| 694 | - int bytes = sqlite3_value_bytes(value); | |
| 695 | - if (bytes == 0) { | |
| 696 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | |
| 697 | - return SQLITE_ERROR; | |
| 698 | - } | |
| 699 | - if ((bytes % sizeof(f32)) != 0) { | |
| 700 | - *pzErr = sqlite3_mprintf("invalid float32 vector BLOB length. Must be " | |
| 701 | - "divisible by %d, found %d", | |
| 702 | - sizeof(f32), bytes); | |
| 703 | - return SQLITE_ERROR; | |
| 704 | - } | |
| 705 | - *vector = (f32 *)blob; | |
| 706 | - *dimensions = bytes / sizeof(f32); | |
| 707 | - *cleanup = fvec_cleanup_noop; | |
| 708 | - return SQLITE_OK; | |
| 709 | - } | |
| 710 | - | |
| 711 | - if (value_type == SQLITE_TEXT) { | |
| 712 | - const char *source = (const char *)sqlite3_value_text(value); | |
| 713 | - int source_len = sqlite3_value_bytes(value); | |
| 714 | - if (source_len == 0) { | |
| 715 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | |
| 716 | - return SQLITE_ERROR; | |
| 717 | - } | |
| 718 | - int i = 0; | |
| 719 | - | |
| 720 | - struct Array x; | |
| 721 | - int rc = array_init(&x, sizeof(f32), ceil(source_len / 2.0)); | |
| 722 | - if (rc != SQLITE_OK) { | |
| 723 | - return rc; | |
| 724 | - } | |
| 725 | - | |
| 726 | - // advance leading whitespace to first '[' | |
| 727 | - while (i < source_len) { | |
| 728 | - if (vecJsonIsspace(source[i])) { | |
| 729 | - i++; | |
| 730 | - continue; | |
| 731 | - } | |
| 732 | - if (source[i] == '[') { | |
| 733 | - break; | |
| 734 | - } | |
| 735 | - | |
| 736 | - *pzErr = sqlite3_mprintf( | |
| 737 | - "JSON array parsing error: Input does not start with '['"); | |
| 738 | - array_cleanup(&x); | |
| 739 | - return SQLITE_ERROR; | |
| 740 | - } | |
| 741 | - if (source[i] != '[') { | |
| 742 | - *pzErr = sqlite3_mprintf( | |
| 743 | - "JSON array parsing error: Input does not start with '['"); | |
| 744 | - array_cleanup(&x); | |
| 745 | - return SQLITE_ERROR; | |
| 746 | - } | |
| 747 | - int offset = i + 1; | |
| 748 | - | |
| 749 | - while (offset < source_len) { | |
| 750 | - char *ptr = (char *)&source[offset]; | |
| 751 | - char *endptr; | |
| 752 | - | |
| 753 | - errno = 0; | |
| 754 | - double result = strtod(ptr, &endptr); | |
| 755 | - if ((errno != 0 && result == 0) // some interval error? | |
| 756 | - || (errno == ERANGE && | |
| 757 | - (result == HUGE_VAL || result == -HUGE_VAL)) // too big / smalls | |
| 758 | - ) { | |
| 759 | - sqlite3_free(x.z); | |
| 760 | - *pzErr = sqlite3_mprintf("JSON parsing error"); | |
| 761 | - return SQLITE_ERROR; | |
| 762 | - } | |
| 763 | - | |
| 764 | - if (endptr == ptr) { | |
| 765 | - if (*ptr != ']') { | |
| 766 | - sqlite3_free(x.z); | |
| 767 | - *pzErr = sqlite3_mprintf("JSON parsing error"); | |
| 768 | - return SQLITE_ERROR; | |
| 769 | - } | |
| 770 | - goto done; | |
| 771 | - } | |
| 772 | - | |
| 773 | - f32 res = (f32)result; | |
| 774 | - array_append(&x, (const void *)&res); | |
| 775 | - | |
| 776 | - offset += (endptr - ptr); | |
| 777 | - while (offset < source_len) { | |
| 778 | - if (vecJsonIsspace(source[offset])) { | |
| 779 | - offset++; | |
| 780 | - continue; | |
| 781 | - } | |
| 782 | - if (source[offset] == ',') { | |
| 783 | - offset++; | |
| 784 | - continue; | |
| 785 | - } | |
| 786 | - if (source[offset] == ']') | |
| 787 | - goto done; | |
| 788 | - break; | |
| 789 | - } | |
| 790 | - } | |
| 791 | - | |
| 792 | - done: | |
| 793 | - | |
| 794 | - if (x.length > 0) { | |
| 795 | - *vector = (f32 *)x.z; | |
| 796 | - *dimensions = x.length; | |
| 797 | - *cleanup = (fvec_cleanup)sqlite3_free; | |
| 798 | - return SQLITE_OK; | |
| 799 | - } | |
| 800 | - sqlite3_free(x.z); | |
| 801 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | |
| 802 | - return SQLITE_ERROR; | |
| 803 | - } | |
| 804 | - | |
| 805 | - *pzErr = sqlite3_mprintf( | |
| 806 | - "Input must have type BLOB (compact format) or TEXT (JSON), found %s", | |
| 807 | - type_name(value_type)); | |
| 808 | - return SQLITE_ERROR; | |
| 809 | -} | |
| 810 | - | |
| 811 | -static int bitvec_from_value(sqlite3_value *value, u8 **vector, | |
| 812 | - size_t *dimensions, vector_cleanup *cleanup, | |
| 813 | - char **pzErr) { | |
| 814 | - int value_type = sqlite3_value_type(value); | |
| 815 | - if (value_type == SQLITE_BLOB) { | |
| 816 | - const void *blob = sqlite3_value_blob(value); | |
| 817 | - int bytes = sqlite3_value_bytes(value); | |
| 818 | - if (bytes == 0) { | |
| 819 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | |
| 820 | - return SQLITE_ERROR; | |
| 821 | - } | |
| 822 | - *vector = (u8 *)blob; | |
| 823 | - *dimensions = bytes * CHAR_BIT; | |
| 824 | - *cleanup = vector_cleanup_noop; | |
| 825 | - return SQLITE_OK; | |
| 826 | - } | |
| 827 | - *pzErr = sqlite3_mprintf("Unknown type for bitvector."); | |
| 828 | - return SQLITE_ERROR; | |
| 829 | -} | |
| 830 | - | |
| 831 | -static int int8_vec_from_value(sqlite3_value *value, i8 **vector, | |
| 832 | - size_t *dimensions, vector_cleanup *cleanup, | |
| 833 | - char **pzErr) { | |
| 834 | - int value_type = sqlite3_value_type(value); | |
| 835 | - if (value_type == SQLITE_BLOB) { | |
| 836 | - const void *blob = sqlite3_value_blob(value); | |
| 837 | - int bytes = sqlite3_value_bytes(value); | |
| 838 | - if (bytes == 0) { | |
| 839 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | |
| 840 | - return SQLITE_ERROR; | |
| 841 | - } | |
| 842 | - *vector = (i8 *)blob; | |
| 843 | - *dimensions = bytes; | |
| 844 | - *cleanup = vector_cleanup_noop; | |
| 845 | - return SQLITE_OK; | |
| 846 | - } | |
| 847 | - | |
| 848 | - if (value_type == SQLITE_TEXT) { | |
| 849 | - const char *source = (const char *)sqlite3_value_text(value); | |
| 850 | - int source_len = sqlite3_value_bytes(value); | |
| 851 | - int i = 0; | |
| 852 | - | |
| 853 | - if (source_len == 0) { | |
| 854 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | |
| 855 | - return SQLITE_ERROR; | |
| 856 | - } | |
| 857 | - | |
| 858 | - struct Array x; | |
| 859 | - int rc = array_init(&x, sizeof(i8), ceil(source_len / 2.0)); | |
| 860 | - if (rc != SQLITE_OK) { | |
| 861 | - return rc; | |
| 862 | - } | |
| 863 | - | |
| 864 | - // advance leading whitespace to first '[' | |
| 865 | - while (i < source_len) { | |
| 866 | - if (vecJsonIsspace(source[i])) { | |
| 867 | - i++; | |
| 868 | - continue; | |
| 869 | - } | |
| 870 | - if (source[i] == '[') { | |
| 871 | - break; | |
| 872 | - } | |
| 873 | - | |
| 874 | - *pzErr = sqlite3_mprintf( | |
| 875 | - "JSON array parsing error: Input does not start with '['"); | |
| 876 | - array_cleanup(&x); | |
| 877 | - return SQLITE_ERROR; | |
| 878 | - } | |
| 879 | - if (source[i] != '[') { | |
| 880 | - *pzErr = sqlite3_mprintf( | |
| 881 | - "JSON array parsing error: Input does not start with '['"); | |
| 882 | - array_cleanup(&x); | |
| 883 | - return SQLITE_ERROR; | |
| 884 | - } | |
| 885 | - int offset = i + 1; | |
| 886 | - | |
| 887 | - while (offset < source_len) { | |
| 888 | - char *ptr = (char *)&source[offset]; | |
| 889 | - char *endptr; | |
| 890 | - | |
| 891 | - errno = 0; | |
| 892 | - long result = strtol(ptr, &endptr, 10); | |
| 893 | - if ((errno != 0 && result == 0) || | |
| 894 | - (errno == ERANGE && (result == LONG_MAX || result == LONG_MIN))) { | |
| 895 | - sqlite3_free(x.z); | |
| 896 | - *pzErr = sqlite3_mprintf("JSON parsing error"); | |
| 897 | - return SQLITE_ERROR; | |
| 898 | - } | |
| 899 | - | |
| 900 | - if (endptr == ptr) { | |
| 901 | - if (*ptr != ']') { | |
| 902 | - sqlite3_free(x.z); | |
| 903 | - *pzErr = sqlite3_mprintf("JSON parsing error"); | |
| 904 | - return SQLITE_ERROR; | |
| 905 | - } | |
| 906 | - goto done; | |
| 907 | - } | |
| 908 | - | |
| 909 | - if (result < INT8_MIN || result > INT8_MAX) { | |
| 910 | - sqlite3_free(x.z); | |
| 911 | - *pzErr = | |
| 912 | - sqlite3_mprintf("JSON parsing error: value out of range for int8"); | |
| 913 | - return SQLITE_ERROR; | |
| 914 | - } | |
| 915 | - | |
| 916 | - i8 res = (i8)result; | |
| 917 | - array_append(&x, (const void *)&res); | |
| 918 | - | |
| 919 | - offset += (endptr - ptr); | |
| 920 | - while (offset < source_len) { | |
| 921 | - if (vecJsonIsspace(source[offset])) { | |
| 922 | - offset++; | |
| 923 | - continue; | |
| 924 | - } | |
| 925 | - if (source[offset] == ',') { | |
| 926 | - offset++; | |
| 927 | - continue; | |
| 928 | - } | |
| 929 | - if (source[offset] == ']') | |
| 930 | - goto done; | |
| 931 | - break; | |
| 932 | - } | |
| 933 | - } | |
| 934 | - | |
| 935 | - done: | |
| 936 | - | |
| 937 | - if (x.length > 0) { | |
| 938 | - *vector = (i8 *)x.z; | |
| 939 | - *dimensions = x.length; | |
| 940 | - *cleanup = (vector_cleanup)sqlite3_free; | |
| 941 | - return SQLITE_OK; | |
| 942 | - } | |
| 943 | - sqlite3_free(x.z); | |
| 944 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | |
| 945 | - return SQLITE_ERROR; | |
| 946 | - } | |
| 947 | - | |
| 948 | - *pzErr = sqlite3_mprintf("Unknown type for int8 vector."); | |
| 949 | - return SQLITE_ERROR; | |
| 950 | -} | |
| 951 | - | |
| 952 | -/** | |
| 953 | - * @brief Extract a vector from a sqlite3_value. Can be a float32, int8, or bit | |
| 954 | - * vector. | |
| 955 | - * | |
| 956 | - * @param value: the sqlite3_value to read from. | |
| 957 | - * @param vector: Output pointer to vector data. | |
| 958 | - * @param dimensions: Output number of dimensions | |
| 959 | - * @param dimensions: Output vector element type | |
| 960 | - * @param cleanup | |
| 961 | - * @param pzErrorMessage | |
| 962 | - * @return int SQLITE_OK on success, error code otherwise | |
| 963 | - */ | |
| 964 | -int vector_from_value(sqlite3_value *value, void **vector, size_t *dimensions, | |
| 965 | - enum VectorElementType *element_type, | |
| 966 | - vector_cleanup *cleanup, char **pzErrorMessage) { | |
| 967 | - int subtype = sqlite3_value_subtype(value); | |
| 968 | - if (!subtype || (subtype == SQLITE_VEC_ELEMENT_TYPE_FLOAT32) || | |
| 969 | - (subtype == JSON_SUBTYPE)) { | |
| 970 | - int rc = fvec_from_value(value, (f32 **)vector, dimensions, | |
| 971 | - (fvec_cleanup *)cleanup, pzErrorMessage); | |
| 972 | - if (rc == SQLITE_OK) { | |
| 973 | - *element_type = SQLITE_VEC_ELEMENT_TYPE_FLOAT32; | |
| 974 | - } | |
| 975 | - return rc; | |
| 976 | - } | |
| 977 | - | |
| 978 | - if (subtype == SQLITE_VEC_ELEMENT_TYPE_BIT) { | |
| 979 | - int rc = bitvec_from_value(value, (u8 **)vector, dimensions, cleanup, | |
| 980 | - pzErrorMessage); | |
| 981 | - if (rc == SQLITE_OK) { | |
| 982 | - *element_type = SQLITE_VEC_ELEMENT_TYPE_BIT; | |
| 983 | - } | |
| 984 | - return rc; | |
| 985 | - } | |
| 986 | - if (subtype == SQLITE_VEC_ELEMENT_TYPE_INT8) { | |
| 987 | - int rc = int8_vec_from_value(value, (i8 **)vector, dimensions, cleanup, | |
| 988 | - pzErrorMessage); | |
| 989 | - if (rc == SQLITE_OK) { | |
| 990 | - *element_type = SQLITE_VEC_ELEMENT_TYPE_INT8; | |
| 991 | - } | |
| 992 | - return rc; | |
| 993 | - } | |
| 994 | - *pzErrorMessage = sqlite3_mprintf("Unknown subtype: %d", subtype); | |
| 995 | - return SQLITE_ERROR; | |
| 996 | -} | |
| 997 | - | |
| 998 | -int ensure_vector_match(sqlite3_value *aValue, sqlite3_value *bValue, void **a, | |
| 999 | - void **b, enum VectorElementType *element_type, | |
| 1000 | - size_t *dimensions, vector_cleanup *outACleanup, | |
| 1001 | - vector_cleanup *outBCleanup, char **outError) { | |
| 1002 | - int rc; | |
| 1003 | - enum VectorElementType aType, bType; | |
| 1004 | - size_t aDims, bDims; | |
| 1005 | - char *error = NULL; | |
| 1006 | - vector_cleanup aCleanup, bCleanup; | |
| 1007 | - | |
| 1008 | - rc = vector_from_value(aValue, a, &aDims, &aType, &aCleanup, &error); | |
| 1009 | - if (rc != SQLITE_OK) { | |
| 1010 | - *outError = sqlite3_mprintf("Error reading 1st vector: %s", error); | |
| 1011 | - sqlite3_free(error); | |
| 1012 | - return SQLITE_ERROR; | |
| 1013 | - } | |
| 1014 | - | |
| 1015 | - rc = vector_from_value(bValue, b, &bDims, &bType, &bCleanup, &error); | |
| 1016 | - if (rc != SQLITE_OK) { | |
| 1017 | - *outError = sqlite3_mprintf("Error reading 2nd vector: %s", error); | |
| 1018 | - sqlite3_free(error); | |
| 1019 | - aCleanup(a); | |
| 1020 | - return SQLITE_ERROR; | |
| 1021 | - } | |
| 1022 | - | |
| 1023 | - if (aType != bType) { | |
| 1024 | - *outError = | |
| 1025 | - sqlite3_mprintf("Vector type mistmatch. First vector has type %s, " | |
| 1026 | - "while the second has type %s.", | |
| 1027 | - vector_subtype_name(aType), vector_subtype_name(bType)); | |
| 1028 | - aCleanup(*a); | |
| 1029 | - bCleanup(*b); | |
| 1030 | - return SQLITE_ERROR; | |
| 1031 | - } | |
| 1032 | - if (aDims != bDims) { | |
| 1033 | - *outError = sqlite3_mprintf( | |
| 1034 | - "Vector dimension mistmatch. First vector has %ld dimensions, " | |
| 1035 | - "while the second has %ld dimensions.", | |
| 1036 | - aDims, bDims); | |
| 1037 | - aCleanup(*a); | |
| 1038 | - bCleanup(*b); | |
| 1039 | - return SQLITE_ERROR; | |
| 1040 | - } | |
| 1041 | - *element_type = aType; | |
| 1042 | - *dimensions = aDims; | |
| 1043 | - *outACleanup = aCleanup; | |
| 1044 | - *outBCleanup = bCleanup; | |
| 1045 | - return SQLITE_OK; | |
| 1046 | -} | |
| 1047 | - | |
| 1048 | -int _cmp(const void *a, const void *b) { return (*(i64 *)a - *(i64 *)b); } | |
| 1049 | - | |
| 1050 | -struct VecNpyFile { | |
| 1051 | - char *path; | |
| 1052 | - size_t pathLength; | |
| 1053 | -}; | |
| 1054 | -#define SQLITE_VEC_NPY_FILE_NAME "vec0-npy-file" | |
| 1055 | - | |
| 1056 | -#ifndef SQLITE_VEC_OMIT_FS | |
| 1057 | -static void vec_npy_file(sqlite3_context *context, int argc, | |
| 1058 | - sqlite3_value **argv) { | |
| 1059 | - assert(argc == 1); | |
| 1060 | - char *path = (char *)sqlite3_value_text(argv[0]); | |
| 1061 | - size_t pathLength = sqlite3_value_bytes(argv[0]); | |
| 1062 | - struct VecNpyFile *f; | |
| 1063 | - | |
| 1064 | - f = sqlite3_malloc(sizeof(*f)); | |
| 1065 | - if (!f) { | |
| 1066 | - sqlite3_result_error_nomem(context); | |
| 1067 | - return; | |
| 1068 | - } | |
| 1069 | - memset(f, 0, sizeof(*f)); | |
| 1070 | - | |
| 1071 | - f->path = path; | |
| 1072 | - f->pathLength = pathLength; | |
| 1073 | - sqlite3_result_pointer(context, f, SQLITE_VEC_NPY_FILE_NAME, sqlite3_free); | |
| 1074 | -} | |
| 1075 | -#endif | |
| 1076 | - | |
| 1077 | -#pragma region scalar functions | |
| 1078 | -static void vec_f32(sqlite3_context *context, int argc, sqlite3_value **argv) { | |
| 1079 | - assert(argc == 1); | |
| 1080 | - int rc; | |
| 1081 | - f32 *vector = NULL; | |
| 1082 | - size_t dimensions; | |
| 1083 | - fvec_cleanup cleanup; | |
| 1084 | - char *errmsg; | |
| 1085 | - rc = fvec_from_value(argv[0], &vector, &dimensions, &cleanup, &errmsg); | |
| 1086 | - if (rc != SQLITE_OK) { | |
| 1087 | - sqlite3_result_error(context, errmsg, -1); | |
| 1088 | - sqlite3_free(errmsg); | |
| 1089 | - return; | |
| 1090 | - } | |
| 1091 | - sqlite3_result_blob(context, vector, dimensions * sizeof(f32), | |
| 1092 | - (void (*)(void *))cleanup); | |
| 1093 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_FLOAT32); | |
| 1094 | -} | |
| 1095 | - | |
| 1096 | -static void vec_bit(sqlite3_context *context, int argc, sqlite3_value **argv) { | |
| 1097 | - assert(argc == 1); | |
| 1098 | - int rc; | |
| 1099 | - u8 *vector; | |
| 1100 | - size_t dimensions; | |
| 1101 | - vector_cleanup cleanup; | |
| 1102 | - char *errmsg; | |
| 1103 | - rc = bitvec_from_value(argv[0], &vector, &dimensions, &cleanup, &errmsg); | |
| 1104 | - if (rc != SQLITE_OK) { | |
| 1105 | - sqlite3_result_error(context, errmsg, -1); | |
| 1106 | - sqlite3_free(errmsg); | |
| 1107 | - return; | |
| 1108 | - } | |
| 1109 | - sqlite3_result_blob(context, vector, dimensions / CHAR_BIT, SQLITE_TRANSIENT); | |
| 1110 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_BIT); | |
| 1111 | - cleanup(vector); | |
| 1112 | -} | |
| 1113 | -static void vec_int8(sqlite3_context *context, int argc, sqlite3_value **argv) { | |
| 1114 | - assert(argc == 1); | |
| 1115 | - int rc; | |
| 1116 | - i8 *vector; | |
| 1117 | - size_t dimensions; | |
| 1118 | - vector_cleanup cleanup; | |
| 1119 | - char *errmsg; | |
| 1120 | - rc = int8_vec_from_value(argv[0], &vector, &dimensions, &cleanup, &errmsg); | |
| 1121 | - if (rc != SQLITE_OK) { | |
| 1122 | - sqlite3_result_error(context, errmsg, -1); | |
| 1123 | - sqlite3_free(errmsg); | |
| 1124 | - return; | |
| 1125 | - } | |
| 1126 | - sqlite3_result_blob(context, vector, dimensions, SQLITE_TRANSIENT); | |
| 1127 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_INT8); | |
| 1128 | - cleanup(vector); | |
| 1129 | -} | |
| 1130 | - | |
| 1131 | -static void vec_length(sqlite3_context *context, int argc, | |
| 1132 | - sqlite3_value **argv) { | |
| 1133 | - assert(argc == 1); | |
| 1134 | - int rc; | |
| 1135 | - void *vector; | |
| 1136 | - size_t dimensions; | |
| 1137 | - vector_cleanup cleanup; | |
| 1138 | - char *errmsg; | |
| 1139 | - enum VectorElementType elementType; | |
| 1140 | - rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, &cleanup, | |
| 1141 | - &errmsg); | |
| 1142 | - if (rc != SQLITE_OK) { | |
| 1143 | - sqlite3_result_error(context, errmsg, -1); | |
| 1144 | - sqlite3_free(errmsg); | |
| 1145 | - return; | |
| 1146 | - } | |
| 1147 | - sqlite3_result_int64(context, dimensions); | |
| 1148 | - cleanup(vector); | |
| 1149 | -} | |
| 1150 | - | |
| 1151 | -static void vec_distance_cosine(sqlite3_context *context, int argc, | |
| 1152 | - sqlite3_value **argv) { | |
| 1153 | - assert(argc == 2); | |
| 1154 | - int rc; | |
| 1155 | - void *a = NULL, *b = NULL; | |
| 1156 | - size_t dimensions; | |
| 1157 | - vector_cleanup aCleanup, bCleanup; | |
| 1158 | - char *error; | |
| 1159 | - enum VectorElementType elementType; | |
| 1160 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | |
| 1161 | - &aCleanup, &bCleanup, &error); | |
| 1162 | - if (rc != SQLITE_OK) { | |
| 1163 | - sqlite3_result_error(context, error, -1); | |
| 1164 | - sqlite3_free(error); | |
| 1165 | - return; | |
| 1166 | - } | |
| 1167 | - | |
| 1168 | - switch (elementType) { | |
| 1169 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 1170 | - sqlite3_result_error( | |
| 1171 | - context, "Cannot calculate cosine distance between two bitvectors.", | |
| 1172 | - -1); | |
| 1173 | - goto finish; | |
| 1174 | - } | |
| 1175 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 1176 | - f32 result = distance_cosine_float(a, b, &dimensions); | |
| 1177 | - sqlite3_result_double(context, result); | |
| 1178 | - goto finish; | |
| 1179 | - } | |
| 1180 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | |
| 1181 | - f32 result = distance_cosine_int8(a, b, &dimensions); | |
| 1182 | - sqlite3_result_double(context, result); | |
| 1183 | - goto finish; | |
| 1184 | - } | |
| 1185 | - } | |
| 1186 | - | |
| 1187 | -finish: | |
| 1188 | - aCleanup(a); | |
| 1189 | - bCleanup(b); | |
| 1190 | - return; | |
| 1191 | -} | |
| 1192 | - | |
| 1193 | -static void vec_distance_l2(sqlite3_context *context, int argc, | |
| 1194 | - sqlite3_value **argv) { | |
| 1195 | - assert(argc == 2); | |
| 1196 | - int rc; | |
| 1197 | - void *a = NULL, *b = NULL; | |
| 1198 | - size_t dimensions; | |
| 1199 | - vector_cleanup aCleanup, bCleanup; | |
| 1200 | - char *error; | |
| 1201 | - enum VectorElementType elementType; | |
| 1202 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | |
| 1203 | - &aCleanup, &bCleanup, &error); | |
| 1204 | - if (rc != SQLITE_OK) { | |
| 1205 | - sqlite3_result_error(context, error, -1); | |
| 1206 | - sqlite3_free(error); | |
| 1207 | - return; | |
| 1208 | - } | |
| 1209 | - | |
| 1210 | - switch (elementType) { | |
| 1211 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 1212 | - sqlite3_result_error( | |
| 1213 | - context, "Cannot calculate L2 distance between two bitvectors.", -1); | |
| 1214 | - goto finish; | |
| 1215 | - } | |
| 1216 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 1217 | - f32 result = distance_l2_sqr_float(a, b, &dimensions); | |
| 1218 | - sqlite3_result_double(context, result); | |
| 1219 | - goto finish; | |
| 1220 | - } | |
| 1221 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | |
| 1222 | - f32 result = distance_l2_sqr_int8(a, b, &dimensions); | |
| 1223 | - sqlite3_result_double(context, result); | |
| 1224 | - goto finish; | |
| 1225 | - } | |
| 1226 | - } | |
| 1227 | - | |
| 1228 | -finish: | |
| 1229 | - aCleanup(a); | |
| 1230 | - bCleanup(b); | |
| 1231 | - return; | |
| 1232 | -} | |
| 1233 | - | |
| 1234 | -static void vec_distance_l1(sqlite3_context *context, int argc, | |
| 1235 | - sqlite3_value **argv) { | |
| 1236 | - assert(argc == 2); | |
| 1237 | - int rc; | |
| 1238 | - void *a, *b; | |
| 1239 | - size_t dimensions; | |
| 1240 | - vector_cleanup aCleanup, bCleanup; | |
| 1241 | - char *error; | |
| 1242 | - enum VectorElementType elementType; | |
| 1243 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | |
| 1244 | - &aCleanup, &bCleanup, &error); | |
| 1245 | - if (rc != SQLITE_OK) { | |
| 1246 | - sqlite3_result_error(context, error, -1); | |
| 1247 | - sqlite3_free(error); | |
| 1248 | - return; | |
| 1249 | - } | |
| 1250 | - | |
| 1251 | - switch (elementType) { | |
| 1252 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 1253 | - sqlite3_result_error( | |
| 1254 | - context, "Cannot calculate L1 distance between two bitvectors.", -1); | |
| 1255 | - goto finish; | |
| 1256 | - } | |
| 1257 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 1258 | - double result = distance_l1_f32(a, b, &dimensions); | |
| 1259 | - sqlite3_result_double(context, result); | |
| 1260 | - goto finish; | |
| 1261 | - } | |
| 1262 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | |
| 1263 | - i64 result = distance_l1_int8(a, b, &dimensions); | |
| 1264 | - sqlite3_result_int(context, result); | |
| 1265 | - goto finish; | |
| 1266 | - } | |
| 1267 | - } | |
| 1268 | - | |
| 1269 | -finish: | |
| 1270 | - aCleanup(a); | |
| 1271 | - bCleanup(b); | |
| 1272 | - return; | |
| 1273 | -} | |
| 1274 | - | |
| 1275 | -static void vec_distance_hamming(sqlite3_context *context, int argc, | |
| 1276 | - sqlite3_value **argv) { | |
| 1277 | - assert(argc == 2); | |
| 1278 | - int rc; | |
| 1279 | - void *a = NULL, *b = NULL; | |
| 1280 | - size_t dimensions; | |
| 1281 | - vector_cleanup aCleanup, bCleanup; | |
| 1282 | - char *error; | |
| 1283 | - enum VectorElementType elementType; | |
| 1284 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | |
| 1285 | - &aCleanup, &bCleanup, &error); | |
| 1286 | - if (rc != SQLITE_OK) { | |
| 1287 | - sqlite3_result_error(context, error, -1); | |
| 1288 | - sqlite3_free(error); | |
| 1289 | - return; | |
| 1290 | - } | |
| 1291 | - | |
| 1292 | - switch (elementType) { | |
| 1293 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 1294 | - sqlite3_result_double(context, distance_hamming(a, b, &dimensions)); | |
| 1295 | - goto finish; | |
| 1296 | - } | |
| 1297 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 1298 | - sqlite3_result_error( | |
| 1299 | - context, | |
| 1300 | - "Cannot calculate hamming distance between two float32 vectors.", -1); | |
| 1301 | - goto finish; | |
| 1302 | - } | |
| 1303 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | |
| 1304 | - sqlite3_result_error( | |
| 1305 | - context, "Cannot calculate hamming distance between two int8 vectors.", | |
| 1306 | - -1); | |
| 1307 | - goto finish; | |
| 1308 | - } | |
| 1309 | - } | |
| 1310 | - | |
| 1311 | -finish: | |
| 1312 | - aCleanup(a); | |
| 1313 | - bCleanup(b); | |
| 1314 | - return; | |
| 1315 | -} | |
| 1316 | - | |
| 1317 | -char *vec_type_name(enum VectorElementType elementType) { | |
| 1318 | - switch (elementType) { | |
| 1319 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: | |
| 1320 | - return "float32"; | |
| 1321 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | |
| 1322 | - return "int8"; | |
| 1323 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: | |
| 1324 | - return "bit"; | |
| 1325 | - } | |
| 1326 | - return ""; | |
| 1327 | -} | |
| 1328 | - | |
| 1329 | -static void vec_type(sqlite3_context *context, int argc, sqlite3_value **argv) { | |
| 1330 | - assert(argc == 1); | |
| 1331 | - void *vector; | |
| 1332 | - size_t dimensions; | |
| 1333 | - vector_cleanup cleanup; | |
| 1334 | - char *pzError; | |
| 1335 | - enum VectorElementType elementType; | |
| 1336 | - int rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, | |
| 1337 | - &cleanup, &pzError); | |
| 1338 | - if (rc != SQLITE_OK) { | |
| 1339 | - sqlite3_result_error(context, pzError, -1); | |
| 1340 | - sqlite3_free(pzError); | |
| 1341 | - return; | |
| 1342 | - } | |
| 1343 | - sqlite3_result_text(context, vec_type_name(elementType), -1, SQLITE_STATIC); | |
| 1344 | - cleanup(vector); | |
| 1345 | -} | |
| 1346 | -static void vec_quantize_binary(sqlite3_context *context, int argc, | |
| 1347 | - sqlite3_value **argv) { | |
| 1348 | - assert(argc == 1); | |
| 1349 | - void *vector; | |
| 1350 | - size_t dimensions; | |
| 1351 | - vector_cleanup vectorCleanup; | |
| 1352 | - char *pzError; | |
| 1353 | - enum VectorElementType elementType; | |
| 1354 | - int rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, | |
| 1355 | - &vectorCleanup, &pzError); | |
| 1356 | - if (rc != SQLITE_OK) { | |
| 1357 | - sqlite3_result_error(context, pzError, -1); | |
| 1358 | - sqlite3_free(pzError); | |
| 1359 | - return; | |
| 1360 | - } | |
| 1361 | - | |
| 1362 | - if (dimensions <= 0) { | |
| 1363 | - sqlite3_result_error(context, "Zero length vectors are not supported.", -1); | |
| 1364 | - goto cleanup; | |
| 1365 | - return; | |
| 1366 | - } | |
| 1367 | - if ((dimensions % CHAR_BIT) != 0) { | |
| 1368 | - sqlite3_result_error( | |
| 1369 | - context, | |
| 1370 | - "Binary quantization requires vectors with a length divisible by 8", | |
| 1371 | - -1); | |
| 1372 | - goto cleanup; | |
| 1373 | - return; | |
| 1374 | - } | |
| 1375 | - | |
| 1376 | - int sz = dimensions / CHAR_BIT; | |
| 1377 | - u8 *out = sqlite3_malloc(sz); | |
| 1378 | - if (!out) { | |
| 1379 | - sqlite3_result_error_code(context, SQLITE_NOMEM); | |
| 1380 | - goto cleanup; | |
| 1381 | - return; | |
| 1382 | - } | |
| 1383 | - memset(out, 0, sz); | |
| 1384 | - | |
| 1385 | - switch (elementType) { | |
| 1386 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 1387 | - | |
| 1388 | - for (size_t i = 0; i < dimensions; i++) { | |
| 1389 | - int res = ((f32 *)vector)[i] > 0.0; | |
| 1390 | - out[i / 8] |= (res << (i % 8)); | |
| 1391 | - } | |
| 1392 | - break; | |
| 1393 | - } | |
| 1394 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | |
| 1395 | - for (size_t i = 0; i < dimensions; i++) { | |
| 1396 | - int res = ((i8 *)vector)[i] > 0; | |
| 1397 | - out[i / 8] |= (res << (i % 8)); | |
| 1398 | - } | |
| 1399 | - break; | |
| 1400 | - } | |
| 1401 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 1402 | - sqlite3_result_error(context, | |
| 1403 | - "Can only binary quantize float or int8 vectors", -1); | |
| 1404 | - sqlite3_free(out); | |
| 1405 | - return; | |
| 1406 | - } | |
| 1407 | - } | |
| 1408 | - sqlite3_result_blob(context, out, sz, sqlite3_free); | |
| 1409 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_BIT); | |
| 1410 | - | |
| 1411 | -cleanup: | |
| 1412 | - vectorCleanup(vector); | |
| 1413 | -} | |
| 1414 | - | |
| 1415 | -static void vec_quantize_int8(sqlite3_context *context, int argc, | |
| 1416 | - sqlite3_value **argv) { | |
| 1417 | - assert(argc == 2); | |
| 1418 | - f32 *srcVector; | |
| 1419 | - size_t dimensions; | |
| 1420 | - fvec_cleanup srcCleanup; | |
| 1421 | - char *err; | |
| 1422 | - i8 *out = NULL; | |
| 1423 | - int rc = fvec_from_value(argv[0], &srcVector, &dimensions, &srcCleanup, &err); | |
| 1424 | - if (rc != SQLITE_OK) { | |
| 1425 | - sqlite3_result_error(context, err, -1); | |
| 1426 | - sqlite3_free(err); | |
| 1427 | - return; | |
| 1428 | - } | |
| 1429 | - | |
| 1430 | - int sz = dimensions * sizeof(i8); | |
| 1431 | - out = sqlite3_malloc(sz); | |
| 1432 | - if (!out) { | |
| 1433 | - sqlite3_result_error_nomem(context); | |
| 1434 | - goto cleanup; | |
| 1435 | - } | |
| 1436 | - memset(out, 0, sz); | |
| 1437 | - | |
| 1438 | - if ((sqlite3_value_type(argv[1]) != SQLITE_TEXT) || | |
| 1439 | - (sqlite3_value_bytes(argv[1]) != strlen("unit")) || | |
| 1440 | - (sqlite3_stricmp((const char *)sqlite3_value_text(argv[1]), "unit") != | |
| 1441 | - 0)) { | |
| 1442 | - sqlite3_result_error( | |
| 1443 | - context, "2nd argument to vec_quantize_int8() must be 'unit'.", -1); | |
| 1444 | - sqlite3_free(out); | |
| 1445 | - goto cleanup; | |
| 1446 | - } | |
| 1447 | - f32 step = (1.0 - (-1.0)) / 255; | |
| 1448 | - for (size_t i = 0; i < dimensions; i++) { | |
| 1449 | - out[i] = ((srcVector[i] - (-1.0)) / step) - 128; | |
| 1450 | - } | |
| 1451 | - | |
| 1452 | - sqlite3_result_blob(context, out, dimensions * sizeof(i8), sqlite3_free); | |
| 1453 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_INT8); | |
| 1454 | - | |
| 1455 | -cleanup: | |
| 1456 | - srcCleanup(srcVector); | |
| 1457 | -} | |
| 1458 | - | |
| 1459 | -static void vec_add(sqlite3_context *context, int argc, sqlite3_value **argv) { | |
| 1460 | - assert(argc == 2); | |
| 1461 | - int rc; | |
| 1462 | - void *a = NULL, *b = NULL; | |
| 1463 | - size_t dimensions; | |
| 1464 | - vector_cleanup aCleanup, bCleanup; | |
| 1465 | - char *error; | |
| 1466 | - enum VectorElementType elementType; | |
| 1467 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | |
| 1468 | - &aCleanup, &bCleanup, &error); | |
| 1469 | - if (rc != SQLITE_OK) { | |
| 1470 | - sqlite3_result_error(context, error, -1); | |
| 1471 | - sqlite3_free(error); | |
| 1472 | - return; | |
| 1473 | - } | |
| 1474 | - | |
| 1475 | - switch (elementType) { | |
| 1476 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 1477 | - sqlite3_result_error(context, "Cannot add two bitvectors together.", -1); | |
| 1478 | - goto finish; | |
| 1479 | - } | |
| 1480 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 1481 | - size_t outSize = dimensions * sizeof(f32); | |
| 1482 | - f32 *out = sqlite3_malloc(outSize); | |
| 1483 | - if (!out) { | |
| 1484 | - sqlite3_result_error_nomem(context); | |
| 1485 | - goto finish; | |
| 1486 | - } | |
| 1487 | - memset(out, 0, outSize); | |
| 1488 | - for (size_t i = 0; i < dimensions; i++) { | |
| 1489 | - out[i] = ((f32 *)a)[i] + ((f32 *)b)[i]; | |
| 1490 | - } | |
| 1491 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | |
| 1492 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_FLOAT32); | |
| 1493 | - goto finish; | |
| 1494 | - } | |
| 1495 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | |
| 1496 | - size_t outSize = dimensions * sizeof(i8); | |
| 1497 | - i8 *out = sqlite3_malloc(outSize); | |
| 1498 | - if (!out) { | |
| 1499 | - sqlite3_result_error_nomem(context); | |
| 1500 | - goto finish; | |
| 1501 | - } | |
| 1502 | - memset(out, 0, outSize); | |
| 1503 | - for (size_t i = 0; i < dimensions; i++) { | |
| 1504 | - out[i] = ((i8 *)a)[i] + ((i8 *)b)[i]; | |
| 1505 | - } | |
| 1506 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | |
| 1507 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_INT8); | |
| 1508 | - goto finish; | |
| 1509 | - } | |
| 1510 | - } | |
| 1511 | -finish: | |
| 1512 | - aCleanup(a); | |
| 1513 | - bCleanup(b); | |
| 1514 | - return; | |
| 1515 | -} | |
| 1516 | -static void vec_sub(sqlite3_context *context, int argc, sqlite3_value **argv) { | |
| 1517 | - assert(argc == 2); | |
| 1518 | - int rc; | |
| 1519 | - void *a = NULL, *b = NULL; | |
| 1520 | - size_t dimensions; | |
| 1521 | - vector_cleanup aCleanup, bCleanup; | |
| 1522 | - char *error; | |
| 1523 | - enum VectorElementType elementType; | |
| 1524 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | |
| 1525 | - &aCleanup, &bCleanup, &error); | |
| 1526 | - if (rc != SQLITE_OK) { | |
| 1527 | - sqlite3_result_error(context, error, -1); | |
| 1528 | - sqlite3_free(error); | |
| 1529 | - return; | |
| 1530 | - } | |
| 1531 | - | |
| 1532 | - switch (elementType) { | |
| 1533 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 1534 | - sqlite3_result_error(context, "Cannot subtract two bitvectors together.", | |
| 1535 | - -1); | |
| 1536 | - goto finish; | |
| 1537 | - } | |
| 1538 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 1539 | - size_t outSize = dimensions * sizeof(f32); | |
| 1540 | - f32 *out = sqlite3_malloc(outSize); | |
| 1541 | - if (!out) { | |
| 1542 | - sqlite3_result_error_nomem(context); | |
| 1543 | - goto finish; | |
| 1544 | - } | |
| 1545 | - memset(out, 0, outSize); | |
| 1546 | - for (size_t i = 0; i < dimensions; i++) { | |
| 1547 | - out[i] = ((f32 *)a)[i] - ((f32 *)b)[i]; | |
| 1548 | - } | |
| 1549 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | |
| 1550 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_FLOAT32); | |
| 1551 | - goto finish; | |
| 1552 | - } | |
| 1553 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | |
| 1554 | - size_t outSize = dimensions * sizeof(i8); | |
| 1555 | - i8 *out = sqlite3_malloc(outSize); | |
| 1556 | - if (!out) { | |
| 1557 | - sqlite3_result_error_nomem(context); | |
| 1558 | - goto finish; | |
| 1559 | - } | |
| 1560 | - memset(out, 0, outSize); | |
| 1561 | - for (size_t i = 0; i < dimensions; i++) { | |
| 1562 | - out[i] = ((i8 *)a)[i] - ((i8 *)b)[i]; | |
| 1563 | - } | |
| 1564 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | |
| 1565 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_INT8); | |
| 1566 | - goto finish; | |
| 1567 | - } | |
| 1568 | - } | |
| 1569 | -finish: | |
| 1570 | - aCleanup(a); | |
| 1571 | - bCleanup(b); | |
| 1572 | - return; | |
| 1573 | -} | |
| 1574 | -static void vec_slice(sqlite3_context *context, int argc, | |
| 1575 | - sqlite3_value **argv) { | |
| 1576 | - assert(argc == 3); | |
| 1577 | - | |
| 1578 | - void *vector; | |
| 1579 | - size_t dimensions; | |
| 1580 | - vector_cleanup cleanup; | |
| 1581 | - char *err; | |
| 1582 | - enum VectorElementType elementType; | |
| 1583 | - | |
| 1584 | - int rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, | |
| 1585 | - &cleanup, &err); | |
| 1586 | - if (rc != SQLITE_OK) { | |
| 1587 | - sqlite3_result_error(context, err, -1); | |
| 1588 | - sqlite3_free(err); | |
| 1589 | - return; | |
| 1590 | - } | |
| 1591 | - | |
| 1592 | - int start = sqlite3_value_int(argv[1]); | |
| 1593 | - int end = sqlite3_value_int(argv[2]); | |
| 1594 | - | |
| 1595 | - if (start < 0) { | |
| 1596 | - sqlite3_result_error(context, | |
| 1597 | - "slice 'start' index must be a postive number.", -1); | |
| 1598 | - goto done; | |
| 1599 | - } | |
| 1600 | - if (end < 0) { | |
| 1601 | - sqlite3_result_error(context, "slice 'end' index must be a postive number.", | |
| 1602 | - -1); | |
| 1603 | - goto done; | |
| 1604 | - } | |
| 1605 | - if (((size_t)start) > dimensions) { | |
| 1606 | - sqlite3_result_error( | |
| 1607 | - context, "slice 'start' index is greater than the number of dimensions", | |
| 1608 | - -1); | |
| 1609 | - goto done; | |
| 1610 | - } | |
| 1611 | - if (((size_t)end) > dimensions) { | |
| 1612 | - sqlite3_result_error( | |
| 1613 | - context, "slice 'end' index is greater than the number of dimensions", | |
| 1614 | - -1); | |
| 1615 | - goto done; | |
| 1616 | - } | |
| 1617 | - if (start > end) { | |
| 1618 | - sqlite3_result_error(context, | |
| 1619 | - "slice 'start' index is greater than 'end' index", -1); | |
| 1620 | - goto done; | |
| 1621 | - } | |
| 1622 | - if (start == end) { | |
| 1623 | - sqlite3_result_error(context, | |
| 1624 | - "slice 'start' index is equal to the 'end' index, " | |
| 1625 | - "vectors must have non-zero length", | |
| 1626 | - -1); | |
| 1627 | - goto done; | |
| 1628 | - } | |
| 1629 | - size_t n = end - start; | |
| 1630 | - | |
| 1631 | - switch (elementType) { | |
| 1632 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 1633 | - int outSize = n * sizeof(f32); | |
| 1634 | - f32 *out = sqlite3_malloc(outSize); | |
| 1635 | - if (!out) { | |
| 1636 | - sqlite3_result_error_nomem(context); | |
| 1637 | - goto done; | |
| 1638 | - } | |
| 1639 | - memset(out, 0, outSize); | |
| 1640 | - for (size_t i = 0; i < n; i++) { | |
| 1641 | - out[i] = ((f32 *)vector)[start + i]; | |
| 1642 | - } | |
| 1643 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | |
| 1644 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_FLOAT32); | |
| 1645 | - goto done; | |
| 1646 | - } | |
| 1647 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | |
| 1648 | - int outSize = n * sizeof(i8); | |
| 1649 | - i8 *out = sqlite3_malloc(outSize); | |
| 1650 | - if (!out) { | |
| 1651 | - sqlite3_result_error_nomem(context); | |
| 1652 | - return; | |
| 1653 | - } | |
| 1654 | - memset(out, 0, outSize); | |
| 1655 | - for (size_t i = 0; i < n; i++) { | |
| 1656 | - out[i] = ((i8 *)vector)[start + i]; | |
| 1657 | - } | |
| 1658 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | |
| 1659 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_INT8); | |
| 1660 | - goto done; | |
| 1661 | - } | |
| 1662 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 1663 | - if ((start % CHAR_BIT) != 0) { | |
| 1664 | - sqlite3_result_error(context, "start index must be divisible by 8.", -1); | |
| 1665 | - goto done; | |
| 1666 | - } | |
| 1667 | - if ((end % CHAR_BIT) != 0) { | |
| 1668 | - sqlite3_result_error(context, "end index must be divisible by 8.", -1); | |
| 1669 | - goto done; | |
| 1670 | - } | |
| 1671 | - int outSize = n / CHAR_BIT; | |
| 1672 | - u8 *out = sqlite3_malloc(outSize); | |
| 1673 | - if (!out) { | |
| 1674 | - sqlite3_result_error_nomem(context); | |
| 1675 | - return; | |
| 1676 | - } | |
| 1677 | - memset(out, 0, outSize); | |
| 1678 | - for (size_t i = 0; i < n / CHAR_BIT; i++) { | |
| 1679 | - out[i] = ((u8 *)vector)[(start / CHAR_BIT) + i]; | |
| 1680 | - } | |
| 1681 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | |
| 1682 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_BIT); | |
| 1683 | - goto done; | |
| 1684 | - } | |
| 1685 | - } | |
| 1686 | -done: | |
| 1687 | - cleanup(vector); | |
| 1688 | -} | |
| 1689 | - | |
| 1690 | -static void vec_to_json(sqlite3_context *context, int argc, | |
| 1691 | - sqlite3_value **argv) { | |
| 1692 | - assert(argc == 1); | |
| 1693 | - void *vector; | |
| 1694 | - size_t dimensions; | |
| 1695 | - vector_cleanup cleanup; | |
| 1696 | - char *err; | |
| 1697 | - enum VectorElementType elementType; | |
| 1698 | - | |
| 1699 | - int rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, | |
| 1700 | - &cleanup, &err); | |
| 1701 | - if (rc != SQLITE_OK) { | |
| 1702 | - sqlite3_result_error(context, err, -1); | |
| 1703 | - sqlite3_free(err); | |
| 1704 | - return; | |
| 1705 | - } | |
| 1706 | - | |
| 1707 | - sqlite3_str *str = sqlite3_str_new(sqlite3_context_db_handle(context)); | |
| 1708 | - sqlite3_str_appendall(str, "["); | |
| 1709 | - for (size_t i = 0; i < dimensions; i++) { | |
| 1710 | - if (i != 0) { | |
| 1711 | - sqlite3_str_appendall(str, ","); | |
| 1712 | - } | |
| 1713 | - if (elementType == SQLITE_VEC_ELEMENT_TYPE_FLOAT32) { | |
| 1714 | - f32 value = ((f32 *)vector)[i]; | |
| 1715 | - if (isnan(value)) { | |
| 1716 | - sqlite3_str_appendall(str, "null"); | |
| 1717 | - } else { | |
| 1718 | - sqlite3_str_appendf(str, "%f", value); | |
| 1719 | - } | |
| 1720 | - | |
| 1721 | - } else if (elementType == SQLITE_VEC_ELEMENT_TYPE_INT8) { | |
| 1722 | - sqlite3_str_appendf(str, "%d", ((i8 *)vector)[i]); | |
| 1723 | - } else if (elementType == SQLITE_VEC_ELEMENT_TYPE_BIT) { | |
| 1724 | - u8 b = (((u8 *)vector)[i / 8] >> (i % CHAR_BIT)) & 1; | |
| 1725 | - sqlite3_str_appendf(str, "%d", b); | |
| 1726 | - } | |
| 1727 | - } | |
| 1728 | - sqlite3_str_appendall(str, "]"); | |
| 1729 | - int len = sqlite3_str_length(str); | |
| 1730 | - char *s = sqlite3_str_finish(str); | |
| 1731 | - if (s) { | |
| 1732 | - sqlite3_result_text(context, s, len, sqlite3_free); | |
| 1733 | - sqlite3_result_subtype(context, JSON_SUBTYPE); | |
| 1734 | - } else { | |
| 1735 | - sqlite3_result_error_nomem(context); | |
| 1736 | - } | |
| 1737 | - cleanup(vector); | |
| 1738 | -} | |
| 1739 | - | |
| 1740 | -static void vec_normalize(sqlite3_context *context, int argc, | |
| 1741 | - sqlite3_value **argv) { | |
| 1742 | - assert(argc == 1); | |
| 1743 | - void *vector; | |
| 1744 | - size_t dimensions; | |
| 1745 | - vector_cleanup cleanup; | |
| 1746 | - char *err; | |
| 1747 | - enum VectorElementType elementType; | |
| 1748 | - | |
| 1749 | - int rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, | |
| 1750 | - &cleanup, &err); | |
| 1751 | - if (rc != SQLITE_OK) { | |
| 1752 | - sqlite3_result_error(context, err, -1); | |
| 1753 | - sqlite3_free(err); | |
| 1754 | - return; | |
| 1755 | - } | |
| 1756 | - | |
| 1757 | - if (elementType != SQLITE_VEC_ELEMENT_TYPE_FLOAT32) { | |
| 1758 | - sqlite3_result_error( | |
| 1759 | - context, "only float32 vectors are supported when normalizing", -1); | |
| 1760 | - cleanup(vector); | |
| 1761 | - return; | |
| 1762 | - } | |
| 1763 | - | |
| 1764 | - int outSize = dimensions * sizeof(f32); | |
| 1765 | - f32 *out = sqlite3_malloc(outSize); | |
| 1766 | - if (!out) { | |
| 1767 | - cleanup(vector); | |
| 1768 | - sqlite3_result_error_code(context, SQLITE_NOMEM); | |
| 1769 | - return; | |
| 1770 | - } | |
| 1771 | - memset(out, 0, outSize); | |
| 1772 | - | |
| 1773 | - f32 *v = (f32 *)vector; | |
| 1774 | - | |
| 1775 | - f32 norm = 0; | |
| 1776 | - for (size_t i = 0; i < dimensions; i++) { | |
| 1777 | - norm += v[i] * v[i]; | |
| 1778 | - } | |
| 1779 | - norm = sqrt(norm); | |
| 1780 | - for (size_t i = 0; i < dimensions; i++) { | |
| 1781 | - out[i] = v[i] / norm; | |
| 1782 | - } | |
| 1783 | - | |
| 1784 | - sqlite3_result_blob(context, out, dimensions * sizeof(f32), sqlite3_free); | |
| 1785 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_FLOAT32); | |
| 1786 | - cleanup(vector); | |
| 1787 | -} | |
| 1788 | - | |
| 1789 | -static void _static_text_func(sqlite3_context *context, int argc, | |
| 1790 | - sqlite3_value **argv) { | |
| 1791 | - UNUSED_PARAMETER(argc); | |
| 1792 | - UNUSED_PARAMETER(argv); | |
| 1793 | - sqlite3_result_text(context, sqlite3_user_data(context), -1, SQLITE_STATIC); | |
| 1794 | -} | |
| 1795 | - | |
| 1796 | -#pragma endregion | |
| 1797 | - | |
| 1798 | -enum Vec0TokenType { | |
| 1799 | - TOKEN_TYPE_IDENTIFIER, | |
| 1800 | - TOKEN_TYPE_DIGIT, | |
| 1801 | - TOKEN_TYPE_LBRACKET, | |
| 1802 | - TOKEN_TYPE_RBRACKET, | |
| 1803 | - TOKEN_TYPE_PLUS, | |
| 1804 | - TOKEN_TYPE_EQ, | |
| 1805 | -}; | |
| 1806 | -struct Vec0Token { | |
| 1807 | - enum Vec0TokenType token_type; | |
| 1808 | - char *start; | |
| 1809 | - char *end; | |
| 1810 | -}; | |
| 1811 | - | |
| 1812 | -int is_alpha(char x) { | |
| 1813 | - return (x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z'); | |
| 1814 | -} | |
| 1815 | -int is_digit(char x) { return (x >= '0' && x <= '9'); } | |
| 1816 | -int is_whitespace(char x) { | |
| 1817 | - return x == ' ' || x == '\t' || x == '\n' || x == '\r'; | |
| 1818 | -} | |
| 1819 | - | |
| 1820 | -#define VEC0_TOKEN_RESULT_EOF 1 | |
| 1821 | -#define VEC0_TOKEN_RESULT_SOME 2 | |
| 1822 | -#define VEC0_TOKEN_RESULT_ERROR 3 | |
| 1823 | - | |
| 1824 | -int vec0_token_next(char *start, char *end, struct Vec0Token *out) { | |
| 1825 | - char *ptr = start; | |
| 1826 | - while (ptr < end) { | |
| 1827 | - char curr = *ptr; | |
| 1828 | - if (is_whitespace(curr)) { | |
| 1829 | - ptr++; | |
| 1830 | - continue; | |
| 1831 | - } else if (curr == '+') { | |
| 1832 | - ptr++; | |
| 1833 | - out->start = ptr; | |
| 1834 | - out->end = ptr; | |
| 1835 | - out->token_type = TOKEN_TYPE_PLUS; | |
| 1836 | - return VEC0_TOKEN_RESULT_SOME; | |
| 1837 | - } else if (curr == '[') { | |
| 1838 | - ptr++; | |
| 1839 | - out->start = ptr; | |
| 1840 | - out->end = ptr; | |
| 1841 | - out->token_type = TOKEN_TYPE_LBRACKET; | |
| 1842 | - return VEC0_TOKEN_RESULT_SOME; | |
| 1843 | - } else if (curr == ']') { | |
| 1844 | - ptr++; | |
| 1845 | - out->start = ptr; | |
| 1846 | - out->end = ptr; | |
| 1847 | - out->token_type = TOKEN_TYPE_RBRACKET; | |
| 1848 | - return VEC0_TOKEN_RESULT_SOME; | |
| 1849 | - } else if (curr == '=') { | |
| 1850 | - ptr++; | |
| 1851 | - out->start = ptr; | |
| 1852 | - out->end = ptr; | |
| 1853 | - out->token_type = TOKEN_TYPE_EQ; | |
| 1854 | - return VEC0_TOKEN_RESULT_SOME; | |
| 1855 | - } else if (is_alpha(curr)) { | |
| 1856 | - char *start = ptr; | |
| 1857 | - while (ptr < end && (is_alpha(*ptr) || is_digit(*ptr) || *ptr == '_')) { | |
| 1858 | - ptr++; | |
| 1859 | - } | |
| 1860 | - out->start = start; | |
| 1861 | - out->end = ptr; | |
| 1862 | - out->token_type = TOKEN_TYPE_IDENTIFIER; | |
| 1863 | - return VEC0_TOKEN_RESULT_SOME; | |
| 1864 | - } else if (is_digit(curr)) { | |
| 1865 | - char *start = ptr; | |
| 1866 | - while (ptr < end && (is_digit(*ptr))) { | |
| 1867 | - ptr++; | |
| 1868 | - } | |
| 1869 | - out->start = start; | |
| 1870 | - out->end = ptr; | |
| 1871 | - out->token_type = TOKEN_TYPE_DIGIT; | |
| 1872 | - return VEC0_TOKEN_RESULT_SOME; | |
| 1873 | - } else { | |
| 1874 | - return VEC0_TOKEN_RESULT_ERROR; | |
| 1875 | - } | |
| 1876 | - } | |
| 1877 | - return VEC0_TOKEN_RESULT_EOF; | |
| 1878 | -} | |
| 1879 | - | |
| 1880 | -struct Vec0Scanner { | |
| 1881 | - char *start; | |
| 1882 | - char *end; | |
| 1883 | - char *ptr; | |
| 1884 | -}; | |
| 1885 | - | |
| 1886 | -void vec0_scanner_init(struct Vec0Scanner *scanner, const char *source, | |
| 1887 | - int source_length) { | |
| 1888 | - scanner->start = (char *)source; | |
| 1889 | - scanner->end = (char *)source + source_length; | |
| 1890 | - scanner->ptr = (char *)source; | |
| 1891 | -} | |
| 1892 | -int vec0_scanner_next(struct Vec0Scanner *scanner, struct Vec0Token *out) { | |
| 1893 | - int rc = vec0_token_next(scanner->start, scanner->end, out); | |
| 1894 | - if (rc == VEC0_TOKEN_RESULT_SOME) { | |
| 1895 | - scanner->start = out->end; | |
| 1896 | - } | |
| 1897 | - return rc; | |
| 1898 | -} | |
| 1899 | - | |
| 1900 | -int vec0_parse_table_option(const char *source, int source_length, | |
| 1901 | - char **out_key, int *out_key_length, | |
| 1902 | - char **out_value, int *out_value_length) { | |
| 1903 | - int rc; | |
| 1904 | - struct Vec0Scanner scanner; | |
| 1905 | - struct Vec0Token token; | |
| 1906 | - char *key; | |
| 1907 | - char *value; | |
| 1908 | - int keyLength, valueLength; | |
| 1909 | - | |
| 1910 | - vec0_scanner_init(&scanner, source, source_length); | |
| 1911 | - | |
| 1912 | - rc = vec0_scanner_next(&scanner, &token); | |
| 1913 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 1914 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 1915 | - return SQLITE_EMPTY; | |
| 1916 | - } | |
| 1917 | - key = token.start; | |
| 1918 | - keyLength = token.end - token.start; | |
| 1919 | - | |
| 1920 | - rc = vec0_scanner_next(&scanner, &token); | |
| 1921 | - if (rc != VEC0_TOKEN_RESULT_SOME && token.token_type != TOKEN_TYPE_EQ) { | |
| 1922 | - return SQLITE_EMPTY; | |
| 1923 | - } | |
| 1924 | - | |
| 1925 | - rc = vec0_scanner_next(&scanner, &token); | |
| 1926 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 1927 | - !((token.token_type == TOKEN_TYPE_IDENTIFIER) || | |
| 1928 | - (token.token_type == TOKEN_TYPE_DIGIT))) { | |
| 1929 | - return SQLITE_ERROR; | |
| 1930 | - } | |
| 1931 | - value = token.start; | |
| 1932 | - valueLength = token.end - token.start; | |
| 1933 | - | |
| 1934 | - rc = vec0_scanner_next(&scanner, &token); | |
| 1935 | - if (rc == VEC0_TOKEN_RESULT_EOF) { | |
| 1936 | - *out_key = key; | |
| 1937 | - *out_key_length = keyLength; | |
| 1938 | - *out_value = value; | |
| 1939 | - *out_value_length = valueLength; | |
| 1940 | - return SQLITE_OK; | |
| 1941 | - } | |
| 1942 | - return SQLITE_ERROR; | |
| 1943 | -} | |
| 1944 | -/** | |
| 1945 | - * @brief Parse an argv[i] entry of a vec0 virtual table definition, and see if | |
| 1946 | - * it's a PARTITION KEY definition. | |
| 1947 | - * | |
| 1948 | - * @param source: argv[i] source string | |
| 1949 | - * @param source_length: length of the source string | |
| 1950 | - * @param out_column_name: If it is a partition key, the output column name. Same lifetime | |
| 1951 | - * as source, points to specific char * | |
| 1952 | - * @param out_column_name_length: Length of out_column_name in bytes | |
| 1953 | - * @param out_column_type: SQLITE_TEXT or SQLITE_INTEGER. | |
| 1954 | - * @return int: SQLITE_EMPTY if not a PK, SQLITE_OK if it is. | |
| 1955 | - */ | |
| 1956 | -int vec0_parse_partition_key_definition(const char *source, int source_length, | |
| 1957 | - char **out_column_name, | |
| 1958 | - int *out_column_name_length, | |
| 1959 | - int *out_column_type) { | |
| 1960 | - struct Vec0Scanner scanner; | |
| 1961 | - struct Vec0Token token; | |
| 1962 | - char *column_name; | |
| 1963 | - int column_name_length; | |
| 1964 | - int column_type; | |
| 1965 | - vec0_scanner_init(&scanner, source, source_length); | |
| 1966 | - | |
| 1967 | - // Check first token is identifier, will be the column name | |
| 1968 | - int rc = vec0_scanner_next(&scanner, &token); | |
| 1969 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 1970 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 1971 | - return SQLITE_EMPTY; | |
| 1972 | - } | |
| 1973 | - | |
| 1974 | - column_name = token.start; | |
| 1975 | - column_name_length = token.end - token.start; | |
| 1976 | - | |
| 1977 | - // Check the next token matches "text" or "integer", as column type | |
| 1978 | - rc = vec0_scanner_next(&scanner, &token); | |
| 1979 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 1980 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 1981 | - return SQLITE_EMPTY; | |
| 1982 | - } | |
| 1983 | - if (sqlite3_strnicmp(token.start, "text", token.end - token.start) == 0) { | |
| 1984 | - column_type = SQLITE_TEXT; | |
| 1985 | - } else if (sqlite3_strnicmp(token.start, "int", token.end - token.start) == | |
| 1986 | - 0 || | |
| 1987 | - sqlite3_strnicmp(token.start, "integer", | |
| 1988 | - token.end - token.start) == 0) { | |
| 1989 | - column_type = SQLITE_INTEGER; | |
| 1990 | - } else { | |
| 1991 | - return SQLITE_EMPTY; | |
| 1992 | - } | |
| 1993 | - | |
| 1994 | - // Check the next token is identifier and matches "partition" | |
| 1995 | - rc = vec0_scanner_next(&scanner, &token); | |
| 1996 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 1997 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 1998 | - return SQLITE_EMPTY; | |
| 1999 | - } | |
| 2000 | - if (sqlite3_strnicmp(token.start, "partition", token.end - token.start) != 0) { | |
| 2001 | - return SQLITE_EMPTY; | |
| 2002 | - } | |
| 2003 | - | |
| 2004 | - // Check the next token is identifier and matches "key" | |
| 2005 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2006 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 2007 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2008 | - return SQLITE_EMPTY; | |
| 2009 | - } | |
| 2010 | - if (sqlite3_strnicmp(token.start, "key", token.end - token.start) != 0) { | |
| 2011 | - return SQLITE_EMPTY; | |
| 2012 | - } | |
| 2013 | - | |
| 2014 | - *out_column_name = column_name; | |
| 2015 | - *out_column_name_length = column_name_length; | |
| 2016 | - *out_column_type = column_type; | |
| 2017 | - | |
| 2018 | - return SQLITE_OK; | |
| 2019 | -} | |
| 2020 | - | |
| 2021 | -/** | |
| 2022 | - * @brief Parse an argv[i] entry of a vec0 virtual table definition, and see if | |
| 2023 | - * it's an auxiliar column definition, ie `+[name] [type]` like `+contents text` | |
| 2024 | - * | |
| 2025 | - * @param source: argv[i] source string | |
| 2026 | - * @param source_length: length of the source string | |
| 2027 | - * @param out_column_name: If it is a partition key, the output column name. Same lifetime | |
| 2028 | - * as source, points to specific char * | |
| 2029 | - * @param out_column_name_length: Length of out_column_name in bytes | |
| 2030 | - * @param out_column_type: SQLITE_TEXT, SQLITE_INTEGER, SQLITE_FLOAT, or SQLITE_BLOB. | |
| 2031 | - * @return int: SQLITE_EMPTY if not an aux column, SQLITE_OK if it is. | |
| 2032 | - */ | |
| 2033 | -int vec0_parse_auxiliary_column_definition(const char *source, int source_length, | |
| 2034 | - char **out_column_name, | |
| 2035 | - int *out_column_name_length, | |
| 2036 | - int *out_column_type) { | |
| 2037 | - struct Vec0Scanner scanner; | |
| 2038 | - struct Vec0Token token; | |
| 2039 | - char *column_name; | |
| 2040 | - int column_name_length; | |
| 2041 | - int column_type; | |
| 2042 | - vec0_scanner_init(&scanner, source, source_length); | |
| 2043 | - | |
| 2044 | - // Check first token is '+', which denotes aux columns | |
| 2045 | - int rc = vec0_scanner_next(&scanner, &token); | |
| 2046 | - if (rc != VEC0_TOKEN_RESULT_SOME || | |
| 2047 | - token.token_type != TOKEN_TYPE_PLUS) { | |
| 2048 | - return SQLITE_EMPTY; | |
| 2049 | - } | |
| 2050 | - | |
| 2051 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2052 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 2053 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2054 | - return SQLITE_EMPTY; | |
| 2055 | - } | |
| 2056 | - | |
| 2057 | - column_name = token.start; | |
| 2058 | - column_name_length = token.end - token.start; | |
| 2059 | - | |
| 2060 | - // Check the next token matches "text" or "integer", as column type | |
| 2061 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2062 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 2063 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2064 | - return SQLITE_EMPTY; | |
| 2065 | - } | |
| 2066 | - if (sqlite3_strnicmp(token.start, "text", token.end - token.start) == 0) { | |
| 2067 | - column_type = SQLITE_TEXT; | |
| 2068 | - } else if (sqlite3_strnicmp(token.start, "int", token.end - token.start) == | |
| 2069 | - 0 || | |
| 2070 | - sqlite3_strnicmp(token.start, "integer", | |
| 2071 | - token.end - token.start) == 0) { | |
| 2072 | - column_type = SQLITE_INTEGER; | |
| 2073 | - } else if (sqlite3_strnicmp(token.start, "float", token.end - token.start) == | |
| 2074 | - 0 || | |
| 2075 | - sqlite3_strnicmp(token.start, "double", | |
| 2076 | - token.end - token.start) == 0) { | |
| 2077 | - column_type = SQLITE_FLOAT; | |
| 2078 | - } else if (sqlite3_strnicmp(token.start, "blob", token.end - token.start) ==0) { | |
| 2079 | - column_type = SQLITE_BLOB; | |
| 2080 | - } else { | |
| 2081 | - return SQLITE_EMPTY; | |
| 2082 | - } | |
| 2083 | - | |
| 2084 | - *out_column_name = column_name; | |
| 2085 | - *out_column_name_length = column_name_length; | |
| 2086 | - *out_column_type = column_type; | |
| 2087 | - | |
| 2088 | - return SQLITE_OK; | |
| 2089 | -} | |
| 2090 | - | |
| 2091 | -typedef enum { | |
| 2092 | - VEC0_METADATA_COLUMN_KIND_BOOLEAN, | |
| 2093 | - VEC0_METADATA_COLUMN_KIND_INTEGER, | |
| 2094 | - VEC0_METADATA_COLUMN_KIND_FLOAT, | |
| 2095 | - VEC0_METADATA_COLUMN_KIND_TEXT, | |
| 2096 | - // future: blob, date, datetime | |
| 2097 | -} vec0_metadata_column_kind; | |
| 2098 | - | |
| 2099 | -/** | |
| 2100 | - * @brief Parse an argv[i] entry of a vec0 virtual table definition, and see if | |
| 2101 | - * it's an metadata column definition, ie `[name] [type]` like `is_released boolean` | |
| 2102 | - * | |
| 2103 | - * @param source: argv[i] source string | |
| 2104 | - * @param source_length: length of the source string | |
| 2105 | - * @param out_column_name: If it is a metadata column, the output column name. Same lifetime | |
| 2106 | - * as source, points to specific char * | |
| 2107 | - * @param out_column_name_length: Length of out_column_name in bytes | |
| 2108 | - * @param out_column_type: one of vec0_metadata_column_kind | |
| 2109 | - * @return int: SQLITE_EMPTY if not an metadata column, SQLITE_OK if it is. | |
| 2110 | - */ | |
| 2111 | -int vec0_parse_metadata_column_definition(const char *source, int source_length, | |
| 2112 | - char **out_column_name, | |
| 2113 | - int *out_column_name_length, | |
| 2114 | - vec0_metadata_column_kind *out_column_type) { | |
| 2115 | - struct Vec0Scanner scanner; | |
| 2116 | - struct Vec0Token token; | |
| 2117 | - char *column_name; | |
| 2118 | - int column_name_length; | |
| 2119 | - vec0_metadata_column_kind column_type; | |
| 2120 | - int rc; | |
| 2121 | - vec0_scanner_init(&scanner, source, source_length); | |
| 2122 | - | |
| 2123 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2124 | - if (rc != VEC0_TOKEN_RESULT_SOME || | |
| 2125 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2126 | - return SQLITE_EMPTY; | |
| 2127 | - } | |
| 2128 | - | |
| 2129 | - column_name = token.start; | |
| 2130 | - column_name_length = token.end - token.start; | |
| 2131 | - | |
| 2132 | - // Check the next token matches a valid metadata type | |
| 2133 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2134 | - if (rc != VEC0_TOKEN_RESULT_SOME || | |
| 2135 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2136 | - return SQLITE_EMPTY; | |
| 2137 | - } | |
| 2138 | - char * t = token.start; | |
| 2139 | - int n = token.end - token.start; | |
| 2140 | - if (sqlite3_strnicmp(t, "boolean", n) == 0 || sqlite3_strnicmp(t, "bool", n) == 0) { | |
| 2141 | - column_type = VEC0_METADATA_COLUMN_KIND_BOOLEAN; | |
| 2142 | - }else if (sqlite3_strnicmp(t, "int64", n) == 0 || sqlite3_strnicmp(t, "integer64", n) == 0 || sqlite3_strnicmp(t, "integer", n) == 0 || sqlite3_strnicmp(t, "int", n) == 0) { | |
| 2143 | - column_type = VEC0_METADATA_COLUMN_KIND_INTEGER; | |
| 2144 | - }else if (sqlite3_strnicmp(t, "float", n) == 0 || sqlite3_strnicmp(t, "double", n) == 0 || sqlite3_strnicmp(t, "float64", n) == 0 || sqlite3_strnicmp(t, "f64", n) == 0) { | |
| 2145 | - column_type = VEC0_METADATA_COLUMN_KIND_FLOAT; | |
| 2146 | - } else if (sqlite3_strnicmp(t, "text", n) == 0) { | |
| 2147 | - column_type = VEC0_METADATA_COLUMN_KIND_TEXT; | |
| 2148 | - } else { | |
| 2149 | - return SQLITE_EMPTY; | |
| 2150 | - } | |
| 2151 | - | |
| 2152 | - *out_column_name = column_name; | |
| 2153 | - *out_column_name_length = column_name_length; | |
| 2154 | - *out_column_type = column_type; | |
| 2155 | - | |
| 2156 | - return SQLITE_OK; | |
| 2157 | -} | |
| 2158 | - | |
| 2159 | -/** | |
| 2160 | - * @brief Parse an argv[i] entry of a vec0 virtual table definition, and see if | |
| 2161 | - * it's a PRIMARY KEY definition. | |
| 2162 | - * | |
| 2163 | - * @param source: argv[i] source string | |
| 2164 | - * @param source_length: length of the source string | |
| 2165 | - * @param out_column_name: If it is a PK, the output column name. Same lifetime | |
| 2166 | - * as source, points to specific char * | |
| 2167 | - * @param out_column_name_length: Length of out_column_name in bytes | |
| 2168 | - * @param out_column_type: SQLITE_TEXT or SQLITE_INTEGER. | |
| 2169 | - * @return int: SQLITE_EMPTY if not a PK, SQLITE_OK if it is. | |
| 2170 | - */ | |
| 2171 | -int vec0_parse_primary_key_definition(const char *source, int source_length, | |
| 2172 | - char **out_column_name, | |
| 2173 | - int *out_column_name_length, | |
| 2174 | - int *out_column_type) { | |
| 2175 | - struct Vec0Scanner scanner; | |
| 2176 | - struct Vec0Token token; | |
| 2177 | - char *column_name; | |
| 2178 | - int column_name_length; | |
| 2179 | - int column_type; | |
| 2180 | - vec0_scanner_init(&scanner, source, source_length); | |
| 2181 | - | |
| 2182 | - // Check first token is identifier, will be the column name | |
| 2183 | - int rc = vec0_scanner_next(&scanner, &token); | |
| 2184 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 2185 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2186 | - return SQLITE_EMPTY; | |
| 2187 | - } | |
| 2188 | - | |
| 2189 | - column_name = token.start; | |
| 2190 | - column_name_length = token.end - token.start; | |
| 2191 | - | |
| 2192 | - // Check the next token matches "text" or "integer", as column type | |
| 2193 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2194 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 2195 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2196 | - return SQLITE_EMPTY; | |
| 2197 | - } | |
| 2198 | - if (sqlite3_strnicmp(token.start, "text", token.end - token.start) == 0) { | |
| 2199 | - column_type = SQLITE_TEXT; | |
| 2200 | - } else if (sqlite3_strnicmp(token.start, "int", token.end - token.start) == | |
| 2201 | - 0 || | |
| 2202 | - sqlite3_strnicmp(token.start, "integer", | |
| 2203 | - token.end - token.start) == 0) { | |
| 2204 | - column_type = SQLITE_INTEGER; | |
| 2205 | - } else { | |
| 2206 | - return SQLITE_EMPTY; | |
| 2207 | - } | |
| 2208 | - | |
| 2209 | - // Check the next token is identifier and matches "primary" | |
| 2210 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2211 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 2212 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2213 | - return SQLITE_EMPTY; | |
| 2214 | - } | |
| 2215 | - if (sqlite3_strnicmp(token.start, "primary", token.end - token.start) != 0) { | |
| 2216 | - return SQLITE_EMPTY; | |
| 2217 | - } | |
| 2218 | - | |
| 2219 | - // Check the next token is identifier and matches "key" | |
| 2220 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2221 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 2222 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2223 | - return SQLITE_EMPTY; | |
| 2224 | - } | |
| 2225 | - if (sqlite3_strnicmp(token.start, "key", token.end - token.start) != 0) { | |
| 2226 | - return SQLITE_EMPTY; | |
| 2227 | - } | |
| 2228 | - | |
| 2229 | - *out_column_name = column_name; | |
| 2230 | - *out_column_name_length = column_name_length; | |
| 2231 | - *out_column_type = column_type; | |
| 2232 | - | |
| 2233 | - return SQLITE_OK; | |
| 2234 | -} | |
| 2235 | - | |
| 2236 | -enum Vec0DistanceMetrics { | |
| 2237 | - VEC0_DISTANCE_METRIC_L2 = 1, | |
| 2238 | - VEC0_DISTANCE_METRIC_COSINE = 2, | |
| 2239 | - VEC0_DISTANCE_METRIC_L1 = 3, | |
| 2240 | -}; | |
| 2241 | - | |
| 2242 | -struct VectorColumnDefinition { | |
| 2243 | - char *name; | |
| 2244 | - int name_length; | |
| 2245 | - size_t dimensions; | |
| 2246 | - enum VectorElementType element_type; | |
| 2247 | - enum Vec0DistanceMetrics distance_metric; | |
| 2248 | -}; | |
| 2249 | - | |
| 2250 | -struct Vec0PartitionColumnDefinition { | |
| 2251 | - int type; | |
| 2252 | - char * name; | |
| 2253 | - int name_length; | |
| 2254 | -}; | |
| 2255 | - | |
| 2256 | -struct Vec0AuxiliaryColumnDefinition { | |
| 2257 | - int type; | |
| 2258 | - char * name; | |
| 2259 | - int name_length; | |
| 2260 | -}; | |
| 2261 | -struct Vec0MetadataColumnDefinition { | |
| 2262 | - vec0_metadata_column_kind kind; | |
| 2263 | - char * name; | |
| 2264 | - int name_length; | |
| 2265 | -}; | |
| 2266 | - | |
| 2267 | -size_t vector_byte_size(enum VectorElementType element_type, | |
| 2268 | - size_t dimensions) { | |
| 2269 | - switch (element_type) { | |
| 2270 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: | |
| 2271 | - return dimensions * sizeof(f32); | |
| 2272 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | |
| 2273 | - return dimensions * sizeof(i8); | |
| 2274 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: | |
| 2275 | - return dimensions / CHAR_BIT; | |
| 2276 | - } | |
| 2277 | - return 0; | |
| 2278 | -} | |
| 2279 | - | |
| 2280 | -size_t vector_column_byte_size(struct VectorColumnDefinition column) { | |
| 2281 | - return vector_byte_size(column.element_type, column.dimensions); | |
| 2282 | -} | |
| 2283 | - | |
| 2284 | -/** | |
| 2285 | - * @brief Parse an vec0 vtab argv[i] column definition and see if | |
| 2286 | - * it's a vector column defintion, ex `contents_embedding float[768]`. | |
| 2287 | - * | |
| 2288 | - * @param source vec0 argv[i] item | |
| 2289 | - * @param source_length length of source in bytes | |
| 2290 | - * @param outColumn Output the parse vector column to this struct, if success | |
| 2291 | - * @return int SQLITE_OK on success, SQLITE_EMPTY is it's not a vector column | |
| 2292 | - * definition, SQLITE_ERROR on error. | |
| 2293 | - */ | |
| 2294 | -int vec0_parse_vector_column(const char *source, int source_length, | |
| 2295 | - struct VectorColumnDefinition *outColumn) { | |
| 2296 | - // parses a vector column definition like so: | |
| 2297 | - // "abc float[123]", "abc_123 bit[1234]", eetc. | |
| 2298 | - // https://github.com/asg017/sqlite-vec/issues/46 | |
| 2299 | - int rc; | |
| 2300 | - struct Vec0Scanner scanner; | |
| 2301 | - struct Vec0Token token; | |
| 2302 | - | |
| 2303 | - char *name; | |
| 2304 | - int nameLength; | |
| 2305 | - enum VectorElementType elementType; | |
| 2306 | - enum Vec0DistanceMetrics distanceMetric = VEC0_DISTANCE_METRIC_L2; | |
| 2307 | - int dimensions; | |
| 2308 | - | |
| 2309 | - vec0_scanner_init(&scanner, source, source_length); | |
| 2310 | - | |
| 2311 | - // starts with an identifier | |
| 2312 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2313 | - | |
| 2314 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 2315 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2316 | - return SQLITE_EMPTY; | |
| 2317 | - } | |
| 2318 | - | |
| 2319 | - name = token.start; | |
| 2320 | - nameLength = token.end - token.start; | |
| 2321 | - | |
| 2322 | - // vector column type comes next: float, int, or bit | |
| 2323 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2324 | - | |
| 2325 | - if (rc != VEC0_TOKEN_RESULT_SOME || | |
| 2326 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2327 | - return SQLITE_EMPTY; | |
| 2328 | - } | |
| 2329 | - if (sqlite3_strnicmp(token.start, "float", 5) == 0 || | |
| 2330 | - sqlite3_strnicmp(token.start, "f32", 3) == 0) { | |
| 2331 | - elementType = SQLITE_VEC_ELEMENT_TYPE_FLOAT32; | |
| 2332 | - } else if (sqlite3_strnicmp(token.start, "int8", 4) == 0 || | |
| 2333 | - sqlite3_strnicmp(token.start, "i8", 2) == 0) { | |
| 2334 | - elementType = SQLITE_VEC_ELEMENT_TYPE_INT8; | |
| 2335 | - } else if (sqlite3_strnicmp(token.start, "bit", 3) == 0) { | |
| 2336 | - elementType = SQLITE_VEC_ELEMENT_TYPE_BIT; | |
| 2337 | - } else { | |
| 2338 | - return SQLITE_EMPTY; | |
| 2339 | - } | |
| 2340 | - | |
| 2341 | - // left '[' bracket | |
| 2342 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2343 | - if (rc != VEC0_TOKEN_RESULT_SOME && token.token_type != TOKEN_TYPE_LBRACKET) { | |
| 2344 | - return SQLITE_EMPTY; | |
| 2345 | - } | |
| 2346 | - | |
| 2347 | - // digit, for vector dimension length | |
| 2348 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2349 | - if (rc != VEC0_TOKEN_RESULT_SOME && token.token_type != TOKEN_TYPE_DIGIT) { | |
| 2350 | - return SQLITE_ERROR; | |
| 2351 | - } | |
| 2352 | - dimensions = atoi(token.start); | |
| 2353 | - if (dimensions <= 0) { | |
| 2354 | - return SQLITE_ERROR; | |
| 2355 | - } | |
| 2356 | - | |
| 2357 | - // // right ']' bracket | |
| 2358 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2359 | - if (rc != VEC0_TOKEN_RESULT_SOME && token.token_type != TOKEN_TYPE_RBRACKET) { | |
| 2360 | - return SQLITE_ERROR; | |
| 2361 | - } | |
| 2362 | - | |
| 2363 | - // any other tokens left should be column-level options , ex `key=value` | |
| 2364 | - // ex `distance_metric=L2 distance_metric=cosine` should error | |
| 2365 | - while (1) { | |
| 2366 | - // should be EOF or identifier (option key) | |
| 2367 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2368 | - if (rc == VEC0_TOKEN_RESULT_EOF) { | |
| 2369 | - break; | |
| 2370 | - } | |
| 2371 | - | |
| 2372 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 2373 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2374 | - return SQLITE_ERROR; | |
| 2375 | - } | |
| 2376 | - | |
| 2377 | - char *key = token.start; | |
| 2378 | - int keyLength = token.end - token.start; | |
| 2379 | - | |
| 2380 | - if (sqlite3_strnicmp(key, "distance_metric", keyLength) == 0) { | |
| 2381 | - | |
| 2382 | - if (elementType == SQLITE_VEC_ELEMENT_TYPE_BIT) { | |
| 2383 | - return SQLITE_ERROR; | |
| 2384 | - } | |
| 2385 | - // ensure equal sign after distance_metric | |
| 2386 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2387 | - if (rc != VEC0_TOKEN_RESULT_SOME && token.token_type != TOKEN_TYPE_EQ) { | |
| 2388 | - return SQLITE_ERROR; | |
| 2389 | - } | |
| 2390 | - | |
| 2391 | - // distance_metric value, an identifier (L2, cosine, etc) | |
| 2392 | - rc = vec0_scanner_next(&scanner, &token); | |
| 2393 | - if (rc != VEC0_TOKEN_RESULT_SOME && | |
| 2394 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | |
| 2395 | - return SQLITE_ERROR; | |
| 2396 | - } | |
| 2397 | - | |
| 2398 | - char *value = token.start; | |
| 2399 | - int valueLength = token.end - token.start; | |
| 2400 | - if (sqlite3_strnicmp(value, "l2", valueLength) == 0) { | |
| 2401 | - distanceMetric = VEC0_DISTANCE_METRIC_L2; | |
| 2402 | - } else if (sqlite3_strnicmp(value, "l1", valueLength) == 0) { | |
| 2403 | - distanceMetric = VEC0_DISTANCE_METRIC_L1; | |
| 2404 | - } else if (sqlite3_strnicmp(value, "cosine", valueLength) == 0) { | |
| 2405 | - distanceMetric = VEC0_DISTANCE_METRIC_COSINE; | |
| 2406 | - } else { | |
| 2407 | - return SQLITE_ERROR; | |
| 2408 | - } | |
| 2409 | - } | |
| 2410 | - // unknown key | |
| 2411 | - else { | |
| 2412 | - return SQLITE_ERROR; | |
| 2413 | - } | |
| 2414 | - } | |
| 2415 | - | |
| 2416 | - outColumn->name = sqlite3_mprintf("%.*s", nameLength, name); | |
| 2417 | - if (!outColumn->name) { | |
| 2418 | - return SQLITE_ERROR; | |
| 2419 | - } | |
| 2420 | - outColumn->name_length = nameLength; | |
| 2421 | - outColumn->distance_metric = distanceMetric; | |
| 2422 | - outColumn->element_type = elementType; | |
| 2423 | - outColumn->dimensions = dimensions; | |
| 2424 | - return SQLITE_OK; | |
| 2425 | -} | |
| 2426 | - | |
| 2427 | -#pragma region vec_each table function | |
| 2428 | - | |
| 2429 | -typedef struct vec_each_vtab vec_each_vtab; | |
| 2430 | -struct vec_each_vtab { | |
| 2431 | - sqlite3_vtab base; | |
| 2432 | -}; | |
| 2433 | - | |
| 2434 | -typedef struct vec_each_cursor vec_each_cursor; | |
| 2435 | -struct vec_each_cursor { | |
| 2436 | - sqlite3_vtab_cursor base; | |
| 2437 | - i64 iRowid; | |
| 2438 | - enum VectorElementType vector_type; | |
| 2439 | - void *vector; | |
| 2440 | - size_t dimensions; | |
| 2441 | - vector_cleanup cleanup; | |
| 2442 | -}; | |
| 2443 | - | |
| 2444 | -static int vec_eachConnect(sqlite3 *db, void *pAux, int argc, | |
| 2445 | - const char *const *argv, sqlite3_vtab **ppVtab, | |
| 2446 | - char **pzErr) { | |
| 2447 | - UNUSED_PARAMETER(pAux); | |
| 2448 | - UNUSED_PARAMETER(argc); | |
| 2449 | - UNUSED_PARAMETER(argv); | |
| 2450 | - UNUSED_PARAMETER(pzErr); | |
| 2451 | - vec_each_vtab *pNew; | |
| 2452 | - int rc; | |
| 2453 | - | |
| 2454 | - rc = sqlite3_declare_vtab(db, "CREATE TABLE x(value, vector hidden)"); | |
| 2455 | -#define VEC_EACH_COLUMN_VALUE 0 | |
| 2456 | -#define VEC_EACH_COLUMN_VECTOR 1 | |
| 2457 | - if (rc == SQLITE_OK) { | |
| 2458 | - pNew = sqlite3_malloc(sizeof(*pNew)); | |
| 2459 | - *ppVtab = (sqlite3_vtab *)pNew; | |
| 2460 | - if (pNew == 0) | |
| 2461 | - return SQLITE_NOMEM; | |
| 2462 | - memset(pNew, 0, sizeof(*pNew)); | |
| 2463 | - } | |
| 2464 | - return rc; | |
| 2465 | -} | |
| 2466 | - | |
| 2467 | -static int vec_eachDisconnect(sqlite3_vtab *pVtab) { | |
| 2468 | - vec_each_vtab *p = (vec_each_vtab *)pVtab; | |
| 2469 | - sqlite3_free(p); | |
| 2470 | - return SQLITE_OK; | |
| 2471 | -} | |
| 2472 | - | |
| 2473 | -static int vec_eachOpen(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor) { | |
| 2474 | - UNUSED_PARAMETER(p); | |
| 2475 | - vec_each_cursor *pCur; | |
| 2476 | - pCur = sqlite3_malloc(sizeof(*pCur)); | |
| 2477 | - if (pCur == 0) | |
| 2478 | - return SQLITE_NOMEM; | |
| 2479 | - memset(pCur, 0, sizeof(*pCur)); | |
| 2480 | - *ppCursor = &pCur->base; | |
| 2481 | - return SQLITE_OK; | |
| 2482 | -} | |
| 2483 | - | |
| 2484 | -static int vec_eachClose(sqlite3_vtab_cursor *cur) { | |
| 2485 | - vec_each_cursor *pCur = (vec_each_cursor *)cur; | |
| 2486 | - pCur->cleanup(pCur->vector); | |
| 2487 | - sqlite3_free(pCur); | |
| 2488 | - return SQLITE_OK; | |
| 2489 | -} | |
| 2490 | - | |
| 2491 | -static int vec_eachBestIndex(sqlite3_vtab *pVTab, | |
| 2492 | - sqlite3_index_info *pIdxInfo) { | |
| 2493 | - UNUSED_PARAMETER(pVTab); | |
| 2494 | - int hasVector = 0; | |
| 2495 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | |
| 2496 | - const struct sqlite3_index_constraint *pCons = &pIdxInfo->aConstraint[i]; | |
| 2497 | - // printf("i=%d iColumn=%d, op=%d, usable=%d\n", i, pCons->iColumn, | |
| 2498 | - // pCons->op, pCons->usable); | |
| 2499 | - switch (pCons->iColumn) { | |
| 2500 | - case VEC_EACH_COLUMN_VECTOR: { | |
| 2501 | - if (pCons->op == SQLITE_INDEX_CONSTRAINT_EQ && pCons->usable) { | |
| 2502 | - hasVector = 1; | |
| 2503 | - pIdxInfo->aConstraintUsage[i].argvIndex = 1; | |
| 2504 | - pIdxInfo->aConstraintUsage[i].omit = 1; | |
| 2505 | - } | |
| 2506 | - break; | |
| 2507 | - } | |
| 2508 | - } | |
| 2509 | - } | |
| 2510 | - if (!hasVector) { | |
| 2511 | - return SQLITE_CONSTRAINT; | |
| 2512 | - } | |
| 2513 | - | |
| 2514 | - pIdxInfo->estimatedCost = (double)100000; | |
| 2515 | - pIdxInfo->estimatedRows = 100000; | |
| 2516 | - | |
| 2517 | - return SQLITE_OK; | |
| 2518 | -} | |
| 2519 | - | |
| 2520 | -static int vec_eachFilter(sqlite3_vtab_cursor *pVtabCursor, int idxNum, | |
| 2521 | - const char *idxStr, int argc, sqlite3_value **argv) { | |
| 2522 | - UNUSED_PARAMETER(idxNum); | |
| 2523 | - UNUSED_PARAMETER(idxStr); | |
| 2524 | - assert(argc == 1); | |
| 2525 | - vec_each_cursor *pCur = (vec_each_cursor *)pVtabCursor; | |
| 2526 | - | |
| 2527 | - if (pCur->vector) { | |
| 2528 | - pCur->cleanup(pCur->vector); | |
| 2529 | - pCur->vector = NULL; | |
| 2530 | - } | |
| 2531 | - | |
| 2532 | - char *pzErrMsg; | |
| 2533 | - int rc = vector_from_value(argv[0], &pCur->vector, &pCur->dimensions, | |
| 2534 | - &pCur->vector_type, &pCur->cleanup, &pzErrMsg); | |
| 2535 | - if (rc != SQLITE_OK) { | |
| 2536 | - return SQLITE_ERROR; | |
| 2537 | - } | |
| 2538 | - pCur->iRowid = 0; | |
| 2539 | - return SQLITE_OK; | |
| 2540 | -} | |
| 2541 | - | |
| 2542 | -static int vec_eachRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid) { | |
| 2543 | - vec_each_cursor *pCur = (vec_each_cursor *)cur; | |
| 2544 | - *pRowid = pCur->iRowid; | |
| 2545 | - return SQLITE_OK; | |
| 2546 | -} | |
| 2547 | - | |
| 2548 | -static int vec_eachEof(sqlite3_vtab_cursor *cur) { | |
| 2549 | - vec_each_cursor *pCur = (vec_each_cursor *)cur; | |
| 2550 | - return pCur->iRowid >= (i64)pCur->dimensions; | |
| 2551 | -} | |
| 2552 | - | |
| 2553 | -static int vec_eachNext(sqlite3_vtab_cursor *cur) { | |
| 2554 | - vec_each_cursor *pCur = (vec_each_cursor *)cur; | |
| 2555 | - pCur->iRowid++; | |
| 2556 | - return SQLITE_OK; | |
| 2557 | -} | |
| 2558 | - | |
| 2559 | -static int vec_eachColumn(sqlite3_vtab_cursor *cur, sqlite3_context *context, | |
| 2560 | - int i) { | |
| 2561 | - vec_each_cursor *pCur = (vec_each_cursor *)cur; | |
| 2562 | - switch (i) { | |
| 2563 | - case VEC_EACH_COLUMN_VALUE: | |
| 2564 | - switch (pCur->vector_type) { | |
| 2565 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 2566 | - sqlite3_result_double(context, ((f32 *)pCur->vector)[pCur->iRowid]); | |
| 2567 | - break; | |
| 2568 | - } | |
| 2569 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 2570 | - u8 x = ((u8 *)pCur->vector)[pCur->iRowid / CHAR_BIT]; | |
| 2571 | - sqlite3_result_int(context, | |
| 2572 | - (x & (0b10000000 >> ((pCur->iRowid % CHAR_BIT)))) > 0); | |
| 2573 | - break; | |
| 2574 | - } | |
| 2575 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | |
| 2576 | - sqlite3_result_int(context, ((i8 *)pCur->vector)[pCur->iRowid]); | |
| 2577 | - break; | |
| 2578 | - } | |
| 2579 | - } | |
| 2580 | - | |
| 2581 | - break; | |
| 2582 | - } | |
| 2583 | - return SQLITE_OK; | |
| 2584 | -} | |
| 2585 | - | |
| 2586 | -static sqlite3_module vec_eachModule = { | |
| 2587 | - /* iVersion */ 0, | |
| 2588 | - /* xCreate */ 0, | |
| 2589 | - /* xConnect */ vec_eachConnect, | |
| 2590 | - /* xBestIndex */ vec_eachBestIndex, | |
| 2591 | - /* xDisconnect */ vec_eachDisconnect, | |
| 2592 | - /* xDestroy */ 0, | |
| 2593 | - /* xOpen */ vec_eachOpen, | |
| 2594 | - /* xClose */ vec_eachClose, | |
| 2595 | - /* xFilter */ vec_eachFilter, | |
| 2596 | - /* xNext */ vec_eachNext, | |
| 2597 | - /* xEof */ vec_eachEof, | |
| 2598 | - /* xColumn */ vec_eachColumn, | |
| 2599 | - /* xRowid */ vec_eachRowid, | |
| 2600 | - /* xUpdate */ 0, | |
| 2601 | - /* xBegin */ 0, | |
| 2602 | - /* xSync */ 0, | |
| 2603 | - /* xCommit */ 0, | |
| 2604 | - /* xRollback */ 0, | |
| 2605 | - /* xFindMethod */ 0, | |
| 2606 | - /* xRename */ 0, | |
| 2607 | - /* xSavepoint */ 0, | |
| 2608 | - /* xRelease */ 0, | |
| 2609 | - /* xRollbackTo */ 0, | |
| 2610 | - /* xShadowName */ 0, | |
| 2611 | -#if SQLITE_VERSION_NUMBER >= 3044000 | |
| 2612 | - /* xIntegrity */ 0 | |
| 2613 | -#endif | |
| 2614 | -}; | |
| 2615 | - | |
| 2616 | -#pragma endregion | |
| 2617 | - | |
| 2618 | -#pragma region vec_npy_each table function | |
| 2619 | - | |
| 2620 | -enum NpyTokenType { | |
| 2621 | - NPY_TOKEN_TYPE_IDENTIFIER, | |
| 2622 | - NPY_TOKEN_TYPE_NUMBER, | |
| 2623 | - NPY_TOKEN_TYPE_LPAREN, | |
| 2624 | - NPY_TOKEN_TYPE_RPAREN, | |
| 2625 | - NPY_TOKEN_TYPE_LBRACE, | |
| 2626 | - NPY_TOKEN_TYPE_RBRACE, | |
| 2627 | - NPY_TOKEN_TYPE_COLON, | |
| 2628 | - NPY_TOKEN_TYPE_COMMA, | |
| 2629 | - NPY_TOKEN_TYPE_STRING, | |
| 2630 | - NPY_TOKEN_TYPE_FALSE, | |
| 2631 | -}; | |
| 2632 | - | |
| 2633 | -struct NpyToken { | |
| 2634 | - enum NpyTokenType token_type; | |
| 2635 | - unsigned char *start; | |
| 2636 | - unsigned char *end; | |
| 2637 | -}; | |
| 2638 | - | |
| 2639 | -int npy_token_next(unsigned char *start, unsigned char *end, | |
| 2640 | - struct NpyToken *out) { | |
| 2641 | - unsigned char *ptr = start; | |
| 2642 | - while (ptr < end) { | |
| 2643 | - unsigned char curr = *ptr; | |
| 2644 | - if (is_whitespace(curr)) { | |
| 2645 | - ptr++; | |
| 2646 | - continue; | |
| 2647 | - } else if (curr == '(') { | |
| 2648 | - out->start = ptr++; | |
| 2649 | - out->end = ptr; | |
| 2650 | - out->token_type = NPY_TOKEN_TYPE_LPAREN; | |
| 2651 | - return VEC0_TOKEN_RESULT_SOME; | |
| 2652 | - } else if (curr == ')') { | |
| 2653 | - out->start = ptr++; | |
| 2654 | - out->end = ptr; | |
| 2655 | - out->token_type = NPY_TOKEN_TYPE_RPAREN; | |
| 2656 | - return VEC0_TOKEN_RESULT_SOME; | |
| 2657 | - } else if (curr == '{') { | |
| 2658 | - out->start = ptr++; | |
| 2659 | - out->end = ptr; | |
| 2660 | - out->token_type = NPY_TOKEN_TYPE_LBRACE; | |
| 2661 | - return VEC0_TOKEN_RESULT_SOME; | |
| 2662 | - } else if (curr == '}') { | |
| 2663 | - out->start = ptr++; | |
| 2664 | - out->end = ptr; | |
| 2665 | - out->token_type = NPY_TOKEN_TYPE_RBRACE; | |
| 2666 | - return VEC0_TOKEN_RESULT_SOME; | |
| 2667 | - } else if (curr == ':') { | |
| 2668 | - out->start = ptr++; | |
| 2669 | - out->end = ptr; | |
| 2670 | - out->token_type = NPY_TOKEN_TYPE_COLON; | |
| 2671 | - return VEC0_TOKEN_RESULT_SOME; | |
| 2672 | - } else if (curr == ',') { | |
| 2673 | - out->start = ptr++; | |
| 2674 | - out->end = ptr; | |
| 2675 | - out->token_type = NPY_TOKEN_TYPE_COMMA; | |
| 2676 | - return VEC0_TOKEN_RESULT_SOME; | |
| 2677 | - } else if (curr == '\'') { | |
| 2678 | - unsigned char *start = ptr; | |
| 2679 | - ptr++; | |
| 2680 | - while (ptr < end) { | |
| 2681 | - if ((*ptr) == '\'') { | |
| 2682 | - break; | |
| 2683 | - } | |
| 2684 | - ptr++; | |
| 2685 | - } | |
| 2686 | - if ((*ptr) != '\'') { | |
| 2687 | - return VEC0_TOKEN_RESULT_ERROR; | |
| 2688 | - } | |
| 2689 | - out->start = start; | |
| 2690 | - out->end = ++ptr; | |
| 2691 | - out->token_type = NPY_TOKEN_TYPE_STRING; | |
| 2692 | - return VEC0_TOKEN_RESULT_SOME; | |
| 2693 | - } else if (curr == 'F' && | |
| 2694 | - strncmp((char *)ptr, "False", strlen("False")) == 0) { | |
| 2695 | - out->start = ptr; | |
| 2696 | - out->end = (ptr + (int)strlen("False")); | |
| 2697 | - ptr = out->end; | |
| 2698 | - out->token_type = NPY_TOKEN_TYPE_FALSE; | |
| 2699 | - return VEC0_TOKEN_RESULT_SOME; | |
| 2700 | - } else if (is_digit(curr)) { | |
| 2701 | - unsigned char *start = ptr; | |
| 2702 | - while (ptr < end && (is_digit(*ptr))) { | |
| 2703 | - ptr++; | |
| 2704 | - } | |
| 2705 | - out->start = start; | |
| 2706 | - out->end = ptr; | |
| 2707 | - out->token_type = NPY_TOKEN_TYPE_NUMBER; | |
| 2708 | - return VEC0_TOKEN_RESULT_SOME; | |
| 2709 | - } else { | |
| 2710 | - return VEC0_TOKEN_RESULT_ERROR; | |
| 2711 | - } | |
| 2712 | - } | |
| 2713 | - return VEC0_TOKEN_RESULT_ERROR; | |
| 2714 | -} | |
| 2715 | - | |
| 2716 | -struct NpyScanner { | |
| 2717 | - unsigned char *start; | |
| 2718 | - unsigned char *end; | |
| 2719 | - unsigned char *ptr; | |
| 2720 | -}; | |
| 2721 | - | |
| 2722 | -void npy_scanner_init(struct NpyScanner *scanner, const unsigned char *source, | |
| 2723 | - int source_length) { | |
| 2724 | - scanner->start = (unsigned char *)source; | |
| 2725 | - scanner->end = (unsigned char *)source + source_length; | |
| 2726 | - scanner->ptr = (unsigned char *)source; | |
| 2727 | -} | |
| 2728 | - | |
| 2729 | -int npy_scanner_next(struct NpyScanner *scanner, struct NpyToken *out) { | |
| 2730 | - int rc = npy_token_next(scanner->start, scanner->end, out); | |
| 2731 | - if (rc == VEC0_TOKEN_RESULT_SOME) { | |
| 2732 | - scanner->start = out->end; | |
| 2733 | - } | |
| 2734 | - return rc; | |
| 2735 | -} | |
| 2736 | - | |
| 2737 | -#define NPY_PARSE_ERROR "Error parsing numpy array: " | |
| 2738 | -int parse_npy_header(sqlite3_vtab *pVTab, const unsigned char *header, | |
| 2739 | - size_t headerLength, | |
| 2740 | - enum VectorElementType *out_element_type, | |
| 2741 | - int *fortran_order, size_t *numElements, | |
| 2742 | - size_t *numDimensions) { | |
| 2743 | - | |
| 2744 | - struct NpyScanner scanner; | |
| 2745 | - struct NpyToken token; | |
| 2746 | - int rc; | |
| 2747 | - npy_scanner_init(&scanner, header, headerLength); | |
| 2748 | - | |
| 2749 | - if (npy_scanner_next(&scanner, &token) != VEC0_TOKEN_RESULT_SOME && | |
| 2750 | - token.token_type != NPY_TOKEN_TYPE_LBRACE) { | |
| 2751 | - vtab_set_error(pVTab, | |
| 2752 | - NPY_PARSE_ERROR "numpy header did not start with '{'"); | |
| 2753 | - return SQLITE_ERROR; | |
| 2754 | - } | |
| 2755 | - while (1) { | |
| 2756 | - rc = npy_scanner_next(&scanner, &token); | |
| 2757 | - if (rc != VEC0_TOKEN_RESULT_SOME) { | |
| 2758 | - vtab_set_error(pVTab, NPY_PARSE_ERROR "expected key in numpy header"); | |
| 2759 | - return SQLITE_ERROR; | |
| 2760 | - } | |
| 2761 | - | |
| 2762 | - if (token.token_type == NPY_TOKEN_TYPE_RBRACE) { | |
| 2763 | - break; | |
| 2764 | - } | |
| 2765 | - if (token.token_type != NPY_TOKEN_TYPE_STRING) { | |
| 2766 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | |
| 2767 | - "expected a string as key in numpy header"); | |
| 2768 | - return SQLITE_ERROR; | |
| 2769 | - } | |
| 2770 | - unsigned char *key = token.start; | |
| 2771 | - | |
| 2772 | - rc = npy_scanner_next(&scanner, &token); | |
| 2773 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | |
| 2774 | - (token.token_type != NPY_TOKEN_TYPE_COLON)) { | |
| 2775 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | |
| 2776 | - "expected a ':' after key in numpy header"); | |
| 2777 | - return SQLITE_ERROR; | |
| 2778 | - } | |
| 2779 | - | |
| 2780 | - if (strncmp((char *)key, "'descr'", strlen("'descr'")) == 0) { | |
| 2781 | - rc = npy_scanner_next(&scanner, &token); | |
| 2782 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | |
| 2783 | - (token.token_type != NPY_TOKEN_TYPE_STRING)) { | |
| 2784 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | |
| 2785 | - "expected a string value after 'descr' key"); | |
| 2786 | - return SQLITE_ERROR; | |
| 2787 | - } | |
| 2788 | - if (strncmp((char *)token.start, "'<f4'", strlen("'<f4'")) != 0) { | |
| 2789 | - vtab_set_error( | |
| 2790 | - pVTab, NPY_PARSE_ERROR | |
| 2791 | - "Only '<f4' values are supported in sqlite-vec numpy functions"); | |
| 2792 | - return SQLITE_ERROR; | |
| 2793 | - } | |
| 2794 | - *out_element_type = SQLITE_VEC_ELEMENT_TYPE_FLOAT32; | |
| 2795 | - } else if (strncmp((char *)key, "'fortran_order'", | |
| 2796 | - strlen("'fortran_order'")) == 0) { | |
| 2797 | - rc = npy_scanner_next(&scanner, &token); | |
| 2798 | - if (rc != VEC0_TOKEN_RESULT_SOME || | |
| 2799 | - token.token_type != NPY_TOKEN_TYPE_FALSE) { | |
| 2800 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | |
| 2801 | - "Only fortran_order = False is supported in sqlite-vec " | |
| 2802 | - "numpy functions"); | |
| 2803 | - return SQLITE_ERROR; | |
| 2804 | - } | |
| 2805 | - *fortran_order = 0; | |
| 2806 | - } else if (strncmp((char *)key, "'shape'", strlen("'shape'")) == 0) { | |
| 2807 | - // "(xxx, xxx)" OR (xxx,) | |
| 2808 | - size_t first; | |
| 2809 | - rc = npy_scanner_next(&scanner, &token); | |
| 2810 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | |
| 2811 | - (token.token_type != NPY_TOKEN_TYPE_LPAREN)) { | |
| 2812 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | |
| 2813 | - "Expected left parenthesis '(' after shape key"); | |
| 2814 | - return SQLITE_ERROR; | |
| 2815 | - } | |
| 2816 | - | |
| 2817 | - rc = npy_scanner_next(&scanner, &token); | |
| 2818 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | |
| 2819 | - (token.token_type != NPY_TOKEN_TYPE_NUMBER)) { | |
| 2820 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | |
| 2821 | - "Expected an initial number in shape value"); | |
| 2822 | - return SQLITE_ERROR; | |
| 2823 | - } | |
| 2824 | - first = strtol((char *)token.start, NULL, 10); | |
| 2825 | - | |
| 2826 | - rc = npy_scanner_next(&scanner, &token); | |
| 2827 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | |
| 2828 | - (token.token_type != NPY_TOKEN_TYPE_COMMA)) { | |
| 2829 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | |
| 2830 | - "Expected comma after first shape value"); | |
| 2831 | - return SQLITE_ERROR; | |
| 2832 | - } | |
| 2833 | - | |
| 2834 | - rc = npy_scanner_next(&scanner, &token); | |
| 2835 | - if (rc != VEC0_TOKEN_RESULT_SOME) { | |
| 2836 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | |
| 2837 | - "unexpected header EOF while parsing shape"); | |
| 2838 | - return SQLITE_ERROR; | |
| 2839 | - } | |
| 2840 | - if (token.token_type == NPY_TOKEN_TYPE_NUMBER) { | |
| 2841 | - *numElements = first; | |
| 2842 | - *numDimensions = strtol((char *)token.start, NULL, 10); | |
| 2843 | - rc = npy_scanner_next(&scanner, &token); | |
| 2844 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | |
| 2845 | - (token.token_type != NPY_TOKEN_TYPE_RPAREN)) { | |
| 2846 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | |
| 2847 | - "expected right parenthesis after shape value"); | |
| 2848 | - return SQLITE_ERROR; | |
| 2849 | - } | |
| 2850 | - } else if (token.token_type == NPY_TOKEN_TYPE_RPAREN) { | |
| 2851 | - // '(0,)' means an empty array! | |
| 2852 | - *numElements = first ? 1 : 0; | |
| 2853 | - *numDimensions = first; | |
| 2854 | - } else { | |
| 2855 | - vtab_set_error(pVTab, NPY_PARSE_ERROR "unknown type in shape value"); | |
| 2856 | - return SQLITE_ERROR; | |
| 2857 | - } | |
| 2858 | - } else { | |
| 2859 | - vtab_set_error(pVTab, NPY_PARSE_ERROR "unknown key in numpy header"); | |
| 2860 | - return SQLITE_ERROR; | |
| 2861 | - } | |
| 2862 | - | |
| 2863 | - rc = npy_scanner_next(&scanner, &token); | |
| 2864 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | |
| 2865 | - (token.token_type != NPY_TOKEN_TYPE_COMMA)) { | |
| 2866 | - vtab_set_error(pVTab, NPY_PARSE_ERROR "unknown extra token after value"); | |
| 2867 | - return SQLITE_ERROR; | |
| 2868 | - } | |
| 2869 | - } | |
| 2870 | - | |
| 2871 | - return SQLITE_OK; | |
| 2872 | -} | |
| 2873 | - | |
| 2874 | -typedef struct vec_npy_each_vtab vec_npy_each_vtab; | |
| 2875 | -struct vec_npy_each_vtab { | |
| 2876 | - sqlite3_vtab base; | |
| 2877 | -}; | |
| 2878 | - | |
| 2879 | -typedef enum { | |
| 2880 | - VEC_NPY_EACH_INPUT_BUFFER, | |
| 2881 | - VEC_NPY_EACH_INPUT_FILE, | |
| 2882 | -} vec_npy_each_input_type; | |
| 2883 | - | |
| 2884 | -typedef struct vec_npy_each_cursor vec_npy_each_cursor; | |
| 2885 | -struct vec_npy_each_cursor { | |
| 2886 | - sqlite3_vtab_cursor base; | |
| 2887 | - i64 iRowid; | |
| 2888 | - // sqlite-vec compatible type of vector | |
| 2889 | - enum VectorElementType elementType; | |
| 2890 | - // number of vectors in the npy array | |
| 2891 | - size_t nElements; | |
| 2892 | - // number of dimensions each vector has | |
| 2893 | - size_t nDimensions; | |
| 2894 | - | |
| 2895 | - vec_npy_each_input_type input_type; | |
| 2896 | - | |
| 2897 | - // when input_type == VEC_NPY_EACH_INPUT_BUFFER | |
| 2898 | - | |
| 2899 | - // Buffer containing the vector data, when reading from an in-memory buffer. | |
| 2900 | - // Size: nElements * nDimensions * element_size | |
| 2901 | - // Clean up with sqlite3_free() once complete | |
| 2902 | - void *vector; | |
| 2903 | - | |
| 2904 | - // when input_type == VEC_NPY_EACH_INPUT_FILE | |
| 2905 | - | |
| 2906 | - // Opened npy file, when reading from a file. | |
| 2907 | - // fclose() when complete. | |
| 2908 | -#ifndef SQLITE_VEC_OMIT_FS | |
| 2909 | - FILE *file; | |
| 2910 | -#endif | |
| 2911 | - | |
| 2912 | - // an in-memory buffer containing a portion of the npy array. | |
| 2913 | - // Used for faster reading, instead of calling fread a lot. | |
| 2914 | - // Will have a byte-size of fileBufferSize | |
| 2915 | - void *chunksBuffer; | |
| 2916 | - // size of allocated fileBuffer in bytes | |
| 2917 | - size_t chunksBufferSize; | |
| 2918 | - //// Maximum length of the buffer, in terms of number of vectors. | |
| 2919 | - size_t maxChunks; | |
| 2920 | - | |
| 2921 | - // Counter index of the current vector into of fileBuffer to yield. | |
| 2922 | - // Starts at 0 once fileBuffer is read, and iterates to bufferLength. | |
| 2923 | - // Resets to 0 once that "buffer" is yielded and a new one is read. | |
| 2924 | - size_t currentChunkIndex; | |
| 2925 | - size_t currentChunkSize; | |
| 2926 | - | |
| 2927 | - // 0 when there are still more elements to read/yield, 1 when complete. | |
| 2928 | - int eof; | |
| 2929 | -}; | |
| 2930 | - | |
| 2931 | -static unsigned char NPY_MAGIC[6] = "\x93NUMPY"; | |
| 2932 | - | |
| 2933 | -#ifndef SQLITE_VEC_OMIT_FS | |
| 2934 | -int parse_npy_file(sqlite3_vtab *pVTab, FILE *file, vec_npy_each_cursor *pCur) { | |
| 2935 | - int n; | |
| 2936 | - fseek(file, 0, SEEK_END); | |
| 2937 | - long fileSize = ftell(file); | |
| 2938 | - | |
| 2939 | - fseek(file, 0L, SEEK_SET); | |
| 2940 | - | |
| 2941 | - unsigned char header[10]; | |
| 2942 | - n = fread(&header, sizeof(unsigned char), 10, file); | |
| 2943 | - if (n != 10) { | |
| 2944 | - vtab_set_error(pVTab, "numpy array file too short"); | |
| 2945 | - return SQLITE_ERROR; | |
| 2946 | - } | |
| 2947 | - | |
| 2948 | - if (memcmp(NPY_MAGIC, header, sizeof(NPY_MAGIC)) != 0) { | |
| 2949 | - vtab_set_error(pVTab, | |
| 2950 | - "numpy array file does not contain the 'magic' header"); | |
| 2951 | - return SQLITE_ERROR; | |
| 2952 | - } | |
| 2953 | - | |
| 2954 | - u8 major = header[6]; | |
| 2955 | - u8 minor = header[7]; | |
| 2956 | - uint16_t headerLength = 0; | |
| 2957 | - memcpy(&headerLength, &header[8], sizeof(uint16_t)); | |
| 2958 | - | |
| 2959 | - size_t totalHeaderLength = sizeof(NPY_MAGIC) + sizeof(major) + sizeof(minor) + | |
| 2960 | - sizeof(headerLength) + headerLength; | |
| 2961 | - i32 dataSize = fileSize - totalHeaderLength; | |
| 2962 | - if (dataSize < 0) { | |
| 2963 | - vtab_set_error(pVTab, "numpy array file header length is invalid"); | |
| 2964 | - return SQLITE_ERROR; | |
| 2965 | - } | |
| 2966 | - | |
| 2967 | - unsigned char *headerX = sqlite3_malloc(headerLength); | |
| 2968 | - if (headerLength && !headerX) { | |
| 2969 | - return SQLITE_NOMEM; | |
| 2970 | - } | |
| 2971 | - | |
| 2972 | - n = fread(headerX, sizeof(char), headerLength, file); | |
| 2973 | - if (n != headerLength) { | |
| 2974 | - sqlite3_free(headerX); | |
| 2975 | - vtab_set_error(pVTab, "numpy array file header length is invalid"); | |
| 2976 | - return SQLITE_ERROR; | |
| 2977 | - } | |
| 2978 | - | |
| 2979 | - int fortran_order; | |
| 2980 | - enum VectorElementType element_type; | |
| 2981 | - size_t numElements; | |
| 2982 | - size_t numDimensions; | |
| 2983 | - int rc = parse_npy_header(pVTab, headerX, headerLength, &element_type, | |
| 2984 | - &fortran_order, &numElements, &numDimensions); | |
| 2985 | - sqlite3_free(headerX); | |
| 2986 | - if (rc != SQLITE_OK) { | |
| 2987 | - // parse_npy_header already attackes an error emssage | |
| 2988 | - return rc; | |
| 2989 | - } | |
| 2990 | - | |
| 2991 | - i32 expectedDataSize = | |
| 2992 | - numElements * vector_byte_size(element_type, numDimensions); | |
| 2993 | - if (expectedDataSize != dataSize) { | |
| 2994 | - vtab_set_error( | |
| 2995 | - pVTab, "numpy array file error: Expected a data size of %d, found %d", | |
| 2996 | - expectedDataSize, dataSize); | |
| 2997 | - return SQLITE_ERROR; | |
| 2998 | - } | |
| 2999 | - | |
| 3000 | - pCur->maxChunks = 1024; | |
| 3001 | - pCur->chunksBufferSize = | |
| 3002 | - (vector_byte_size(element_type, numDimensions)) * pCur->maxChunks; | |
| 3003 | - pCur->chunksBuffer = sqlite3_malloc(pCur->chunksBufferSize); | |
| 3004 | - if (pCur->chunksBufferSize && !pCur->chunksBuffer) { | |
| 3005 | - return SQLITE_NOMEM; | |
| 3006 | - } | |
| 3007 | - | |
| 3008 | - pCur->currentChunkSize = | |
| 3009 | - fread(pCur->chunksBuffer, vector_byte_size(element_type, numDimensions), | |
| 3010 | - pCur->maxChunks, file); | |
| 3011 | - | |
| 3012 | - pCur->currentChunkIndex = 0; | |
| 3013 | - pCur->elementType = element_type; | |
| 3014 | - pCur->nElements = numElements; | |
| 3015 | - pCur->nDimensions = numDimensions; | |
| 3016 | - pCur->input_type = VEC_NPY_EACH_INPUT_FILE; | |
| 3017 | - | |
| 3018 | - pCur->eof = pCur->currentChunkSize == 0; | |
| 3019 | - pCur->file = file; | |
| 3020 | - return SQLITE_OK; | |
| 3021 | -} | |
| 3022 | -#endif | |
| 3023 | - | |
| 3024 | -int parse_npy_buffer(sqlite3_vtab *pVTab, const unsigned char *buffer, | |
| 3025 | - int bufferLength, void **data, size_t *numElements, | |
| 3026 | - size_t *numDimensions, | |
| 3027 | - enum VectorElementType *element_type) { | |
| 3028 | - | |
| 3029 | - if (bufferLength < 10) { | |
| 3030 | - // IMP: V03312_20150 | |
| 3031 | - vtab_set_error(pVTab, "numpy array too short"); | |
| 3032 | - return SQLITE_ERROR; | |
| 3033 | - } | |
| 3034 | - if (memcmp(NPY_MAGIC, buffer, sizeof(NPY_MAGIC)) != 0) { | |
| 3035 | - // V11954_28792 | |
| 3036 | - vtab_set_error(pVTab, "numpy array does not contain the 'magic' header"); | |
| 3037 | - return SQLITE_ERROR; | |
| 3038 | - } | |
| 3039 | - | |
| 3040 | - u8 major = buffer[6]; | |
| 3041 | - u8 minor = buffer[7]; | |
| 3042 | - uint16_t headerLength = 0; | |
| 3043 | - memcpy(&headerLength, &buffer[8], sizeof(uint16_t)); | |
| 3044 | - | |
| 3045 | - i32 totalHeaderLength = sizeof(NPY_MAGIC) + sizeof(major) + sizeof(minor) + | |
| 3046 | - sizeof(headerLength) + headerLength; | |
| 3047 | - i32 dataSize = bufferLength - totalHeaderLength; | |
| 3048 | - | |
| 3049 | - if (dataSize < 0) { | |
| 3050 | - vtab_set_error(pVTab, "numpy array header length is invalid"); | |
| 3051 | - return SQLITE_ERROR; | |
| 3052 | - } | |
| 3053 | - | |
| 3054 | - const unsigned char *header = &buffer[10]; | |
| 3055 | - int fortran_order; | |
| 3056 | - | |
| 3057 | - int rc = parse_npy_header(pVTab, header, headerLength, element_type, | |
| 3058 | - &fortran_order, numElements, numDimensions); | |
| 3059 | - if (rc != SQLITE_OK) { | |
| 3060 | - return rc; | |
| 3061 | - } | |
| 3062 | - | |
| 3063 | - i32 expectedDataSize = | |
| 3064 | - (*numElements * vector_byte_size(*element_type, *numDimensions)); | |
| 3065 | - if (expectedDataSize != dataSize) { | |
| 3066 | - vtab_set_error(pVTab, | |
| 3067 | - "numpy array error: Expected a data size of %d, found %d", | |
| 3068 | - expectedDataSize, dataSize); | |
| 3069 | - return SQLITE_ERROR; | |
| 3070 | - } | |
| 3071 | - | |
| 3072 | - *data = (void *)&buffer[totalHeaderLength]; | |
| 3073 | - return SQLITE_OK; | |
| 3074 | -} | |
| 3075 | - | |
| 3076 | -static int vec_npy_eachConnect(sqlite3 *db, void *pAux, int argc, | |
| 3077 | - const char *const *argv, sqlite3_vtab **ppVtab, | |
| 3078 | - char **pzErr) { | |
| 3079 | - UNUSED_PARAMETER(pAux); | |
| 3080 | - UNUSED_PARAMETER(argc); | |
| 3081 | - UNUSED_PARAMETER(argv); | |
| 3082 | - UNUSED_PARAMETER(pzErr); | |
| 3083 | - vec_npy_each_vtab *pNew; | |
| 3084 | - int rc; | |
| 3085 | - | |
| 3086 | - rc = sqlite3_declare_vtab(db, "CREATE TABLE x(vector, input hidden)"); | |
| 3087 | -#define VEC_NPY_EACH_COLUMN_VECTOR 0 | |
| 3088 | -#define VEC_NPY_EACH_COLUMN_INPUT 1 | |
| 3089 | - if (rc == SQLITE_OK) { | |
| 3090 | - pNew = sqlite3_malloc(sizeof(*pNew)); | |
| 3091 | - *ppVtab = (sqlite3_vtab *)pNew; | |
| 3092 | - if (pNew == 0) | |
| 3093 | - return SQLITE_NOMEM; | |
| 3094 | - memset(pNew, 0, sizeof(*pNew)); | |
| 3095 | - } | |
| 3096 | - return rc; | |
| 3097 | -} | |
| 3098 | - | |
| 3099 | -static int vec_npy_eachDisconnect(sqlite3_vtab *pVtab) { | |
| 3100 | - vec_npy_each_vtab *p = (vec_npy_each_vtab *)pVtab; | |
| 3101 | - sqlite3_free(p); | |
| 3102 | - return SQLITE_OK; | |
| 3103 | -} | |
| 3104 | - | |
| 3105 | -static int vec_npy_eachOpen(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor) { | |
| 3106 | - UNUSED_PARAMETER(p); | |
| 3107 | - vec_npy_each_cursor *pCur; | |
| 3108 | - pCur = sqlite3_malloc(sizeof(*pCur)); | |
| 3109 | - if (pCur == 0) | |
| 3110 | - return SQLITE_NOMEM; | |
| 3111 | - memset(pCur, 0, sizeof(*pCur)); | |
| 3112 | - *ppCursor = &pCur->base; | |
| 3113 | - return SQLITE_OK; | |
| 3114 | -} | |
| 3115 | - | |
| 3116 | -static int vec_npy_eachClose(sqlite3_vtab_cursor *cur) { | |
| 3117 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)cur; | |
| 3118 | -#ifndef SQLITE_VEC_OMIT_FS | |
| 3119 | - if (pCur->file) { | |
| 3120 | - fclose(pCur->file); | |
| 3121 | - pCur->file = NULL; | |
| 3122 | - } | |
| 3123 | -#endif | |
| 3124 | - if (pCur->chunksBuffer) { | |
| 3125 | - sqlite3_free(pCur->chunksBuffer); | |
| 3126 | - pCur->chunksBuffer = NULL; | |
| 3127 | - } | |
| 3128 | - if (pCur->vector) { | |
| 3129 | - pCur->vector = NULL; | |
| 3130 | - } | |
| 3131 | - sqlite3_free(pCur); | |
| 3132 | - return SQLITE_OK; | |
| 3133 | -} | |
| 3134 | - | |
| 3135 | -static int vec_npy_eachBestIndex(sqlite3_vtab *pVTab, | |
| 3136 | - sqlite3_index_info *pIdxInfo) { | |
| 3137 | - int hasInput; | |
| 3138 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | |
| 3139 | - const struct sqlite3_index_constraint *pCons = &pIdxInfo->aConstraint[i]; | |
| 3140 | - // printf("i=%d iColumn=%d, op=%d, usable=%d\n", i, pCons->iColumn, | |
| 3141 | - // pCons->op, pCons->usable); | |
| 3142 | - switch (pCons->iColumn) { | |
| 3143 | - case VEC_NPY_EACH_COLUMN_INPUT: { | |
| 3144 | - if (pCons->op == SQLITE_INDEX_CONSTRAINT_EQ && pCons->usable) { | |
| 3145 | - hasInput = 1; | |
| 3146 | - pIdxInfo->aConstraintUsage[i].argvIndex = 1; | |
| 3147 | - pIdxInfo->aConstraintUsage[i].omit = 1; | |
| 3148 | - } | |
| 3149 | - break; | |
| 3150 | - } | |
| 3151 | - } | |
| 3152 | - } | |
| 3153 | - if (!hasInput) { | |
| 3154 | - pVTab->zErrMsg = sqlite3_mprintf("input argument is required"); | |
| 3155 | - return SQLITE_ERROR; | |
| 3156 | - } | |
| 3157 | - | |
| 3158 | - pIdxInfo->estimatedCost = (double)100000; | |
| 3159 | - pIdxInfo->estimatedRows = 100000; | |
| 3160 | - | |
| 3161 | - return SQLITE_OK; | |
| 3162 | -} | |
| 3163 | - | |
| 3164 | -static int vec_npy_eachFilter(sqlite3_vtab_cursor *pVtabCursor, int idxNum, | |
| 3165 | - const char *idxStr, int argc, | |
| 3166 | - sqlite3_value **argv) { | |
| 3167 | - UNUSED_PARAMETER(idxNum); | |
| 3168 | - UNUSED_PARAMETER(idxStr); | |
| 3169 | - assert(argc == 1); | |
| 3170 | - int rc; | |
| 3171 | - | |
| 3172 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)pVtabCursor; | |
| 3173 | - | |
| 3174 | -#ifndef SQLITE_VEC_OMIT_FS | |
| 3175 | - if (pCur->file) { | |
| 3176 | - fclose(pCur->file); | |
| 3177 | - pCur->file = NULL; | |
| 3178 | - } | |
| 3179 | -#endif | |
| 3180 | - if (pCur->chunksBuffer) { | |
| 3181 | - sqlite3_free(pCur->chunksBuffer); | |
| 3182 | - pCur->chunksBuffer = NULL; | |
| 3183 | - } | |
| 3184 | - if (pCur->vector) { | |
| 3185 | - pCur->vector = NULL; | |
| 3186 | - } | |
| 3187 | - | |
| 3188 | -#ifndef SQLITE_VEC_OMIT_FS | |
| 3189 | - struct VecNpyFile *f = NULL; | |
| 3190 | - if ((f = sqlite3_value_pointer(argv[0], SQLITE_VEC_NPY_FILE_NAME))) { | |
| 3191 | - FILE *file = fopen(f->path, "r"); | |
| 3192 | - if (!file) { | |
| 3193 | - vtab_set_error(pVtabCursor->pVtab, "Could not open numpy file"); | |
| 3194 | - return SQLITE_ERROR; | |
| 3195 | - } | |
| 3196 | - | |
| 3197 | - rc = parse_npy_file(pVtabCursor->pVtab, file, pCur); | |
| 3198 | - if (rc != SQLITE_OK) { | |
| 3199 | -#ifndef SQLITE_VEC_OMIT_FS | |
| 3200 | - fclose(file); | |
| 3201 | -#endif | |
| 3202 | - return rc; | |
| 3203 | - } | |
| 3204 | - | |
| 3205 | - } else | |
| 3206 | -#endif | |
| 3207 | - { | |
| 3208 | - | |
| 3209 | - const unsigned char *input = sqlite3_value_blob(argv[0]); | |
| 3210 | - int inputLength = sqlite3_value_bytes(argv[0]); | |
| 3211 | - void *data; | |
| 3212 | - size_t numElements; | |
| 3213 | - size_t numDimensions; | |
| 3214 | - enum VectorElementType element_type; | |
| 3215 | - | |
| 3216 | - rc = parse_npy_buffer(pVtabCursor->pVtab, input, inputLength, &data, | |
| 3217 | - &numElements, &numDimensions, &element_type); | |
| 3218 | - if (rc != SQLITE_OK) { | |
| 3219 | - return rc; | |
| 3220 | - } | |
| 3221 | - | |
| 3222 | - pCur->vector = data; | |
| 3223 | - pCur->elementType = element_type; | |
| 3224 | - pCur->nElements = numElements; | |
| 3225 | - pCur->nDimensions = numDimensions; | |
| 3226 | - pCur->input_type = VEC_NPY_EACH_INPUT_BUFFER; | |
| 3227 | - } | |
| 3228 | - | |
| 3229 | - pCur->iRowid = 0; | |
| 3230 | - return SQLITE_OK; | |
| 3231 | -} | |
| 3232 | - | |
| 3233 | -static int vec_npy_eachRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid) { | |
| 3234 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)cur; | |
| 3235 | - *pRowid = pCur->iRowid; | |
| 3236 | - return SQLITE_OK; | |
| 3237 | -} | |
| 3238 | - | |
| 3239 | -static int vec_npy_eachEof(sqlite3_vtab_cursor *cur) { | |
| 3240 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)cur; | |
| 3241 | - if (pCur->input_type == VEC_NPY_EACH_INPUT_BUFFER) { | |
| 3242 | - return (!pCur->nElements) || (size_t)pCur->iRowid >= pCur->nElements; | |
| 3243 | - } | |
| 3244 | - return pCur->eof; | |
| 3245 | -} | |
| 3246 | - | |
| 3247 | -static int vec_npy_eachNext(sqlite3_vtab_cursor *cur) { | |
| 3248 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)cur; | |
| 3249 | - pCur->iRowid++; | |
| 3250 | - if (pCur->input_type == VEC_NPY_EACH_INPUT_BUFFER) { | |
| 3251 | - return SQLITE_OK; | |
| 3252 | - } | |
| 3253 | - | |
| 3254 | -#ifndef SQLITE_VEC_OMIT_FS | |
| 3255 | - // else: input is a file | |
| 3256 | - pCur->currentChunkIndex++; | |
| 3257 | - if (pCur->currentChunkIndex >= pCur->currentChunkSize) { | |
| 3258 | - pCur->currentChunkSize = | |
| 3259 | - fread(pCur->chunksBuffer, | |
| 3260 | - vector_byte_size(pCur->elementType, pCur->nDimensions), | |
| 3261 | - pCur->maxChunks, pCur->file); | |
| 3262 | - if (!pCur->currentChunkSize) { | |
| 3263 | - pCur->eof = 1; | |
| 3264 | - } | |
| 3265 | - pCur->currentChunkIndex = 0; | |
| 3266 | - } | |
| 3267 | -#endif | |
| 3268 | - return SQLITE_OK; | |
| 3269 | -} | |
| 3270 | - | |
| 3271 | -static int vec_npy_eachColumnBuffer(vec_npy_each_cursor *pCur, | |
| 3272 | - sqlite3_context *context, int i) { | |
| 3273 | - switch (i) { | |
| 3274 | - case VEC_NPY_EACH_COLUMN_VECTOR: { | |
| 3275 | - sqlite3_result_subtype(context, pCur->elementType); | |
| 3276 | - switch (pCur->elementType) { | |
| 3277 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 3278 | - sqlite3_result_blob( | |
| 3279 | - context, | |
| 3280 | - &((unsigned char *) | |
| 3281 | - pCur->vector)[pCur->iRowid * pCur->nDimensions * sizeof(f32)], | |
| 3282 | - pCur->nDimensions * sizeof(f32), SQLITE_TRANSIENT); | |
| 3283 | - | |
| 3284 | - break; | |
| 3285 | - } | |
| 3286 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | |
| 3287 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 3288 | - // https://github.com/asg017/sqlite-vec/issues/42 | |
| 3289 | - sqlite3_result_error(context, | |
| 3290 | - "vec_npy_each only supports float32 vectors", -1); | |
| 3291 | - break; | |
| 3292 | - } | |
| 3293 | - } | |
| 3294 | - | |
| 3295 | - break; | |
| 3296 | - } | |
| 3297 | - } | |
| 3298 | - return SQLITE_OK; | |
| 3299 | -} | |
| 3300 | -static int vec_npy_eachColumnFile(vec_npy_each_cursor *pCur, | |
| 3301 | - sqlite3_context *context, int i) { | |
| 3302 | - switch (i) { | |
| 3303 | - case VEC_NPY_EACH_COLUMN_VECTOR: { | |
| 3304 | - switch (pCur->elementType) { | |
| 3305 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 3306 | - sqlite3_result_blob( | |
| 3307 | - context, | |
| 3308 | - &((unsigned char *) | |
| 3309 | - pCur->chunksBuffer)[pCur->currentChunkIndex * | |
| 3310 | - pCur->nDimensions * sizeof(f32)], | |
| 3311 | - pCur->nDimensions * sizeof(f32), SQLITE_TRANSIENT); | |
| 3312 | - break; | |
| 3313 | - } | |
| 3314 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | |
| 3315 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 3316 | - // https://github.com/asg017/sqlite-vec/issues/42 | |
| 3317 | - sqlite3_result_error(context, | |
| 3318 | - "vec_npy_each only supports float32 vectors", -1); | |
| 3319 | - break; | |
| 3320 | - } | |
| 3321 | - } | |
| 3322 | - break; | |
| 3323 | - } | |
| 3324 | - } | |
| 3325 | - return SQLITE_OK; | |
| 3326 | -} | |
| 3327 | -static int vec_npy_eachColumn(sqlite3_vtab_cursor *cur, | |
| 3328 | - sqlite3_context *context, int i) { | |
| 3329 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)cur; | |
| 3330 | - switch (pCur->input_type) { | |
| 3331 | - case VEC_NPY_EACH_INPUT_BUFFER: | |
| 3332 | - return vec_npy_eachColumnBuffer(pCur, context, i); | |
| 3333 | - case VEC_NPY_EACH_INPUT_FILE: | |
| 3334 | - return vec_npy_eachColumnFile(pCur, context, i); | |
| 3335 | - } | |
| 3336 | - return SQLITE_ERROR; | |
| 3337 | -} | |
| 3338 | - | |
| 3339 | -static sqlite3_module vec_npy_eachModule = { | |
| 3340 | - /* iVersion */ 0, | |
| 3341 | - /* xCreate */ 0, | |
| 3342 | - /* xConnect */ vec_npy_eachConnect, | |
| 3343 | - /* xBestIndex */ vec_npy_eachBestIndex, | |
| 3344 | - /* xDisconnect */ vec_npy_eachDisconnect, | |
| 3345 | - /* xDestroy */ 0, | |
| 3346 | - /* xOpen */ vec_npy_eachOpen, | |
| 3347 | - /* xClose */ vec_npy_eachClose, | |
| 3348 | - /* xFilter */ vec_npy_eachFilter, | |
| 3349 | - /* xNext */ vec_npy_eachNext, | |
| 3350 | - /* xEof */ vec_npy_eachEof, | |
| 3351 | - /* xColumn */ vec_npy_eachColumn, | |
| 3352 | - /* xRowid */ vec_npy_eachRowid, | |
| 3353 | - /* xUpdate */ 0, | |
| 3354 | - /* xBegin */ 0, | |
| 3355 | - /* xSync */ 0, | |
| 3356 | - /* xCommit */ 0, | |
| 3357 | - /* xRollback */ 0, | |
| 3358 | - /* xFindMethod */ 0, | |
| 3359 | - /* xRename */ 0, | |
| 3360 | - /* xSavepoint */ 0, | |
| 3361 | - /* xRelease */ 0, | |
| 3362 | - /* xRollbackTo */ 0, | |
| 3363 | - /* xShadowName */ 0, | |
| 3364 | -#if SQLITE_VERSION_NUMBER >= 3044000 | |
| 3365 | - /* xIntegrity */ 0, | |
| 3366 | -#endif | |
| 3367 | -}; | |
| 3368 | - | |
| 3369 | -#pragma endregion | |
| 3370 | - | |
| 3371 | -#pragma region vec0 virtual table | |
| 3372 | - | |
| 3373 | -#define VEC0_COLUMN_ID 0 | |
| 3374 | -#define VEC0_COLUMN_USERN_START 1 | |
| 3375 | -#define VEC0_COLUMN_OFFSET_DISTANCE 1 | |
| 3376 | -#define VEC0_COLUMN_OFFSET_K 2 | |
| 3377 | - | |
| 3378 | -#define VEC0_SHADOW_INFO_NAME "\"%w\".\"%w_info\"" | |
| 3379 | - | |
| 3380 | -#define VEC0_SHADOW_CHUNKS_NAME "\"%w\".\"%w_chunks\"" | |
| 3381 | -/// 1) schema, 2) original vtab table name | |
| 3382 | -#define VEC0_SHADOW_CHUNKS_CREATE \ | |
| 3383 | - "CREATE TABLE " VEC0_SHADOW_CHUNKS_NAME "(" \ | |
| 3384 | - "chunk_id INTEGER PRIMARY KEY AUTOINCREMENT," \ | |
| 3385 | - "size INTEGER NOT NULL," \ | |
| 3386 | - "validity BLOB NOT NULL," \ | |
| 3387 | - "rowids BLOB NOT NULL" \ | |
| 3388 | - ");" | |
| 3389 | - | |
| 3390 | -#define VEC0_SHADOW_ROWIDS_NAME "\"%w\".\"%w_rowids\"" | |
| 3391 | -/// 1) schema, 2) original vtab table name | |
| 3392 | -#define VEC0_SHADOW_ROWIDS_CREATE_BASIC \ | |
| 3393 | - "CREATE TABLE " VEC0_SHADOW_ROWIDS_NAME "(" \ | |
| 3394 | - "rowid INTEGER PRIMARY KEY AUTOINCREMENT," \ | |
| 3395 | - "id," \ | |
| 3396 | - "chunk_id INTEGER," \ | |
| 3397 | - "chunk_offset INTEGER" \ | |
| 3398 | - ");" | |
| 3399 | - | |
| 3400 | -// vec0 tables with a text primary keys are still backed by int64 primary keys, | |
| 3401 | -// since a fixed-length rowid is required for vec0 chunks. But we add a new 'id | |
| 3402 | -// text unique' column to emulate a text primary key interface. | |
| 3403 | -#define VEC0_SHADOW_ROWIDS_CREATE_PK_TEXT \ | |
| 3404 | - "CREATE TABLE " VEC0_SHADOW_ROWIDS_NAME "(" \ | |
| 3405 | - "rowid INTEGER PRIMARY KEY AUTOINCREMENT," \ | |
| 3406 | - "id TEXT UNIQUE NOT NULL," \ | |
| 3407 | - "chunk_id INTEGER," \ | |
| 3408 | - "chunk_offset INTEGER" \ | |
| 3409 | - ");" | |
| 3410 | - | |
| 3411 | -/// 1) schema, 2) original vtab table name | |
| 3412 | -#define VEC0_SHADOW_VECTOR_N_NAME "\"%w\".\"%w_vector_chunks%02d\"" | |
| 3413 | - | |
| 3414 | -/// 1) schema, 2) original vtab table name | |
| 3415 | -#define VEC0_SHADOW_VECTOR_N_CREATE \ | |
| 3416 | - "CREATE TABLE " VEC0_SHADOW_VECTOR_N_NAME "(" \ | |
| 3417 | - "rowid PRIMARY KEY," \ | |
| 3418 | - "vectors BLOB NOT NULL" \ | |
| 3419 | - ");" | |
| 3420 | - | |
| 3421 | -#define VEC0_SHADOW_AUXILIARY_NAME "\"%w\".\"%w_auxiliary\"" | |
| 3422 | - | |
| 3423 | -#define VEC0_SHADOW_METADATA_N_NAME "\"%w\".\"%w_metadatachunks%02d\"" | |
| 3424 | -#define VEC0_SHADOW_METADATA_TEXT_DATA_NAME "\"%w\".\"%w_metadatatext%02d\"" | |
| 3425 | - | |
| 3426 | -#define VEC_INTERAL_ERROR "Internal sqlite-vec error: " | |
| 3427 | -#define REPORT_URL "https://github.com/asg017/sqlite-vec/issues/new" | |
| 3428 | - | |
| 3429 | -typedef struct vec0_vtab vec0_vtab; | |
| 3430 | - | |
| 3431 | -#define VEC0_MAX_VECTOR_COLUMNS 16 | |
| 3432 | -#define VEC0_MAX_PARTITION_COLUMNS 4 | |
| 3433 | -#define VEC0_MAX_AUXILIARY_COLUMNS 16 | |
| 3434 | -#define VEC0_MAX_METADATA_COLUMNS 16 | |
| 3435 | - | |
| 3436 | -#define SQLITE_VEC_VEC0_MAX_DIMENSIONS 8192 | |
| 3437 | -#define VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH 16 | |
| 3438 | -#define VEC0_METADATA_TEXT_VIEW_DATA_LENGTH 12 | |
| 3439 | - | |
| 3440 | -typedef enum { | |
| 3441 | - // vector column, ie "contents_embedding float[1024]" | |
| 3442 | - SQLITE_VEC0_USER_COLUMN_KIND_VECTOR = 1, | |
| 3443 | - | |
| 3444 | - // partition key column, ie "user_id integer partition key" | |
| 3445 | - SQLITE_VEC0_USER_COLUMN_KIND_PARTITION = 2, | |
| 3446 | - | |
| 3447 | - // | |
| 3448 | - SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY = 3, | |
| 3449 | - | |
| 3450 | - // metadata column that can be filtered, ie "genre text" | |
| 3451 | - SQLITE_VEC0_USER_COLUMN_KIND_METADATA = 4, | |
| 3452 | -} vec0_user_column_kind; | |
| 3453 | - | |
| 3454 | -struct vec0_vtab { | |
| 3455 | - sqlite3_vtab base; | |
| 3456 | - | |
| 3457 | - // the SQLite connection of the host database | |
| 3458 | - sqlite3 *db; | |
| 3459 | - | |
| 3460 | - // True if the primary key of the vec0 table has a column type TEXT. | |
| 3461 | - // Will change the schema of the _rowids table, and insert/query logic. | |
| 3462 | - int pkIsText; | |
| 3463 | - | |
| 3464 | - // number of defined vector columns. | |
| 3465 | - int numVectorColumns; | |
| 3466 | - | |
| 3467 | - // number of defined PARTITION KEY columns. | |
| 3468 | - int numPartitionColumns; | |
| 3469 | - | |
| 3470 | - // number of defined auxiliary columns | |
| 3471 | - int numAuxiliaryColumns; | |
| 3472 | - | |
| 3473 | - // number of defined metadata columns | |
| 3474 | - int numMetadataColumns; | |
| 3475 | - | |
| 3476 | - | |
| 3477 | - // Name of the schema the table exists on. | |
| 3478 | - // Must be freed with sqlite3_free() | |
| 3479 | - char *schemaName; | |
| 3480 | - | |
| 3481 | - // Name of the table the table exists on. | |
| 3482 | - // Must be freed with sqlite3_free() | |
| 3483 | - char *tableName; | |
| 3484 | - | |
| 3485 | - // Name of the _rowids shadow table. | |
| 3486 | - // Must be freed with sqlite3_free() | |
| 3487 | - char *shadowRowidsName; | |
| 3488 | - | |
| 3489 | - // Name of the _chunks shadow table. | |
| 3490 | - // Must be freed with sqlite3_free() | |
| 3491 | - char *shadowChunksName; | |
| 3492 | - | |
| 3493 | - // contains enum vec0_user_column_kind values for up to | |
| 3494 | - // numVectorColumns + numPartitionColumns entries | |
| 3495 | - vec0_user_column_kind user_column_kinds[VEC0_MAX_VECTOR_COLUMNS + VEC0_MAX_PARTITION_COLUMNS + VEC0_MAX_AUXILIARY_COLUMNS + VEC0_MAX_METADATA_COLUMNS]; | |
| 3496 | - | |
| 3497 | - uint8_t user_column_idxs[VEC0_MAX_VECTOR_COLUMNS + VEC0_MAX_PARTITION_COLUMNS + VEC0_MAX_AUXILIARY_COLUMNS + VEC0_MAX_METADATA_COLUMNS]; | |
| 3498 | - | |
| 3499 | - | |
| 3500 | - // Name of all the vector chunk shadow tables. | |
| 3501 | - // Ex '_vector_chunks00' | |
| 3502 | - // Only the first numVectorColumns entries will be available. | |
| 3503 | - // The first numVectorColumns entries must be freed with sqlite3_free() | |
| 3504 | - char *shadowVectorChunksNames[VEC0_MAX_VECTOR_COLUMNS]; | |
| 3505 | - | |
| 3506 | - // Name of all metadata chunk shadow tables, ie `_metadatachunks00` | |
| 3507 | - // Only the first numMetadataColumns entries will be available. | |
| 3508 | - // The first numMetadataColumns entries must be freed with sqlite3_free() | |
| 3509 | - char *shadowMetadataChunksNames[VEC0_MAX_METADATA_COLUMNS]; | |
| 3510 | - | |
| 3511 | - struct VectorColumnDefinition vector_columns[VEC0_MAX_VECTOR_COLUMNS]; | |
| 3512 | - struct Vec0PartitionColumnDefinition paritition_columns[VEC0_MAX_PARTITION_COLUMNS]; | |
| 3513 | - struct Vec0AuxiliaryColumnDefinition auxiliary_columns[VEC0_MAX_AUXILIARY_COLUMNS]; | |
| 3514 | - struct Vec0MetadataColumnDefinition metadata_columns[VEC0_MAX_METADATA_COLUMNS]; | |
| 3515 | - | |
| 3516 | - int chunk_size; | |
| 3517 | - | |
| 3518 | - // select latest chunk from _chunks, getting chunk_id | |
| 3519 | - sqlite3_stmt *stmtLatestChunk; | |
| 3520 | - | |
| 3521 | - /** | |
| 3522 | - * Statement to insert a row into the _rowids table, with a rowid. | |
| 3523 | - * Parameters: | |
| 3524 | - * 1: int64, rowid to insert | |
| 3525 | - * Result columns: none | |
| 3526 | - * SQL: "INSERT INTO _rowids(rowid) VALUES (?)" | |
| 3527 | - * | |
| 3528 | - * Must be cleaned up with sqlite3_finalize(). | |
| 3529 | - */ | |
| 3530 | - sqlite3_stmt *stmtRowidsInsertRowid; | |
| 3531 | - | |
| 3532 | - /** | |
| 3533 | - * Statement to insert a row into the _rowids table, with an id. | |
| 3534 | - * The id column isn't a tradition primary key, but instead a unique | |
| 3535 | - * column to handle "text primary key" vec0 tables. The true int64 rowid | |
| 3536 | - * can be retrieved after inserting with sqlite3_last_rowid(). | |
| 3537 | - * | |
| 3538 | - * Parameters: | |
| 3539 | - * 1: text or null, id to insert | |
| 3540 | - * Result columns: none | |
| 3541 | - * | |
| 3542 | - * Must be cleaned up with sqlite3_finalize(). | |
| 3543 | - */ | |
| 3544 | - sqlite3_stmt *stmtRowidsInsertId; | |
| 3545 | - | |
| 3546 | - /** | |
| 3547 | - * Statement to update the "position" columns chunk_id and chunk_offset for | |
| 3548 | - * a given _rowids row. Used when the "next available" chunk position is found | |
| 3549 | - * for a vector. | |
| 3550 | - * | |
| 3551 | - * Parameters: | |
| 3552 | - * 1: int64, chunk_id value | |
| 3553 | - * 2: int64, chunk_offset value | |
| 3554 | - * 3: int64, rowid value | |
| 3555 | - * Result columns: none | |
| 3556 | - * | |
| 3557 | - * Must be cleaned up with sqlite3_finalize(). | |
| 3558 | - */ | |
| 3559 | - sqlite3_stmt *stmtRowidsUpdatePosition; | |
| 3560 | - | |
| 3561 | - /** | |
| 3562 | - * Statement to quickly find the chunk_id + chunk_offset of a given row. | |
| 3563 | - * Parameters: | |
| 3564 | - * 1: rowid of the row/vector to lookup | |
| 3565 | - * Result columns: | |
| 3566 | - * 0: chunk_id (i64) | |
| 3567 | - * 1: chunk_offset (i64) | |
| 3568 | - * SQL: "SELECT id, chunk_id, chunk_offset FROM _rowids WHERE rowid = ?"" | |
| 3569 | - * | |
| 3570 | - * Must be cleaned up with sqlite3_finalize(). | |
| 3571 | - */ | |
| 3572 | - sqlite3_stmt *stmtRowidsGetChunkPosition; | |
| 3573 | -}; | |
| 3574 | - | |
| 3575 | -/** | |
| 3576 | - * @brief Finalize all the sqlite3_stmt members in a vec0_vtab. | |
| 3577 | - * | |
| 3578 | - * @param p vec0_vtab pointer | |
| 3579 | - */ | |
| 3580 | -void vec0_free_resources(vec0_vtab *p) { | |
| 3581 | - sqlite3_finalize(p->stmtLatestChunk); | |
| 3582 | - p->stmtLatestChunk = NULL; | |
| 3583 | - sqlite3_finalize(p->stmtRowidsInsertRowid); | |
| 3584 | - p->stmtRowidsInsertRowid = NULL; | |
| 3585 | - sqlite3_finalize(p->stmtRowidsInsertId); | |
| 3586 | - p->stmtRowidsInsertId = NULL; | |
| 3587 | - sqlite3_finalize(p->stmtRowidsUpdatePosition); | |
| 3588 | - p->stmtRowidsUpdatePosition = NULL; | |
| 3589 | - sqlite3_finalize(p->stmtRowidsGetChunkPosition); | |
| 3590 | - p->stmtRowidsGetChunkPosition = NULL; | |
| 3591 | -} | |
| 3592 | - | |
| 3593 | -/** | |
| 3594 | - * @brief Free all memory and sqlite3_stmt members of a vec0_vtab | |
| 3595 | - * | |
| 3596 | - * @param p vec0_vtab pointer | |
| 3597 | - */ | |
| 3598 | -void vec0_free(vec0_vtab *p) { | |
| 3599 | - vec0_free_resources(p); | |
| 3600 | - | |
| 3601 | - sqlite3_free(p->schemaName); | |
| 3602 | - p->schemaName = NULL; | |
| 3603 | - sqlite3_free(p->tableName); | |
| 3604 | - p->tableName = NULL; | |
| 3605 | - sqlite3_free(p->shadowChunksName); | |
| 3606 | - p->shadowChunksName = NULL; | |
| 3607 | - sqlite3_free(p->shadowRowidsName); | |
| 3608 | - p->shadowRowidsName = NULL; | |
| 3609 | - | |
| 3610 | - for (int i = 0; i < p->numVectorColumns; i++) { | |
| 3611 | - sqlite3_free(p->shadowVectorChunksNames[i]); | |
| 3612 | - p->shadowVectorChunksNames[i] = NULL; | |
| 3613 | - | |
| 3614 | - sqlite3_free(p->vector_columns[i].name); | |
| 3615 | - p->vector_columns[i].name = NULL; | |
| 3616 | - } | |
| 3617 | -} | |
| 3618 | - | |
| 3619 | -int vec0_num_defined_user_columns(vec0_vtab *p) { | |
| 3620 | - return p->numVectorColumns + p->numPartitionColumns + p->numAuxiliaryColumns + p->numMetadataColumns; | |
| 3621 | -} | |
| 3622 | - | |
| 3623 | -/** | |
| 3624 | - * @brief Returns the index of the distance hidden column for the given vec0 | |
| 3625 | - * table. | |
| 3626 | - * | |
| 3627 | - * @param p vec0 table | |
| 3628 | - * @return int | |
| 3629 | - */ | |
| 3630 | -int vec0_column_distance_idx(vec0_vtab *p) { | |
| 3631 | - return VEC0_COLUMN_USERN_START + (vec0_num_defined_user_columns(p) - 1) + | |
| 3632 | - VEC0_COLUMN_OFFSET_DISTANCE; | |
| 3633 | -} | |
| 3634 | - | |
| 3635 | -/** | |
| 3636 | - * @brief Returns the index of the k hidden column for the given vec0 table. | |
| 3637 | - * | |
| 3638 | - * @param p vec0 table | |
| 3639 | - * @return int k column index | |
| 3640 | - */ | |
| 3641 | -int vec0_column_k_idx(vec0_vtab *p) { | |
| 3642 | - return VEC0_COLUMN_USERN_START + (vec0_num_defined_user_columns(p) - 1) + | |
| 3643 | - VEC0_COLUMN_OFFSET_K; | |
| 3644 | -} | |
| 3645 | - | |
| 3646 | -/** | |
| 3647 | - * Returns 1 if the given column-based index is a valid vector column, | |
| 3648 | - * 0 otherwise. | |
| 3649 | - */ | |
| 3650 | -int vec0_column_idx_is_vector(vec0_vtab *pVtab, int column_idx) { | |
| 3651 | - return column_idx >= VEC0_COLUMN_USERN_START && | |
| 3652 | - column_idx <= (VEC0_COLUMN_USERN_START + vec0_num_defined_user_columns(pVtab) - 1) && | |
| 3653 | - pVtab->user_column_kinds[column_idx - VEC0_COLUMN_USERN_START] == SQLITE_VEC0_USER_COLUMN_KIND_VECTOR; | |
| 3654 | -} | |
| 3655 | - | |
| 3656 | -/** | |
| 3657 | - * Returns the vector index of the given user column index. | |
| 3658 | - * ONLY call if validated with vec0_column_idx_is_vector before | |
| 3659 | - */ | |
| 3660 | -int vec0_column_idx_to_vector_idx(vec0_vtab *pVtab, int column_idx) { | |
| 3661 | - UNUSED_PARAMETER(pVtab); | |
| 3662 | - return pVtab->user_column_idxs[column_idx - VEC0_COLUMN_USERN_START]; | |
| 3663 | -} | |
| 3664 | -/** | |
| 3665 | - * Returns 1 if the given column-based index is a "partition key" column, | |
| 3666 | - * 0 otherwise. | |
| 3667 | - */ | |
| 3668 | -int vec0_column_idx_is_partition(vec0_vtab *pVtab, int column_idx) { | |
| 3669 | - return column_idx >= VEC0_COLUMN_USERN_START && | |
| 3670 | - column_idx <= (VEC0_COLUMN_USERN_START + vec0_num_defined_user_columns(pVtab) - 1) && | |
| 3671 | - pVtab->user_column_kinds[column_idx - VEC0_COLUMN_USERN_START] == SQLITE_VEC0_USER_COLUMN_KIND_PARTITION; | |
| 3672 | -} | |
| 3673 | - | |
| 3674 | -/** | |
| 3675 | - * Returns the partition column index of the given user column index. | |
| 3676 | - * ONLY call if validated with vec0_column_idx_is_vector before | |
| 3677 | - */ | |
| 3678 | -int vec0_column_idx_to_partition_idx(vec0_vtab *pVtab, int column_idx) { | |
| 3679 | - UNUSED_PARAMETER(pVtab); | |
| 3680 | - return pVtab->user_column_idxs[column_idx - VEC0_COLUMN_USERN_START]; | |
| 3681 | -} | |
| 3682 | - | |
| 3683 | -/** | |
| 3684 | - * Returns 1 if the given column-based index is a auxiliary column, | |
| 3685 | - * 0 otherwise. | |
| 3686 | - */ | |
| 3687 | -int vec0_column_idx_is_auxiliary(vec0_vtab *pVtab, int column_idx) { | |
| 3688 | - return column_idx >= VEC0_COLUMN_USERN_START && | |
| 3689 | - column_idx <= (VEC0_COLUMN_USERN_START + vec0_num_defined_user_columns(pVtab) - 1) && | |
| 3690 | - pVtab->user_column_kinds[column_idx - VEC0_COLUMN_USERN_START] == SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY; | |
| 3691 | -} | |
| 3692 | - | |
| 3693 | -/** | |
| 3694 | - * Returns the auxiliary column index of the given user column index. | |
| 3695 | - * ONLY call if validated with vec0_column_idx_to_partition_idx before | |
| 3696 | - */ | |
| 3697 | -int vec0_column_idx_to_auxiliary_idx(vec0_vtab *pVtab, int column_idx) { | |
| 3698 | - UNUSED_PARAMETER(pVtab); | |
| 3699 | - return pVtab->user_column_idxs[column_idx - VEC0_COLUMN_USERN_START]; | |
| 3700 | -} | |
| 3701 | - | |
| 3702 | -/** | |
| 3703 | - * Returns 1 if the given column-based index is a metadata column, | |
| 3704 | - * 0 otherwise. | |
| 3705 | - */ | |
| 3706 | -int vec0_column_idx_is_metadata(vec0_vtab *pVtab, int column_idx) { | |
| 3707 | - return column_idx >= VEC0_COLUMN_USERN_START && | |
| 3708 | - column_idx <= (VEC0_COLUMN_USERN_START + vec0_num_defined_user_columns(pVtab) - 1) && | |
| 3709 | - pVtab->user_column_kinds[column_idx - VEC0_COLUMN_USERN_START] == SQLITE_VEC0_USER_COLUMN_KIND_METADATA; | |
| 3710 | -} | |
| 3711 | - | |
| 3712 | -/** | |
| 3713 | - * Returns the metadata column index of the given user column index. | |
| 3714 | - * ONLY call if validated with vec0_column_idx_is_metadata before | |
| 3715 | - */ | |
| 3716 | -int vec0_column_idx_to_metadata_idx(vec0_vtab *pVtab, int column_idx) { | |
| 3717 | - UNUSED_PARAMETER(pVtab); | |
| 3718 | - return pVtab->user_column_idxs[column_idx - VEC0_COLUMN_USERN_START]; | |
| 3719 | -} | |
| 3720 | - | |
| 3721 | -/** | |
| 3722 | - * @brief Retrieve the chunk_id, chunk_offset, and possible "id" value | |
| 3723 | - * of a vec0_vtab row with the provided rowid | |
| 3724 | - * | |
| 3725 | - * @param p vec0_vtab | |
| 3726 | - * @param rowid the rowid of the row to query | |
| 3727 | - * @param id output, optional sqlite3_value to provide the id. | |
| 3728 | - * Useful for text PK rows. Must be freed with sqlite3_value_free() | |
| 3729 | - * @param chunk_id output, the chunk_id the row belongs to | |
| 3730 | - * @param chunk_offset output, the offset within the chunk the row belongs to | |
| 3731 | - * @return SQLITE_ROW on success, error code otherwise. SQLITE_EMPTY if row DNE | |
| 3732 | - */ | |
| 3733 | -int vec0_get_chunk_position(vec0_vtab *p, i64 rowid, sqlite3_value **id, | |
| 3734 | - i64 *chunk_id, i64 *chunk_offset) { | |
| 3735 | - int rc; | |
| 3736 | - | |
| 3737 | - if (!p->stmtRowidsGetChunkPosition) { | |
| 3738 | - const char *zSql = | |
| 3739 | - sqlite3_mprintf("SELECT id, chunk_id, chunk_offset " | |
| 3740 | - "FROM " VEC0_SHADOW_ROWIDS_NAME " WHERE rowid = ?", | |
| 3741 | - p->schemaName, p->tableName); | |
| 3742 | - if (!zSql) { | |
| 3743 | - rc = SQLITE_NOMEM; | |
| 3744 | - goto cleanup; | |
| 3745 | - } | |
| 3746 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &p->stmtRowidsGetChunkPosition, 0); | |
| 3747 | - sqlite3_free((void *)zSql); | |
| 3748 | - if (rc != SQLITE_OK) { | |
| 3749 | - vtab_set_error( | |
| 3750 | - &p->base, VEC_INTERAL_ERROR | |
| 3751 | - "could not initialize 'rowids get chunk position' statement"); | |
| 3752 | - goto cleanup; | |
| 3753 | - } | |
| 3754 | - } | |
| 3755 | - | |
| 3756 | - sqlite3_bind_int64(p->stmtRowidsGetChunkPosition, 1, rowid); | |
| 3757 | - rc = sqlite3_step(p->stmtRowidsGetChunkPosition); | |
| 3758 | - // special case: when no results, return SQLITE_EMPTY to convey "that chunk | |
| 3759 | - // position doesnt exist" | |
| 3760 | - if (rc == SQLITE_DONE) { | |
| 3761 | - rc = SQLITE_EMPTY; | |
| 3762 | - goto cleanup; | |
| 3763 | - } | |
| 3764 | - if (rc != SQLITE_ROW) { | |
| 3765 | - goto cleanup; | |
| 3766 | - } | |
| 3767 | - | |
| 3768 | - if (id) { | |
| 3769 | - sqlite3_value *value = | |
| 3770 | - sqlite3_column_value(p->stmtRowidsGetChunkPosition, 0); | |
| 3771 | - *id = sqlite3_value_dup(value); | |
| 3772 | - if (!*id) { | |
| 3773 | - rc = SQLITE_NOMEM; | |
| 3774 | - goto cleanup; | |
| 3775 | - } | |
| 3776 | - } | |
| 3777 | - | |
| 3778 | - if (chunk_id) { | |
| 3779 | - *chunk_id = sqlite3_column_int64(p->stmtRowidsGetChunkPosition, 1); | |
| 3780 | - } | |
| 3781 | - if (chunk_offset) { | |
| 3782 | - *chunk_offset = sqlite3_column_int64(p->stmtRowidsGetChunkPosition, 2); | |
| 3783 | - } | |
| 3784 | - | |
| 3785 | - rc = SQLITE_OK; | |
| 3786 | - | |
| 3787 | -cleanup: | |
| 3788 | - sqlite3_reset(p->stmtRowidsGetChunkPosition); | |
| 3789 | - sqlite3_clear_bindings(p->stmtRowidsGetChunkPosition); | |
| 3790 | - return rc; | |
| 3791 | -} | |
| 3792 | - | |
| 3793 | -/** | |
| 3794 | - * @brief Return the id value from the _rowids table where _rowids.rowid = | |
| 3795 | - * rowid. | |
| 3796 | - * | |
| 3797 | - * @param pVtab: vec0 table to query | |
| 3798 | - * @param rowid: rowid of the row to query. | |
| 3799 | - * @param out: A dup'ed sqlite3_value of the id column. Might be null. | |
| 3800 | - * Must be cleaned up with sqlite3_value_free(). | |
| 3801 | - * @returns SQLITE_OK on success, error code on failure | |
| 3802 | - */ | |
| 3803 | -int vec0_get_id_value_from_rowid(vec0_vtab *pVtab, i64 rowid, | |
| 3804 | - sqlite3_value **out) { | |
| 3805 | - // PERF: different strategy than get_chunk_position? | |
| 3806 | - return vec0_get_chunk_position((vec0_vtab *)pVtab, rowid, out, NULL, NULL); | |
| 3807 | -} | |
| 3808 | - | |
| 3809 | -int vec0_rowid_from_id(vec0_vtab *p, sqlite3_value *valueId, i64 *rowid) { | |
| 3810 | - sqlite3_stmt *stmt = NULL; | |
| 3811 | - int rc; | |
| 3812 | - char *zSql; | |
| 3813 | - zSql = sqlite3_mprintf("SELECT rowid" | |
| 3814 | - " FROM " VEC0_SHADOW_ROWIDS_NAME " WHERE id = ?", | |
| 3815 | - p->schemaName, p->tableName); | |
| 3816 | - if (!zSql) { | |
| 3817 | - rc = SQLITE_NOMEM; | |
| 3818 | - goto cleanup; | |
| 3819 | - } | |
| 3820 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 3821 | - sqlite3_free(zSql); | |
| 3822 | - if (rc != SQLITE_OK) { | |
| 3823 | - goto cleanup; | |
| 3824 | - } | |
| 3825 | - sqlite3_bind_value(stmt, 1, valueId); | |
| 3826 | - rc = sqlite3_step(stmt); | |
| 3827 | - if (rc == SQLITE_DONE) { | |
| 3828 | - rc = SQLITE_EMPTY; | |
| 3829 | - goto cleanup; | |
| 3830 | - } | |
| 3831 | - if (rc != SQLITE_ROW) { | |
| 3832 | - goto cleanup; | |
| 3833 | - } | |
| 3834 | - *rowid = sqlite3_column_int64(stmt, 0); | |
| 3835 | - rc = sqlite3_step(stmt); | |
| 3836 | - if (rc != SQLITE_DONE) { | |
| 3837 | - goto cleanup; | |
| 3838 | - } | |
| 3839 | - | |
| 3840 | - rc = SQLITE_OK; | |
| 3841 | - | |
| 3842 | -cleanup: | |
| 3843 | - sqlite3_finalize(stmt); | |
| 3844 | - return rc; | |
| 3845 | -} | |
| 3846 | - | |
| 3847 | -int vec0_result_id(vec0_vtab *p, sqlite3_context *context, i64 rowid) { | |
| 3848 | - if (!p->pkIsText) { | |
| 3849 | - sqlite3_result_int64(context, rowid); | |
| 3850 | - return SQLITE_OK; | |
| 3851 | - } | |
| 3852 | - sqlite3_value *valueId; | |
| 3853 | - int rc = vec0_get_id_value_from_rowid(p, rowid, &valueId); | |
| 3854 | - if (rc != SQLITE_OK) { | |
| 3855 | - return rc; | |
| 3856 | - } | |
| 3857 | - if (!valueId) { | |
| 3858 | - sqlite3_result_error_nomem(context); | |
| 3859 | - } else { | |
| 3860 | - sqlite3_result_value(context, valueId); | |
| 3861 | - sqlite3_value_free(valueId); | |
| 3862 | - } | |
| 3863 | - return SQLITE_OK; | |
| 3864 | -} | |
| 3865 | - | |
| 3866 | -/** | |
| 3867 | - * @brief | |
| 3868 | - * | |
| 3869 | - * @param pVtab: virtual table to query | |
| 3870 | - * @param rowid: row to lookup | |
| 3871 | - * @param vector_column_idx: which vector column to query | |
| 3872 | - * @param outVector: Output pointer to the vector buffer. | |
| 3873 | - * Must be sqlite3_free()'ed. | |
| 3874 | - * @param outVectorSize: Pointer to a int where the size of outVector | |
| 3875 | - * will be stored. | |
| 3876 | - * @return int SQLITE_OK on success. | |
| 3877 | - */ | |
| 3878 | -int vec0_get_vector_data(vec0_vtab *pVtab, i64 rowid, int vector_column_idx, | |
| 3879 | - void **outVector, int *outVectorSize) { | |
| 3880 | - vec0_vtab *p = pVtab; | |
| 3881 | - int rc, brc; | |
| 3882 | - i64 chunk_id; | |
| 3883 | - i64 chunk_offset; | |
| 3884 | - size_t size; | |
| 3885 | - void *buf = NULL; | |
| 3886 | - int blobOffset; | |
| 3887 | - sqlite3_blob *vectorBlob = NULL; | |
| 3888 | - assert((vector_column_idx >= 0) && | |
| 3889 | - (vector_column_idx < pVtab->numVectorColumns)); | |
| 3890 | - | |
| 3891 | - rc = vec0_get_chunk_position(pVtab, rowid, NULL, &chunk_id, &chunk_offset); | |
| 3892 | - if (rc == SQLITE_EMPTY) { | |
| 3893 | - vtab_set_error(&pVtab->base, "Could not find a row with rowid %lld", rowid); | |
| 3894 | - goto cleanup; | |
| 3895 | - } | |
| 3896 | - if (rc != SQLITE_OK) { | |
| 3897 | - goto cleanup; | |
| 3898 | - } | |
| 3899 | - | |
| 3900 | - rc = sqlite3_blob_open(p->db, p->schemaName, | |
| 3901 | - p->shadowVectorChunksNames[vector_column_idx], | |
| 3902 | - "vectors", chunk_id, 0, &vectorBlob); | |
| 3903 | - | |
| 3904 | - if (rc != SQLITE_OK) { | |
| 3905 | - vtab_set_error(&pVtab->base, | |
| 3906 | - "Could not fetch vector data for %lld, opening blob failed", | |
| 3907 | - rowid); | |
| 3908 | - rc = SQLITE_ERROR; | |
| 3909 | - goto cleanup; | |
| 3910 | - } | |
| 3911 | - | |
| 3912 | - size = vector_column_byte_size(pVtab->vector_columns[vector_column_idx]); | |
| 3913 | - blobOffset = chunk_offset * size; | |
| 3914 | - | |
| 3915 | - buf = sqlite3_malloc(size); | |
| 3916 | - if (!buf) { | |
| 3917 | - rc = SQLITE_NOMEM; | |
| 3918 | - goto cleanup; | |
| 3919 | - } | |
| 3920 | - | |
| 3921 | - rc = sqlite3_blob_read(vectorBlob, buf, size, blobOffset); | |
| 3922 | - if (rc != SQLITE_OK) { | |
| 3923 | - sqlite3_free(buf); | |
| 3924 | - buf = NULL; | |
| 3925 | - vtab_set_error( | |
| 3926 | - &pVtab->base, | |
| 3927 | - "Could not fetch vector data for %lld, reading from blob failed", | |
| 3928 | - rowid); | |
| 3929 | - rc = SQLITE_ERROR; | |
| 3930 | - goto cleanup; | |
| 3931 | - } | |
| 3932 | - | |
| 3933 | - *outVector = buf; | |
| 3934 | - if (outVectorSize) { | |
| 3935 | - *outVectorSize = size; | |
| 3936 | - } | |
| 3937 | - rc = SQLITE_OK; | |
| 3938 | - | |
| 3939 | -cleanup: | |
| 3940 | - brc = sqlite3_blob_close(vectorBlob); | |
| 3941 | - if ((rc == SQLITE_OK) && (brc != SQLITE_OK)) { | |
| 3942 | - vtab_set_error( | |
| 3943 | - &p->base, VEC_INTERAL_ERROR | |
| 3944 | - "unknown error, could not close vector blob, please file an issue"); | |
| 3945 | - return brc; | |
| 3946 | - } | |
| 3947 | - | |
| 3948 | - return rc; | |
| 3949 | -} | |
| 3950 | - | |
| 3951 | -/** | |
| 3952 | - * @brief Retrieve the sqlite3_value of the i'th partition value for the given row. | |
| 3953 | - * | |
| 3954 | - * @param pVtab - the vec0_vtab in questions | |
| 3955 | - * @param rowid - rowid of target row | |
| 3956 | - * @param partition_idx - which partition column to retrieve | |
| 3957 | - * @param outValue - output sqlite3_value | |
| 3958 | - * @return int - SQLITE_OK on success, otherwise error code | |
| 3959 | - */ | |
| 3960 | -int vec0_get_partition_value_for_rowid(vec0_vtab *pVtab, i64 rowid, int partition_idx, sqlite3_value ** outValue) { | |
| 3961 | - int rc; | |
| 3962 | - i64 chunk_id; | |
| 3963 | - i64 chunk_offset; | |
| 3964 | - rc = vec0_get_chunk_position(pVtab, rowid, NULL, &chunk_id, &chunk_offset); | |
| 3965 | - if(rc != SQLITE_OK) { | |
| 3966 | - return rc; | |
| 3967 | - } | |
| 3968 | - sqlite3_stmt * stmt = NULL; | |
| 3969 | - char * zSql = sqlite3_mprintf("SELECT partition%02d FROM " VEC0_SHADOW_CHUNKS_NAME " WHERE chunk_id = ?", partition_idx, pVtab->schemaName, pVtab->tableName); | |
| 3970 | - if(!zSql) { | |
| 3971 | - return SQLITE_NOMEM; | |
| 3972 | - } | |
| 3973 | - rc = sqlite3_prepare_v2(pVtab->db, zSql, -1, &stmt, NULL); | |
| 3974 | - sqlite3_free(zSql); | |
| 3975 | - if(rc != SQLITE_OK) { | |
| 3976 | - return rc; | |
| 3977 | - } | |
| 3978 | - sqlite3_bind_int64(stmt, 1, chunk_id); | |
| 3979 | - rc = sqlite3_step(stmt); | |
| 3980 | - if(rc != SQLITE_ROW) { | |
| 3981 | - rc = SQLITE_ERROR; | |
| 3982 | - goto done; | |
| 3983 | - } | |
| 3984 | - *outValue = sqlite3_value_dup(sqlite3_column_value(stmt, 0)); | |
| 3985 | - if(!*outValue) { | |
| 3986 | - rc = SQLITE_NOMEM; | |
| 3987 | - goto done; | |
| 3988 | - } | |
| 3989 | - rc = SQLITE_OK; | |
| 3990 | - | |
| 3991 | - done: | |
| 3992 | - sqlite3_finalize(stmt); | |
| 3993 | - return rc; | |
| 3994 | - | |
| 3995 | -} | |
| 3996 | - | |
| 3997 | -/** | |
| 3998 | - * @brief Get the value of an auxiliary column for the given rowid | |
| 3999 | - * | |
| 4000 | - * @param pVtab vec0_vtab | |
| 4001 | - * @param rowid the rowid of the row to lookup | |
| 4002 | - * @param auxiliary_idx aux index of the column we care about | |
| 4003 | - * @param outValue Output sqlite3_value to store | |
| 4004 | - * @return int SQLITE_OK on success, error code otherwise | |
| 4005 | - */ | |
| 4006 | -int vec0_get_auxiliary_value_for_rowid(vec0_vtab *pVtab, i64 rowid, int auxiliary_idx, sqlite3_value ** outValue) { | |
| 4007 | - int rc; | |
| 4008 | - sqlite3_stmt * stmt = NULL; | |
| 4009 | - char * zSql = sqlite3_mprintf("SELECT value%02d FROM " VEC0_SHADOW_AUXILIARY_NAME " WHERE rowid = ?", auxiliary_idx, pVtab->schemaName, pVtab->tableName); | |
| 4010 | - if(!zSql) { | |
| 4011 | - return SQLITE_NOMEM; | |
| 4012 | - } | |
| 4013 | - rc = sqlite3_prepare_v2(pVtab->db, zSql, -1, &stmt, NULL); | |
| 4014 | - sqlite3_free(zSql); | |
| 4015 | - if(rc != SQLITE_OK) { | |
| 4016 | - return rc; | |
| 4017 | - } | |
| 4018 | - sqlite3_bind_int64(stmt, 1, rowid); | |
| 4019 | - rc = sqlite3_step(stmt); | |
| 4020 | - if(rc != SQLITE_ROW) { | |
| 4021 | - rc = SQLITE_ERROR; | |
| 4022 | - goto done; | |
| 4023 | - } | |
| 4024 | - *outValue = sqlite3_value_dup(sqlite3_column_value(stmt, 0)); | |
| 4025 | - if(!*outValue) { | |
| 4026 | - rc = SQLITE_NOMEM; | |
| 4027 | - goto done; | |
| 4028 | - } | |
| 4029 | - rc = SQLITE_OK; | |
| 4030 | - | |
| 4031 | - done: | |
| 4032 | - sqlite3_finalize(stmt); | |
| 4033 | - return rc; | |
| 4034 | -} | |
| 4035 | - | |
| 4036 | -/** | |
| 4037 | - * @brief Result the given metadata value for the given row and metadata column index. | |
| 4038 | - * Will traverse the metadatachunksNN table with BLOB I/0 for the given rowid. | |
| 4039 | - * | |
| 4040 | - * @param p | |
| 4041 | - * @param rowid | |
| 4042 | - * @param metadata_idx | |
| 4043 | - * @param context | |
| 4044 | - * @return int | |
| 4045 | - */ | |
| 4046 | -int vec0_result_metadata_value_for_rowid(vec0_vtab *p, i64 rowid, int metadata_idx, sqlite3_context * context) { | |
| 4047 | - int rc; | |
| 4048 | - i64 chunk_id; | |
| 4049 | - i64 chunk_offset; | |
| 4050 | - rc = vec0_get_chunk_position(p, rowid, NULL, &chunk_id, &chunk_offset); | |
| 4051 | - if(rc != SQLITE_OK) { | |
| 4052 | - return rc; | |
| 4053 | - } | |
| 4054 | - sqlite3_blob * blobValue; | |
| 4055 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowMetadataChunksNames[metadata_idx], "data", chunk_id, 0, &blobValue); | |
| 4056 | - if(rc != SQLITE_OK) { | |
| 4057 | - return rc; | |
| 4058 | - } | |
| 4059 | - | |
| 4060 | - switch(p->metadata_columns[metadata_idx].kind) { | |
| 4061 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | |
| 4062 | - u8 block; | |
| 4063 | - rc = sqlite3_blob_read(blobValue, &block, sizeof(block), chunk_offset / CHAR_BIT); | |
| 4064 | - if(rc != SQLITE_OK) { | |
| 4065 | - goto done; | |
| 4066 | - } | |
| 4067 | - int value = block >> ((chunk_offset % CHAR_BIT)) & 1; | |
| 4068 | - sqlite3_result_int(context, value); | |
| 4069 | - break; | |
| 4070 | - } | |
| 4071 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | |
| 4072 | - i64 value; | |
| 4073 | - rc = sqlite3_blob_read(blobValue, &value, sizeof(value), chunk_offset * sizeof(i64)); | |
| 4074 | - if(rc != SQLITE_OK) { | |
| 4075 | - goto done; | |
| 4076 | - } | |
| 4077 | - sqlite3_result_int64(context, value); | |
| 4078 | - break; | |
| 4079 | - } | |
| 4080 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | |
| 4081 | - double value; | |
| 4082 | - rc = sqlite3_blob_read(blobValue, &value, sizeof(value), chunk_offset * sizeof(double)); | |
| 4083 | - if(rc != SQLITE_OK) { | |
| 4084 | - goto done; | |
| 4085 | - } | |
| 4086 | - sqlite3_result_double(context, value); | |
| 4087 | - break; | |
| 4088 | - } | |
| 4089 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | |
| 4090 | - u8 view[VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | |
| 4091 | - rc = sqlite3_blob_read(blobValue, &view, VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH, chunk_offset * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | |
| 4092 | - if(rc != SQLITE_OK) { | |
| 4093 | - goto done; | |
| 4094 | - } | |
| 4095 | - int length = ((int *)view)[0]; | |
| 4096 | - if(length <= VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | |
| 4097 | - sqlite3_result_text(context, (const char*) (view + 4), length, SQLITE_TRANSIENT); | |
| 4098 | - } | |
| 4099 | - else { | |
| 4100 | - sqlite3_stmt * stmt; | |
| 4101 | - const char * zSql = sqlite3_mprintf("SELECT data FROM " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " WHERE rowid = ?", p->schemaName, p->tableName, metadata_idx); | |
| 4102 | - if(!zSql) { | |
| 4103 | - rc = SQLITE_ERROR; | |
| 4104 | - goto done; | |
| 4105 | - } | |
| 4106 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 4107 | - sqlite3_free((void *) zSql); | |
| 4108 | - if(rc != SQLITE_OK) { | |
| 4109 | - goto done; | |
| 4110 | - } | |
| 4111 | - sqlite3_bind_int64(stmt, 1, rowid); | |
| 4112 | - rc = sqlite3_step(stmt); | |
| 4113 | - if(rc != SQLITE_ROW) { | |
| 4114 | - sqlite3_finalize(stmt); | |
| 4115 | - rc = SQLITE_ERROR; | |
| 4116 | - goto done; | |
| 4117 | - } | |
| 4118 | - sqlite3_result_value(context, sqlite3_column_value(stmt, 0)); | |
| 4119 | - sqlite3_finalize(stmt); | |
| 4120 | - rc = SQLITE_OK; | |
| 4121 | - } | |
| 4122 | - break; | |
| 4123 | - } | |
| 4124 | - } | |
| 4125 | - done: | |
| 4126 | - // blobValue is read-only, will not fail on close | |
| 4127 | - sqlite3_blob_close(blobValue); | |
| 4128 | - return rc; | |
| 4129 | - | |
| 4130 | -} | |
| 4131 | - | |
| 4132 | -int vec0_get_latest_chunk_rowid(vec0_vtab *p, i64 *chunk_rowid, sqlite3_value ** partitionKeyValues) { | |
| 4133 | - int rc; | |
| 4134 | - const char *zSql; | |
| 4135 | - // lazy initialize stmtLatestChunk when needed. May be cleared during xSync() | |
| 4136 | - if (!p->stmtLatestChunk) { | |
| 4137 | - if(p->numPartitionColumns > 0) { | |
| 4138 | - sqlite3_str * s = sqlite3_str_new(NULL); | |
| 4139 | - sqlite3_str_appendf(s, "SELECT max(rowid) FROM " VEC0_SHADOW_CHUNKS_NAME " WHERE ", | |
| 4140 | - p->schemaName, p->tableName); | |
| 4141 | - | |
| 4142 | - for(int i = 0; i < p->numPartitionColumns; i++) { | |
| 4143 | - if(i != 0) { | |
| 4144 | - sqlite3_str_appendall(s, " AND "); | |
| 4145 | - } | |
| 4146 | - sqlite3_str_appendf(s, " partition%02d = ? ", i); | |
| 4147 | - } | |
| 4148 | - zSql = sqlite3_str_finish(s); | |
| 4149 | - }else { | |
| 4150 | - zSql = sqlite3_mprintf("SELECT max(rowid) FROM " VEC0_SHADOW_CHUNKS_NAME, | |
| 4151 | - p->schemaName, p->tableName); | |
| 4152 | - } | |
| 4153 | - | |
| 4154 | - if (!zSql) { | |
| 4155 | - rc = SQLITE_NOMEM; | |
| 4156 | - goto cleanup; | |
| 4157 | - } | |
| 4158 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &p->stmtLatestChunk, 0); | |
| 4159 | - sqlite3_free((void *)zSql); | |
| 4160 | - if (rc != SQLITE_OK) { | |
| 4161 | - // IMP: V21406_05476 | |
| 4162 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | |
| 4163 | - "could not initialize 'latest chunk' statement"); | |
| 4164 | - goto cleanup; | |
| 4165 | - } | |
| 4166 | - } | |
| 4167 | - | |
| 4168 | - for(int i = 0; i < p->numPartitionColumns; i++) { | |
| 4169 | - sqlite3_bind_value(p->stmtLatestChunk, i+1, (partitionKeyValues[i])); | |
| 4170 | - } | |
| 4171 | - | |
| 4172 | - rc = sqlite3_step(p->stmtLatestChunk); | |
| 4173 | - if (rc != SQLITE_ROW) { | |
| 4174 | - // IMP: V31559_15629 | |
| 4175 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR "Could not find latest chunk"); | |
| 4176 | - rc = SQLITE_ERROR; | |
| 4177 | - goto cleanup; | |
| 4178 | - } | |
| 4179 | - if(sqlite3_column_type(p->stmtLatestChunk, 0) == SQLITE_NULL){ | |
| 4180 | - rc = SQLITE_EMPTY; | |
| 4181 | - goto cleanup; | |
| 4182 | - } | |
| 4183 | - *chunk_rowid = sqlite3_column_int64(p->stmtLatestChunk, 0); | |
| 4184 | - rc = sqlite3_step(p->stmtLatestChunk); | |
| 4185 | - if (rc != SQLITE_DONE) { | |
| 4186 | - vtab_set_error(&p->base, | |
| 4187 | - VEC_INTERAL_ERROR | |
| 4188 | - "unknown result code when closing out stmtLatestChunk. " | |
| 4189 | - "Please file an issue: " REPORT_URL, | |
| 4190 | - p->schemaName, p->shadowChunksName); | |
| 4191 | - goto cleanup; | |
| 4192 | - } | |
| 4193 | - rc = SQLITE_OK; | |
| 4194 | - | |
| 4195 | -cleanup: | |
| 4196 | - if (p->stmtLatestChunk) { | |
| 4197 | - sqlite3_reset(p->stmtLatestChunk); | |
| 4198 | - sqlite3_clear_bindings(p->stmtLatestChunk); | |
| 4199 | - } | |
| 4200 | - return rc; | |
| 4201 | -} | |
| 4202 | - | |
| 4203 | -int vec0_rowids_insert_rowid(vec0_vtab *p, i64 rowid) { | |
| 4204 | - int rc = SQLITE_OK; | |
| 4205 | - int entered = 0; | |
| 4206 | - UNUSED_PARAMETER(entered); // temporary | |
| 4207 | - if (!p->stmtRowidsInsertRowid) { | |
| 4208 | - const char *zSql = | |
| 4209 | - sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_ROWIDS_NAME "(rowid)" | |
| 4210 | - "VALUES (?);", | |
| 4211 | - p->schemaName, p->tableName); | |
| 4212 | - if (!zSql) { | |
| 4213 | - rc = SQLITE_NOMEM; | |
| 4214 | - goto cleanup; | |
| 4215 | - } | |
| 4216 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &p->stmtRowidsInsertRowid, 0); | |
| 4217 | - sqlite3_free((void *)zSql); | |
| 4218 | - if (rc != SQLITE_OK) { | |
| 4219 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | |
| 4220 | - "could not initialize 'insert rowids' statement"); | |
| 4221 | - goto cleanup; | |
| 4222 | - } | |
| 4223 | - } | |
| 4224 | - | |
| 4225 | -#if SQLITE_THREADSAFE | |
| 4226 | - if (sqlite3_mutex_enter) { | |
| 4227 | - sqlite3_mutex_enter(sqlite3_db_mutex(p->db)); | |
| 4228 | - entered = 1; | |
| 4229 | - } | |
| 4230 | -#endif | |
| 4231 | - sqlite3_bind_int64(p->stmtRowidsInsertRowid, 1, rowid); | |
| 4232 | - rc = sqlite3_step(p->stmtRowidsInsertRowid); | |
| 4233 | - | |
| 4234 | - if (rc != SQLITE_DONE) { | |
| 4235 | - if (sqlite3_extended_errcode(p->db) == SQLITE_CONSTRAINT_PRIMARYKEY) { | |
| 4236 | - // IMP: V17090_01160 | |
| 4237 | - vtab_set_error(&p->base, "UNIQUE constraint failed on %s primary key", | |
| 4238 | - p->tableName); | |
| 4239 | - } else { | |
| 4240 | - // IMP: V04679_21517 | |
| 4241 | - vtab_set_error(&p->base, | |
| 4242 | - "Error inserting rowid into rowids shadow table: %s", | |
| 4243 | - sqlite3_errmsg(sqlite3_db_handle(p->stmtRowidsInsertId))); | |
| 4244 | - } | |
| 4245 | - rc = SQLITE_ERROR; | |
| 4246 | - goto cleanup; | |
| 4247 | - } | |
| 4248 | - | |
| 4249 | - rc = SQLITE_OK; | |
| 4250 | - | |
| 4251 | -cleanup: | |
| 4252 | - if (p->stmtRowidsInsertRowid) { | |
| 4253 | - sqlite3_reset(p->stmtRowidsInsertRowid); | |
| 4254 | - sqlite3_clear_bindings(p->stmtRowidsInsertRowid); | |
| 4255 | - } | |
| 4256 | - | |
| 4257 | -#if SQLITE_THREADSAFE | |
| 4258 | - if (sqlite3_mutex_leave && entered) { | |
| 4259 | - sqlite3_mutex_leave(sqlite3_db_mutex(p->db)); | |
| 4260 | - } | |
| 4261 | -#endif | |
| 4262 | - return rc; | |
| 4263 | -} | |
| 4264 | - | |
| 4265 | -int vec0_rowids_insert_id(vec0_vtab *p, sqlite3_value *idValue, i64 *rowid) { | |
| 4266 | - int rc = SQLITE_OK; | |
| 4267 | - int entered = 0; | |
| 4268 | - UNUSED_PARAMETER(entered); // temporary | |
| 4269 | - if (!p->stmtRowidsInsertId) { | |
| 4270 | - const char *zSql = | |
| 4271 | - sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_ROWIDS_NAME "(id)" | |
| 4272 | - "VALUES (?);", | |
| 4273 | - p->schemaName, p->tableName); | |
| 4274 | - if (!zSql) { | |
| 4275 | - rc = SQLITE_NOMEM; | |
| 4276 | - goto complete; | |
| 4277 | - } | |
| 4278 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &p->stmtRowidsInsertId, 0); | |
| 4279 | - sqlite3_free((void *)zSql); | |
| 4280 | - if (rc != SQLITE_OK) { | |
| 4281 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | |
| 4282 | - "could not initialize 'insert rowids id' statement"); | |
| 4283 | - goto complete; | |
| 4284 | - } | |
| 4285 | - } | |
| 4286 | - | |
| 4287 | -#if SQLITE_THREADSAFE | |
| 4288 | - if (sqlite3_mutex_enter) { | |
| 4289 | - sqlite3_mutex_enter(sqlite3_db_mutex(p->db)); | |
| 4290 | - entered = 1; | |
| 4291 | - } | |
| 4292 | -#endif | |
| 4293 | - | |
| 4294 | - if (idValue) { | |
| 4295 | - sqlite3_bind_value(p->stmtRowidsInsertId, 1, idValue); | |
| 4296 | - } | |
| 4297 | - rc = sqlite3_step(p->stmtRowidsInsertId); | |
| 4298 | - | |
| 4299 | - if (rc != SQLITE_DONE) { | |
| 4300 | - if (sqlite3_extended_errcode(p->db) == SQLITE_CONSTRAINT_UNIQUE) { | |
| 4301 | - // IMP: V20497_04568 | |
| 4302 | - vtab_set_error(&p->base, "UNIQUE constraint failed on %s primary key", | |
| 4303 | - p->tableName); | |
| 4304 | - } else { | |
| 4305 | - // IMP: V24016_08086 | |
| 4306 | - // IMP: V15177_32015 | |
| 4307 | - vtab_set_error(&p->base, | |
| 4308 | - "Error inserting id into rowids shadow table: %s", | |
| 4309 | - sqlite3_errmsg(sqlite3_db_handle(p->stmtRowidsInsertId))); | |
| 4310 | - } | |
| 4311 | - rc = SQLITE_ERROR; | |
| 4312 | - goto complete; | |
| 4313 | - } | |
| 4314 | - | |
| 4315 | - *rowid = sqlite3_last_insert_rowid(p->db); | |
| 4316 | - rc = SQLITE_OK; | |
| 4317 | - | |
| 4318 | -complete: | |
| 4319 | - if (p->stmtRowidsInsertId) { | |
| 4320 | - sqlite3_reset(p->stmtRowidsInsertId); | |
| 4321 | - sqlite3_clear_bindings(p->stmtRowidsInsertId); | |
| 4322 | - } | |
| 4323 | - | |
| 4324 | -#if SQLITE_THREADSAFE | |
| 4325 | - if (sqlite3_mutex_leave && entered) { | |
| 4326 | - sqlite3_mutex_leave(sqlite3_db_mutex(p->db)); | |
| 4327 | - } | |
| 4328 | -#endif | |
| 4329 | - return rc; | |
| 4330 | -} | |
| 4331 | - | |
| 4332 | -int vec0_metadata_chunk_size(vec0_metadata_column_kind kind, int chunk_size) { | |
| 4333 | - switch(kind) { | |
| 4334 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: | |
| 4335 | - return chunk_size / 8; | |
| 4336 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: | |
| 4337 | - return chunk_size * sizeof(i64); | |
| 4338 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: | |
| 4339 | - return chunk_size * sizeof(double); | |
| 4340 | - case VEC0_METADATA_COLUMN_KIND_TEXT: | |
| 4341 | - return chunk_size * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH; | |
| 4342 | - } | |
| 4343 | - return 0; | |
| 4344 | -} | |
| 4345 | - | |
| 4346 | -int vec0_rowids_update_position(vec0_vtab *p, i64 rowid, i64 chunk_rowid, | |
| 4347 | - i64 chunk_offset) { | |
| 4348 | - int rc = SQLITE_OK; | |
| 4349 | - | |
| 4350 | - if (!p->stmtRowidsUpdatePosition) { | |
| 4351 | - const char *zSql = sqlite3_mprintf(" UPDATE " VEC0_SHADOW_ROWIDS_NAME | |
| 4352 | - " SET chunk_id = ?, chunk_offset = ?" | |
| 4353 | - " WHERE rowid = ?", | |
| 4354 | - p->schemaName, p->tableName); | |
| 4355 | - if (!zSql) { | |
| 4356 | - rc = SQLITE_NOMEM; | |
| 4357 | - goto cleanup; | |
| 4358 | - } | |
| 4359 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &p->stmtRowidsUpdatePosition, 0); | |
| 4360 | - sqlite3_free((void *)zSql); | |
| 4361 | - if (rc != SQLITE_OK) { | |
| 4362 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | |
| 4363 | - "could not initialize 'update rowids position' statement"); | |
| 4364 | - goto cleanup; | |
| 4365 | - } | |
| 4366 | - } | |
| 4367 | - | |
| 4368 | - sqlite3_bind_int64(p->stmtRowidsUpdatePosition, 1, chunk_rowid); | |
| 4369 | - sqlite3_bind_int64(p->stmtRowidsUpdatePosition, 2, chunk_offset); | |
| 4370 | - sqlite3_bind_int64(p->stmtRowidsUpdatePosition, 3, rowid); | |
| 4371 | - | |
| 4372 | - rc = sqlite3_step(p->stmtRowidsUpdatePosition); | |
| 4373 | - if (rc != SQLITE_DONE) { | |
| 4374 | - // IMP: V21925_05995 | |
| 4375 | - vtab_set_error(&p->base, | |
| 4376 | - VEC_INTERAL_ERROR | |
| 4377 | - "could not update rowids position for rowid=%lld, " | |
| 4378 | - "chunk_rowid=%lld, chunk_offset=%lld", | |
| 4379 | - rowid, chunk_rowid, chunk_offset); | |
| 4380 | - rc = SQLITE_ERROR; | |
| 4381 | - goto cleanup; | |
| 4382 | - } | |
| 4383 | - rc = SQLITE_OK; | |
| 4384 | - | |
| 4385 | -cleanup: | |
| 4386 | - if (p->stmtRowidsUpdatePosition) { | |
| 4387 | - sqlite3_reset(p->stmtRowidsUpdatePosition); | |
| 4388 | - sqlite3_clear_bindings(p->stmtRowidsUpdatePosition); | |
| 4389 | - } | |
| 4390 | - | |
| 4391 | - return rc; | |
| 4392 | -} | |
| 4393 | - | |
| 4394 | -/** | |
| 4395 | - * @brief Adds a new chunk for the vec0 table, and the corresponding vector | |
| 4396 | - * chunks. | |
| 4397 | - * | |
| 4398 | - * Inserts a new row into the _chunks table, with blank data, and uses that new | |
| 4399 | - * rowid to insert new blank rows into _vector_chunksXX tables. | |
| 4400 | - * | |
| 4401 | - * @param p: vec0 table to add new chunk | |
| 4402 | - * @param paritionKeyValues: Array of partition key valeus for the new chunk, if available | |
| 4403 | - * @param chunk_rowid: Output pointer, if not NULL, then will be filled with the | |
| 4404 | - * new chunk rowid. | |
| 4405 | - * @return int SQLITE_OK on success, error code otherwise. | |
| 4406 | - */ | |
| 4407 | -int vec0_new_chunk(vec0_vtab *p, sqlite3_value ** partitionKeyValues, i64 *chunk_rowid) { | |
| 4408 | - int rc; | |
| 4409 | - char *zSql; | |
| 4410 | - sqlite3_stmt *stmt; | |
| 4411 | - i64 rowid; | |
| 4412 | - | |
| 4413 | - // Step 1: Insert a new row in _chunks, capture that new rowid | |
| 4414 | - if(p->numPartitionColumns > 0) { | |
| 4415 | - sqlite3_str * s = sqlite3_str_new(NULL); | |
| 4416 | - sqlite3_str_appendf(s, "INSERT INTO " VEC0_SHADOW_CHUNKS_NAME, p->schemaName, p->tableName); | |
| 4417 | - sqlite3_str_appendall(s, "(size, validity, rowids"); | |
| 4418 | - for(int i = 0; i < p->numPartitionColumns; i++) { | |
| 4419 | - sqlite3_str_appendf(s, ", partition%02d", i); | |
| 4420 | - } | |
| 4421 | - sqlite3_str_appendall(s, ") VALUES (?, ?, ?"); | |
| 4422 | - for(int i = 0; i < p->numPartitionColumns; i++) { | |
| 4423 | - sqlite3_str_appendall(s, ", ?"); | |
| 4424 | - } | |
| 4425 | - sqlite3_str_appendall(s, ")"); | |
| 4426 | - | |
| 4427 | - zSql = sqlite3_str_finish(s); | |
| 4428 | - }else { | |
| 4429 | - zSql = sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_CHUNKS_NAME | |
| 4430 | - "(size, validity, rowids) " | |
| 4431 | - "VALUES (?, ?, ?);", | |
| 4432 | - p->schemaName, p->tableName); | |
| 4433 | - } | |
| 4434 | - | |
| 4435 | - if (!zSql) { | |
| 4436 | - return SQLITE_NOMEM; | |
| 4437 | - } | |
| 4438 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 4439 | - sqlite3_free(zSql); | |
| 4440 | - if (rc != SQLITE_OK) { | |
| 4441 | - sqlite3_finalize(stmt); | |
| 4442 | - return rc; | |
| 4443 | - } | |
| 4444 | - | |
| 4445 | -#if SQLITE_THREADSAFE | |
| 4446 | - if (sqlite3_mutex_enter) { | |
| 4447 | - sqlite3_mutex_enter(sqlite3_db_mutex(p->db)); | |
| 4448 | - } | |
| 4449 | -#endif | |
| 4450 | - | |
| 4451 | - sqlite3_bind_int64(stmt, 1, p->chunk_size); // size | |
| 4452 | - sqlite3_bind_zeroblob(stmt, 2, p->chunk_size / CHAR_BIT); // validity bitmap | |
| 4453 | - sqlite3_bind_zeroblob(stmt, 3, p->chunk_size * sizeof(i64)); // rowids | |
| 4454 | - | |
| 4455 | - for(int i = 0; i < p->numPartitionColumns; i++) { | |
| 4456 | - sqlite3_bind_value(stmt, 4 + i, partitionKeyValues[i]); | |
| 4457 | - } | |
| 4458 | - | |
| 4459 | - rc = sqlite3_step(stmt); | |
| 4460 | - int failed = rc != SQLITE_DONE; | |
| 4461 | - rowid = sqlite3_last_insert_rowid(p->db); | |
| 4462 | -#if SQLITE_THREADSAFE | |
| 4463 | - if (sqlite3_mutex_leave) { | |
| 4464 | - sqlite3_mutex_leave(sqlite3_db_mutex(p->db)); | |
| 4465 | - } | |
| 4466 | -#endif | |
| 4467 | - sqlite3_finalize(stmt); | |
| 4468 | - if (failed) { | |
| 4469 | - return SQLITE_ERROR; | |
| 4470 | - } | |
| 4471 | - | |
| 4472 | - // Step 2: Create new vector chunks for each vector column, with | |
| 4473 | - // that new chunk_rowid. | |
| 4474 | - | |
| 4475 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | |
| 4476 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_VECTOR) { | |
| 4477 | - continue; | |
| 4478 | - } | |
| 4479 | - int vector_column_idx = p->user_column_idxs[i]; | |
| 4480 | - i64 vectorsSize = | |
| 4481 | - p->chunk_size * vector_column_byte_size(p->vector_columns[vector_column_idx]); | |
| 4482 | - | |
| 4483 | - zSql = sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_VECTOR_N_NAME | |
| 4484 | - "(rowid, vectors)" | |
| 4485 | - "VALUES (?, ?)", | |
| 4486 | - p->schemaName, p->tableName, vector_column_idx); | |
| 4487 | - if (!zSql) { | |
| 4488 | - return SQLITE_NOMEM; | |
| 4489 | - } | |
| 4490 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 4491 | - sqlite3_free(zSql); | |
| 4492 | - | |
| 4493 | - if (rc != SQLITE_OK) { | |
| 4494 | - sqlite3_finalize(stmt); | |
| 4495 | - return rc; | |
| 4496 | - } | |
| 4497 | - | |
| 4498 | - sqlite3_bind_int64(stmt, 1, rowid); | |
| 4499 | - sqlite3_bind_zeroblob64(stmt, 2, vectorsSize); | |
| 4500 | - | |
| 4501 | - rc = sqlite3_step(stmt); | |
| 4502 | - sqlite3_finalize(stmt); | |
| 4503 | - if (rc != SQLITE_DONE) { | |
| 4504 | - return rc; | |
| 4505 | - } | |
| 4506 | - } | |
| 4507 | - | |
| 4508 | - // Step 3: Create new metadata chunks for each metadata column | |
| 4509 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | |
| 4510 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_METADATA) { | |
| 4511 | - continue; | |
| 4512 | - } | |
| 4513 | - int metadata_column_idx = p->user_column_idxs[i]; | |
| 4514 | - zSql = sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_METADATA_N_NAME | |
| 4515 | - "(rowid, data)" | |
| 4516 | - "VALUES (?, ?)", | |
| 4517 | - p->schemaName, p->tableName, metadata_column_idx); | |
| 4518 | - if (!zSql) { | |
| 4519 | - return SQLITE_NOMEM; | |
| 4520 | - } | |
| 4521 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 4522 | - sqlite3_free(zSql); | |
| 4523 | - | |
| 4524 | - if (rc != SQLITE_OK) { | |
| 4525 | - sqlite3_finalize(stmt); | |
| 4526 | - return rc; | |
| 4527 | - } | |
| 4528 | - | |
| 4529 | - sqlite3_bind_int64(stmt, 1, rowid); | |
| 4530 | - sqlite3_bind_zeroblob64(stmt, 2, vec0_metadata_chunk_size(p->metadata_columns[metadata_column_idx].kind, p->chunk_size)); | |
| 4531 | - | |
| 4532 | - rc = sqlite3_step(stmt); | |
| 4533 | - sqlite3_finalize(stmt); | |
| 4534 | - if (rc != SQLITE_DONE) { | |
| 4535 | - return rc; | |
| 4536 | - } | |
| 4537 | - } | |
| 4538 | - | |
| 4539 | - | |
| 4540 | - if (chunk_rowid) { | |
| 4541 | - *chunk_rowid = rowid; | |
| 4542 | - } | |
| 4543 | - | |
| 4544 | - return SQLITE_OK; | |
| 4545 | -} | |
| 4546 | - | |
| 4547 | -struct vec0_query_fullscan_data { | |
| 4548 | - sqlite3_stmt *rowids_stmt; | |
| 4549 | - i8 done; | |
| 4550 | -}; | |
| 4551 | -void vec0_query_fullscan_data_clear( | |
| 4552 | - struct vec0_query_fullscan_data *fullscan_data) { | |
| 4553 | - if (!fullscan_data) | |
| 4554 | - return; | |
| 4555 | - | |
| 4556 | - if (fullscan_data->rowids_stmt) { | |
| 4557 | - sqlite3_finalize(fullscan_data->rowids_stmt); | |
| 4558 | - fullscan_data->rowids_stmt = NULL; | |
| 4559 | - } | |
| 4560 | -} | |
| 4561 | - | |
| 4562 | -struct vec0_query_knn_data { | |
| 4563 | - i64 k; | |
| 4564 | - i64 k_used; | |
| 4565 | - // Array of rowids of size k. Must be freed with sqlite3_free(). | |
| 4566 | - i64 *rowids; | |
| 4567 | - // Array of distances of size k. Must be freed with sqlite3_free(). | |
| 4568 | - f32 *distances; | |
| 4569 | - i64 current_idx; | |
| 4570 | -}; | |
| 4571 | -void vec0_query_knn_data_clear(struct vec0_query_knn_data *knn_data) { | |
| 4572 | - if (!knn_data) | |
| 4573 | - return; | |
| 4574 | - | |
| 4575 | - if (knn_data->rowids) { | |
| 4576 | - sqlite3_free(knn_data->rowids); | |
| 4577 | - knn_data->rowids = NULL; | |
| 4578 | - } | |
| 4579 | - if (knn_data->distances) { | |
| 4580 | - sqlite3_free(knn_data->distances); | |
| 4581 | - knn_data->distances = NULL; | |
| 4582 | - } | |
| 4583 | -} | |
| 4584 | - | |
| 4585 | -struct vec0_query_point_data { | |
| 4586 | - i64 rowid; | |
| 4587 | - void *vectors[VEC0_MAX_VECTOR_COLUMNS]; | |
| 4588 | - int done; | |
| 4589 | -}; | |
| 4590 | -void vec0_query_point_data_clear(struct vec0_query_point_data *point_data) { | |
| 4591 | - if (!point_data) | |
| 4592 | - return; | |
| 4593 | - for (int i = 0; i < VEC0_MAX_VECTOR_COLUMNS; i++) { | |
| 4594 | - sqlite3_free(point_data->vectors[i]); | |
| 4595 | - point_data->vectors[i] = NULL; | |
| 4596 | - } | |
| 4597 | -} | |
| 4598 | - | |
| 4599 | -typedef enum { | |
| 4600 | - // If any values are updated, please update the ARCHITECTURE.md docs accordingly! | |
| 4601 | - | |
| 4602 | - VEC0_QUERY_PLAN_FULLSCAN = '1', | |
| 4603 | - VEC0_QUERY_PLAN_POINT = '2', | |
| 4604 | - VEC0_QUERY_PLAN_KNN = '3', | |
| 4605 | -} vec0_query_plan; | |
| 4606 | - | |
| 4607 | -typedef struct vec0_cursor vec0_cursor; | |
| 4608 | -struct vec0_cursor { | |
| 4609 | - sqlite3_vtab_cursor base; | |
| 4610 | - | |
| 4611 | - vec0_query_plan query_plan; | |
| 4612 | - struct vec0_query_fullscan_data *fullscan_data; | |
| 4613 | - struct vec0_query_knn_data *knn_data; | |
| 4614 | - struct vec0_query_point_data *point_data; | |
| 4615 | -}; | |
| 4616 | - | |
| 4617 | -void vec0_cursor_clear(vec0_cursor *pCur) { | |
| 4618 | - if (pCur->fullscan_data) { | |
| 4619 | - vec0_query_fullscan_data_clear(pCur->fullscan_data); | |
| 4620 | - sqlite3_free(pCur->fullscan_data); | |
| 4621 | - pCur->fullscan_data = NULL; | |
| 4622 | - } | |
| 4623 | - if (pCur->knn_data) { | |
| 4624 | - vec0_query_knn_data_clear(pCur->knn_data); | |
| 4625 | - sqlite3_free(pCur->knn_data); | |
| 4626 | - pCur->knn_data = NULL; | |
| 4627 | - } | |
| 4628 | - if (pCur->point_data) { | |
| 4629 | - vec0_query_point_data_clear(pCur->point_data); | |
| 4630 | - sqlite3_free(pCur->point_data); | |
| 4631 | - pCur->point_data = NULL; | |
| 4632 | - } | |
| 4633 | -} | |
| 4634 | - | |
| 4635 | -#define VEC_CONSTRUCTOR_ERROR "vec0 constructor error: " | |
| 4636 | -static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv, | |
| 4637 | - sqlite3_vtab **ppVtab, char **pzErr, bool isCreate) { | |
| 4638 | - UNUSED_PARAMETER(pAux); | |
| 4639 | - vec0_vtab *pNew; | |
| 4640 | - int rc; | |
| 4641 | - const char *zSql; | |
| 4642 | - | |
| 4643 | - pNew = sqlite3_malloc(sizeof(*pNew)); | |
| 4644 | - if (pNew == 0) | |
| 4645 | - return SQLITE_NOMEM; | |
| 4646 | - memset(pNew, 0, sizeof(*pNew)); | |
| 4647 | - | |
| 4648 | - // Declared chunk_size=N for entire table. | |
| 4649 | - // -1 to use the defualt, otherwise will get re-assigned on `chunk_size=N` | |
| 4650 | - // option | |
| 4651 | - int chunk_size = -1; | |
| 4652 | - int numVectorColumns = 0; | |
| 4653 | - int numPartitionColumns = 0; | |
| 4654 | - int numAuxiliaryColumns = 0; | |
| 4655 | - int numMetadataColumns = 0; | |
| 4656 | - int user_column_idx = 0; | |
| 4657 | - | |
| 4658 | - // track if a "primary key" column is defined | |
| 4659 | - char *pkColumnName = NULL; | |
| 4660 | - int pkColumnNameLength; | |
| 4661 | - int pkColumnType = SQLITE_INTEGER; | |
| 4662 | - | |
| 4663 | - for (int i = 3; i < argc; i++) { | |
| 4664 | - struct VectorColumnDefinition vecColumn; | |
| 4665 | - struct Vec0PartitionColumnDefinition partitionColumn; | |
| 4666 | - struct Vec0AuxiliaryColumnDefinition auxColumn; | |
| 4667 | - struct Vec0MetadataColumnDefinition metadataColumn; | |
| 4668 | - char *cName = NULL; | |
| 4669 | - int cNameLength; | |
| 4670 | - int cType; | |
| 4671 | - | |
| 4672 | - // Scenario #1: Constructor argument is a vector column definition, ie `foo float[1024]` | |
| 4673 | - rc = vec0_parse_vector_column(argv[i], strlen(argv[i]), &vecColumn); | |
| 4674 | - if (rc == SQLITE_ERROR) { | |
| 4675 | - *pzErr = sqlite3_mprintf( | |
| 4676 | - VEC_CONSTRUCTOR_ERROR "could not parse vector column '%s'", argv[i]); | |
| 4677 | - goto error; | |
| 4678 | - } | |
| 4679 | - if (rc == SQLITE_OK) { | |
| 4680 | - if (numVectorColumns >= VEC0_MAX_VECTOR_COLUMNS) { | |
| 4681 | - sqlite3_free(vecColumn.name); | |
| 4682 | - *pzErr = sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR | |
| 4683 | - "Too many provided vector columns, maximum %d", | |
| 4684 | - VEC0_MAX_VECTOR_COLUMNS); | |
| 4685 | - goto error; | |
| 4686 | - } | |
| 4687 | - | |
| 4688 | - if (vecColumn.dimensions > SQLITE_VEC_VEC0_MAX_DIMENSIONS) { | |
| 4689 | - sqlite3_free(vecColumn.name); | |
| 4690 | - *pzErr = sqlite3_mprintf( | |
| 4691 | - VEC_CONSTRUCTOR_ERROR | |
| 4692 | - "Dimension on vector column too large, provided %lld, maximum %lld", | |
| 4693 | - (i64)vecColumn.dimensions, SQLITE_VEC_VEC0_MAX_DIMENSIONS); | |
| 4694 | - goto error; | |
| 4695 | - } | |
| 4696 | - pNew->user_column_kinds[user_column_idx] = SQLITE_VEC0_USER_COLUMN_KIND_VECTOR; | |
| 4697 | - pNew->user_column_idxs[user_column_idx] = numVectorColumns; | |
| 4698 | - memcpy(&pNew->vector_columns[numVectorColumns], &vecColumn, sizeof(vecColumn)); | |
| 4699 | - numVectorColumns++; | |
| 4700 | - user_column_idx++; | |
| 4701 | - | |
| 4702 | - continue; | |
| 4703 | - } | |
| 4704 | - | |
| 4705 | - // Scenario #2: Constructor argument is a partition key column definition, ie `user_id text partition key` | |
| 4706 | - rc = vec0_parse_partition_key_definition(argv[i], strlen(argv[i]), &cName, | |
| 4707 | - &cNameLength, &cType); | |
| 4708 | - if (rc == SQLITE_OK) { | |
| 4709 | - if (numPartitionColumns >= VEC0_MAX_PARTITION_COLUMNS) { | |
| 4710 | - *pzErr = sqlite3_mprintf( | |
| 4711 | - VEC_CONSTRUCTOR_ERROR | |
| 4712 | - "More than %d partition key columns were provided", | |
| 4713 | - VEC0_MAX_PARTITION_COLUMNS); | |
| 4714 | - goto error; | |
| 4715 | - } | |
| 4716 | - partitionColumn.type = cType; | |
| 4717 | - partitionColumn.name_length = cNameLength; | |
| 4718 | - partitionColumn.name = sqlite3_mprintf("%.*s", cNameLength, cName); | |
| 4719 | - if(!partitionColumn.name) { | |
| 4720 | - rc = SQLITE_NOMEM; | |
| 4721 | - goto error; | |
| 4722 | - } | |
| 4723 | - | |
| 4724 | - pNew->user_column_kinds[user_column_idx] = SQLITE_VEC0_USER_COLUMN_KIND_PARTITION; | |
| 4725 | - pNew->user_column_idxs[user_column_idx] = numPartitionColumns; | |
| 4726 | - memcpy(&pNew->paritition_columns[numPartitionColumns], &partitionColumn, sizeof(partitionColumn)); | |
| 4727 | - numPartitionColumns++; | |
| 4728 | - user_column_idx++; | |
| 4729 | - continue; | |
| 4730 | - } | |
| 4731 | - | |
| 4732 | - // Scenario #3: Constructor argument is a primary key column definition, ie `article_id text primary key` | |
| 4733 | - rc = vec0_parse_primary_key_definition(argv[i], strlen(argv[i]), &cName, | |
| 4734 | - &cNameLength, &cType); | |
| 4735 | - if (rc == SQLITE_OK) { | |
| 4736 | - if (pkColumnName) { | |
| 4737 | - *pzErr = sqlite3_mprintf( | |
| 4738 | - VEC_CONSTRUCTOR_ERROR | |
| 4739 | - "More than one primary key definition was provided, vec0 only " | |
| 4740 | - "suports a single primary key column", | |
| 4741 | - argv[i]); | |
| 4742 | - goto error; | |
| 4743 | - } | |
| 4744 | - pkColumnName = cName; | |
| 4745 | - pkColumnNameLength = cNameLength; | |
| 4746 | - pkColumnType = cType; | |
| 4747 | - continue; | |
| 4748 | - } | |
| 4749 | - | |
| 4750 | - // Scenario #4: Constructor argument is a auxiliary column definition, ie `+contents text` | |
| 4751 | - rc = vec0_parse_auxiliary_column_definition(argv[i], strlen(argv[i]), &cName, | |
| 4752 | - &cNameLength, &cType); | |
| 4753 | - if(rc == SQLITE_OK) { | |
| 4754 | - if (numAuxiliaryColumns >= VEC0_MAX_AUXILIARY_COLUMNS) { | |
| 4755 | - *pzErr = sqlite3_mprintf( | |
| 4756 | - VEC_CONSTRUCTOR_ERROR | |
| 4757 | - "More than %d auxiliary columns were provided", | |
| 4758 | - VEC0_MAX_AUXILIARY_COLUMNS); | |
| 4759 | - goto error; | |
| 4760 | - } | |
| 4761 | - auxColumn.type = cType; | |
| 4762 | - auxColumn.name_length = cNameLength; | |
| 4763 | - auxColumn.name = sqlite3_mprintf("%.*s", cNameLength, cName); | |
| 4764 | - if(!auxColumn.name) { | |
| 4765 | - rc = SQLITE_NOMEM; | |
| 4766 | - goto error; | |
| 4767 | - } | |
| 4768 | - | |
| 4769 | - pNew->user_column_kinds[user_column_idx] = SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY; | |
| 4770 | - pNew->user_column_idxs[user_column_idx] = numAuxiliaryColumns; | |
| 4771 | - memcpy(&pNew->auxiliary_columns[numAuxiliaryColumns], &auxColumn, sizeof(auxColumn)); | |
| 4772 | - numAuxiliaryColumns++; | |
| 4773 | - user_column_idx++; | |
| 4774 | - continue; | |
| 4775 | - } | |
| 4776 | - | |
| 4777 | - vec0_metadata_column_kind kind; | |
| 4778 | - rc = vec0_parse_metadata_column_definition(argv[i], strlen(argv[i]), &cName, | |
| 4779 | - &cNameLength, &kind); | |
| 4780 | - if(rc == SQLITE_OK) { | |
| 4781 | - if (numMetadataColumns >= VEC0_MAX_METADATA_COLUMNS) { | |
| 4782 | - *pzErr = sqlite3_mprintf( | |
| 4783 | - VEC_CONSTRUCTOR_ERROR | |
| 4784 | - "More than %d metadata columns were provided", | |
| 4785 | - VEC0_MAX_METADATA_COLUMNS); | |
| 4786 | - goto error; | |
| 4787 | - } | |
| 4788 | - metadataColumn.kind = kind; | |
| 4789 | - metadataColumn.name_length = cNameLength; | |
| 4790 | - metadataColumn.name = sqlite3_mprintf("%.*s", cNameLength, cName); | |
| 4791 | - if(!metadataColumn.name) { | |
| 4792 | - rc = SQLITE_NOMEM; | |
| 4793 | - goto error; | |
| 4794 | - } | |
| 4795 | - | |
| 4796 | - pNew->user_column_kinds[user_column_idx] = SQLITE_VEC0_USER_COLUMN_KIND_METADATA; | |
| 4797 | - pNew->user_column_idxs[user_column_idx] = numMetadataColumns; | |
| 4798 | - memcpy(&pNew->metadata_columns[numMetadataColumns], &metadataColumn, sizeof(metadataColumn)); | |
| 4799 | - numMetadataColumns++; | |
| 4800 | - user_column_idx++; | |
| 4801 | - continue; | |
| 4802 | - } | |
| 4803 | - | |
| 4804 | - // Scenario #4: Constructor argument is a table-level option, ie `chunk_size` | |
| 4805 | - | |
| 4806 | - char *key; | |
| 4807 | - char *value; | |
| 4808 | - int keyLength, valueLength; | |
| 4809 | - rc = vec0_parse_table_option(argv[i], strlen(argv[i]), &key, &keyLength, | |
| 4810 | - &value, &valueLength); | |
| 4811 | - if (rc == SQLITE_ERROR) { | |
| 4812 | - *pzErr = sqlite3_mprintf( | |
| 4813 | - VEC_CONSTRUCTOR_ERROR "could not parse table option '%s'", argv[i]); | |
| 4814 | - goto error; | |
| 4815 | - } | |
| 4816 | - if (rc == SQLITE_OK) { | |
| 4817 | - if (sqlite3_strnicmp(key, "chunk_size", keyLength) == 0) { | |
| 4818 | - chunk_size = atoi(value); | |
| 4819 | - if (chunk_size <= 0) { | |
| 4820 | - // IMP: V01931_18769 | |
| 4821 | - *pzErr = | |
| 4822 | - sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR | |
| 4823 | - "chunk_size must be a non-zero positive integer"); | |
| 4824 | - goto error; | |
| 4825 | - } | |
| 4826 | - if ((chunk_size % 8) != 0) { | |
| 4827 | - // IMP: V14110_30948 | |
| 4828 | - *pzErr = sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR | |
| 4829 | - "chunk_size must be divisible by 8"); | |
| 4830 | - goto error; | |
| 4831 | - } | |
| 4832 | -#define SQLITE_VEC_CHUNK_SIZE_MAX 4096 | |
| 4833 | - if (chunk_size > SQLITE_VEC_CHUNK_SIZE_MAX) { | |
| 4834 | - *pzErr = | |
| 4835 | - sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR "chunk_size too large"); | |
| 4836 | - goto error; | |
| 4837 | - } | |
| 4838 | - } else { | |
| 4839 | - // IMP: V27642_11712 | |
| 4840 | - *pzErr = sqlite3_mprintf( | |
| 4841 | - VEC_CONSTRUCTOR_ERROR "Unknown table option: %.*s", keyLength, key); | |
| 4842 | - goto error; | |
| 4843 | - } | |
| 4844 | - continue; | |
| 4845 | - } | |
| 4846 | - | |
| 4847 | - // Scenario #5: Unknown constructor argument | |
| 4848 | - *pzErr = | |
| 4849 | - sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR "Could not parse '%s'", argv[i]); | |
| 4850 | - goto error; | |
| 4851 | - } | |
| 4852 | - | |
| 4853 | - if (chunk_size < 0) { | |
| 4854 | - chunk_size = 1024; | |
| 4855 | - } | |
| 4856 | - | |
| 4857 | - if (numVectorColumns <= 0) { | |
| 4858 | - *pzErr = sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR | |
| 4859 | - "At least one vector column is required"); | |
| 4860 | - goto error; | |
| 4861 | - } | |
| 4862 | - | |
| 4863 | - sqlite3_str *createStr = sqlite3_str_new(NULL); | |
| 4864 | - sqlite3_str_appendall(createStr, "CREATE TABLE x("); | |
| 4865 | - if (pkColumnName) { | |
| 4866 | - sqlite3_str_appendf(createStr, "\"%.*w\" primary key, ", pkColumnNameLength, | |
| 4867 | - pkColumnName); | |
| 4868 | - } else { | |
| 4869 | - sqlite3_str_appendall(createStr, "rowid, "); | |
| 4870 | - } | |
| 4871 | - for (int i = 0; i < numVectorColumns + numPartitionColumns + numAuxiliaryColumns + numMetadataColumns; i++) { | |
| 4872 | - switch(pNew->user_column_kinds[i]) { | |
| 4873 | - case SQLITE_VEC0_USER_COLUMN_KIND_VECTOR: { | |
| 4874 | - int vector_idx = pNew->user_column_idxs[i]; | |
| 4875 | - sqlite3_str_appendf(createStr, "\"%.*w\", ", | |
| 4876 | - pNew->vector_columns[vector_idx].name_length, | |
| 4877 | - pNew->vector_columns[vector_idx].name); | |
| 4878 | - break; | |
| 4879 | - } | |
| 4880 | - case SQLITE_VEC0_USER_COLUMN_KIND_PARTITION: { | |
| 4881 | - int partition_idx = pNew->user_column_idxs[i]; | |
| 4882 | - sqlite3_str_appendf(createStr, "\"%.*w\", ", | |
| 4883 | - pNew->paritition_columns[partition_idx].name_length, | |
| 4884 | - pNew->paritition_columns[partition_idx].name); | |
| 4885 | - break; | |
| 4886 | - } | |
| 4887 | - case SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY: { | |
| 4888 | - int auxiliary_idx = pNew->user_column_idxs[i]; | |
| 4889 | - sqlite3_str_appendf(createStr, "\"%.*w\", ", | |
| 4890 | - pNew->auxiliary_columns[auxiliary_idx].name_length, | |
| 4891 | - pNew->auxiliary_columns[auxiliary_idx].name); | |
| 4892 | - break; | |
| 4893 | - } | |
| 4894 | - case SQLITE_VEC0_USER_COLUMN_KIND_METADATA: { | |
| 4895 | - int metadata_idx = pNew->user_column_idxs[i]; | |
| 4896 | - sqlite3_str_appendf(createStr, "\"%.*w\", ", | |
| 4897 | - pNew->metadata_columns[metadata_idx].name_length, | |
| 4898 | - pNew->metadata_columns[metadata_idx].name); | |
| 4899 | - break; | |
| 4900 | - } | |
| 4901 | - } | |
| 4902 | - | |
| 4903 | - } | |
| 4904 | - sqlite3_str_appendall(createStr, " distance hidden, k hidden) "); | |
| 4905 | - if (pkColumnName) { | |
| 4906 | - sqlite3_str_appendall(createStr, "without rowid "); | |
| 4907 | - } | |
| 4908 | - zSql = sqlite3_str_finish(createStr); | |
| 4909 | - if (!zSql) { | |
| 4910 | - goto error; | |
| 4911 | - } | |
| 4912 | - rc = sqlite3_declare_vtab(db, zSql); | |
| 4913 | - sqlite3_free((void *)zSql); | |
| 4914 | - if (rc != SQLITE_OK) { | |
| 4915 | - *pzErr = sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR | |
| 4916 | - "could not declare virtual table, '%s'", | |
| 4917 | - sqlite3_errmsg(db)); | |
| 4918 | - goto error; | |
| 4919 | - } | |
| 4920 | - | |
| 4921 | - const char *schemaName = argv[1]; | |
| 4922 | - const char *tableName = argv[2]; | |
| 4923 | - | |
| 4924 | - pNew->db = db; | |
| 4925 | - pNew->pkIsText = pkColumnType == SQLITE_TEXT; | |
| 4926 | - pNew->schemaName = sqlite3_mprintf("%s", schemaName); | |
| 4927 | - if (!pNew->schemaName) { | |
| 4928 | - goto error; | |
| 4929 | - } | |
| 4930 | - pNew->tableName = sqlite3_mprintf("%s", tableName); | |
| 4931 | - if (!pNew->tableName) { | |
| 4932 | - goto error; | |
| 4933 | - } | |
| 4934 | - pNew->shadowRowidsName = sqlite3_mprintf("%s_rowids", tableName); | |
| 4935 | - if (!pNew->shadowRowidsName) { | |
| 4936 | - goto error; | |
| 4937 | - } | |
| 4938 | - pNew->shadowChunksName = sqlite3_mprintf("%s_chunks", tableName); | |
| 4939 | - if (!pNew->shadowChunksName) { | |
| 4940 | - goto error; | |
| 4941 | - } | |
| 4942 | - pNew->numVectorColumns = numVectorColumns; | |
| 4943 | - pNew->numPartitionColumns = numPartitionColumns; | |
| 4944 | - pNew->numAuxiliaryColumns = numAuxiliaryColumns; | |
| 4945 | - pNew->numMetadataColumns = numMetadataColumns; | |
| 4946 | - | |
| 4947 | - for (int i = 0; i < pNew->numVectorColumns; i++) { | |
| 4948 | - pNew->shadowVectorChunksNames[i] = | |
| 4949 | - sqlite3_mprintf("%s_vector_chunks%02d", tableName, i); | |
| 4950 | - if (!pNew->shadowVectorChunksNames[i]) { | |
| 4951 | - goto error; | |
| 4952 | - } | |
| 4953 | - } | |
| 4954 | - for (int i = 0; i < pNew->numMetadataColumns; i++) { | |
| 4955 | - pNew->shadowMetadataChunksNames[i] = | |
| 4956 | - sqlite3_mprintf("%s_metadatachunks%02d", tableName, i); | |
| 4957 | - if (!pNew->shadowMetadataChunksNames[i]) { | |
| 4958 | - goto error; | |
| 4959 | - } | |
| 4960 | - } | |
| 4961 | - pNew->chunk_size = chunk_size; | |
| 4962 | - | |
| 4963 | - // if xCreate, then create the necessary shadow tables | |
| 4964 | - if (isCreate) { | |
| 4965 | - sqlite3_stmt *stmt; | |
| 4966 | - int rc; | |
| 4967 | - | |
| 4968 | - char * zCreateInfo = sqlite3_mprintf("CREATE TABLE "VEC0_SHADOW_INFO_NAME " (key text primary key, value any)", pNew->schemaName, pNew->tableName); | |
| 4969 | - if(!zCreateInfo) { | |
| 4970 | - goto error; | |
| 4971 | - } | |
| 4972 | - rc = sqlite3_prepare_v2(db, zCreateInfo, -1, &stmt, NULL); | |
| 4973 | - | |
| 4974 | - sqlite3_free((void *) zCreateInfo); | |
| 4975 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 4976 | - // TODO(IMP) | |
| 4977 | - sqlite3_finalize(stmt); | |
| 4978 | - *pzErr = sqlite3_mprintf("Could not create '_info' shadow table: %s", | |
| 4979 | - sqlite3_errmsg(db)); | |
| 4980 | - goto error; | |
| 4981 | - } | |
| 4982 | - sqlite3_finalize(stmt); | |
| 4983 | - | |
| 4984 | - char * zSeedInfo = sqlite3_mprintf( | |
| 4985 | - "INSERT INTO "VEC0_SHADOW_INFO_NAME "(key, value) VALUES " | |
| 4986 | - "(?1, ?2), (?3, ?4), (?5, ?6), (?7, ?8) ", | |
| 4987 | - pNew->schemaName, pNew->tableName | |
| 4988 | - ); | |
| 4989 | - if(!zSeedInfo) { | |
| 4990 | - goto error; | |
| 4991 | - } | |
| 4992 | - rc = sqlite3_prepare_v2(db, zSeedInfo, -1, &stmt, NULL); | |
| 4993 | - sqlite3_free((void *) zSeedInfo); | |
| 4994 | - if (rc != SQLITE_OK) { | |
| 4995 | - // TODO(IMP) | |
| 4996 | - sqlite3_finalize(stmt); | |
| 4997 | - *pzErr = sqlite3_mprintf("Could not seed '_info' shadow table: %s", | |
| 4998 | - sqlite3_errmsg(db)); | |
| 4999 | - goto error; | |
| 5000 | - } | |
| 5001 | - sqlite3_bind_text(stmt, 1, "CREATE_VERSION", -1, SQLITE_STATIC); | |
| 5002 | - sqlite3_bind_text(stmt, 2, SQLITE_VEC_VERSION, -1, SQLITE_STATIC); | |
| 5003 | - sqlite3_bind_text(stmt, 3, "CREATE_VERSION_MAJOR", -1, SQLITE_STATIC); | |
| 5004 | - sqlite3_bind_int(stmt, 4, SQLITE_VEC_VERSION_MAJOR); | |
| 5005 | - sqlite3_bind_text(stmt, 5, "CREATE_VERSION_MINOR", -1, SQLITE_STATIC); | |
| 5006 | - sqlite3_bind_int(stmt, 6, SQLITE_VEC_VERSION_MINOR); | |
| 5007 | - sqlite3_bind_text(stmt, 7, "CREATE_VERSION_PATCH", -1, SQLITE_STATIC); | |
| 5008 | - sqlite3_bind_int(stmt, 8, SQLITE_VEC_VERSION_PATCH); | |
| 5009 | - | |
| 5010 | - if(sqlite3_step(stmt) != SQLITE_DONE) { | |
| 5011 | - // TODO(IMP) | |
| 5012 | - sqlite3_finalize(stmt); | |
| 5013 | - *pzErr = sqlite3_mprintf("Could not seed '_info' shadow table: %s", | |
| 5014 | - sqlite3_errmsg(db)); | |
| 5015 | - goto error; | |
| 5016 | - } | |
| 5017 | - sqlite3_finalize(stmt); | |
| 5018 | - | |
| 5019 | - | |
| 5020 | - | |
| 5021 | - // create the _chunks shadow table | |
| 5022 | - char *zCreateShadowChunks = NULL; | |
| 5023 | - if(pNew->numPartitionColumns) { | |
| 5024 | - sqlite3_str * s = sqlite3_str_new(NULL); | |
| 5025 | - sqlite3_str_appendf(s, "CREATE TABLE " VEC0_SHADOW_CHUNKS_NAME "(", pNew->schemaName, pNew->tableName); | |
| 5026 | - sqlite3_str_appendall(s, "chunk_id INTEGER PRIMARY KEY AUTOINCREMENT," "size INTEGER NOT NULL,"); | |
| 5027 | - sqlite3_str_appendall(s, "sequence_id integer,"); | |
| 5028 | - for(int i = 0; i < pNew->numPartitionColumns;i++) { | |
| 5029 | - sqlite3_str_appendf(s, "partition%02d,", i); | |
| 5030 | - } | |
| 5031 | - sqlite3_str_appendall(s, "validity BLOB NOT NULL, rowids BLOB NOT NULL);"); | |
| 5032 | - zCreateShadowChunks = sqlite3_str_finish(s); | |
| 5033 | - }else { | |
| 5034 | - zCreateShadowChunks = sqlite3_mprintf(VEC0_SHADOW_CHUNKS_CREATE, | |
| 5035 | - pNew->schemaName, pNew->tableName); | |
| 5036 | - } | |
| 5037 | - if (!zCreateShadowChunks) { | |
| 5038 | - goto error; | |
| 5039 | - } | |
| 5040 | - rc = sqlite3_prepare_v2(db, zCreateShadowChunks, -1, &stmt, 0); | |
| 5041 | - sqlite3_free((void *)zCreateShadowChunks); | |
| 5042 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5043 | - // IMP: V17740_01811 | |
| 5044 | - sqlite3_finalize(stmt); | |
| 5045 | - *pzErr = sqlite3_mprintf("Could not create '_chunks' shadow table: %s", | |
| 5046 | - sqlite3_errmsg(db)); | |
| 5047 | - goto error; | |
| 5048 | - } | |
| 5049 | - sqlite3_finalize(stmt); | |
| 5050 | - | |
| 5051 | - // create the _rowids shadow table | |
| 5052 | - char *zCreateShadowRowids; | |
| 5053 | - if (pNew->pkIsText) { | |
| 5054 | - // adds a "text unique not null" constraint to the id column | |
| 5055 | - zCreateShadowRowids = sqlite3_mprintf(VEC0_SHADOW_ROWIDS_CREATE_PK_TEXT, | |
| 5056 | - pNew->schemaName, pNew->tableName); | |
| 5057 | - } else { | |
| 5058 | - zCreateShadowRowids = sqlite3_mprintf(VEC0_SHADOW_ROWIDS_CREATE_BASIC, | |
| 5059 | - pNew->schemaName, pNew->tableName); | |
| 5060 | - } | |
| 5061 | - if (!zCreateShadowRowids) { | |
| 5062 | - goto error; | |
| 5063 | - } | |
| 5064 | - rc = sqlite3_prepare_v2(db, zCreateShadowRowids, -1, &stmt, 0); | |
| 5065 | - sqlite3_free((void *)zCreateShadowRowids); | |
| 5066 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5067 | - // IMP: V11631_28470 | |
| 5068 | - sqlite3_finalize(stmt); | |
| 5069 | - *pzErr = sqlite3_mprintf("Could not create '_rowids' shadow table: %s", | |
| 5070 | - sqlite3_errmsg(db)); | |
| 5071 | - goto error; | |
| 5072 | - } | |
| 5073 | - sqlite3_finalize(stmt); | |
| 5074 | - | |
| 5075 | - for (int i = 0; i < pNew->numVectorColumns; i++) { | |
| 5076 | - char *zSql = sqlite3_mprintf(VEC0_SHADOW_VECTOR_N_CREATE, | |
| 5077 | - pNew->schemaName, pNew->tableName, i); | |
| 5078 | - if (!zSql) { | |
| 5079 | - goto error; | |
| 5080 | - } | |
| 5081 | - rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0); | |
| 5082 | - sqlite3_free((void *)zSql); | |
| 5083 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5084 | - // IMP: V25919_09989 | |
| 5085 | - sqlite3_finalize(stmt); | |
| 5086 | - *pzErr = sqlite3_mprintf( | |
| 5087 | - "Could not create '_vector_chunks%02d' shadow table: %s", i, | |
| 5088 | - sqlite3_errmsg(db)); | |
| 5089 | - goto error; | |
| 5090 | - } | |
| 5091 | - sqlite3_finalize(stmt); | |
| 5092 | - } | |
| 5093 | - | |
| 5094 | - for (int i = 0; i < pNew->numMetadataColumns; i++) { | |
| 5095 | - char *zSql = sqlite3_mprintf("CREATE TABLE " VEC0_SHADOW_METADATA_N_NAME "(rowid PRIMARY KEY, data BLOB NOT NULL);", | |
| 5096 | - pNew->schemaName, pNew->tableName, i); | |
| 5097 | - if (!zSql) { | |
| 5098 | - goto error; | |
| 5099 | - } | |
| 5100 | - rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0); | |
| 5101 | - sqlite3_free((void *)zSql); | |
| 5102 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5103 | - sqlite3_finalize(stmt); | |
| 5104 | - *pzErr = sqlite3_mprintf( | |
| 5105 | - "Could not create '_metata_chunks%02d' shadow table: %s", i, | |
| 5106 | - sqlite3_errmsg(db)); | |
| 5107 | - goto error; | |
| 5108 | - } | |
| 5109 | - sqlite3_finalize(stmt); | |
| 5110 | - | |
| 5111 | - if(pNew->metadata_columns[i].kind == VEC0_METADATA_COLUMN_KIND_TEXT) { | |
| 5112 | - char *zSql = sqlite3_mprintf("CREATE TABLE " VEC0_SHADOW_METADATA_TEXT_DATA_NAME "(rowid PRIMARY KEY, data TEXT);", | |
| 5113 | - pNew->schemaName, pNew->tableName, i); | |
| 5114 | - if (!zSql) { | |
| 5115 | - goto error; | |
| 5116 | - } | |
| 5117 | - rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0); | |
| 5118 | - sqlite3_free((void *)zSql); | |
| 5119 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5120 | - sqlite3_finalize(stmt); | |
| 5121 | - *pzErr = sqlite3_mprintf( | |
| 5122 | - "Could not create '_metadatatext%02d' shadow table: %s", i, | |
| 5123 | - sqlite3_errmsg(db)); | |
| 5124 | - goto error; | |
| 5125 | - } | |
| 5126 | - sqlite3_finalize(stmt); | |
| 5127 | - | |
| 5128 | - } | |
| 5129 | - } | |
| 5130 | - | |
| 5131 | - if(pNew->numAuxiliaryColumns > 0) { | |
| 5132 | - sqlite3_stmt * stmt; | |
| 5133 | - sqlite3_str * s = sqlite3_str_new(NULL); | |
| 5134 | - sqlite3_str_appendf(s, "CREATE TABLE " VEC0_SHADOW_AUXILIARY_NAME "( rowid integer PRIMARY KEY ", pNew->schemaName, pNew->tableName); | |
| 5135 | - for(int i = 0; i < pNew->numAuxiliaryColumns; i++) { | |
| 5136 | - sqlite3_str_appendf(s, ", value%02d", i); | |
| 5137 | - } | |
| 5138 | - sqlite3_str_appendall(s, ")"); | |
| 5139 | - char *zSql = sqlite3_str_finish(s); | |
| 5140 | - if(!zSql) { | |
| 5141 | - goto error; | |
| 5142 | - } | |
| 5143 | - rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, NULL); | |
| 5144 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5145 | - sqlite3_finalize(stmt); | |
| 5146 | - *pzErr = sqlite3_mprintf( | |
| 5147 | - "Could not create auxiliary shadow table: %s", | |
| 5148 | - sqlite3_errmsg(db)); | |
| 5149 | - | |
| 5150 | - goto error; | |
| 5151 | - } | |
| 5152 | - sqlite3_finalize(stmt); | |
| 5153 | - } | |
| 5154 | - } | |
| 5155 | - | |
| 5156 | - *ppVtab = (sqlite3_vtab *)pNew; | |
| 5157 | - return SQLITE_OK; | |
| 5158 | - | |
| 5159 | -error: | |
| 5160 | - vec0_free(pNew); | |
| 5161 | - return SQLITE_ERROR; | |
| 5162 | -} | |
| 5163 | - | |
| 5164 | -static int vec0Create(sqlite3 *db, void *pAux, int argc, | |
| 5165 | - const char *const *argv, sqlite3_vtab **ppVtab, | |
| 5166 | - char **pzErr) { | |
| 5167 | - return vec0_init(db, pAux, argc, argv, ppVtab, pzErr, true); | |
| 5168 | -} | |
| 5169 | -static int vec0Connect(sqlite3 *db, void *pAux, int argc, | |
| 5170 | - const char *const *argv, sqlite3_vtab **ppVtab, | |
| 5171 | - char **pzErr) { | |
| 5172 | - return vec0_init(db, pAux, argc, argv, ppVtab, pzErr, false); | |
| 5173 | -} | |
| 5174 | - | |
| 5175 | -static int vec0Disconnect(sqlite3_vtab *pVtab) { | |
| 5176 | - vec0_vtab *p = (vec0_vtab *)pVtab; | |
| 5177 | - vec0_free(p); | |
| 5178 | - sqlite3_free(p); | |
| 5179 | - return SQLITE_OK; | |
| 5180 | -} | |
| 5181 | -static int vec0Destroy(sqlite3_vtab *pVtab) { | |
| 5182 | - vec0_vtab *p = (vec0_vtab *)pVtab; | |
| 5183 | - sqlite3_stmt *stmt; | |
| 5184 | - int rc; | |
| 5185 | - const char *zSql; | |
| 5186 | - | |
| 5187 | - // Free up any sqlite3_stmt, otherwise DROPs on those tables will fail | |
| 5188 | - vec0_free_resources(p); | |
| 5189 | - | |
| 5190 | - // TODO(test) later: can't evidence-of here, bc always gives "SQL logic error" instead of | |
| 5191 | - // provided error | |
| 5192 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_CHUNKS_NAME, p->schemaName, | |
| 5193 | - p->tableName); | |
| 5194 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | |
| 5195 | - sqlite3_free((void *)zSql); | |
| 5196 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5197 | - rc = SQLITE_ERROR; | |
| 5198 | - vtab_set_error(pVtab, "could not drop chunks shadow table"); | |
| 5199 | - goto done; | |
| 5200 | - } | |
| 5201 | - sqlite3_finalize(stmt); | |
| 5202 | - | |
| 5203 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_INFO_NAME, p->schemaName, | |
| 5204 | - p->tableName); | |
| 5205 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | |
| 5206 | - sqlite3_free((void *)zSql); | |
| 5207 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5208 | - rc = SQLITE_ERROR; | |
| 5209 | - vtab_set_error(pVtab, "could not drop info shadow table"); | |
| 5210 | - goto done; | |
| 5211 | - } | |
| 5212 | - sqlite3_finalize(stmt); | |
| 5213 | - | |
| 5214 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_ROWIDS_NAME, p->schemaName, | |
| 5215 | - p->tableName); | |
| 5216 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | |
| 5217 | - sqlite3_free((void *)zSql); | |
| 5218 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5219 | - rc = SQLITE_ERROR; | |
| 5220 | - goto done; | |
| 5221 | - } | |
| 5222 | - sqlite3_finalize(stmt); | |
| 5223 | - | |
| 5224 | - for (int i = 0; i < p->numVectorColumns; i++) { | |
| 5225 | - zSql = sqlite3_mprintf("DROP TABLE \"%w\".\"%w\"", p->schemaName, | |
| 5226 | - p->shadowVectorChunksNames[i]); | |
| 5227 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | |
| 5228 | - sqlite3_free((void *)zSql); | |
| 5229 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5230 | - rc = SQLITE_ERROR; | |
| 5231 | - goto done; | |
| 5232 | - } | |
| 5233 | - sqlite3_finalize(stmt); | |
| 5234 | - } | |
| 5235 | - | |
| 5236 | - if(p->numAuxiliaryColumns > 0) { | |
| 5237 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_AUXILIARY_NAME, p->schemaName, p->tableName); | |
| 5238 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | |
| 5239 | - sqlite3_free((void *)zSql); | |
| 5240 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5241 | - rc = SQLITE_ERROR; | |
| 5242 | - goto done; | |
| 5243 | - } | |
| 5244 | - sqlite3_finalize(stmt); | |
| 5245 | - } | |
| 5246 | - | |
| 5247 | - | |
| 5248 | - for (int i = 0; i < p->numMetadataColumns; i++) { | |
| 5249 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_METADATA_N_NAME, p->schemaName,p->tableName, i); | |
| 5250 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | |
| 5251 | - sqlite3_free((void *)zSql); | |
| 5252 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5253 | - rc = SQLITE_ERROR; | |
| 5254 | - goto done; | |
| 5255 | - } | |
| 5256 | - sqlite3_finalize(stmt); | |
| 5257 | - | |
| 5258 | - if(p->metadata_columns[i].kind == VEC0_METADATA_COLUMN_KIND_TEXT) { | |
| 5259 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_METADATA_TEXT_DATA_NAME, p->schemaName,p->tableName, i); | |
| 5260 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | |
| 5261 | - sqlite3_free((void *)zSql); | |
| 5262 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | |
| 5263 | - rc = SQLITE_ERROR; | |
| 5264 | - goto done; | |
| 5265 | - } | |
| 5266 | - sqlite3_finalize(stmt); | |
| 5267 | - } | |
| 5268 | - } | |
| 5269 | - | |
| 5270 | - stmt = NULL; | |
| 5271 | - rc = SQLITE_OK; | |
| 5272 | - | |
| 5273 | -done: | |
| 5274 | - sqlite3_finalize(stmt); | |
| 5275 | - vec0_free(p); | |
| 5276 | - // If there was an error | |
| 5277 | - if (rc == SQLITE_OK) { | |
| 5278 | - sqlite3_free(p); | |
| 5279 | - } | |
| 5280 | - return rc; | |
| 5281 | -} | |
| 5282 | - | |
| 5283 | -static int vec0Open(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor) { | |
| 5284 | - UNUSED_PARAMETER(p); | |
| 5285 | - vec0_cursor *pCur; | |
| 5286 | - pCur = sqlite3_malloc(sizeof(*pCur)); | |
| 5287 | - if (pCur == 0) | |
| 5288 | - return SQLITE_NOMEM; | |
| 5289 | - memset(pCur, 0, sizeof(*pCur)); | |
| 5290 | - *ppCursor = &pCur->base; | |
| 5291 | - return SQLITE_OK; | |
| 5292 | -} | |
| 5293 | - | |
| 5294 | -static int vec0Close(sqlite3_vtab_cursor *cur) { | |
| 5295 | - vec0_cursor *pCur = (vec0_cursor *)cur; | |
| 5296 | - vec0_cursor_clear(pCur); | |
| 5297 | - sqlite3_free(pCur); | |
| 5298 | - return SQLITE_OK; | |
| 5299 | -} | |
| 5300 | - | |
| 5301 | -// All the different type of "values" provided to argv/argc in vec0Filter. | |
| 5302 | -// These enums denote the use and purpose of all of them. | |
| 5303 | -typedef enum { | |
| 5304 | - // If any values are updated, please update the ARCHITECTURE.md docs accordingly! | |
| 5305 | - | |
| 5306 | - VEC0_IDXSTR_KIND_KNN_MATCH = '{', | |
| 5307 | - VEC0_IDXSTR_KIND_KNN_K = '}', | |
| 5308 | - VEC0_IDXSTR_KIND_KNN_ROWID_IN = '[', | |
| 5309 | - VEC0_IDXSTR_KIND_KNN_PARTITON_CONSTRAINT = ']', | |
| 5310 | - VEC0_IDXSTR_KIND_POINT_ID = '!', | |
| 5311 | - VEC0_IDXSTR_KIND_METADATA_CONSTRAINT = '&', | |
| 5312 | -} vec0_idxstr_kind; | |
| 5313 | - | |
| 5314 | -// The different SQLITE_INDEX_CONSTRAINT values that vec0 partition key columns | |
| 5315 | -// support, but as characters that fit nicely in idxstr. | |
| 5316 | -typedef enum { | |
| 5317 | - // If any values are updated, please update the ARCHITECTURE.md docs accordingly! | |
| 5318 | - | |
| 5319 | - VEC0_PARTITION_OPERATOR_EQ = 'a', | |
| 5320 | - VEC0_PARTITION_OPERATOR_GT = 'b', | |
| 5321 | - VEC0_PARTITION_OPERATOR_LE = 'c', | |
| 5322 | - VEC0_PARTITION_OPERATOR_LT = 'd', | |
| 5323 | - VEC0_PARTITION_OPERATOR_GE = 'e', | |
| 5324 | - VEC0_PARTITION_OPERATOR_NE = 'f', | |
| 5325 | -} vec0_partition_operator; | |
| 5326 | -typedef enum { | |
| 5327 | - VEC0_METADATA_OPERATOR_EQ = 'a', | |
| 5328 | - VEC0_METADATA_OPERATOR_GT = 'b', | |
| 5329 | - VEC0_METADATA_OPERATOR_LE = 'c', | |
| 5330 | - VEC0_METADATA_OPERATOR_LT = 'd', | |
| 5331 | - VEC0_METADATA_OPERATOR_GE = 'e', | |
| 5332 | - VEC0_METADATA_OPERATOR_NE = 'f', | |
| 5333 | - VEC0_METADATA_OPERATOR_IN = 'g', | |
| 5334 | -} vec0_metadata_operator; | |
| 5335 | - | |
| 5336 | -static int vec0BestIndex(sqlite3_vtab *pVTab, sqlite3_index_info *pIdxInfo) { | |
| 5337 | - vec0_vtab *p = (vec0_vtab *)pVTab; | |
| 5338 | - /** | |
| 5339 | - * Possible query plans are: | |
| 5340 | - * 1. KNN when: | |
| 5341 | - * a) An `MATCH` op on vector column | |
| 5342 | - * b) ORDER BY on distance column | |
| 5343 | - * c) LIMIT | |
| 5344 | - * d) rowid in (...) OPTIONAL | |
| 5345 | - * 2. Point when: | |
| 5346 | - * a) An `EQ` op on rowid column | |
| 5347 | - * 3. else: fullscan | |
| 5348 | - * | |
| 5349 | - */ | |
| 5350 | - int iMatchTerm = -1; | |
| 5351 | - int iMatchVectorTerm = -1; | |
| 5352 | - int iLimitTerm = -1; | |
| 5353 | - int iRowidTerm = -1; | |
| 5354 | - int iKTerm = -1; | |
| 5355 | - int iRowidInTerm = -1; | |
| 5356 | - int hasAuxConstraint = 0; | |
| 5357 | - | |
| 5358 | -#ifdef SQLITE_VEC_DEBUG | |
| 5359 | - printf("pIdxInfo->nOrderBy=%d, pIdxInfo->nConstraint=%d\n", pIdxInfo->nOrderBy, pIdxInfo->nConstraint); | |
| 5360 | -#endif | |
| 5361 | - | |
| 5362 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | |
| 5363 | - u8 vtabIn = 0; | |
| 5364 | - | |
| 5365 | -#if COMPILER_SUPPORTS_VTAB_IN | |
| 5366 | - if (sqlite3_libversion_number() >= 3038000) { | |
| 5367 | - vtabIn = sqlite3_vtab_in(pIdxInfo, i, -1); | |
| 5368 | - } | |
| 5369 | -#endif | |
| 5370 | - | |
| 5371 | -#ifdef SQLITE_VEC_DEBUG | |
| 5372 | - printf("xBestIndex [%d] usable=%d iColumn=%d op=%d vtabin=%d\n", i, | |
| 5373 | - pIdxInfo->aConstraint[i].usable, pIdxInfo->aConstraint[i].iColumn, | |
| 5374 | - pIdxInfo->aConstraint[i].op, vtabIn); | |
| 5375 | -#endif | |
| 5376 | - if (!pIdxInfo->aConstraint[i].usable) | |
| 5377 | - continue; | |
| 5378 | - | |
| 5379 | - int iColumn = pIdxInfo->aConstraint[i].iColumn; | |
| 5380 | - int op = pIdxInfo->aConstraint[i].op; | |
| 5381 | - | |
| 5382 | - if (op == SQLITE_INDEX_CONSTRAINT_LIMIT) { | |
| 5383 | - iLimitTerm = i; | |
| 5384 | - } | |
| 5385 | - if (op == SQLITE_INDEX_CONSTRAINT_MATCH && | |
| 5386 | - vec0_column_idx_is_vector(p, iColumn)) { | |
| 5387 | - if (iMatchTerm > -1) { | |
| 5388 | - vtab_set_error( | |
| 5389 | - pVTab, "only 1 MATCH operator is allowed in a single vec0 query"); | |
| 5390 | - return SQLITE_ERROR; | |
| 5391 | - } | |
| 5392 | - iMatchTerm = i; | |
| 5393 | - iMatchVectorTerm = vec0_column_idx_to_vector_idx(p, iColumn); | |
| 5394 | - } | |
| 5395 | - if (op == SQLITE_INDEX_CONSTRAINT_EQ && iColumn == VEC0_COLUMN_ID) { | |
| 5396 | - if (vtabIn) { | |
| 5397 | - if (iRowidInTerm != -1) { | |
| 5398 | - vtab_set_error(pVTab, "only 1 'rowid in (..)' operator is allowed in " | |
| 5399 | - "a single vec0 query"); | |
| 5400 | - return SQLITE_ERROR; | |
| 5401 | - } | |
| 5402 | - iRowidInTerm = i; | |
| 5403 | - | |
| 5404 | - } else { | |
| 5405 | - iRowidTerm = i; | |
| 5406 | - } | |
| 5407 | - } | |
| 5408 | - if (op == SQLITE_INDEX_CONSTRAINT_EQ && iColumn == vec0_column_k_idx(p)) { | |
| 5409 | - iKTerm = i; | |
| 5410 | - } | |
| 5411 | - if( | |
| 5412 | - (op != SQLITE_INDEX_CONSTRAINT_LIMIT && op != SQLITE_INDEX_CONSTRAINT_OFFSET) | |
| 5413 | - && vec0_column_idx_is_auxiliary(p, iColumn)) { | |
| 5414 | - hasAuxConstraint = 1; | |
| 5415 | - } | |
| 5416 | - } | |
| 5417 | - | |
| 5418 | - sqlite3_str *idxStr = sqlite3_str_new(NULL); | |
| 5419 | - int rc; | |
| 5420 | - | |
| 5421 | - if (iMatchTerm >= 0) { | |
| 5422 | - if (iLimitTerm < 0 && iKTerm < 0) { | |
| 5423 | - vtab_set_error( | |
| 5424 | - pVTab, | |
| 5425 | - "A LIMIT or 'k = ?' constraint is required on vec0 knn queries."); | |
| 5426 | - rc = SQLITE_ERROR; | |
| 5427 | - goto done; | |
| 5428 | - } | |
| 5429 | - if (iLimitTerm >= 0 && iKTerm >= 0) { | |
| 5430 | - vtab_set_error(pVTab, "Only LIMIT or 'k =?' can be provided, not both"); | |
| 5431 | - rc = SQLITE_ERROR; | |
| 5432 | - goto done; | |
| 5433 | - } | |
| 5434 | - | |
| 5435 | - if (pIdxInfo->nOrderBy) { | |
| 5436 | - if (pIdxInfo->nOrderBy > 1) { | |
| 5437 | - vtab_set_error(pVTab, "Only a single 'ORDER BY distance' clause is " | |
| 5438 | - "allowed on vec0 KNN queries"); | |
| 5439 | - rc = SQLITE_ERROR; | |
| 5440 | - goto done; | |
| 5441 | - } | |
| 5442 | - if (pIdxInfo->aOrderBy[0].iColumn != vec0_column_distance_idx(p)) { | |
| 5443 | - vtab_set_error(pVTab, | |
| 5444 | - "Only a single 'ORDER BY distance' clause is allowed on " | |
| 5445 | - "vec0 KNN queries, not on other columns"); | |
| 5446 | - rc = SQLITE_ERROR; | |
| 5447 | - goto done; | |
| 5448 | - } | |
| 5449 | - if (pIdxInfo->aOrderBy[0].desc) { | |
| 5450 | - vtab_set_error( | |
| 5451 | - pVTab, "Only ascending in ORDER BY distance clause is supported, " | |
| 5452 | - "DESC is not supported yet."); | |
| 5453 | - rc = SQLITE_ERROR; | |
| 5454 | - goto done; | |
| 5455 | - } | |
| 5456 | - } | |
| 5457 | - | |
| 5458 | - if(hasAuxConstraint) { | |
| 5459 | - // IMP: V25623_09693 | |
| 5460 | - vtab_set_error(pVTab, "An illegal WHERE constraint was provided on a vec0 auxiliary column in a KNN query."); | |
| 5461 | - rc = SQLITE_ERROR; | |
| 5462 | - goto done; | |
| 5463 | - } | |
| 5464 | - | |
| 5465 | - sqlite3_str_appendchar(idxStr, 1, VEC0_QUERY_PLAN_KNN); | |
| 5466 | - | |
| 5467 | - int argvIndex = 1; | |
| 5468 | - pIdxInfo->aConstraintUsage[iMatchTerm].argvIndex = argvIndex++; | |
| 5469 | - pIdxInfo->aConstraintUsage[iMatchTerm].omit = 1; | |
| 5470 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_KNN_MATCH); | |
| 5471 | - sqlite3_str_appendchar(idxStr, 3, '_'); | |
| 5472 | - | |
| 5473 | - if (iLimitTerm >= 0) { | |
| 5474 | - pIdxInfo->aConstraintUsage[iLimitTerm].argvIndex = argvIndex++; | |
| 5475 | - pIdxInfo->aConstraintUsage[iLimitTerm].omit = 1; | |
| 5476 | - } else { | |
| 5477 | - pIdxInfo->aConstraintUsage[iKTerm].argvIndex = argvIndex++; | |
| 5478 | - pIdxInfo->aConstraintUsage[iKTerm].omit = 1; | |
| 5479 | - } | |
| 5480 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_KNN_K); | |
| 5481 | - sqlite3_str_appendchar(idxStr, 3, '_'); | |
| 5482 | - | |
| 5483 | -#if COMPILER_SUPPORTS_VTAB_IN | |
| 5484 | - if (iRowidInTerm >= 0) { | |
| 5485 | - // already validated as >= SQLite 3.38 bc iRowidInTerm is only >= 0 when | |
| 5486 | - // vtabIn == 1 | |
| 5487 | - sqlite3_vtab_in(pIdxInfo, iRowidInTerm, 1); | |
| 5488 | - pIdxInfo->aConstraintUsage[iRowidInTerm].argvIndex = argvIndex++; | |
| 5489 | - pIdxInfo->aConstraintUsage[iRowidInTerm].omit = 1; | |
| 5490 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_KNN_ROWID_IN); | |
| 5491 | - sqlite3_str_appendchar(idxStr, 3, '_'); | |
| 5492 | - } | |
| 5493 | -#endif | |
| 5494 | - | |
| 5495 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | |
| 5496 | - if (!pIdxInfo->aConstraint[i].usable) | |
| 5497 | - continue; | |
| 5498 | - | |
| 5499 | - int iColumn = pIdxInfo->aConstraint[i].iColumn; | |
| 5500 | - int op = pIdxInfo->aConstraint[i].op; | |
| 5501 | - if(op == SQLITE_INDEX_CONSTRAINT_LIMIT || op == SQLITE_INDEX_CONSTRAINT_OFFSET) { | |
| 5502 | - continue; | |
| 5503 | - } | |
| 5504 | - if(!vec0_column_idx_is_partition(p, iColumn)) { | |
| 5505 | - continue; | |
| 5506 | - } | |
| 5507 | - | |
| 5508 | - int partition_idx = vec0_column_idx_to_partition_idx(p, iColumn); | |
| 5509 | - char value = 0; | |
| 5510 | - | |
| 5511 | - switch(op) { | |
| 5512 | - case SQLITE_INDEX_CONSTRAINT_EQ: { | |
| 5513 | - value = VEC0_PARTITION_OPERATOR_EQ; | |
| 5514 | - break; | |
| 5515 | - } | |
| 5516 | - case SQLITE_INDEX_CONSTRAINT_GT: { | |
| 5517 | - value = VEC0_PARTITION_OPERATOR_GT; | |
| 5518 | - break; | |
| 5519 | - } | |
| 5520 | - case SQLITE_INDEX_CONSTRAINT_LE: { | |
| 5521 | - value = VEC0_PARTITION_OPERATOR_LE; | |
| 5522 | - break; | |
| 5523 | - } | |
| 5524 | - case SQLITE_INDEX_CONSTRAINT_LT: { | |
| 5525 | - value = VEC0_PARTITION_OPERATOR_LT; | |
| 5526 | - break; | |
| 5527 | - } | |
| 5528 | - case SQLITE_INDEX_CONSTRAINT_GE: { | |
| 5529 | - value = VEC0_PARTITION_OPERATOR_GE; | |
| 5530 | - break; | |
| 5531 | - } | |
| 5532 | - case SQLITE_INDEX_CONSTRAINT_NE: { | |
| 5533 | - value = VEC0_PARTITION_OPERATOR_NE; | |
| 5534 | - break; | |
| 5535 | - } | |
| 5536 | - } | |
| 5537 | - | |
| 5538 | - if(value) { | |
| 5539 | - pIdxInfo->aConstraintUsage[i].argvIndex = argvIndex++; | |
| 5540 | - pIdxInfo->aConstraintUsage[i].omit = 1; | |
| 5541 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_KNN_PARTITON_CONSTRAINT); | |
| 5542 | - sqlite3_str_appendchar(idxStr, 1, 'A' + partition_idx); | |
| 5543 | - sqlite3_str_appendchar(idxStr, 1, value); | |
| 5544 | - sqlite3_str_appendchar(idxStr, 1, '_'); | |
| 5545 | - } | |
| 5546 | - | |
| 5547 | - } | |
| 5548 | - | |
| 5549 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | |
| 5550 | - if (!pIdxInfo->aConstraint[i].usable) | |
| 5551 | - continue; | |
| 5552 | - | |
| 5553 | - int iColumn = pIdxInfo->aConstraint[i].iColumn; | |
| 5554 | - int op = pIdxInfo->aConstraint[i].op; | |
| 5555 | - if(op == SQLITE_INDEX_CONSTRAINT_LIMIT || op == SQLITE_INDEX_CONSTRAINT_OFFSET) { | |
| 5556 | - continue; | |
| 5557 | - } | |
| 5558 | - if(!vec0_column_idx_is_metadata(p, iColumn)) { | |
| 5559 | - continue; | |
| 5560 | - } | |
| 5561 | - | |
| 5562 | - int metadata_idx = vec0_column_idx_to_metadata_idx(p, iColumn); | |
| 5563 | - char value = 0; | |
| 5564 | - | |
| 5565 | - switch(op) { | |
| 5566 | - case SQLITE_INDEX_CONSTRAINT_EQ: { | |
| 5567 | - int vtabIn = 0; | |
| 5568 | - #if COMPILER_SUPPORTS_VTAB_IN | |
| 5569 | - if (sqlite3_libversion_number() >= 3038000) { | |
| 5570 | - vtabIn = sqlite3_vtab_in(pIdxInfo, i, -1); | |
| 5571 | - } | |
| 5572 | - if(vtabIn) { | |
| 5573 | - switch(p->metadata_columns[metadata_idx].kind) { | |
| 5574 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: | |
| 5575 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | |
| 5576 | - // IMP: V15248_32086 | |
| 5577 | - rc = SQLITE_ERROR; | |
| 5578 | - vtab_set_error(pVTab, "'xxx in (...)' is only available on INTEGER or TEXT metadata columns."); | |
| 5579 | - goto done; | |
| 5580 | - break; | |
| 5581 | - } | |
| 5582 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: | |
| 5583 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | |
| 5584 | - break; | |
| 5585 | - } | |
| 5586 | - } | |
| 5587 | - value = VEC0_METADATA_OPERATOR_IN; | |
| 5588 | - sqlite3_vtab_in(pIdxInfo, i, 1); | |
| 5589 | - }else | |
| 5590 | - #endif | |
| 5591 | - { | |
| 5592 | - value = VEC0_PARTITION_OPERATOR_EQ; | |
| 5593 | - } | |
| 5594 | - break; | |
| 5595 | - } | |
| 5596 | - case SQLITE_INDEX_CONSTRAINT_GT: { | |
| 5597 | - value = VEC0_METADATA_OPERATOR_GT; | |
| 5598 | - break; | |
| 5599 | - } | |
| 5600 | - case SQLITE_INDEX_CONSTRAINT_LE: { | |
| 5601 | - value = VEC0_METADATA_OPERATOR_LE; | |
| 5602 | - break; | |
| 5603 | - } | |
| 5604 | - case SQLITE_INDEX_CONSTRAINT_LT: { | |
| 5605 | - value = VEC0_METADATA_OPERATOR_LT; | |
| 5606 | - break; | |
| 5607 | - } | |
| 5608 | - case SQLITE_INDEX_CONSTRAINT_GE: { | |
| 5609 | - value = VEC0_METADATA_OPERATOR_GE; | |
| 5610 | - break; | |
| 5611 | - } | |
| 5612 | - case SQLITE_INDEX_CONSTRAINT_NE: { | |
| 5613 | - value = VEC0_METADATA_OPERATOR_NE; | |
| 5614 | - break; | |
| 5615 | - } | |
| 5616 | - default: { | |
| 5617 | - // IMP: V16511_00582 | |
| 5618 | - rc = SQLITE_ERROR; | |
| 5619 | - vtab_set_error(pVTab, | |
| 5620 | - "An illegal WHERE constraint was provided on a vec0 metadata column in a KNN query. " | |
| 5621 | - "Only one of EQUALS, GREATER_THAN, LESS_THAN_OR_EQUAL, LESS_THAN, GREATER_THAN_OR_EQUAL, NOT_EQUALS is allowed." | |
| 5622 | - ); | |
| 5623 | - goto done; | |
| 5624 | - } | |
| 5625 | - } | |
| 5626 | - | |
| 5627 | - if(p->metadata_columns[metadata_idx].kind == VEC0_METADATA_COLUMN_KIND_BOOLEAN) { | |
| 5628 | - if(!(value == VEC0_METADATA_OPERATOR_EQ || value == VEC0_METADATA_OPERATOR_NE)) { | |
| 5629 | - // IMP: V10145_26984 | |
| 5630 | - rc = SQLITE_ERROR; | |
| 5631 | - vtab_set_error(pVTab, "ONLY EQUALS (=) or NOT_EQUALS (!=) operators are allowed on boolean metadata columns."); | |
| 5632 | - goto done; | |
| 5633 | - } | |
| 5634 | - } | |
| 5635 | - | |
| 5636 | - pIdxInfo->aConstraintUsage[i].argvIndex = argvIndex++; | |
| 5637 | - pIdxInfo->aConstraintUsage[i].omit = 1; | |
| 5638 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_METADATA_CONSTRAINT); | |
| 5639 | - sqlite3_str_appendchar(idxStr, 1, 'A' + metadata_idx); | |
| 5640 | - sqlite3_str_appendchar(idxStr, 1, value); | |
| 5641 | - sqlite3_str_appendchar(idxStr, 1, '_'); | |
| 5642 | - | |
| 5643 | - } | |
| 5644 | - | |
| 5645 | - | |
| 5646 | - | |
| 5647 | - pIdxInfo->idxNum = iMatchVectorTerm; | |
| 5648 | - pIdxInfo->estimatedCost = 30.0; | |
| 5649 | - pIdxInfo->estimatedRows = 10; | |
| 5650 | - | |
| 5651 | - } else if (iRowidTerm >= 0) { | |
| 5652 | - sqlite3_str_appendchar(idxStr, 1, VEC0_QUERY_PLAN_POINT); | |
| 5653 | - pIdxInfo->aConstraintUsage[iRowidTerm].argvIndex = 1; | |
| 5654 | - pIdxInfo->aConstraintUsage[iRowidTerm].omit = 1; | |
| 5655 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_POINT_ID); | |
| 5656 | - sqlite3_str_appendchar(idxStr, 3, '_'); | |
| 5657 | - pIdxInfo->idxNum = pIdxInfo->colUsed; | |
| 5658 | - pIdxInfo->estimatedCost = 10.0; | |
| 5659 | - pIdxInfo->estimatedRows = 1; | |
| 5660 | - } else { | |
| 5661 | - sqlite3_str_appendchar(idxStr, 1, VEC0_QUERY_PLAN_FULLSCAN); | |
| 5662 | - pIdxInfo->estimatedCost = 3000000.0; | |
| 5663 | - pIdxInfo->estimatedRows = 100000; | |
| 5664 | - } | |
| 5665 | - pIdxInfo->idxStr = sqlite3_str_finish(idxStr); | |
| 5666 | - idxStr = NULL; | |
| 5667 | - if (!pIdxInfo->idxStr) { | |
| 5668 | - rc = SQLITE_OK; | |
| 5669 | - goto done; | |
| 5670 | - } | |
| 5671 | - pIdxInfo->needToFreeIdxStr = 1; | |
| 5672 | - | |
| 5673 | - | |
| 5674 | - rc = SQLITE_OK; | |
| 5675 | - | |
| 5676 | - done: | |
| 5677 | - if(idxStr) { | |
| 5678 | - sqlite3_str_finish(idxStr); | |
| 5679 | - } | |
| 5680 | - return rc; | |
| 5681 | -} | |
| 5682 | - | |
| 5683 | -// forward delcaration bc vec0Filter uses it | |
| 5684 | -static int vec0Next(sqlite3_vtab_cursor *cur); | |
| 5685 | - | |
| 5686 | -void merge_sorted_lists(f32 *a, i64 *a_rowids, i64 a_length, f32 *b, | |
| 5687 | - i64 *b_rowids, i32 *b_top_idxs, i64 b_length, f32 *out, | |
| 5688 | - i64 *out_rowids, i64 out_length, i64 *out_used) { | |
| 5689 | - // assert((a_length >= out_length) || (b_length >= out_length)); | |
| 5690 | - i64 ptrA = 0; | |
| 5691 | - i64 ptrB = 0; | |
| 5692 | - for (int i = 0; i < out_length; i++) { | |
| 5693 | - if ((ptrA >= a_length) && (ptrB >= b_length)) { | |
| 5694 | - *out_used = i; | |
| 5695 | - return; | |
| 5696 | - } | |
| 5697 | - if (ptrA >= a_length) { | |
| 5698 | - out[i] = b[b_top_idxs[ptrB]]; | |
| 5699 | - out_rowids[i] = b_rowids[b_top_idxs[ptrB]]; | |
| 5700 | - ptrB++; | |
| 5701 | - } else if (ptrB >= b_length) { | |
| 5702 | - out[i] = a[ptrA]; | |
| 5703 | - out_rowids[i] = a_rowids[ptrA]; | |
| 5704 | - ptrA++; | |
| 5705 | - } else { | |
| 5706 | - if (a[ptrA] <= b[b_top_idxs[ptrB]]) { | |
| 5707 | - out[i] = a[ptrA]; | |
| 5708 | - out_rowids[i] = a_rowids[ptrA]; | |
| 5709 | - ptrA++; | |
| 5710 | - } else { | |
| 5711 | - out[i] = b[b_top_idxs[ptrB]]; | |
| 5712 | - out_rowids[i] = b_rowids[b_top_idxs[ptrB]]; | |
| 5713 | - ptrB++; | |
| 5714 | - } | |
| 5715 | - } | |
| 5716 | - } | |
| 5717 | - | |
| 5718 | - *out_used = out_length; | |
| 5719 | -} | |
| 5720 | - | |
| 5721 | -u8 *bitmap_new(i32 n) { | |
| 5722 | - assert(n % 8 == 0); | |
| 5723 | - u8 *p = sqlite3_malloc(n * sizeof(u8) / CHAR_BIT); | |
| 5724 | - if (p) { | |
| 5725 | - memset(p, 0, n * sizeof(u8) / CHAR_BIT); | |
| 5726 | - } | |
| 5727 | - return p; | |
| 5728 | -} | |
| 5729 | -u8 *bitmap_new_from(i32 n, u8 *from) { | |
| 5730 | - assert(n % 8 == 0); | |
| 5731 | - u8 *p = sqlite3_malloc(n * sizeof(u8) / CHAR_BIT); | |
| 5732 | - if (p) { | |
| 5733 | - memcpy(p, from, n / CHAR_BIT); | |
| 5734 | - } | |
| 5735 | - return p; | |
| 5736 | -} | |
| 5737 | - | |
| 5738 | -void bitmap_copy(u8 *base, u8 *from, i32 n) { | |
| 5739 | - assert(n % 8 == 0); | |
| 5740 | - memcpy(base, from, n / CHAR_BIT); | |
| 5741 | -} | |
| 5742 | - | |
| 5743 | -void bitmap_and_inplace(u8 *base, u8 *other, i32 n) { | |
| 5744 | - assert((n % 8) == 0); | |
| 5745 | - for (int i = 0; i < n / CHAR_BIT; i++) { | |
| 5746 | - base[i] = base[i] & other[i]; | |
| 5747 | - } | |
| 5748 | -} | |
| 5749 | - | |
| 5750 | -void bitmap_set(u8 *bitmap, i32 position, int value) { | |
| 5751 | - if (value) { | |
| 5752 | - bitmap[position / CHAR_BIT] |= 1 << (position % CHAR_BIT); | |
| 5753 | - } else { | |
| 5754 | - bitmap[position / CHAR_BIT] &= ~(1 << (position % CHAR_BIT)); | |
| 5755 | - } | |
| 5756 | -} | |
| 5757 | - | |
| 5758 | -int bitmap_get(u8 *bitmap, i32 position) { | |
| 5759 | - return (((bitmap[position / CHAR_BIT]) >> (position % CHAR_BIT)) & 1); | |
| 5760 | -} | |
| 5761 | - | |
| 5762 | -void bitmap_clear(u8 *bitmap, i32 n) { | |
| 5763 | - assert((n % 8) == 0); | |
| 5764 | - memset(bitmap, 0, n / CHAR_BIT); | |
| 5765 | -} | |
| 5766 | - | |
| 5767 | -void bitmap_fill(u8 *bitmap, i32 n) { | |
| 5768 | - assert((n % 8) == 0); | |
| 5769 | - memset(bitmap, 0xFF, n / CHAR_BIT); | |
| 5770 | -} | |
| 5771 | - | |
| 5772 | -/** | |
| 5773 | - * @brief Finds the minimum k items in distances, and writes the indicies to | |
| 5774 | - * out. | |
| 5775 | - * | |
| 5776 | - * @param distances input f32 array of size n, the items to consider. | |
| 5777 | - * @param n: size of distances array. | |
| 5778 | - * @param out: Output array of size k, will contain at most k element indicies | |
| 5779 | - * @param k: Size of output array | |
| 5780 | - * @return int | |
| 5781 | - */ | |
| 5782 | -int min_idx(const f32 *distances, i32 n, u8 *candidates, i32 *out, i32 k, | |
| 5783 | - u8 *bTaken, i32 *k_used) { | |
| 5784 | - assert(k > 0); | |
| 5785 | - assert(k <= n); | |
| 5786 | - | |
| 5787 | - bitmap_clear(bTaken, n); | |
| 5788 | - | |
| 5789 | - for (int ik = 0; ik < k; ik++) { | |
| 5790 | - int min_idx = 0; | |
| 5791 | - while (min_idx < n && | |
| 5792 | - (bitmap_get(bTaken, min_idx) || !bitmap_get(candidates, min_idx))) { | |
| 5793 | - min_idx++; | |
| 5794 | - } | |
| 5795 | - if (min_idx >= n) { | |
| 5796 | - *k_used = ik; | |
| 5797 | - return SQLITE_OK; | |
| 5798 | - } | |
| 5799 | - | |
| 5800 | - for (int i = 0; i < n; i++) { | |
| 5801 | - if (distances[i] <= distances[min_idx] && !bitmap_get(bTaken, i) && | |
| 5802 | - (bitmap_get(candidates, i))) { | |
| 5803 | - min_idx = i; | |
| 5804 | - } | |
| 5805 | - } | |
| 5806 | - | |
| 5807 | - out[ik] = min_idx; | |
| 5808 | - bitmap_set(bTaken, min_idx, 1); | |
| 5809 | - } | |
| 5810 | - *k_used = k; | |
| 5811 | - return SQLITE_OK; | |
| 5812 | -} | |
| 5813 | - | |
| 5814 | -int vec0_get_metadata_text_long_value( | |
| 5815 | - vec0_vtab * p, | |
| 5816 | - sqlite3_stmt ** stmt, | |
| 5817 | - int metadata_idx, | |
| 5818 | - i64 rowid, | |
| 5819 | - int *n, | |
| 5820 | - char ** s) { | |
| 5821 | - int rc; | |
| 5822 | - if(!(*stmt)) { | |
| 5823 | - const char * zSql = sqlite3_mprintf("select data from " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " where rowid = ?", p->schemaName, p->tableName, metadata_idx); | |
| 5824 | - if(!zSql) { | |
| 5825 | - rc = SQLITE_NOMEM; | |
| 5826 | - goto done; | |
| 5827 | - } | |
| 5828 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, stmt, NULL); | |
| 5829 | - sqlite3_free( (void *) zSql); | |
| 5830 | - if(rc != SQLITE_OK) { | |
| 5831 | - goto done; | |
| 5832 | - } | |
| 5833 | - } | |
| 5834 | - | |
| 5835 | - sqlite3_reset(*stmt); | |
| 5836 | - sqlite3_bind_int64(*stmt, 1, rowid); | |
| 5837 | - rc = sqlite3_step(*stmt); | |
| 5838 | - if(rc != SQLITE_ROW) { | |
| 5839 | - rc = SQLITE_ERROR; | |
| 5840 | - goto done; | |
| 5841 | - } | |
| 5842 | - *s = (char *) sqlite3_column_text(*stmt, 0); | |
| 5843 | - *n = sqlite3_column_bytes(*stmt, 0); | |
| 5844 | - rc = SQLITE_OK; | |
| 5845 | - done: | |
| 5846 | - return rc; | |
| 5847 | -} | |
| 5848 | - | |
| 5849 | -/** | |
| 5850 | - * @brief Crete at "iterator" (sqlite3_stmt) of chunks with the given constraints | |
| 5851 | - * | |
| 5852 | - * Any VEC0_IDXSTR_KIND_KNN_PARTITON_CONSTRAINT values in idxStr/argv will be applied | |
| 5853 | - * as WHERE constraints in the underlying stmt SQL, and any consumer of the stmt | |
| 5854 | - * can freely step through the stmt with all constraints satisfied. | |
| 5855 | - * | |
| 5856 | - * @param p - vec0_vtab | |
| 5857 | - * @param idxStr - the xBestIndex/xFilter idxstr containing VEC0_IDXSTR values | |
| 5858 | - * @param argc - number of argv values from xFilter | |
| 5859 | - * @param argv - array of sqlite3_value from xFilter | |
| 5860 | - * @param outStmt - output sqlite3_stmt of chunks with all filters applied | |
| 5861 | - * @return int SQLITE_OK on success, error code otherwise | |
| 5862 | - */ | |
| 5863 | -int vec0_chunks_iter(vec0_vtab * p, const char * idxStr, int argc, sqlite3_value ** argv, sqlite3_stmt** outStmt) { | |
| 5864 | - // always null terminated, enforced by SQLite | |
| 5865 | - int idxStrLength = strlen(idxStr); | |
| 5866 | - // "1" refers to the initial vec0_query_plan char, 4 is the number of chars per "element" | |
| 5867 | - int numValueEntries = (idxStrLength-1) / 4; | |
| 5868 | - assert(argc == numValueEntries); | |
| 5869 | - | |
| 5870 | - int rc; | |
| 5871 | - sqlite3_str * s = sqlite3_str_new(NULL); | |
| 5872 | - sqlite3_str_appendf(s, "select chunk_id, validity, rowids " | |
| 5873 | - " from " VEC0_SHADOW_CHUNKS_NAME, | |
| 5874 | - p->schemaName, p->tableName); | |
| 5875 | - | |
| 5876 | - int appendedWhere = 0; | |
| 5877 | - for(int i = 0; i < numValueEntries; i++) { | |
| 5878 | - int idx = 1 + (i * 4); | |
| 5879 | - char kind = idxStr[idx + 0]; | |
| 5880 | - if(kind != VEC0_IDXSTR_KIND_KNN_PARTITON_CONSTRAINT) { | |
| 5881 | - continue; | |
| 5882 | - } | |
| 5883 | - | |
| 5884 | - int partition_idx = idxStr[idx + 1] - 'A'; | |
| 5885 | - int operator = idxStr[idx + 2]; | |
| 5886 | - // idxStr[idx + 3] is just null, a '_' placeholder | |
| 5887 | - | |
| 5888 | - if(!appendedWhere) { | |
| 5889 | - sqlite3_str_appendall(s, " WHERE "); | |
| 5890 | - appendedWhere = 1; | |
| 5891 | - }else { | |
| 5892 | - sqlite3_str_appendall(s, " AND "); | |
| 5893 | - } | |
| 5894 | - switch(operator) { | |
| 5895 | - case VEC0_PARTITION_OPERATOR_EQ: | |
| 5896 | - sqlite3_str_appendf(s, " partition%02d = ? ", partition_idx); | |
| 5897 | - break; | |
| 5898 | - case VEC0_PARTITION_OPERATOR_GT: | |
| 5899 | - sqlite3_str_appendf(s, " partition%02d > ? ", partition_idx); | |
| 5900 | - break; | |
| 5901 | - case VEC0_PARTITION_OPERATOR_LE: | |
| 5902 | - sqlite3_str_appendf(s, " partition%02d <= ? ", partition_idx); | |
| 5903 | - break; | |
| 5904 | - case VEC0_PARTITION_OPERATOR_LT: | |
| 5905 | - sqlite3_str_appendf(s, " partition%02d < ? ", partition_idx); | |
| 5906 | - break; | |
| 5907 | - case VEC0_PARTITION_OPERATOR_GE: | |
| 5908 | - sqlite3_str_appendf(s, " partition%02d >= ? ", partition_idx); | |
| 5909 | - break; | |
| 5910 | - case VEC0_PARTITION_OPERATOR_NE: | |
| 5911 | - sqlite3_str_appendf(s, " partition%02d != ? ", partition_idx); | |
| 5912 | - break; | |
| 5913 | - default: { | |
| 5914 | - char * zSql = sqlite3_str_finish(s); | |
| 5915 | - sqlite3_free(zSql); | |
| 5916 | - return SQLITE_ERROR; | |
| 5917 | - } | |
| 5918 | - | |
| 5919 | - } | |
| 5920 | - | |
| 5921 | - } | |
| 5922 | - | |
| 5923 | - char *zSql = sqlite3_str_finish(s); | |
| 5924 | - if (!zSql) { | |
| 5925 | - return SQLITE_NOMEM; | |
| 5926 | - } | |
| 5927 | - | |
| 5928 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, outStmt, NULL); | |
| 5929 | - sqlite3_free(zSql); | |
| 5930 | - if(rc != SQLITE_OK) { | |
| 5931 | - return rc; | |
| 5932 | - } | |
| 5933 | - | |
| 5934 | - int n = 1; | |
| 5935 | - for(int i = 0; i < numValueEntries; i++) { | |
| 5936 | - int idx = 1 + (i * 4); | |
| 5937 | - char kind = idxStr[idx + 0]; | |
| 5938 | - if(kind != VEC0_IDXSTR_KIND_KNN_PARTITON_CONSTRAINT) { | |
| 5939 | - continue; | |
| 5940 | - } | |
| 5941 | - sqlite3_bind_value(*outStmt, n++, argv[i]); | |
| 5942 | - } | |
| 5943 | - | |
| 5944 | - return rc; | |
| 5945 | -} | |
| 5946 | - | |
| 5947 | -// a single `xxx in (...)` constraint on a metadata column. TEXT or INTEGER only for now. | |
| 5948 | -struct Vec0MetadataIn{ | |
| 5949 | - // index of argv[i]` the constraint is on | |
| 5950 | - int argv_idx; | |
| 5951 | - // metadata column index of the constraint, derived from idxStr + argv_idx | |
| 5952 | - int metadata_idx; | |
| 5953 | - // array of the copied `(...)` values from sqlite3_vtab_in_first()/sqlite3_vtab_in_next() | |
| 5954 | - struct Array array; | |
| 5955 | -}; | |
| 5956 | - | |
| 5957 | -// Array elements for `xxx in (...)` values for a text column. basically just a string | |
| 5958 | -struct Vec0MetadataInTextEntry { | |
| 5959 | - int n; | |
| 5960 | - char * zString; | |
| 5961 | -}; | |
| 5962 | - | |
| 5963 | - | |
| 5964 | -int vec0_metadata_filter_text(vec0_vtab * p, sqlite3_value * value, const void * buffer, int size, vec0_metadata_operator op, u8* b, int metadata_idx, int chunk_rowid, struct Array * aMetadataIn, int argv_idx) { | |
| 5965 | - int rc; | |
| 5966 | - sqlite3_stmt * stmt = NULL; | |
| 5967 | - i64 * rowids = NULL; | |
| 5968 | - sqlite3_blob * rowidsBlob; | |
| 5969 | - const char * sTarget = (const char *) sqlite3_value_text(value); | |
| 5970 | - int nTarget = sqlite3_value_bytes(value); | |
| 5971 | - | |
| 5972 | - | |
| 5973 | - // TODO(perf): only text metadata news the rowids BLOB. Make it so that | |
| 5974 | - // rowids BLOB is re-used when multiple fitlers on text columns, | |
| 5975 | - // ex "name BETWEEN 'a' and 'b'"" | |
| 5976 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowChunksName, "rowids", chunk_rowid, 0, &rowidsBlob); | |
| 5977 | - if(rc != SQLITE_OK) { | |
| 5978 | - return rc; | |
| 5979 | - } | |
| 5980 | - assert(sqlite3_blob_bytes(rowidsBlob) % sizeof(i64) == 0); | |
| 5981 | - assert((sqlite3_blob_bytes(rowidsBlob) / sizeof(i64)) == size); | |
| 5982 | - | |
| 5983 | - rowids = sqlite3_malloc(sqlite3_blob_bytes(rowidsBlob)); | |
| 5984 | - if(!rowids) { | |
| 5985 | - sqlite3_blob_close(rowidsBlob); | |
| 5986 | - return SQLITE_NOMEM; | |
| 5987 | - } | |
| 5988 | - | |
| 5989 | - rc = sqlite3_blob_read(rowidsBlob, rowids, sqlite3_blob_bytes(rowidsBlob), 0); | |
| 5990 | - if(rc != SQLITE_OK) { | |
| 5991 | - sqlite3_blob_close(rowidsBlob); | |
| 5992 | - return rc; | |
| 5993 | - } | |
| 5994 | - sqlite3_blob_close(rowidsBlob); | |
| 5995 | - | |
| 5996 | - switch(op) { | |
| 5997 | - int nPrefix; | |
| 5998 | - char * sPrefix; | |
| 5999 | - char *sFull; | |
| 6000 | - int nFull; | |
| 6001 | - u8 * view; | |
| 6002 | - case VEC0_METADATA_OPERATOR_EQ: { | |
| 6003 | - for(int i = 0; i < size; i++) { | |
| 6004 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | |
| 6005 | - nPrefix = ((int*) view)[0]; | |
| 6006 | - sPrefix = (char *) &view[4]; | |
| 6007 | - | |
| 6008 | - // for EQ the text lengths must match | |
| 6009 | - if(nPrefix != nTarget) { | |
| 6010 | - bitmap_set(b, i, 0); | |
| 6011 | - continue; | |
| 6012 | - } | |
| 6013 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH)); | |
| 6014 | - | |
| 6015 | - // for short strings, use the prefix comparison direclty | |
| 6016 | - if(nPrefix <= VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | |
| 6017 | - bitmap_set(b, i, cmpPrefix == 0); | |
| 6018 | - continue; | |
| 6019 | - } | |
| 6020 | - // for EQ on longs strings, the prefix must match | |
| 6021 | - if(cmpPrefix) { | |
| 6022 | - bitmap_set(b, i, 0); | |
| 6023 | - continue; | |
| 6024 | - } | |
| 6025 | - // consult the full string | |
| 6026 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | |
| 6027 | - if(rc != SQLITE_OK) { | |
| 6028 | - goto done; | |
| 6029 | - } | |
| 6030 | - if(nPrefix != nFull) { | |
| 6031 | - rc = SQLITE_ERROR; | |
| 6032 | - goto done; | |
| 6033 | - } | |
| 6034 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) == 0); | |
| 6035 | - } | |
| 6036 | - break; | |
| 6037 | - } | |
| 6038 | - case VEC0_METADATA_OPERATOR_NE: { | |
| 6039 | - for(int i = 0; i < size; i++) { | |
| 6040 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | |
| 6041 | - nPrefix = ((int*) view)[0]; | |
| 6042 | - sPrefix = (char *) &view[4]; | |
| 6043 | - | |
| 6044 | - // for NE if text lengths dont match, it never will | |
| 6045 | - if(nPrefix != nTarget) { | |
| 6046 | - bitmap_set(b, i, 1); | |
| 6047 | - continue; | |
| 6048 | - } | |
| 6049 | - | |
| 6050 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH)); | |
| 6051 | - | |
| 6052 | - // for short strings, use the prefix comparison direclty | |
| 6053 | - if(nPrefix <= VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | |
| 6054 | - bitmap_set(b, i, cmpPrefix != 0); | |
| 6055 | - continue; | |
| 6056 | - } | |
| 6057 | - // for NE on longs strings, if prefixes dont match, then long string wont | |
| 6058 | - if(cmpPrefix) { | |
| 6059 | - bitmap_set(b, i, 1); | |
| 6060 | - continue; | |
| 6061 | - } | |
| 6062 | - // consult the full string | |
| 6063 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | |
| 6064 | - if(rc != SQLITE_OK) { | |
| 6065 | - goto done; | |
| 6066 | - } | |
| 6067 | - if(nPrefix != nFull) { | |
| 6068 | - rc = SQLITE_ERROR; | |
| 6069 | - goto done; | |
| 6070 | - } | |
| 6071 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) != 0); | |
| 6072 | - } | |
| 6073 | - break; | |
| 6074 | - } | |
| 6075 | - case VEC0_METADATA_OPERATOR_GT: { | |
| 6076 | - for(int i = 0; i < size; i++) { | |
| 6077 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | |
| 6078 | - nPrefix = ((int*) view)[0]; | |
| 6079 | - sPrefix = (char *) &view[4]; | |
| 6080 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH), nTarget)); | |
| 6081 | - | |
| 6082 | - if(nPrefix < VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | |
| 6083 | - // if prefix match, check which is longer | |
| 6084 | - if(cmpPrefix == 0) { | |
| 6085 | - bitmap_set(b, i, nPrefix > nTarget); | |
| 6086 | - } | |
| 6087 | - else { | |
| 6088 | - bitmap_set(b, i, cmpPrefix > 0); | |
| 6089 | - } | |
| 6090 | - continue; | |
| 6091 | - } | |
| 6092 | - // TODO(perf): may not need to compare full text in some cases | |
| 6093 | - | |
| 6094 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | |
| 6095 | - if(rc != SQLITE_OK) { | |
| 6096 | - goto done; | |
| 6097 | - } | |
| 6098 | - if(nPrefix != nFull) { | |
| 6099 | - rc = SQLITE_ERROR; | |
| 6100 | - goto done; | |
| 6101 | - } | |
| 6102 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) > 0); | |
| 6103 | - } | |
| 6104 | - break; | |
| 6105 | - } | |
| 6106 | - case VEC0_METADATA_OPERATOR_GE: { | |
| 6107 | - for(int i = 0; i < size; i++) { | |
| 6108 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | |
| 6109 | - nPrefix = ((int*) view)[0]; | |
| 6110 | - sPrefix = (char *) &view[4]; | |
| 6111 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH), nTarget)); | |
| 6112 | - | |
| 6113 | - if(nPrefix < VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | |
| 6114 | - // if prefix match, check which is longer | |
| 6115 | - if(cmpPrefix == 0) { | |
| 6116 | - bitmap_set(b, i, nPrefix >= nTarget); | |
| 6117 | - } | |
| 6118 | - else { | |
| 6119 | - bitmap_set(b, i, cmpPrefix >= 0); | |
| 6120 | - } | |
| 6121 | - continue; | |
| 6122 | - } | |
| 6123 | - // TODO(perf): may not need to compare full text in some cases | |
| 6124 | - | |
| 6125 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | |
| 6126 | - if(rc != SQLITE_OK) { | |
| 6127 | - goto done; | |
| 6128 | - } | |
| 6129 | - if(nPrefix != nFull) { | |
| 6130 | - rc = SQLITE_ERROR; | |
| 6131 | - goto done; | |
| 6132 | - } | |
| 6133 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) >= 0); | |
| 6134 | - } | |
| 6135 | - break; | |
| 6136 | - } | |
| 6137 | - case VEC0_METADATA_OPERATOR_LE: { | |
| 6138 | - for(int i = 0; i < size; i++) { | |
| 6139 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | |
| 6140 | - nPrefix = ((int*) view)[0]; | |
| 6141 | - sPrefix = (char *) &view[4]; | |
| 6142 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH), nTarget)); | |
| 6143 | - | |
| 6144 | - if(nPrefix < VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | |
| 6145 | - // if prefix match, check which is longer | |
| 6146 | - if(cmpPrefix == 0) { | |
| 6147 | - bitmap_set(b, i, nPrefix <= nTarget); | |
| 6148 | - } | |
| 6149 | - else { | |
| 6150 | - bitmap_set(b, i, cmpPrefix <= 0); | |
| 6151 | - } | |
| 6152 | - continue; | |
| 6153 | - } | |
| 6154 | - // TODO(perf): may not need to compare full text in some cases | |
| 6155 | - | |
| 6156 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | |
| 6157 | - if(rc != SQLITE_OK) { | |
| 6158 | - goto done; | |
| 6159 | - } | |
| 6160 | - if(nPrefix != nFull) { | |
| 6161 | - rc = SQLITE_ERROR; | |
| 6162 | - goto done; | |
| 6163 | - } | |
| 6164 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) <= 0); | |
| 6165 | - } | |
| 6166 | - break; | |
| 6167 | - } | |
| 6168 | - case VEC0_METADATA_OPERATOR_LT: { | |
| 6169 | - for(int i = 0; i < size; i++) { | |
| 6170 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | |
| 6171 | - nPrefix = ((int*) view)[0]; | |
| 6172 | - sPrefix = (char *) &view[4]; | |
| 6173 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH), nTarget)); | |
| 6174 | - | |
| 6175 | - if(nPrefix < VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | |
| 6176 | - // if prefix match, check which is longer | |
| 6177 | - if(cmpPrefix == 0) { | |
| 6178 | - bitmap_set(b, i, nPrefix < nTarget); | |
| 6179 | - } | |
| 6180 | - else { | |
| 6181 | - bitmap_set(b, i, cmpPrefix < 0); | |
| 6182 | - } | |
| 6183 | - continue; | |
| 6184 | - } | |
| 6185 | - // TODO(perf): may not need to compare full text in some cases | |
| 6186 | - | |
| 6187 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | |
| 6188 | - if(rc != SQLITE_OK) { | |
| 6189 | - goto done; | |
| 6190 | - } | |
| 6191 | - if(nPrefix != nFull) { | |
| 6192 | - rc = SQLITE_ERROR; | |
| 6193 | - goto done; | |
| 6194 | - } | |
| 6195 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) < 0); | |
| 6196 | - } | |
| 6197 | - break; | |
| 6198 | - } | |
| 6199 | - | |
| 6200 | - case VEC0_METADATA_OPERATOR_IN: { | |
| 6201 | - size_t metadataInIdx = -1; | |
| 6202 | - for(size_t i = 0; i < aMetadataIn->length; i++) { | |
| 6203 | - struct Vec0MetadataIn * metadataIn = &(((struct Vec0MetadataIn *) aMetadataIn->z)[i]); | |
| 6204 | - if(metadataIn->argv_idx == argv_idx) { | |
| 6205 | - metadataInIdx = i; | |
| 6206 | - break; | |
| 6207 | - } | |
| 6208 | - } | |
| 6209 | - if(metadataInIdx < 0) { | |
| 6210 | - rc = SQLITE_ERROR; | |
| 6211 | - goto done; | |
| 6212 | - } | |
| 6213 | - | |
| 6214 | - struct Vec0MetadataIn * metadataIn = &((struct Vec0MetadataIn *) aMetadataIn->z)[metadataInIdx]; | |
| 6215 | - struct Array * aTarget = &(metadataIn->array); | |
| 6216 | - | |
| 6217 | - | |
| 6218 | - int nPrefix; | |
| 6219 | - char * sPrefix; | |
| 6220 | - char *sFull; | |
| 6221 | - int nFull; | |
| 6222 | - u8 * view; | |
| 6223 | - for(int i = 0; i < size; i++) { | |
| 6224 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | |
| 6225 | - nPrefix = ((int*) view)[0]; | |
| 6226 | - sPrefix = (char *) &view[4]; | |
| 6227 | - for(size_t target_idx = 0; target_idx < aTarget->length; target_idx++) { | |
| 6228 | - struct Vec0MetadataInTextEntry * entry = &(((struct Vec0MetadataInTextEntry*)aTarget->z)[target_idx]); | |
| 6229 | - if(entry->n != nPrefix) { | |
| 6230 | - continue; | |
| 6231 | - } | |
| 6232 | - int cmpPrefix = strncmp(sPrefix, entry->zString, min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH)); | |
| 6233 | - if(nPrefix <= VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | |
| 6234 | - if(cmpPrefix == 0) { | |
| 6235 | - bitmap_set(b, i, 1); | |
| 6236 | - break; | |
| 6237 | - } | |
| 6238 | - continue; | |
| 6239 | - } | |
| 6240 | - if(cmpPrefix) { | |
| 6241 | - continue; | |
| 6242 | - } | |
| 6243 | - | |
| 6244 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | |
| 6245 | - if(rc != SQLITE_OK) { | |
| 6246 | - goto done; | |
| 6247 | - } | |
| 6248 | - if(nPrefix != nFull) { | |
| 6249 | - rc = SQLITE_ERROR; | |
| 6250 | - goto done; | |
| 6251 | - } | |
| 6252 | - if(strncmp(sFull, entry->zString, nFull) == 0) { | |
| 6253 | - bitmap_set(b, i, 1); | |
| 6254 | - break; | |
| 6255 | - } | |
| 6256 | - } | |
| 6257 | - } | |
| 6258 | - break; | |
| 6259 | - } | |
| 6260 | - | |
| 6261 | - } | |
| 6262 | - rc = SQLITE_OK; | |
| 6263 | - | |
| 6264 | - done: | |
| 6265 | - sqlite3_finalize(stmt); | |
| 6266 | - sqlite3_free(rowids); | |
| 6267 | - return rc; | |
| 6268 | - | |
| 6269 | -} | |
| 6270 | - | |
| 6271 | -/** | |
| 6272 | - * @brief Fill in bitmap of chunk values, whether or not the values match a metadata constraint | |
| 6273 | - * | |
| 6274 | - * @param p vec0_vtab | |
| 6275 | - * @param metadata_idx index of the metatadata column to perfrom constraints on | |
| 6276 | - * @param value sqlite3_value of the constraints value | |
| 6277 | - * @param blob sqlite3_blob that is already opened on the metdata column's shadow chunk table | |
| 6278 | - * @param chunk_rowid rowid of the chunk to calculate on | |
| 6279 | - * @param b pre-allocated and zero'd out bitmap to write results to | |
| 6280 | - * @param size size of the chunk | |
| 6281 | - * @return int SQLITE_OK on success, error code otherwise | |
| 6282 | - */ | |
| 6283 | -int vec0_set_metadata_filter_bitmap( | |
| 6284 | - vec0_vtab *p, | |
| 6285 | - int metadata_idx, | |
| 6286 | - vec0_metadata_operator op, | |
| 6287 | - sqlite3_value * value, | |
| 6288 | - sqlite3_blob * blob, | |
| 6289 | - i64 chunk_rowid, | |
| 6290 | - u8* b, | |
| 6291 | - int size, | |
| 6292 | - struct Array * aMetadataIn, int argv_idx) { | |
| 6293 | - // TODO: shouldn't this skip in-valid entries from the chunk's validity bitmap? | |
| 6294 | - | |
| 6295 | - int rc; | |
| 6296 | - rc = sqlite3_blob_reopen(blob, chunk_rowid); | |
| 6297 | - if(rc != SQLITE_OK) { | |
| 6298 | - return rc; | |
| 6299 | - } | |
| 6300 | - | |
| 6301 | - vec0_metadata_column_kind kind = p->metadata_columns[metadata_idx].kind; | |
| 6302 | - int szMatch = 0; | |
| 6303 | - int blobSize = sqlite3_blob_bytes(blob); | |
| 6304 | - switch(kind) { | |
| 6305 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | |
| 6306 | - szMatch = blobSize == size / CHAR_BIT; | |
| 6307 | - break; | |
| 6308 | - } | |
| 6309 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | |
| 6310 | - szMatch = blobSize == size * sizeof(i64); | |
| 6311 | - break; | |
| 6312 | - } | |
| 6313 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | |
| 6314 | - szMatch = blobSize == size * sizeof(double); | |
| 6315 | - break; | |
| 6316 | - } | |
| 6317 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | |
| 6318 | - szMatch = blobSize == size * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH; | |
| 6319 | - break; | |
| 6320 | - } | |
| 6321 | - } | |
| 6322 | - if(!szMatch) { | |
| 6323 | - return SQLITE_ERROR; | |
| 6324 | - } | |
| 6325 | - void * buffer = sqlite3_malloc(blobSize); | |
| 6326 | - if(!buffer) { | |
| 6327 | - return SQLITE_NOMEM; | |
| 6328 | - } | |
| 6329 | - rc = sqlite3_blob_read(blob, buffer, blobSize, 0); | |
| 6330 | - if(rc != SQLITE_OK) { | |
| 6331 | - goto done; | |
| 6332 | - } | |
| 6333 | - switch(kind) { | |
| 6334 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | |
| 6335 | - int target = sqlite3_value_int(value); | |
| 6336 | - if( (target && op == VEC0_METADATA_OPERATOR_EQ) || (!target && op == VEC0_METADATA_OPERATOR_NE)) { | |
| 6337 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, bitmap_get((u8*) buffer, i)); } | |
| 6338 | - } | |
| 6339 | - else { | |
| 6340 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, !bitmap_get((u8*) buffer, i)); } | |
| 6341 | - } | |
| 6342 | - break; | |
| 6343 | - } | |
| 6344 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | |
| 6345 | - i64 * array = (i64*) buffer; | |
| 6346 | - i64 target = sqlite3_value_int64(value); | |
| 6347 | - switch(op) { | |
| 6348 | - case VEC0_METADATA_OPERATOR_EQ: { | |
| 6349 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] == target); } | |
| 6350 | - break; | |
| 6351 | - } | |
| 6352 | - case VEC0_METADATA_OPERATOR_GT: { | |
| 6353 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] > target); } | |
| 6354 | - break; | |
| 6355 | - } | |
| 6356 | - case VEC0_METADATA_OPERATOR_LE: { | |
| 6357 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] <= target); } | |
| 6358 | - break; | |
| 6359 | - } | |
| 6360 | - case VEC0_METADATA_OPERATOR_LT: { | |
| 6361 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] < target); } | |
| 6362 | - break; | |
| 6363 | - } | |
| 6364 | - case VEC0_METADATA_OPERATOR_GE: { | |
| 6365 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] >= target); } | |
| 6366 | - break; | |
| 6367 | - } | |
| 6368 | - case VEC0_METADATA_OPERATOR_NE: { | |
| 6369 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] != target); } | |
| 6370 | - break; | |
| 6371 | - } | |
| 6372 | - case VEC0_METADATA_OPERATOR_IN: { | |
| 6373 | - int metadataInIdx = -1; | |
| 6374 | - for(size_t i = 0; i < aMetadataIn->length; i++) { | |
| 6375 | - struct Vec0MetadataIn * metadataIn = &((struct Vec0MetadataIn *) aMetadataIn->z)[i]; | |
| 6376 | - if(metadataIn->argv_idx == argv_idx) { | |
| 6377 | - metadataInIdx = i; | |
| 6378 | - break; | |
| 6379 | - } | |
| 6380 | - } | |
| 6381 | - if(metadataInIdx < 0) { | |
| 6382 | - rc = SQLITE_ERROR; | |
| 6383 | - goto done; | |
| 6384 | - } | |
| 6385 | - struct Vec0MetadataIn * metadataIn = &((struct Vec0MetadataIn *) aMetadataIn->z)[metadataInIdx]; | |
| 6386 | - struct Array * aTarget = &(metadataIn->array); | |
| 6387 | - | |
| 6388 | - for(int i = 0; i < size; i++) { | |
| 6389 | - for(size_t target_idx = 0; target_idx < aTarget->length; target_idx++) { | |
| 6390 | - if( ((i64*)aTarget->z)[target_idx] == array[i]) { | |
| 6391 | - bitmap_set(b, i, 1); | |
| 6392 | - break; | |
| 6393 | - } | |
| 6394 | - } | |
| 6395 | - } | |
| 6396 | - break; | |
| 6397 | - } | |
| 6398 | - } | |
| 6399 | - break; | |
| 6400 | - } | |
| 6401 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | |
| 6402 | - double * array = (double*) buffer; | |
| 6403 | - double target = sqlite3_value_double(value); | |
| 6404 | - switch(op) { | |
| 6405 | - case VEC0_METADATA_OPERATOR_EQ: { | |
| 6406 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] == target); } | |
| 6407 | - break; | |
| 6408 | - } | |
| 6409 | - case VEC0_METADATA_OPERATOR_GT: { | |
| 6410 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] > target); } | |
| 6411 | - break; | |
| 6412 | - } | |
| 6413 | - case VEC0_METADATA_OPERATOR_LE: { | |
| 6414 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] <= target); } | |
| 6415 | - break; | |
| 6416 | - } | |
| 6417 | - case VEC0_METADATA_OPERATOR_LT: { | |
| 6418 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] < target); } | |
| 6419 | - break; | |
| 6420 | - } | |
| 6421 | - case VEC0_METADATA_OPERATOR_GE: { | |
| 6422 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] >= target); } | |
| 6423 | - break; | |
| 6424 | - } | |
| 6425 | - case VEC0_METADATA_OPERATOR_NE: { | |
| 6426 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] != target); } | |
| 6427 | - break; | |
| 6428 | - } | |
| 6429 | - case VEC0_METADATA_OPERATOR_IN: { | |
| 6430 | - // should never be reached | |
| 6431 | - break; | |
| 6432 | - } | |
| 6433 | - } | |
| 6434 | - break; | |
| 6435 | - } | |
| 6436 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | |
| 6437 | - rc = vec0_metadata_filter_text(p, value, buffer, size, op, b, metadata_idx, chunk_rowid, aMetadataIn, argv_idx); | |
| 6438 | - if(rc != SQLITE_OK) { | |
| 6439 | - goto done; | |
| 6440 | - } | |
| 6441 | - break; | |
| 6442 | - } | |
| 6443 | - } | |
| 6444 | - done: | |
| 6445 | - sqlite3_free(buffer); | |
| 6446 | - return rc; | |
| 6447 | -} | |
| 6448 | - | |
| 6449 | -int vec0Filter_knn_chunks_iter(vec0_vtab *p, sqlite3_stmt *stmtChunks, | |
| 6450 | - struct VectorColumnDefinition *vector_column, | |
| 6451 | - int vectorColumnIdx, struct Array *arrayRowidsIn, | |
| 6452 | - struct Array * aMetadataIn, | |
| 6453 | - const char * idxStr, int argc, sqlite3_value ** argv, | |
| 6454 | - void *queryVector, i64 k, i64 **out_topk_rowids, | |
| 6455 | - f32 **out_topk_distances, i64 *out_used) { | |
| 6456 | - // for each chunk, get top min(k, chunk_size) rowid + distances to query vec. | |
| 6457 | - // then reconcile all topk_chunks for a true top k. | |
| 6458 | - // output only rowids + distances for now | |
| 6459 | - | |
| 6460 | - int rc = SQLITE_OK; | |
| 6461 | - sqlite3_blob *blobVectors = NULL; | |
| 6462 | - | |
| 6463 | - void *baseVectors = NULL; // memory: chunk_size * dimensions * element_size | |
| 6464 | - | |
| 6465 | - // OWNED BY CALLER ON SUCCESS | |
| 6466 | - i64 *topk_rowids = NULL; // memory: k * 4 | |
| 6467 | - // OWNED BY CALLER ON SUCCESS | |
| 6468 | - f32 *topk_distances = NULL; // memory: k * 4 | |
| 6469 | - | |
| 6470 | - i64 *tmp_topk_rowids = NULL; // memory: k * 4 | |
| 6471 | - f32 *tmp_topk_distances = NULL; // memory: k * 4 | |
| 6472 | - f32 *chunk_distances = NULL; // memory: chunk_size * 4 | |
| 6473 | - u8 *b = NULL; // memory: chunk_size / 8 | |
| 6474 | - u8 *bTaken = NULL; // memory: chunk_size / 8 | |
| 6475 | - i32 *chunk_topk_idxs = NULL; // memory: k * 4 | |
| 6476 | - u8 *bmRowids = NULL; // memory: chunk_size / 8 | |
| 6477 | - u8 *bmMetadata = NULL; // memory: chunk_size / 8 | |
| 6478 | - // // total: a lot??? | |
| 6479 | - | |
| 6480 | - // 6 * (k * 4) + (k * 2) + (chunk_size / 8) + (chunk_size * dimensions * 4) | |
| 6481 | - | |
| 6482 | - topk_rowids = sqlite3_malloc(k * sizeof(i64)); | |
| 6483 | - if (!topk_rowids) { | |
| 6484 | - rc = SQLITE_NOMEM; | |
| 6485 | - goto cleanup; | |
| 6486 | - } | |
| 6487 | - memset(topk_rowids, 0, k * sizeof(i64)); | |
| 6488 | - | |
| 6489 | - topk_distances = sqlite3_malloc(k * sizeof(f32)); | |
| 6490 | - if (!topk_distances) { | |
| 6491 | - rc = SQLITE_NOMEM; | |
| 6492 | - goto cleanup; | |
| 6493 | - } | |
| 6494 | - memset(topk_distances, 0, k * sizeof(f32)); | |
| 6495 | - | |
| 6496 | - tmp_topk_rowids = sqlite3_malloc(k * sizeof(i64)); | |
| 6497 | - if (!tmp_topk_rowids) { | |
| 6498 | - rc = SQLITE_NOMEM; | |
| 6499 | - goto cleanup; | |
| 6500 | - } | |
| 6501 | - memset(tmp_topk_rowids, 0, k * sizeof(i64)); | |
| 6502 | - | |
| 6503 | - tmp_topk_distances = sqlite3_malloc(k * sizeof(f32)); | |
| 6504 | - if (!tmp_topk_distances) { | |
| 6505 | - rc = SQLITE_NOMEM; | |
| 6506 | - goto cleanup; | |
| 6507 | - } | |
| 6508 | - memset(tmp_topk_distances, 0, k * sizeof(f32)); | |
| 6509 | - | |
| 6510 | - i64 k_used = 0; | |
| 6511 | - i64 baseVectorsSize = p->chunk_size * vector_column_byte_size(*vector_column); | |
| 6512 | - baseVectors = sqlite3_malloc(baseVectorsSize); | |
| 6513 | - if (!baseVectors) { | |
| 6514 | - rc = SQLITE_NOMEM; | |
| 6515 | - goto cleanup; | |
| 6516 | - } | |
| 6517 | - | |
| 6518 | - chunk_distances = sqlite3_malloc(p->chunk_size * sizeof(f32)); | |
| 6519 | - if (!chunk_distances) { | |
| 6520 | - rc = SQLITE_NOMEM; | |
| 6521 | - goto cleanup; | |
| 6522 | - } | |
| 6523 | - | |
| 6524 | - b = bitmap_new(p->chunk_size); | |
| 6525 | - if (!b) { | |
| 6526 | - rc = SQLITE_NOMEM; | |
| 6527 | - goto cleanup; | |
| 6528 | - } | |
| 6529 | - | |
| 6530 | - bTaken = bitmap_new(p->chunk_size); | |
| 6531 | - if (!bTaken) { | |
| 6532 | - rc = SQLITE_NOMEM; | |
| 6533 | - goto cleanup; | |
| 6534 | - } | |
| 6535 | - | |
| 6536 | - chunk_topk_idxs = sqlite3_malloc(k * sizeof(i32)); | |
| 6537 | - if (!chunk_topk_idxs) { | |
| 6538 | - rc = SQLITE_NOMEM; | |
| 6539 | - goto cleanup; | |
| 6540 | - } | |
| 6541 | - | |
| 6542 | - bmRowids = arrayRowidsIn ? bitmap_new(p->chunk_size) : NULL; | |
| 6543 | - if (arrayRowidsIn && !bmRowids) { | |
| 6544 | - rc = SQLITE_NOMEM; | |
| 6545 | - goto cleanup; | |
| 6546 | - } | |
| 6547 | - | |
| 6548 | - sqlite3_blob * metadataBlobs[VEC0_MAX_METADATA_COLUMNS]; | |
| 6549 | - memset(metadataBlobs, 0, sizeof(sqlite3_blob*) * VEC0_MAX_METADATA_COLUMNS); | |
| 6550 | - | |
| 6551 | - bmMetadata = bitmap_new(p->chunk_size); | |
| 6552 | - if(!bmMetadata) { | |
| 6553 | - rc = SQLITE_NOMEM; | |
| 6554 | - goto cleanup; | |
| 6555 | - } | |
| 6556 | - | |
| 6557 | - int idxStrLength = strlen(idxStr); | |
| 6558 | - int numValueEntries = (idxStrLength-1) / 4; | |
| 6559 | - assert(numValueEntries == argc); | |
| 6560 | - int hasMetadataFilters = 0; | |
| 6561 | - for(int i = 0; i < argc; i++) { | |
| 6562 | - int idx = 1 + (i * 4); | |
| 6563 | - char kind = idxStr[idx + 0]; | |
| 6564 | - if(kind == VEC0_IDXSTR_KIND_METADATA_CONSTRAINT) { | |
| 6565 | - hasMetadataFilters = 1; | |
| 6566 | - break; | |
| 6567 | - } | |
| 6568 | - } | |
| 6569 | - | |
| 6570 | - while (true) { | |
| 6571 | - rc = sqlite3_step(stmtChunks); | |
| 6572 | - if (rc == SQLITE_DONE) { | |
| 6573 | - break; | |
| 6574 | - } | |
| 6575 | - if (rc != SQLITE_ROW) { | |
| 6576 | - vtab_set_error(&p->base, "chunks iter error"); | |
| 6577 | - rc = SQLITE_ERROR; | |
| 6578 | - goto cleanup; | |
| 6579 | - } | |
| 6580 | - memset(chunk_distances, 0, p->chunk_size * sizeof(f32)); | |
| 6581 | - memset(chunk_topk_idxs, 0, k * sizeof(i32)); | |
| 6582 | - bitmap_clear(b, p->chunk_size); | |
| 6583 | - | |
| 6584 | - i64 chunk_id = sqlite3_column_int64(stmtChunks, 0); | |
| 6585 | - unsigned char *chunkValidity = | |
| 6586 | - (unsigned char *)sqlite3_column_blob(stmtChunks, 1); | |
| 6587 | - i64 validitySize = sqlite3_column_bytes(stmtChunks, 1); | |
| 6588 | - if (validitySize != p->chunk_size / CHAR_BIT) { | |
| 6589 | - // IMP: V05271_22109 | |
| 6590 | - vtab_set_error( | |
| 6591 | - &p->base, | |
| 6592 | - "chunk validity size doesn't match - expected %lld, found %lld", | |
| 6593 | - p->chunk_size / CHAR_BIT, validitySize); | |
| 6594 | - rc = SQLITE_ERROR; | |
| 6595 | - goto cleanup; | |
| 6596 | - } | |
| 6597 | - | |
| 6598 | - i64 *chunkRowids = (i64 *)sqlite3_column_blob(stmtChunks, 2); | |
| 6599 | - i64 rowidsSize = sqlite3_column_bytes(stmtChunks, 2); | |
| 6600 | - if (rowidsSize != p->chunk_size * sizeof(i64)) { | |
| 6601 | - // IMP: V02796_19635 | |
| 6602 | - vtab_set_error(&p->base, "rowids size doesn't match"); | |
| 6603 | - vtab_set_error( | |
| 6604 | - &p->base, | |
| 6605 | - "chunk rowids size doesn't match - expected %lld, found %lld", | |
| 6606 | - p->chunk_size * sizeof(i64), rowidsSize); | |
| 6607 | - rc = SQLITE_ERROR; | |
| 6608 | - goto cleanup; | |
| 6609 | - } | |
| 6610 | - | |
| 6611 | - // open the vector chunk blob for the current chunk | |
| 6612 | - rc = sqlite3_blob_open(p->db, p->schemaName, | |
| 6613 | - p->shadowVectorChunksNames[vectorColumnIdx], | |
| 6614 | - "vectors", chunk_id, 0, &blobVectors); | |
| 6615 | - if (rc != SQLITE_OK) { | |
| 6616 | - vtab_set_error(&p->base, "could not open vectors blob for chunk %lld", | |
| 6617 | - chunk_id); | |
| 6618 | - rc = SQLITE_ERROR; | |
| 6619 | - goto cleanup; | |
| 6620 | - } | |
| 6621 | - | |
| 6622 | - i64 currentBaseVectorsSize = sqlite3_blob_bytes(blobVectors); | |
| 6623 | - i64 expectedBaseVectorsSize = | |
| 6624 | - p->chunk_size * vector_column_byte_size(*vector_column); | |
| 6625 | - if (currentBaseVectorsSize != expectedBaseVectorsSize) { | |
| 6626 | - // IMP: V16465_00535 | |
| 6627 | - vtab_set_error( | |
| 6628 | - &p->base, | |
| 6629 | - "vectors blob size doesn't match - expected %lld, found %lld", | |
| 6630 | - expectedBaseVectorsSize, currentBaseVectorsSize); | |
| 6631 | - rc = SQLITE_ERROR; | |
| 6632 | - goto cleanup; | |
| 6633 | - } | |
| 6634 | - rc = sqlite3_blob_read(blobVectors, baseVectors, currentBaseVectorsSize, 0); | |
| 6635 | - | |
| 6636 | - if (rc != SQLITE_OK) { | |
| 6637 | - vtab_set_error(&p->base, "vectors blob read error for %lld", chunk_id); | |
| 6638 | - rc = SQLITE_ERROR; | |
| 6639 | - goto cleanup; | |
| 6640 | - } | |
| 6641 | - | |
| 6642 | - bitmap_copy(b, chunkValidity, p->chunk_size); | |
| 6643 | - if (arrayRowidsIn) { | |
| 6644 | - bitmap_clear(bmRowids, p->chunk_size); | |
| 6645 | - | |
| 6646 | - for (int i = 0; i < p->chunk_size; i++) { | |
| 6647 | - if (!bitmap_get(chunkValidity, i)) { | |
| 6648 | - continue; | |
| 6649 | - } | |
| 6650 | - i64 rowid = chunkRowids[i]; | |
| 6651 | - void *in = bsearch(&rowid, arrayRowidsIn->z, arrayRowidsIn->length, | |
| 6652 | - sizeof(i64), _cmp); | |
| 6653 | - bitmap_set(bmRowids, i, in ? 1 : 0); | |
| 6654 | - } | |
| 6655 | - bitmap_and_inplace(b, bmRowids, p->chunk_size); | |
| 6656 | - } | |
| 6657 | - | |
| 6658 | - if(hasMetadataFilters) { | |
| 6659 | - for(int i = 0; i < argc; i++) { | |
| 6660 | - int idx = 1 + (i * 4); | |
| 6661 | - char kind = idxStr[idx + 0]; | |
| 6662 | - if(kind != VEC0_IDXSTR_KIND_METADATA_CONSTRAINT) { | |
| 6663 | - continue; | |
| 6664 | - } | |
| 6665 | - int metadata_idx = idxStr[idx + 1] - 'A'; | |
| 6666 | - int operator = idxStr[idx + 2]; | |
| 6667 | - | |
| 6668 | - if(!metadataBlobs[metadata_idx]) { | |
| 6669 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowMetadataChunksNames[metadata_idx], "data", chunk_id, 0, &metadataBlobs[metadata_idx]); | |
| 6670 | - vtab_set_error(&p->base, "Could not open metadata blob"); | |
| 6671 | - if(rc != SQLITE_OK) { | |
| 6672 | - goto cleanup; | |
| 6673 | - } | |
| 6674 | - } | |
| 6675 | - | |
| 6676 | - bitmap_clear(bmMetadata, p->chunk_size); | |
| 6677 | - rc = vec0_set_metadata_filter_bitmap(p, metadata_idx, operator, argv[i], metadataBlobs[metadata_idx], chunk_id, bmMetadata, p->chunk_size, aMetadataIn, i); | |
| 6678 | - if(rc != SQLITE_OK) { | |
| 6679 | - vtab_set_error(&p->base, "Could not filter metadata fields"); | |
| 6680 | - if(rc != SQLITE_OK) { | |
| 6681 | - goto cleanup; | |
| 6682 | - } | |
| 6683 | - } | |
| 6684 | - bitmap_and_inplace(b, bmMetadata, p->chunk_size); | |
| 6685 | - } | |
| 6686 | - } | |
| 6687 | - | |
| 6688 | - | |
| 6689 | - for (int i = 0; i < p->chunk_size; i++) { | |
| 6690 | - if (!bitmap_get(b, i)) { | |
| 6691 | - continue; | |
| 6692 | - }; | |
| 6693 | - | |
| 6694 | - f32 result; | |
| 6695 | - switch (vector_column->element_type) { | |
| 6696 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | |
| 6697 | - const f32 *base_i = | |
| 6698 | - ((f32 *)baseVectors) + (i * vector_column->dimensions); | |
| 6699 | - switch (vector_column->distance_metric) { | |
| 6700 | - case VEC0_DISTANCE_METRIC_L2: { | |
| 6701 | - result = distance_l2_sqr_float(base_i, (f32 *)queryVector, | |
| 6702 | - &vector_column->dimensions); | |
| 6703 | - break; | |
| 6704 | - } | |
| 6705 | - case VEC0_DISTANCE_METRIC_L1: { | |
| 6706 | - result = distance_l1_f32(base_i, (f32 *)queryVector, | |
| 6707 | - &vector_column->dimensions); | |
| 6708 | - break; | |
| 6709 | - } | |
| 6710 | - case VEC0_DISTANCE_METRIC_COSINE: { | |
| 6711 | - result = distance_cosine_float(base_i, (f32 *)queryVector, | |
| 6712 | - &vector_column->dimensions); | |
| 6713 | - break; | |
| 6714 | - } | |
| 6715 | - } | |
| 6716 | - break; | |
| 6717 | - } | |
| 6718 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | |
| 6719 | - const i8 *base_i = | |
| 6720 | - ((i8 *)baseVectors) + (i * vector_column->dimensions); | |
| 6721 | - switch (vector_column->distance_metric) { | |
| 6722 | - case VEC0_DISTANCE_METRIC_L2: { | |
| 6723 | - result = distance_l2_sqr_int8(base_i, (i8 *)queryVector, | |
| 6724 | - &vector_column->dimensions); | |
| 6725 | - break; | |
| 6726 | - } | |
| 6727 | - case VEC0_DISTANCE_METRIC_L1: { | |
| 6728 | - result = distance_l1_int8(base_i, (i8 *)queryVector, | |
| 6729 | - &vector_column->dimensions); | |
| 6730 | - break; | |
| 6731 | - } | |
| 6732 | - case VEC0_DISTANCE_METRIC_COSINE: { | |
| 6733 | - result = distance_cosine_int8(base_i, (i8 *)queryVector, | |
| 6734 | - &vector_column->dimensions); | |
| 6735 | - break; | |
| 6736 | - } | |
| 6737 | - } | |
| 6738 | - | |
| 6739 | - break; | |
| 6740 | - } | |
| 6741 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | |
| 6742 | - const u8 *base_i = | |
| 6743 | - ((u8 *)baseVectors) + (i * (vector_column->dimensions / CHAR_BIT)); | |
| 6744 | - result = distance_hamming(base_i, (u8 *)queryVector, | |
| 6745 | - &vector_column->dimensions); | |
| 6746 | - break; | |
| 6747 | - } | |
| 6748 | - } | |
| 6749 | - | |
| 6750 | - chunk_distances[i] = result; | |
| 6751 | - } | |
| 6752 | - | |
| 6753 | - int used1; | |
| 6754 | - min_idx(chunk_distances, p->chunk_size, b, chunk_topk_idxs, | |
| 6755 | - min(k, p->chunk_size), bTaken, &used1); | |
| 6756 | - | |
| 6757 | - i64 used; | |
| 6758 | - merge_sorted_lists(topk_distances, topk_rowids, k_used, chunk_distances, | |
| 6759 | - chunkRowids, chunk_topk_idxs, | |
| 6760 | - min(min(k, p->chunk_size), used1), tmp_topk_distances, | |
| 6761 | - tmp_topk_rowids, k, &used); | |
| 6762 | - | |
| 6763 | - for (int i = 0; i < used; i++) { | |
| 6764 | - topk_rowids[i] = tmp_topk_rowids[i]; | |
| 6765 | - topk_distances[i] = tmp_topk_distances[i]; | |
| 6766 | - } | |
| 6767 | - k_used = used; | |
| 6768 | - // blobVectors is always opened with read-only permissions, so this never | |
| 6769 | - // fails. | |
| 6770 | - sqlite3_blob_close(blobVectors); | |
| 6771 | - blobVectors = NULL; | |
| 6772 | - } | |
| 6773 | - | |
| 6774 | - *out_topk_rowids = topk_rowids; | |
| 6775 | - *out_topk_distances = topk_distances; | |
| 6776 | - *out_used = k_used; | |
| 6777 | - rc = SQLITE_OK; | |
| 6778 | - | |
| 6779 | -cleanup: | |
| 6780 | - if (rc != SQLITE_OK) { | |
| 6781 | - sqlite3_free(topk_rowids); | |
| 6782 | - sqlite3_free(topk_distances); | |
| 6783 | - } | |
| 6784 | - sqlite3_free(chunk_topk_idxs); | |
| 6785 | - sqlite3_free(tmp_topk_rowids); | |
| 6786 | - sqlite3_free(tmp_topk_distances); | |
| 6787 | - sqlite3_free(b); | |
| 6788 | - sqlite3_free(bTaken); | |
| 6789 | - sqlite3_free(bmRowids); | |
| 6790 | - sqlite3_free(baseVectors); | |
| 6791 | - sqlite3_free(chunk_distances); | |
| 6792 | - sqlite3_free(bmMetadata); | |
| 6793 | - for(int i = 0; i < VEC0_MAX_METADATA_COLUMNS; i++) { | |
| 6794 | - sqlite3_blob_close(metadataBlobs[i]); | |
| 6795 | - } | |
| 6796 | - // blobVectors is always opened with read-only permissions, so this never | |
| 6797 | - // fails. | |
| 6798 | - sqlite3_blob_close(blobVectors); | |
| 6799 | - return rc; | |
| 6800 | -} | |
| 6801 | - | |
| 6802 | -int vec0Filter_knn(vec0_cursor *pCur, vec0_vtab *p, int idxNum, | |
| 6803 | - const char *idxStr, int argc, sqlite3_value **argv) { | |
| 6804 | - assert(argc == (strlen(idxStr)-1) / 4); | |
| 6805 | - int rc; | |
| 6806 | - struct vec0_query_knn_data *knn_data; | |
| 6807 | - | |
| 6808 | - int vectorColumnIdx = idxNum; | |
| 6809 | - struct VectorColumnDefinition *vector_column = | |
| 6810 | - &p->vector_columns[vectorColumnIdx]; | |
| 6811 | - | |
| 6812 | - struct Array *arrayRowidsIn = NULL; | |
| 6813 | - sqlite3_stmt *stmtChunks = NULL; | |
| 6814 | - void *queryVector; | |
| 6815 | - size_t dimensions; | |
| 6816 | - enum VectorElementType elementType; | |
| 6817 | - vector_cleanup queryVectorCleanup = vector_cleanup_noop; | |
| 6818 | - char *pzError; | |
| 6819 | - knn_data = sqlite3_malloc(sizeof(*knn_data)); | |
| 6820 | - if (!knn_data) { | |
| 6821 | - return SQLITE_NOMEM; | |
| 6822 | - } | |
| 6823 | - memset(knn_data, 0, sizeof(*knn_data)); | |
| 6824 | - // array of `struct Vec0MetadataIn`, IF there are any `xxx in (...)` metadata constraints | |
| 6825 | - struct Array * aMetadataIn = NULL; | |
| 6826 | - | |
| 6827 | - int query_idx =-1; | |
| 6828 | - int k_idx = -1; | |
| 6829 | - int rowid_in_idx = -1; | |
| 6830 | - for(int i = 0; i < argc; i++) { | |
| 6831 | - if(idxStr[1 + (i*4)] == VEC0_IDXSTR_KIND_KNN_MATCH) { | |
| 6832 | - query_idx = i; | |
| 6833 | - } | |
| 6834 | - if(idxStr[1 + (i*4)] == VEC0_IDXSTR_KIND_KNN_K) { | |
| 6835 | - k_idx = i; | |
| 6836 | - } | |
| 6837 | - if(idxStr[1 + (i*4)] == VEC0_IDXSTR_KIND_KNN_ROWID_IN) { | |
| 6838 | - rowid_in_idx = i; | |
| 6839 | - } | |
| 6840 | - } | |
| 6841 | - assert(query_idx >= 0); | |
| 6842 | - assert(k_idx >= 0); | |
| 6843 | - | |
| 6844 | - // make sure the query vector matches the vector column (type dimensions etc.) | |
| 6845 | - rc = vector_from_value(argv[query_idx], &queryVector, &dimensions, &elementType, | |
| 6846 | - &queryVectorCleanup, &pzError); | |
| 6847 | - | |
| 6848 | - if (rc != SQLITE_OK) { | |
| 6849 | - vtab_set_error(&p->base, | |
| 6850 | - "Query vector on the \"%.*s\" column is invalid: %z", | |
| 6851 | - vector_column->name_length, vector_column->name, pzError); | |
| 6852 | - rc = SQLITE_ERROR; | |
| 6853 | - goto cleanup; | |
| 6854 | - } | |
| 6855 | - if (elementType != vector_column->element_type) { | |
| 6856 | - vtab_set_error( | |
| 6857 | - &p->base, | |
| 6858 | - "Query vector for the \"%.*s\" column is expected to be of type " | |
| 6859 | - "%s, but a %s vector was provided.", | |
| 6860 | - vector_column->name_length, vector_column->name, | |
| 6861 | - vector_subtype_name(vector_column->element_type), | |
| 6862 | - vector_subtype_name(elementType)); | |
| 6863 | - rc = SQLITE_ERROR; | |
| 6864 | - goto cleanup; | |
| 6865 | - } | |
| 6866 | - if (dimensions != vector_column->dimensions) { | |
| 6867 | - vtab_set_error( | |
| 6868 | - &p->base, | |
| 6869 | - "Dimension mismatch for query vector for the \"%.*s\" column. " | |
| 6870 | - "Expected %d dimensions but received %d.", | |
| 6871 | - vector_column->name_length, vector_column->name, | |
| 6872 | - vector_column->dimensions, dimensions); | |
| 6873 | - rc = SQLITE_ERROR; | |
| 6874 | - goto cleanup; | |
| 6875 | - } | |
| 6876 | - | |
| 6877 | - i64 k = sqlite3_value_int64(argv[k_idx]); | |
| 6878 | - if (k < 0) { | |
| 6879 | - vtab_set_error( | |
| 6880 | - &p->base, "k value in knn queries must be greater than or equal to 0."); | |
| 6881 | - rc = SQLITE_ERROR; | |
| 6882 | - goto cleanup; | |
| 6883 | - } | |
| 6884 | -#define SQLITE_VEC_VEC0_K_MAX 4096 | |
| 6885 | - if (k > SQLITE_VEC_VEC0_K_MAX) { | |
| 6886 | - vtab_set_error( | |
| 6887 | - &p->base, | |
| 6888 | - "k value in knn query too large, provided %lld and the limit is %lld", | |
| 6889 | - k, SQLITE_VEC_VEC0_K_MAX); | |
| 6890 | - rc = SQLITE_ERROR; | |
| 6891 | - goto cleanup; | |
| 6892 | - } | |
| 6893 | - | |
| 6894 | - if (k == 0) { | |
| 6895 | - knn_data->k = 0; | |
| 6896 | - pCur->knn_data = knn_data; | |
| 6897 | - pCur->query_plan = VEC0_QUERY_PLAN_KNN; | |
| 6898 | - rc = SQLITE_OK; | |
| 6899 | - goto cleanup; | |
| 6900 | - } | |
| 6901 | - | |
| 6902 | -// handle when a `rowid in (...)` operation was provided | |
| 6903 | -// Array of all the rowids that appear in any `rowid in (...)` constraint. | |
| 6904 | -// NULL if none were provided, which means a "full" scan. | |
| 6905 | -#if COMPILER_SUPPORTS_VTAB_IN | |
| 6906 | - if (rowid_in_idx >= 0) { | |
| 6907 | - sqlite3_value *item; | |
| 6908 | - int rc; | |
| 6909 | - arrayRowidsIn = sqlite3_malloc(sizeof(*arrayRowidsIn)); | |
| 6910 | - if (!arrayRowidsIn) { | |
| 6911 | - rc = SQLITE_NOMEM; | |
| 6912 | - goto cleanup; | |
| 6913 | - } | |
| 6914 | - memset(arrayRowidsIn, 0, sizeof(*arrayRowidsIn)); | |
| 6915 | - | |
| 6916 | - rc = array_init(arrayRowidsIn, sizeof(i64), 32); | |
| 6917 | - if (rc != SQLITE_OK) { | |
| 6918 | - goto cleanup; | |
| 6919 | - } | |
| 6920 | - for (rc = sqlite3_vtab_in_first(argv[rowid_in_idx], &item); rc == SQLITE_OK && item; | |
| 6921 | - rc = sqlite3_vtab_in_next(argv[rowid_in_idx], &item)) { | |
| 6922 | - i64 rowid; | |
| 6923 | - if (p->pkIsText) { | |
| 6924 | - rc = vec0_rowid_from_id(p, item, &rowid); | |
| 6925 | - if (rc != SQLITE_OK) { | |
| 6926 | - goto cleanup; | |
| 6927 | - } | |
| 6928 | - } else { | |
| 6929 | - rowid = sqlite3_value_int64(item); | |
| 6930 | - } | |
| 6931 | - rc = array_append(arrayRowidsIn, &rowid); | |
| 6932 | - if (rc != SQLITE_OK) { | |
| 6933 | - goto cleanup; | |
| 6934 | - } | |
| 6935 | - } | |
| 6936 | - if (rc != SQLITE_DONE) { | |
| 6937 | - vtab_set_error(&p->base, "error processing rowid in (...) array"); | |
| 6938 | - goto cleanup; | |
| 6939 | - } | |
| 6940 | - qsort(arrayRowidsIn->z, arrayRowidsIn->length, arrayRowidsIn->element_size, | |
| 6941 | - _cmp); | |
| 6942 | - } | |
| 6943 | -#endif | |
| 6944 | - | |
| 6945 | - #if COMPILER_SUPPORTS_VTAB_IN | |
| 6946 | - for(int i = 0; i < argc; i++) { | |
| 6947 | - if(!(idxStr[1 + (i*4)] == VEC0_IDXSTR_KIND_METADATA_CONSTRAINT && idxStr[1 + (i*4) + 2] == VEC0_METADATA_OPERATOR_IN)) { | |
| 6948 | - continue; | |
| 6949 | - } | |
| 6950 | - int metadata_idx = idxStr[1 + (i*4) + 1] - 'A'; | |
| 6951 | - if(!aMetadataIn) { | |
| 6952 | - aMetadataIn = sqlite3_malloc(sizeof(*aMetadataIn)); | |
| 6953 | - if(!aMetadataIn) { | |
| 6954 | - rc = SQLITE_NOMEM; | |
| 6955 | - goto cleanup; | |
| 6956 | - } | |
| 6957 | - memset(aMetadataIn, 0, sizeof(*aMetadataIn)); | |
| 6958 | - rc = array_init(aMetadataIn, sizeof(struct Vec0MetadataIn), 8); | |
| 6959 | - if(rc != SQLITE_OK) { | |
| 6960 | - goto cleanup; | |
| 6961 | - } | |
| 6962 | - } | |
| 6963 | - | |
| 6964 | - struct Vec0MetadataIn item; | |
| 6965 | - memset(&item, 0, sizeof(item)); | |
| 6966 | - item.metadata_idx=metadata_idx; | |
| 6967 | - item.argv_idx = i; | |
| 6968 | - | |
| 6969 | - switch(p->metadata_columns[metadata_idx].kind) { | |
| 6970 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | |
| 6971 | - rc = array_init(&item.array, sizeof(i64), 16); | |
| 6972 | - if(rc != SQLITE_OK) { | |
| 6973 | - goto cleanup; | |
| 6974 | - } | |
| 6975 | - sqlite3_value *entry; | |
| 6976 | - for (rc = sqlite3_vtab_in_first(argv[i], &entry); rc == SQLITE_OK && entry; rc = sqlite3_vtab_in_next(argv[i], &entry)) { | |
| 6977 | - i64 v = sqlite3_value_int64(entry); | |
| 6978 | - rc = array_append(&item.array, &v); | |
| 6979 | - if (rc != SQLITE_OK) { | |
| 6980 | - goto cleanup; | |
| 6981 | - } | |
| 6982 | - } | |
| 6983 | - | |
| 6984 | - if (rc != SQLITE_DONE) { | |
| 6985 | - vtab_set_error(&p->base, "Error fetching next value in `x in (...)` integer expression"); | |
| 6986 | - goto cleanup; | |
| 6987 | - } | |
| 6988 | - | |
| 6989 | - break; | |
| 6990 | - } | |
| 6991 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | |
| 6992 | - rc = array_init(&item.array, sizeof(struct Vec0MetadataInTextEntry), 16); | |
| 6993 | - if(rc != SQLITE_OK) { | |
| 6994 | - goto cleanup; | |
| 6995 | - } | |
| 6996 | - sqlite3_value *entry; | |
| 6997 | - for (rc = sqlite3_vtab_in_first(argv[i], &entry); rc == SQLITE_OK && entry; rc = sqlite3_vtab_in_next(argv[i], &entry)) { | |
| 6998 | - const char * s = (const char *) sqlite3_value_text(entry); | |
| 6999 | - int n = sqlite3_value_bytes(entry); | |
| 7000 | - | |
| 7001 | - struct Vec0MetadataInTextEntry entry; | |
| 7002 | - entry.zString = sqlite3_mprintf("%.*s", n, s); | |
| 7003 | - if(!entry.zString) { | |
| 7004 | - rc = SQLITE_NOMEM; | |
| 7005 | - goto cleanup; | |
| 7006 | - } | |
| 7007 | - entry.n = n; | |
| 7008 | - rc = array_append(&item.array, &entry); | |
| 7009 | - if (rc != SQLITE_OK) { | |
| 7010 | - goto cleanup; | |
| 7011 | - } | |
| 7012 | - } | |
| 7013 | - | |
| 7014 | - if (rc != SQLITE_DONE) { | |
| 7015 | - vtab_set_error(&p->base, "Error fetching next value in `x in (...)` text expression"); | |
| 7016 | - goto cleanup; | |
| 7017 | - } | |
| 7018 | - | |
| 7019 | - break; | |
| 7020 | - } | |
| 7021 | - default: { | |
| 7022 | - vtab_set_error(&p->base, "Internal sqlite-vec error"); | |
| 7023 | - goto cleanup; | |
| 7024 | - } | |
| 7025 | - } | |
| 7026 | - | |
| 7027 | - rc = array_append(aMetadataIn, &item); | |
| 7028 | - if(rc != SQLITE_OK) { | |
| 7029 | - goto cleanup; | |
| 7030 | - } | |
| 7031 | - } | |
| 7032 | - #endif | |
| 7033 | - | |
| 7034 | - rc = vec0_chunks_iter(p, idxStr, argc, argv, &stmtChunks); | |
| 7035 | - if (rc != SQLITE_OK) { | |
| 7036 | - // IMP: V06942_23781 | |
| 7037 | - vtab_set_error(&p->base, "Error preparing stmtChunk: %s", | |
| 7038 | - sqlite3_errmsg(p->db)); | |
| 7039 | - goto cleanup; | |
| 7040 | - } | |
| 7041 | - | |
| 7042 | - i64 *topk_rowids = NULL; | |
| 7043 | - f32 *topk_distances = NULL; | |
| 7044 | - i64 k_used = 0; | |
| 7045 | - rc = vec0Filter_knn_chunks_iter(p, stmtChunks, vector_column, vectorColumnIdx, | |
| 7046 | - arrayRowidsIn, aMetadataIn, idxStr, argc, argv, queryVector, k, &topk_rowids, | |
| 7047 | - &topk_distances, &k_used); | |
| 7048 | - if (rc != SQLITE_OK) { | |
| 7049 | - goto cleanup; | |
| 7050 | - } | |
| 7051 | - | |
| 7052 | - knn_data->current_idx = 0; | |
| 7053 | - knn_data->k = k; | |
| 7054 | - knn_data->rowids = topk_rowids; | |
| 7055 | - knn_data->distances = topk_distances; | |
| 7056 | - knn_data->k_used = k_used; | |
| 7057 | - | |
| 7058 | - pCur->knn_data = knn_data; | |
| 7059 | - pCur->query_plan = VEC0_QUERY_PLAN_KNN; | |
| 7060 | - rc = SQLITE_OK; | |
| 7061 | - | |
| 7062 | -cleanup: | |
| 7063 | - sqlite3_finalize(stmtChunks); | |
| 7064 | - array_cleanup(arrayRowidsIn); | |
| 7065 | - sqlite3_free(arrayRowidsIn); | |
| 7066 | - queryVectorCleanup(queryVector); | |
| 7067 | - if(aMetadataIn) { | |
| 7068 | - for(size_t i = 0; i < aMetadataIn->length; i++) { | |
| 7069 | - struct Vec0MetadataIn* item = &((struct Vec0MetadataIn *) aMetadataIn->z)[i]; | |
| 7070 | - for(size_t j = 0; j < item->array.length; j++) { | |
| 7071 | - if(p->metadata_columns[item->metadata_idx].kind == VEC0_METADATA_COLUMN_KIND_TEXT) { | |
| 7072 | - struct Vec0MetadataInTextEntry entry = ((struct Vec0MetadataInTextEntry*)item->array.z)[j]; | |
| 7073 | - sqlite3_free(entry.zString); | |
| 7074 | - } | |
| 7075 | - } | |
| 7076 | - array_cleanup(&item->array); | |
| 7077 | - } | |
| 7078 | - array_cleanup(aMetadataIn); | |
| 7079 | - } | |
| 7080 | - | |
| 7081 | - sqlite3_free(aMetadataIn); | |
| 7082 | - | |
| 7083 | - return rc; | |
| 7084 | -} | |
| 7085 | - | |
| 7086 | -int vec0Filter_fullscan(vec0_vtab *p, vec0_cursor *pCur) { | |
| 7087 | - int rc; | |
| 7088 | - char *zSql; | |
| 7089 | - struct vec0_query_fullscan_data *fullscan_data; | |
| 7090 | - | |
| 7091 | - fullscan_data = sqlite3_malloc(sizeof(*fullscan_data)); | |
| 7092 | - if (!fullscan_data) { | |
| 7093 | - return SQLITE_NOMEM; | |
| 7094 | - } | |
| 7095 | - memset(fullscan_data, 0, sizeof(*fullscan_data)); | |
| 7096 | - | |
| 7097 | - zSql = sqlite3_mprintf(" SELECT rowid " | |
| 7098 | - " FROM " VEC0_SHADOW_ROWIDS_NAME | |
| 7099 | - " ORDER by chunk_id, chunk_offset ", | |
| 7100 | - p->schemaName, p->tableName); | |
| 7101 | - if (!zSql) { | |
| 7102 | - rc = SQLITE_NOMEM; | |
| 7103 | - goto error; | |
| 7104 | - } | |
| 7105 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &fullscan_data->rowids_stmt, NULL); | |
| 7106 | - sqlite3_free(zSql); | |
| 7107 | - if (rc != SQLITE_OK) { | |
| 7108 | - // IMP: V09901_26739 | |
| 7109 | - vtab_set_error(&p->base, "Error preparing rowid scan: %s", | |
| 7110 | - sqlite3_errmsg(p->db)); | |
| 7111 | - goto error; | |
| 7112 | - } | |
| 7113 | - | |
| 7114 | - rc = sqlite3_step(fullscan_data->rowids_stmt); | |
| 7115 | - | |
| 7116 | - // DONE when there's no rowids, ROW when there are, both "success" | |
| 7117 | - if (!(rc == SQLITE_ROW || rc == SQLITE_DONE)) { | |
| 7118 | - goto error; | |
| 7119 | - } | |
| 7120 | - | |
| 7121 | - fullscan_data->done = rc == SQLITE_DONE; | |
| 7122 | - pCur->query_plan = VEC0_QUERY_PLAN_FULLSCAN; | |
| 7123 | - pCur->fullscan_data = fullscan_data; | |
| 7124 | - return SQLITE_OK; | |
| 7125 | - | |
| 7126 | -error: | |
| 7127 | - vec0_query_fullscan_data_clear(fullscan_data); | |
| 7128 | - sqlite3_free(fullscan_data); | |
| 7129 | - return rc; | |
| 7130 | -} | |
| 7131 | - | |
| 7132 | -int vec0Filter_point(vec0_cursor *pCur, vec0_vtab *p, int argc, | |
| 7133 | - sqlite3_value **argv) { | |
| 7134 | - int rc; | |
| 7135 | - assert(argc == 1); | |
| 7136 | - i64 rowid; | |
| 7137 | - struct vec0_query_point_data *point_data = NULL; | |
| 7138 | - | |
| 7139 | - point_data = sqlite3_malloc(sizeof(*point_data)); | |
| 7140 | - if (!point_data) { | |
| 7141 | - rc = SQLITE_NOMEM; | |
| 7142 | - goto error; | |
| 7143 | - } | |
| 7144 | - memset(point_data, 0, sizeof(*point_data)); | |
| 7145 | - | |
| 7146 | - if (p->pkIsText) { | |
| 7147 | - rc = vec0_rowid_from_id(p, argv[0], &rowid); | |
| 7148 | - if (rc == SQLITE_EMPTY) { | |
| 7149 | - goto eof; | |
| 7150 | - } | |
| 7151 | - if (rc != SQLITE_OK) { | |
| 7152 | - goto error; | |
| 7153 | - } | |
| 7154 | - } else { | |
| 7155 | - rowid = sqlite3_value_int64(argv[0]); | |
| 7156 | - } | |
| 7157 | - | |
| 7158 | - for (int i = 0; i < p->numVectorColumns; i++) { | |
| 7159 | - rc = vec0_get_vector_data(p, rowid, i, &point_data->vectors[i], NULL); | |
| 7160 | - if (rc == SQLITE_EMPTY) { | |
| 7161 | - goto eof; | |
| 7162 | - } | |
| 7163 | - if (rc != SQLITE_OK) { | |
| 7164 | - goto error; | |
| 7165 | - } | |
| 7166 | - } | |
| 7167 | - | |
| 7168 | - point_data->rowid = rowid; | |
| 7169 | - point_data->done = 0; | |
| 7170 | - pCur->point_data = point_data; | |
| 7171 | - pCur->query_plan = VEC0_QUERY_PLAN_POINT; | |
| 7172 | - return SQLITE_OK; | |
| 7173 | - | |
| 7174 | -eof: | |
| 7175 | - point_data->rowid = rowid; | |
| 7176 | - point_data->done = 1; | |
| 7177 | - pCur->point_data = point_data; | |
| 7178 | - pCur->query_plan = VEC0_QUERY_PLAN_POINT; | |
| 7179 | - return SQLITE_OK; | |
| 7180 | - | |
| 7181 | -error: | |
| 7182 | - vec0_query_point_data_clear(point_data); | |
| 7183 | - sqlite3_free(point_data); | |
| 7184 | - return rc; | |
| 7185 | -} | |
| 7186 | - | |
| 7187 | -static int vec0Filter(sqlite3_vtab_cursor *pVtabCursor, int idxNum, | |
| 7188 | - const char *idxStr, int argc, sqlite3_value **argv) { | |
| 7189 | - vec0_vtab *p = (vec0_vtab *)pVtabCursor->pVtab; | |
| 7190 | - vec0_cursor *pCur = (vec0_cursor *)pVtabCursor; | |
| 7191 | - vec0_cursor_clear(pCur); | |
| 7192 | - | |
| 7193 | - int idxStrLength = strlen(idxStr); | |
| 7194 | - if(idxStrLength <= 0) { | |
| 7195 | - return SQLITE_ERROR; | |
| 7196 | - } | |
| 7197 | - if((idxStrLength-1) % 4 != 0) { | |
| 7198 | - return SQLITE_ERROR; | |
| 7199 | - } | |
| 7200 | - int numValueEntries = (idxStrLength-1) / 4; | |
| 7201 | - if(numValueEntries != argc) { | |
| 7202 | - return SQLITE_ERROR; | |
| 7203 | - } | |
| 7204 | - | |
| 7205 | - char query_plan = idxStr[0]; | |
| 7206 | - switch(query_plan) { | |
| 7207 | - case VEC0_QUERY_PLAN_FULLSCAN: | |
| 7208 | - return vec0Filter_fullscan(p, pCur); | |
| 7209 | - case VEC0_QUERY_PLAN_KNN: | |
| 7210 | - return vec0Filter_knn(pCur, p, idxNum, idxStr, argc, argv); | |
| 7211 | - case VEC0_QUERY_PLAN_POINT: | |
| 7212 | - return vec0Filter_point(pCur, p, argc, argv); | |
| 7213 | - default: | |
| 7214 | - vtab_set_error(pVtabCursor->pVtab, "unknown idxStr '%s'", idxStr); | |
| 7215 | - return SQLITE_ERROR; | |
| 7216 | - } | |
| 7217 | -} | |
| 7218 | - | |
| 7219 | -static int vec0Rowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid) { | |
| 7220 | - vec0_cursor *pCur = (vec0_cursor *)cur; | |
| 7221 | - switch (pCur->query_plan) { | |
| 7222 | - case VEC0_QUERY_PLAN_FULLSCAN: { | |
| 7223 | - *pRowid = sqlite3_column_int64(pCur->fullscan_data->rowids_stmt, 0); | |
| 7224 | - return SQLITE_OK; | |
| 7225 | - } | |
| 7226 | - case VEC0_QUERY_PLAN_POINT: { | |
| 7227 | - *pRowid = pCur->point_data->rowid; | |
| 7228 | - return SQLITE_OK; | |
| 7229 | - } | |
| 7230 | - case VEC0_QUERY_PLAN_KNN: { | |
| 7231 | - vtab_set_error(cur->pVtab, | |
| 7232 | - "Internal sqlite-vec error: expected point query plan in " | |
| 7233 | - "vec0Rowid, found %d", | |
| 7234 | - pCur->query_plan); | |
| 7235 | - return SQLITE_ERROR; | |
| 7236 | - } | |
| 7237 | - } | |
| 7238 | - return SQLITE_ERROR; | |
| 7239 | -} | |
| 7240 | - | |
| 7241 | -static int vec0Next(sqlite3_vtab_cursor *cur) { | |
| 7242 | - vec0_cursor *pCur = (vec0_cursor *)cur; | |
| 7243 | - switch (pCur->query_plan) { | |
| 7244 | - case VEC0_QUERY_PLAN_FULLSCAN: { | |
| 7245 | - if (!pCur->fullscan_data) { | |
| 7246 | - return SQLITE_ERROR; | |
| 7247 | - } | |
| 7248 | - int rc = sqlite3_step(pCur->fullscan_data->rowids_stmt); | |
| 7249 | - if (rc == SQLITE_DONE) { | |
| 7250 | - pCur->fullscan_data->done = 1; | |
| 7251 | - return SQLITE_OK; | |
| 7252 | - } | |
| 7253 | - if (rc == SQLITE_ROW) { | |
| 7254 | - return SQLITE_OK; | |
| 7255 | - } | |
| 7256 | - return SQLITE_ERROR; | |
| 7257 | - } | |
| 7258 | - case VEC0_QUERY_PLAN_KNN: { | |
| 7259 | - if (!pCur->knn_data) { | |
| 7260 | - return SQLITE_ERROR; | |
| 7261 | - } | |
| 7262 | - | |
| 7263 | - pCur->knn_data->current_idx++; | |
| 7264 | - return SQLITE_OK; | |
| 7265 | - } | |
| 7266 | - case VEC0_QUERY_PLAN_POINT: { | |
| 7267 | - if (!pCur->point_data) { | |
| 7268 | - return SQLITE_ERROR; | |
| 7269 | - } | |
| 7270 | - pCur->point_data->done = 1; | |
| 7271 | - return SQLITE_OK; | |
| 7272 | - } | |
| 7273 | - } | |
| 7274 | - return SQLITE_ERROR; | |
| 7275 | -} | |
| 7276 | - | |
| 7277 | -static int vec0Eof(sqlite3_vtab_cursor *cur) { | |
| 7278 | - vec0_cursor *pCur = (vec0_cursor *)cur; | |
| 7279 | - switch (pCur->query_plan) { | |
| 7280 | - case VEC0_QUERY_PLAN_FULLSCAN: { | |
| 7281 | - if (!pCur->fullscan_data) { | |
| 7282 | - return 1; | |
| 7283 | - } | |
| 7284 | - return pCur->fullscan_data->done; | |
| 7285 | - } | |
| 7286 | - case VEC0_QUERY_PLAN_KNN: { | |
| 7287 | - if (!pCur->knn_data) { | |
| 7288 | - return 1; | |
| 7289 | - } | |
| 7290 | - // return (pCur->knn_data->current_idx >= pCur->knn_data->k) || | |
| 7291 | - // (pCur->knn_data->distances[pCur->knn_data->current_idx] == FLT_MAX); | |
| 7292 | - return (pCur->knn_data->current_idx >= pCur->knn_data->k_used); | |
| 7293 | - } | |
| 7294 | - case VEC0_QUERY_PLAN_POINT: { | |
| 7295 | - if (!pCur->point_data) { | |
| 7296 | - return 1; | |
| 7297 | - } | |
| 7298 | - return pCur->point_data->done; | |
| 7299 | - } | |
| 7300 | - } | |
| 7301 | - return 1; | |
| 7302 | -} | |
| 7303 | - | |
| 7304 | -static int vec0Column_fullscan(vec0_vtab *pVtab, vec0_cursor *pCur, | |
| 7305 | - sqlite3_context *context, int i) { | |
| 7306 | - if (!pCur->fullscan_data) { | |
| 7307 | - sqlite3_result_error( | |
| 7308 | - context, "Internal sqlite-vec error: fullscan_data is NULL.", -1); | |
| 7309 | - return SQLITE_ERROR; | |
| 7310 | - } | |
| 7311 | - i64 rowid = sqlite3_column_int64(pCur->fullscan_data->rowids_stmt, 0); | |
| 7312 | - if (i == VEC0_COLUMN_ID) { | |
| 7313 | - return vec0_result_id(pVtab, context, rowid); | |
| 7314 | - } | |
| 7315 | - else if (vec0_column_idx_is_vector(pVtab, i)) { | |
| 7316 | - void *v; | |
| 7317 | - int sz; | |
| 7318 | - int vector_idx = vec0_column_idx_to_vector_idx(pVtab, i); | |
| 7319 | - int rc = vec0_get_vector_data(pVtab, rowid, vector_idx, &v, &sz); | |
| 7320 | - if (rc != SQLITE_OK) { | |
| 7321 | - return rc; | |
| 7322 | - } | |
| 7323 | - sqlite3_result_blob(context, v, sz, sqlite3_free); | |
| 7324 | - sqlite3_result_subtype(context, | |
| 7325 | - pVtab->vector_columns[vector_idx].element_type); | |
| 7326 | - | |
| 7327 | - } | |
| 7328 | - else if (i == vec0_column_distance_idx(pVtab)) { | |
| 7329 | - sqlite3_result_null(context); | |
| 7330 | - } | |
| 7331 | - else if(vec0_column_idx_is_partition(pVtab, i)) { | |
| 7332 | - int partition_idx = vec0_column_idx_to_partition_idx(pVtab, i); | |
| 7333 | - sqlite3_value * v; | |
| 7334 | - int rc = vec0_get_partition_value_for_rowid(pVtab, rowid, partition_idx, &v); | |
| 7335 | - if(rc == SQLITE_OK) { | |
| 7336 | - sqlite3_result_value(context, v); | |
| 7337 | - sqlite3_value_free(v); | |
| 7338 | - }else { | |
| 7339 | - sqlite3_result_error_code(context, rc); | |
| 7340 | - } | |
| 7341 | - } | |
| 7342 | - else if(vec0_column_idx_is_auxiliary(pVtab, i)) { | |
| 7343 | - int auxiliary_idx = vec0_column_idx_to_auxiliary_idx(pVtab, i); | |
| 7344 | - sqlite3_value * v; | |
| 7345 | - int rc = vec0_get_auxiliary_value_for_rowid(pVtab, rowid, auxiliary_idx, &v); | |
| 7346 | - if(rc == SQLITE_OK) { | |
| 7347 | - sqlite3_result_value(context, v); | |
| 7348 | - sqlite3_value_free(v); | |
| 7349 | - }else { | |
| 7350 | - sqlite3_result_error_code(context, rc); | |
| 7351 | - } | |
| 7352 | - } | |
| 7353 | - | |
| 7354 | - else if(vec0_column_idx_is_metadata(pVtab, i)) { | |
| 7355 | - if(sqlite3_vtab_nochange(context)) { | |
| 7356 | - return SQLITE_OK; | |
| 7357 | - } | |
| 7358 | - int metadata_idx = vec0_column_idx_to_metadata_idx(pVtab, i); | |
| 7359 | - int rc = vec0_result_metadata_value_for_rowid(pVtab, rowid, metadata_idx, context); | |
| 7360 | - if(rc != SQLITE_OK) { | |
| 7361 | - // IMP: V15466_32305 | |
| 7362 | - const char * zErr = sqlite3_mprintf( | |
| 7363 | - "Could not extract metadata value for column %.*s at rowid %lld", | |
| 7364 | - pVtab->metadata_columns[metadata_idx].name_length, | |
| 7365 | - pVtab->metadata_columns[metadata_idx].name, rowid | |
| 7366 | - ); | |
| 7367 | - if(zErr) { | |
| 7368 | - sqlite3_result_error(context, zErr, -1); | |
| 7369 | - sqlite3_free((void *) zErr); | |
| 7370 | - }else { | |
| 7371 | - sqlite3_result_error_nomem(context); | |
| 7372 | - } | |
| 7373 | - } | |
| 7374 | - } | |
| 7375 | - | |
| 7376 | - return SQLITE_OK; | |
| 7377 | -} | |
| 7378 | - | |
| 7379 | -static int vec0Column_point(vec0_vtab *pVtab, vec0_cursor *pCur, | |
| 7380 | - sqlite3_context *context, int i) { | |
| 7381 | - if (!pCur->point_data) { | |
| 7382 | - sqlite3_result_error(context, | |
| 7383 | - "Internal sqlite-vec error: point_data is NULL.", -1); | |
| 7384 | - return SQLITE_ERROR; | |
| 7385 | - } | |
| 7386 | - if (i == VEC0_COLUMN_ID) { | |
| 7387 | - return vec0_result_id(pVtab, context, pCur->point_data->rowid); | |
| 7388 | - } | |
| 7389 | - else if (i == vec0_column_distance_idx(pVtab)) { | |
| 7390 | - sqlite3_result_null(context); | |
| 7391 | - return SQLITE_OK; | |
| 7392 | - } | |
| 7393 | - else if (vec0_column_idx_is_vector(pVtab, i)) { | |
| 7394 | - if (sqlite3_vtab_nochange(context)) { | |
| 7395 | - sqlite3_result_null(context); | |
| 7396 | - return SQLITE_OK; | |
| 7397 | - } | |
| 7398 | - int vector_idx = vec0_column_idx_to_vector_idx(pVtab, i); | |
| 7399 | - sqlite3_result_blob( | |
| 7400 | - context, pCur->point_data->vectors[vector_idx], | |
| 7401 | - vector_column_byte_size(pVtab->vector_columns[vector_idx]), | |
| 7402 | - SQLITE_TRANSIENT); | |
| 7403 | - sqlite3_result_subtype(context, | |
| 7404 | - pVtab->vector_columns[vector_idx].element_type); | |
| 7405 | - return SQLITE_OK; | |
| 7406 | - } | |
| 7407 | - else if(vec0_column_idx_is_partition(pVtab, i)) { | |
| 7408 | - if(sqlite3_vtab_nochange(context)) { | |
| 7409 | - return SQLITE_OK; | |
| 7410 | - } | |
| 7411 | - int partition_idx = vec0_column_idx_to_partition_idx(pVtab, i); | |
| 7412 | - i64 rowid = pCur->point_data->rowid; | |
| 7413 | - sqlite3_value * v; | |
| 7414 | - int rc = vec0_get_partition_value_for_rowid(pVtab, rowid, partition_idx, &v); | |
| 7415 | - if(rc == SQLITE_OK) { | |
| 7416 | - sqlite3_result_value(context, v); | |
| 7417 | - sqlite3_value_free(v); | |
| 7418 | - }else { | |
| 7419 | - sqlite3_result_error_code(context, rc); | |
| 7420 | - } | |
| 7421 | - } | |
| 7422 | - else if(vec0_column_idx_is_auxiliary(pVtab, i)) { | |
| 7423 | - if(sqlite3_vtab_nochange(context)) { | |
| 7424 | - return SQLITE_OK; | |
| 7425 | - } | |
| 7426 | - i64 rowid = pCur->point_data->rowid; | |
| 7427 | - int auxiliary_idx = vec0_column_idx_to_auxiliary_idx(pVtab, i); | |
| 7428 | - sqlite3_value * v; | |
| 7429 | - int rc = vec0_get_auxiliary_value_for_rowid(pVtab, rowid, auxiliary_idx, &v); | |
| 7430 | - if(rc == SQLITE_OK) { | |
| 7431 | - sqlite3_result_value(context, v); | |
| 7432 | - sqlite3_value_free(v); | |
| 7433 | - }else { | |
| 7434 | - sqlite3_result_error_code(context, rc); | |
| 7435 | - } | |
| 7436 | - } | |
| 7437 | - | |
| 7438 | - else if(vec0_column_idx_is_metadata(pVtab, i)) { | |
| 7439 | - if(sqlite3_vtab_nochange(context)) { | |
| 7440 | - return SQLITE_OK; | |
| 7441 | - } | |
| 7442 | - i64 rowid = pCur->point_data->rowid; | |
| 7443 | - int metadata_idx = vec0_column_idx_to_metadata_idx(pVtab, i); | |
| 7444 | - int rc = vec0_result_metadata_value_for_rowid(pVtab, rowid, metadata_idx, context); | |
| 7445 | - if(rc != SQLITE_OK) { | |
| 7446 | - const char * zErr = sqlite3_mprintf( | |
| 7447 | - "Could not extract metadata value for column %.*s at rowid %lld", | |
| 7448 | - pVtab->metadata_columns[metadata_idx].name_length, | |
| 7449 | - pVtab->metadata_columns[metadata_idx].name, rowid | |
| 7450 | - ); | |
| 7451 | - if(zErr) { | |
| 7452 | - sqlite3_result_error(context, zErr, -1); | |
| 7453 | - sqlite3_free((void *) zErr); | |
| 7454 | - }else { | |
| 7455 | - sqlite3_result_error_nomem(context); | |
| 7456 | - } | |
| 7457 | - } | |
| 7458 | - } | |
| 7459 | - | |
| 7460 | - return SQLITE_OK; | |
| 7461 | -} | |
| 7462 | - | |
| 7463 | -static int vec0Column_knn(vec0_vtab *pVtab, vec0_cursor *pCur, | |
| 7464 | - sqlite3_context *context, int i) { | |
| 7465 | - if (!pCur->knn_data) { | |
| 7466 | - sqlite3_result_error(context, | |
| 7467 | - "Internal sqlite-vec error: knn_data is NULL.", -1); | |
| 7468 | - return SQLITE_ERROR; | |
| 7469 | - } | |
| 7470 | - if (i == VEC0_COLUMN_ID) { | |
| 7471 | - i64 rowid = pCur->knn_data->rowids[pCur->knn_data->current_idx]; | |
| 7472 | - return vec0_result_id(pVtab, context, rowid); | |
| 7473 | - } | |
| 7474 | - else if (i == vec0_column_distance_idx(pVtab)) { | |
| 7475 | - sqlite3_result_double( | |
| 7476 | - context, pCur->knn_data->distances[pCur->knn_data->current_idx]); | |
| 7477 | - return SQLITE_OK; | |
| 7478 | - } | |
| 7479 | - else if (vec0_column_idx_is_vector(pVtab, i)) { | |
| 7480 | - void *out; | |
| 7481 | - int sz; | |
| 7482 | - int vector_idx = vec0_column_idx_to_vector_idx(pVtab, i); | |
| 7483 | - int rc = vec0_get_vector_data( | |
| 7484 | - pVtab, pCur->knn_data->rowids[pCur->knn_data->current_idx], vector_idx, | |
| 7485 | - &out, &sz); | |
| 7486 | - if (rc != SQLITE_OK) { | |
| 7487 | - return rc; | |
| 7488 | - } | |
| 7489 | - sqlite3_result_blob(context, out, sz, sqlite3_free); | |
| 7490 | - sqlite3_result_subtype(context, | |
| 7491 | - pVtab->vector_columns[vector_idx].element_type); | |
| 7492 | - return SQLITE_OK; | |
| 7493 | - } | |
| 7494 | - else if(vec0_column_idx_is_partition(pVtab, i)) { | |
| 7495 | - int partition_idx = vec0_column_idx_to_partition_idx(pVtab, i); | |
| 7496 | - i64 rowid = pCur->knn_data->rowids[pCur->knn_data->current_idx]; | |
| 7497 | - sqlite3_value * v; | |
| 7498 | - int rc = vec0_get_partition_value_for_rowid(pVtab, rowid, partition_idx, &v); | |
| 7499 | - if(rc == SQLITE_OK) { | |
| 7500 | - sqlite3_result_value(context, v); | |
| 7501 | - sqlite3_value_free(v); | |
| 7502 | - }else { | |
| 7503 | - sqlite3_result_error_code(context, rc); | |
| 7504 | - } | |
| 7505 | - } | |
| 7506 | - else if(vec0_column_idx_is_auxiliary(pVtab, i)) { | |
| 7507 | - int auxiliary_idx = vec0_column_idx_to_auxiliary_idx(pVtab, i); | |
| 7508 | - i64 rowid = pCur->knn_data->rowids[pCur->knn_data->current_idx]; | |
| 7509 | - sqlite3_value * v; | |
| 7510 | - int rc = vec0_get_auxiliary_value_for_rowid(pVtab, rowid, auxiliary_idx, &v); | |
| 7511 | - if(rc == SQLITE_OK) { | |
| 7512 | - sqlite3_result_value(context, v); | |
| 7513 | - sqlite3_value_free(v); | |
| 7514 | - }else { | |
| 7515 | - sqlite3_result_error_code(context, rc); | |
| 7516 | - } | |
| 7517 | - } | |
| 7518 | - | |
| 7519 | - else if(vec0_column_idx_is_metadata(pVtab, i)) { | |
| 7520 | - int metadata_idx = vec0_column_idx_to_metadata_idx(pVtab, i); | |
| 7521 | - i64 rowid = pCur->knn_data->rowids[pCur->knn_data->current_idx]; | |
| 7522 | - int rc = vec0_result_metadata_value_for_rowid(pVtab, rowid, metadata_idx, context); | |
| 7523 | - if(rc != SQLITE_OK) { | |
| 7524 | - const char * zErr = sqlite3_mprintf( | |
| 7525 | - "Could not extract metadata value for column %.*s at rowid %lld", | |
| 7526 | - pVtab->metadata_columns[metadata_idx].name_length, | |
| 7527 | - pVtab->metadata_columns[metadata_idx].name, rowid | |
| 7528 | - ); | |
| 7529 | - if(zErr) { | |
| 7530 | - sqlite3_result_error(context, zErr, -1); | |
| 7531 | - sqlite3_free((void *) zErr); | |
| 7532 | - }else { | |
| 7533 | - sqlite3_result_error_nomem(context); | |
| 7534 | - } | |
| 7535 | - } | |
| 7536 | - } | |
| 7537 | - | |
| 7538 | - return SQLITE_OK; | |
| 7539 | -} | |
| 7540 | - | |
| 7541 | -static int vec0Column(sqlite3_vtab_cursor *cur, sqlite3_context *context, | |
| 7542 | - int i) { | |
| 7543 | - vec0_cursor *pCur = (vec0_cursor *)cur; | |
| 7544 | - vec0_vtab *pVtab = (vec0_vtab *)cur->pVtab; | |
| 7545 | - switch (pCur->query_plan) { | |
| 7546 | - case VEC0_QUERY_PLAN_FULLSCAN: { | |
| 7547 | - return vec0Column_fullscan(pVtab, pCur, context, i); | |
| 7548 | - } | |
| 7549 | - case VEC0_QUERY_PLAN_KNN: { | |
| 7550 | - return vec0Column_knn(pVtab, pCur, context, i); | |
| 7551 | - } | |
| 7552 | - case VEC0_QUERY_PLAN_POINT: { | |
| 7553 | - return vec0Column_point(pVtab, pCur, context, i); | |
| 7554 | - } | |
| 7555 | - } | |
| 7556 | - return SQLITE_OK; | |
| 7557 | -} | |
| 7558 | - | |
| 7559 | -/** | |
| 7560 | - * @brief Handles the "insert rowid" step of a row insert operation of a vec0 | |
| 7561 | - * table. | |
| 7562 | - * | |
| 7563 | - * This function will insert a new row into the _rowids vec0 shadow table. | |
| 7564 | - * | |
| 7565 | - * @param p: virtual table | |
| 7566 | - * @param idValue: Value containing the inserted rowid/id value. | |
| 7567 | - * @param rowid: Output rowid, will point to the "real" i64 rowid | |
| 7568 | - * value that was inserted | |
| 7569 | - * @return int SQLITE_OK on success, error code on failure | |
| 7570 | - */ | |
| 7571 | -int vec0Update_InsertRowidStep(vec0_vtab *p, sqlite3_value *idValue, | |
| 7572 | - i64 *rowid) { | |
| 7573 | - | |
| 7574 | - /** | |
| 7575 | - * An insert into a vec0 table can happen a few different ways: | |
| 7576 | - * 1) With default INTEGER primary key: With a supplied i64 rowid | |
| 7577 | - * 2) With default INTEGER primary key: WITHOUT a supplied rowid | |
| 7578 | - * 3) With TEXT primary key: supplied text rowid | |
| 7579 | - */ | |
| 7580 | - | |
| 7581 | - int rc; | |
| 7582 | - | |
| 7583 | - // Option 3: vtab has a user-defined TEXT primary key, so ensure a text value | |
| 7584 | - // is provided. | |
| 7585 | - if (p->pkIsText) { | |
| 7586 | - if (sqlite3_value_type(idValue) != SQLITE_TEXT) { | |
| 7587 | - // IMP: V04200_21039 | |
| 7588 | - vtab_set_error(&p->base, | |
| 7589 | - "The %s virtual table was declared with a TEXT primary " | |
| 7590 | - "key, but a non-TEXT value was provided in an INSERT.", | |
| 7591 | - p->tableName); | |
| 7592 | - return SQLITE_ERROR; | |
| 7593 | - } | |
| 7594 | - | |
| 7595 | - return vec0_rowids_insert_id(p, idValue, rowid); | |
| 7596 | - } | |
| 7597 | - | |
| 7598 | - // Option 1: User supplied a i64 rowid | |
| 7599 | - if (sqlite3_value_type(idValue) == SQLITE_INTEGER) { | |
| 7600 | - i64 suppliedRowid = sqlite3_value_int64(idValue); | |
| 7601 | - rc = vec0_rowids_insert_rowid(p, suppliedRowid); | |
| 7602 | - if (rc == SQLITE_OK) { | |
| 7603 | - *rowid = suppliedRowid; | |
| 7604 | - } | |
| 7605 | - return rc; | |
| 7606 | - } | |
| 7607 | - | |
| 7608 | - // Option 2: User did not suppled a rowid | |
| 7609 | - | |
| 7610 | - if (sqlite3_value_type(idValue) != SQLITE_NULL) { | |
| 7611 | - // IMP: V30855_14925 | |
| 7612 | - vtab_set_error(&p->base, | |
| 7613 | - "Only integers are allows for primary key values on %s", | |
| 7614 | - p->tableName); | |
| 7615 | - return SQLITE_ERROR; | |
| 7616 | - } | |
| 7617 | - // NULL to get next auto-incremented value | |
| 7618 | - return vec0_rowids_insert_id(p, NULL, rowid); | |
| 7619 | -} | |
| 7620 | - | |
| 7621 | -/** | |
| 7622 | - * @brief Determines the "next available" chunk position for a newly inserted | |
| 7623 | - * vec0 row. | |
| 7624 | - * | |
| 7625 | - * This operation may insert a new "blank" chunk the _chunks table, if there is | |
| 7626 | - * no more space in previous chunks. | |
| 7627 | - * | |
| 7628 | - * @param p: virtual table | |
| 7629 | - * @param partitionKeyValues: array of partition key column values, to constrain | |
| 7630 | - * against any partition key columns. | |
| 7631 | - * @param chunk_rowid: Output rowid of the chunk in the _chunks virtual table | |
| 7632 | - * that has the avialabiity. | |
| 7633 | - * @param chunk_offset: Output the index of the available space insert the | |
| 7634 | - * chunk, based on the index of the first available validity bit. | |
| 7635 | - * @param pBlobValidity: Output blob of the validity column of the available | |
| 7636 | - * chunk. Will be opened with read/write permissions. | |
| 7637 | - * @param pValidity: Output buffer of the original chunk's validity column. | |
| 7638 | - * Needs to be cleaned up with sqlite3_free(). | |
| 7639 | - * @return int SQLITE_OK on success, error code on failure | |
| 7640 | - */ | |
| 7641 | -int vec0Update_InsertNextAvailableStep( | |
| 7642 | - vec0_vtab *p, | |
| 7643 | - sqlite3_value ** partitionKeyValues, | |
| 7644 | - i64 *chunk_rowid, i64 *chunk_offset, | |
| 7645 | - sqlite3_blob **blobChunksValidity, | |
| 7646 | - const unsigned char **bufferChunksValidity) { | |
| 7647 | - | |
| 7648 | - int rc; | |
| 7649 | - i64 validitySize; | |
| 7650 | - *chunk_offset = -1; | |
| 7651 | - | |
| 7652 | - rc = vec0_get_latest_chunk_rowid(p, chunk_rowid, partitionKeyValues); | |
| 7653 | - if(rc == SQLITE_EMPTY) { | |
| 7654 | - goto done; | |
| 7655 | - } | |
| 7656 | - if (rc != SQLITE_OK) { | |
| 7657 | - goto cleanup; | |
| 7658 | - } | |
| 7659 | - | |
| 7660 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowChunksName, "validity", | |
| 7661 | - *chunk_rowid, 1, blobChunksValidity); | |
| 7662 | - if (rc != SQLITE_OK) { | |
| 7663 | - // IMP: V22053_06123 | |
| 7664 | - vtab_set_error(&p->base, | |
| 7665 | - VEC_INTERAL_ERROR | |
| 7666 | - "could not open validity blob on %s.%s.%lld", | |
| 7667 | - p->schemaName, p->shadowChunksName, *chunk_rowid); | |
| 7668 | - goto cleanup; | |
| 7669 | - } | |
| 7670 | - | |
| 7671 | - validitySize = sqlite3_blob_bytes(*blobChunksValidity); | |
| 7672 | - if (validitySize != p->chunk_size / CHAR_BIT) { | |
| 7673 | - // IMP: V29362_13432 | |
| 7674 | - vtab_set_error(&p->base, | |
| 7675 | - VEC_INTERAL_ERROR | |
| 7676 | - "validity blob size mismatch on " | |
| 7677 | - "%s.%s.%lld, expected %lld but received %lld.", | |
| 7678 | - p->schemaName, p->shadowChunksName, *chunk_rowid, | |
| 7679 | - (i64)(p->chunk_size / CHAR_BIT), validitySize); | |
| 7680 | - rc = SQLITE_ERROR; | |
| 7681 | - goto cleanup; | |
| 7682 | - } | |
| 7683 | - | |
| 7684 | - *bufferChunksValidity = sqlite3_malloc(validitySize); | |
| 7685 | - if (!(*bufferChunksValidity)) { | |
| 7686 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | |
| 7687 | - "Could not allocate memory for validity bitmap"); | |
| 7688 | - rc = SQLITE_NOMEM; | |
| 7689 | - goto cleanup; | |
| 7690 | - } | |
| 7691 | - | |
| 7692 | - rc = sqlite3_blob_read(*blobChunksValidity, (void *)*bufferChunksValidity, | |
| 7693 | - validitySize, 0); | |
| 7694 | - | |
| 7695 | - if (rc != SQLITE_OK) { | |
| 7696 | - vtab_set_error(&p->base, | |
| 7697 | - VEC_INTERAL_ERROR | |
| 7698 | - "Could not read validity bitmap for %s.%s.%lld", | |
| 7699 | - p->schemaName, p->shadowChunksName, *chunk_rowid); | |
| 7700 | - goto cleanup; | |
| 7701 | - } | |
| 7702 | - | |
| 7703 | - // find the next available offset, ie first `0` in the bitmap. | |
| 7704 | - for (int i = 0; i < validitySize; i++) { | |
| 7705 | - if ((*bufferChunksValidity)[i] == 0b11111111) | |
| 7706 | - continue; | |
| 7707 | - for (int j = 0; j < CHAR_BIT; j++) { | |
| 7708 | - if (((((*bufferChunksValidity)[i] >> j) & 1) == 0)) { | |
| 7709 | - *chunk_offset = (i * CHAR_BIT) + j; | |
| 7710 | - goto done; | |
| 7711 | - } | |
| 7712 | - } | |
| 7713 | - } | |
| 7714 | - | |
| 7715 | -done: | |
| 7716 | - // latest chunk was full, so need to create a new one | |
| 7717 | - if (*chunk_offset == -1) { | |
| 7718 | - rc = vec0_new_chunk(p, partitionKeyValues, chunk_rowid); | |
| 7719 | - if (rc != SQLITE_OK) { | |
| 7720 | - // IMP: V08441_25279 | |
| 7721 | - vtab_set_error(&p->base, | |
| 7722 | - VEC_INTERAL_ERROR "Could not insert a new vector chunk"); | |
| 7723 | - rc = SQLITE_ERROR; // otherwise raises a DatabaseError and not operational | |
| 7724 | - // error? | |
| 7725 | - goto cleanup; | |
| 7726 | - } | |
| 7727 | - *chunk_offset = 0; | |
| 7728 | - | |
| 7729 | - // blobChunksValidity and pValidity are stale, pointing to the previous | |
| 7730 | - // (full) chunk. to re-assign them | |
| 7731 | - rc = sqlite3_blob_close(*blobChunksValidity); | |
| 7732 | - sqlite3_free((void *)*bufferChunksValidity); | |
| 7733 | - *blobChunksValidity = NULL; | |
| 7734 | - *bufferChunksValidity = NULL; | |
| 7735 | - if (rc != SQLITE_OK) { | |
| 7736 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | |
| 7737 | - "unknown error, blobChunksValidity could not be closed, " | |
| 7738 | - "please file an issue."); | |
| 7739 | - rc = SQLITE_ERROR; | |
| 7740 | - goto cleanup; | |
| 7741 | - } | |
| 7742 | - | |
| 7743 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowChunksName, | |
| 7744 | - "validity", *chunk_rowid, 1, blobChunksValidity); | |
| 7745 | - if (rc != SQLITE_OK) { | |
| 7746 | - vtab_set_error( | |
| 7747 | - &p->base, | |
| 7748 | - VEC_INTERAL_ERROR | |
| 7749 | - "Could not open validity blob for newly created chunk %s.%s.%lld", | |
| 7750 | - p->schemaName, p->shadowChunksName, *chunk_rowid); | |
| 7751 | - goto cleanup; | |
| 7752 | - } | |
| 7753 | - validitySize = sqlite3_blob_bytes(*blobChunksValidity); | |
| 7754 | - if (validitySize != p->chunk_size / CHAR_BIT) { | |
| 7755 | - vtab_set_error(&p->base, | |
| 7756 | - VEC_INTERAL_ERROR | |
| 7757 | - "validity blob size mismatch for newly created chunk " | |
| 7758 | - "%s.%s.%lld. Exepcted %lld, got %lld", | |
| 7759 | - p->schemaName, p->shadowChunksName, *chunk_rowid, | |
| 7760 | - p->chunk_size / CHAR_BIT, validitySize); | |
| 7761 | - goto cleanup; | |
| 7762 | - } | |
| 7763 | - *bufferChunksValidity = sqlite3_malloc(validitySize); | |
| 7764 | - rc = sqlite3_blob_read(*blobChunksValidity, (void *)*bufferChunksValidity, | |
| 7765 | - validitySize, 0); | |
| 7766 | - if (rc != SQLITE_OK) { | |
| 7767 | - vtab_set_error(&p->base, | |
| 7768 | - VEC_INTERAL_ERROR | |
| 7769 | - "could not read validity blob newly created chunk " | |
| 7770 | - "%s.%s.%lld", | |
| 7771 | - p->schemaName, p->shadowChunksName, *chunk_rowid); | |
| 7772 | - goto cleanup; | |
| 7773 | - } | |
| 7774 | - } | |
| 7775 | - | |
| 7776 | - rc = SQLITE_OK; | |
| 7777 | - | |
| 7778 | -cleanup: | |
| 7779 | - return rc; | |
| 7780 | -} | |
| 7781 | - | |
| 7782 | -/** | |
| 7783 | - * @brief Write the vector data into the provided vector blob at the given | |
| 7784 | - * offset | |
| 7785 | - * | |
| 7786 | - * @param blobVectors SQLite BLOB to write to | |
| 7787 | - * @param chunk_offset the "offset" (ie validity bitmap position) to write the | |
| 7788 | - * vector to | |
| 7789 | - * @param bVector pointer to the vector containing data | |
| 7790 | - * @param dimensions how many dimensions the vector has | |
| 7791 | - * @param element_type the vector type | |
| 7792 | - * @return result of sqlite3_blob_write, SQLITE_OK on success, otherwise failure | |
| 7793 | - */ | |
| 7794 | -static int | |
| 7795 | -vec0_write_vector_to_vector_blob(sqlite3_blob *blobVectors, i64 chunk_offset, | |
| 7796 | - const void *bVector, size_t dimensions, | |
| 7797 | - enum VectorElementType element_type) { | |
| 7798 | - int n; | |
| 7799 | - int offset; | |
| 7800 | - | |
| 7801 | - switch (element_type) { | |
| 7802 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: | |
| 7803 | - n = dimensions * sizeof(f32); | |
| 7804 | - offset = chunk_offset * dimensions * sizeof(f32); | |
| 7805 | - break; | |
| 7806 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | |
| 7807 | - n = dimensions * sizeof(i8); | |
| 7808 | - offset = chunk_offset * dimensions * sizeof(i8); | |
| 7809 | - break; | |
| 7810 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: | |
| 7811 | - n = dimensions / CHAR_BIT; | |
| 7812 | - offset = chunk_offset * dimensions / CHAR_BIT; | |
| 7813 | - break; | |
| 7814 | - } | |
| 7815 | - | |
| 7816 | - return sqlite3_blob_write(blobVectors, bVector, n, offset); | |
| 7817 | -} | |
| 7818 | - | |
| 7819 | -/** | |
| 7820 | - * @brief | |
| 7821 | - * | |
| 7822 | - * @param p vec0 virtual table | |
| 7823 | - * @param chunk_rowid: which chunk to write to | |
| 7824 | - * @param chunk_offset: the offset inside the chunk to write the vector to. | |
| 7825 | - * @param rowid: the rowid of the inserting row | |
| 7826 | - * @param vectorDatas: array of the vector data to insert | |
| 7827 | - * @param blobValidity: writeable validity blob of the row's assigned chunk. | |
| 7828 | - * @param validity: snapshot buffer of the valdity column from the row's | |
| 7829 | - * assigned chunk. | |
| 7830 | - * @return int SQLITE_OK on success, error code on failure | |
| 7831 | - */ | |
| 7832 | -int vec0Update_InsertWriteFinalStep(vec0_vtab *p, i64 chunk_rowid, | |
| 7833 | - i64 chunk_offset, i64 rowid, | |
| 7834 | - void *vectorDatas[], | |
| 7835 | - sqlite3_blob *blobChunksValidity, | |
| 7836 | - const unsigned char *bufferChunksValidity) { | |
| 7837 | - int rc, brc; | |
| 7838 | - sqlite3_blob *blobChunksRowids = NULL; | |
| 7839 | - | |
| 7840 | - // mark the validity bit for this row in the chunk's validity bitmap | |
| 7841 | - // Get the byte offset of the bitmap | |
| 7842 | - char unsigned bx = bufferChunksValidity[chunk_offset / CHAR_BIT]; | |
| 7843 | - // set the bit at the chunk_offset position inside that byte | |
| 7844 | - bx = bx | (1 << (chunk_offset % CHAR_BIT)); | |
| 7845 | - // write that 1 byte | |
| 7846 | - rc = sqlite3_blob_write(blobChunksValidity, &bx, 1, chunk_offset / CHAR_BIT); | |
| 7847 | - if (rc != SQLITE_OK) { | |
| 7848 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR "could not mark validity bit "); | |
| 7849 | - return rc; | |
| 7850 | - } | |
| 7851 | - | |
| 7852 | - // Go insert the vector data into the vector chunk shadow tables | |
| 7853 | - for (int i = 0; i < p->numVectorColumns; i++) { | |
| 7854 | - sqlite3_blob *blobVectors; | |
| 7855 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowVectorChunksNames[i], | |
| 7856 | - "vectors", chunk_rowid, 1, &blobVectors); | |
| 7857 | - if (rc != SQLITE_OK) { | |
| 7858 | - vtab_set_error(&p->base, "Error opening vector blob at %s.%s.%lld", | |
| 7859 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_rowid); | |
| 7860 | - goto cleanup; | |
| 7861 | - } | |
| 7862 | - | |
| 7863 | - i64 expected = | |
| 7864 | - p->chunk_size * vector_column_byte_size(p->vector_columns[i]); | |
| 7865 | - i64 actual = sqlite3_blob_bytes(blobVectors); | |
| 7866 | - | |
| 7867 | - if (actual != expected) { | |
| 7868 | - // IMP: V16386_00456 | |
| 7869 | - vtab_set_error( | |
| 7870 | - &p->base, | |
| 7871 | - VEC_INTERAL_ERROR | |
| 7872 | - "vector blob size mismatch on %s.%s.%lld. Expected %lld, actual %lld", | |
| 7873 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_rowid, expected, | |
| 7874 | - actual); | |
| 7875 | - rc = SQLITE_ERROR; | |
| 7876 | - // already error, can ignore result code | |
| 7877 | - sqlite3_blob_close(blobVectors); | |
| 7878 | - goto cleanup; | |
| 7879 | - }; | |
| 7880 | - | |
| 7881 | - rc = vec0_write_vector_to_vector_blob( | |
| 7882 | - blobVectors, chunk_offset, vectorDatas[i], | |
| 7883 | - p->vector_columns[i].dimensions, p->vector_columns[i].element_type); | |
| 7884 | - if (rc != SQLITE_OK) { | |
| 7885 | - vtab_set_error(&p->base, | |
| 7886 | - VEC_INTERAL_ERROR | |
| 7887 | - "could not write vector blob on %s.%s.%lld", | |
| 7888 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_rowid); | |
| 7889 | - rc = SQLITE_ERROR; | |
| 7890 | - // already error, can ignore result code | |
| 7891 | - sqlite3_blob_close(blobVectors); | |
| 7892 | - goto cleanup; | |
| 7893 | - } | |
| 7894 | - rc = sqlite3_blob_close(blobVectors); | |
| 7895 | - if (rc != SQLITE_OK) { | |
| 7896 | - vtab_set_error(&p->base, | |
| 7897 | - VEC_INTERAL_ERROR | |
| 7898 | - "could not close vector blob on %s.%s.%lld", | |
| 7899 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_rowid); | |
| 7900 | - rc = SQLITE_ERROR; | |
| 7901 | - goto cleanup; | |
| 7902 | - } | |
| 7903 | - } | |
| 7904 | - | |
| 7905 | - // write the new rowid to the rowids column of the _chunks table | |
| 7906 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowChunksName, "rowids", | |
| 7907 | - chunk_rowid, 1, &blobChunksRowids); | |
| 7908 | - if (rc != SQLITE_OK) { | |
| 7909 | - // IMP: V09221_26060 | |
| 7910 | - vtab_set_error(&p->base, | |
| 7911 | - VEC_INTERAL_ERROR "could not open rowids blob on %s.%s.%lld", | |
| 7912 | - p->schemaName, p->shadowChunksName, chunk_rowid); | |
| 7913 | - goto cleanup; | |
| 7914 | - } | |
| 7915 | - i64 expected = p->chunk_size * sizeof(i64); | |
| 7916 | - i64 actual = sqlite3_blob_bytes(blobChunksRowids); | |
| 7917 | - if (expected != actual) { | |
| 7918 | - // IMP: V12779_29618 | |
| 7919 | - vtab_set_error( | |
| 7920 | - &p->base, | |
| 7921 | - VEC_INTERAL_ERROR | |
| 7922 | - "rowids blob size mismatch on %s.%s.%lld. Expected %lld, actual %lld", | |
| 7923 | - p->schemaName, p->shadowChunksName, chunk_rowid, expected, actual); | |
| 7924 | - rc = SQLITE_ERROR; | |
| 7925 | - goto cleanup; | |
| 7926 | - } | |
| 7927 | - rc = sqlite3_blob_write(blobChunksRowids, &rowid, sizeof(i64), | |
| 7928 | - chunk_offset * sizeof(i64)); | |
| 7929 | - if (rc != SQLITE_OK) { | |
| 7930 | - vtab_set_error( | |
| 7931 | - &p->base, VEC_INTERAL_ERROR "could not write rowids blob on %s.%s.%lld", | |
| 7932 | - p->schemaName, p->shadowChunksName, chunk_rowid); | |
| 7933 | - rc = SQLITE_ERROR; | |
| 7934 | - goto cleanup; | |
| 7935 | - } | |
| 7936 | - | |
| 7937 | - // Now with all the vectors inserted, go back and update the _rowids table | |
| 7938 | - // with the new chunk_rowid/chunk_offset values | |
| 7939 | - rc = vec0_rowids_update_position(p, rowid, chunk_rowid, chunk_offset); | |
| 7940 | - | |
| 7941 | -cleanup: | |
| 7942 | - brc = sqlite3_blob_close(blobChunksRowids); | |
| 7943 | - if ((rc == SQLITE_OK) && (brc != SQLITE_OK)) { | |
| 7944 | - vtab_set_error( | |
| 7945 | - &p->base, VEC_INTERAL_ERROR "could not close rowids blob on %s.%s.%lld", | |
| 7946 | - p->schemaName, p->shadowChunksName, chunk_rowid); | |
| 7947 | - return brc; | |
| 7948 | - } | |
| 7949 | - return rc; | |
| 7950 | -} | |
| 7951 | - | |
| 7952 | -int vec0_write_metadata_value(vec0_vtab *p, int metadata_column_idx, i64 rowid, i64 chunk_id, i64 chunk_offset, sqlite3_value * v, int isupdate) { | |
| 7953 | - int rc; | |
| 7954 | - struct Vec0MetadataColumnDefinition * metadata_column = &p->metadata_columns[metadata_column_idx]; | |
| 7955 | - vec0_metadata_column_kind kind = metadata_column->kind; | |
| 7956 | - | |
| 7957 | - // verify input value matches column type | |
| 7958 | - switch(kind) { | |
| 7959 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | |
| 7960 | - if(sqlite3_value_type(v) != SQLITE_INTEGER || ((sqlite3_value_int(v) != 0) && (sqlite3_value_int(v) != 1))) { | |
| 7961 | - rc = SQLITE_ERROR; | |
| 7962 | - vtab_set_error(&p->base, "Expected 0 or 1 for BOOLEAN metadata column %.*s", metadata_column->name_length, metadata_column->name); | |
| 7963 | - goto done; | |
| 7964 | - } | |
| 7965 | - break; | |
| 7966 | - } | |
| 7967 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | |
| 7968 | - if(sqlite3_value_type(v) != SQLITE_INTEGER) { | |
| 7969 | - rc = SQLITE_ERROR; | |
| 7970 | - vtab_set_error(&p->base, "Expected integer for INTEGER metadata column %.*s, received %s", metadata_column->name_length, metadata_column->name, type_name(sqlite3_value_type(v))); | |
| 7971 | - goto done; | |
| 7972 | - } | |
| 7973 | - break; | |
| 7974 | - } | |
| 7975 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | |
| 7976 | - if(sqlite3_value_type(v) != SQLITE_FLOAT) { | |
| 7977 | - rc = SQLITE_ERROR; | |
| 7978 | - vtab_set_error(&p->base, "Expected float for FLOAT metadata column %.*s, received %s", metadata_column->name_length, metadata_column->name, type_name(sqlite3_value_type(v))); | |
| 7979 | - goto done; | |
| 7980 | - } | |
| 7981 | - break; | |
| 7982 | - } | |
| 7983 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | |
| 7984 | - if(sqlite3_value_type(v) != SQLITE_TEXT) { | |
| 7985 | - rc = SQLITE_ERROR; | |
| 7986 | - vtab_set_error(&p->base, "Expected text for TEXT metadata column %.*s, received %s", metadata_column->name_length, metadata_column->name, type_name(sqlite3_value_type(v))); | |
| 7987 | - goto done; | |
| 7988 | - } | |
| 7989 | - break; | |
| 7990 | - } | |
| 7991 | - } | |
| 7992 | - | |
| 7993 | - sqlite3_blob * blobValue = NULL; | |
| 7994 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowMetadataChunksNames[metadata_column_idx], "data", chunk_id, 1, &blobValue); | |
| 7995 | - if(rc != SQLITE_OK) { | |
| 7996 | - goto done; | |
| 7997 | - } | |
| 7998 | - | |
| 7999 | - switch(kind) { | |
| 8000 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | |
| 8001 | - u8 block; | |
| 8002 | - int value = sqlite3_value_int(v); | |
| 8003 | - rc = sqlite3_blob_read(blobValue, &block, sizeof(u8), (int) (chunk_offset / CHAR_BIT)); | |
| 8004 | - if(rc != SQLITE_OK) { | |
| 8005 | - goto done; | |
| 8006 | - } | |
| 8007 | - | |
| 8008 | - if (value) { | |
| 8009 | - block |= 1 << (chunk_offset % CHAR_BIT); | |
| 8010 | - } else { | |
| 8011 | - block &= ~(1 << (chunk_offset % CHAR_BIT)); | |
| 8012 | - } | |
| 8013 | - | |
| 8014 | - rc = sqlite3_blob_write(blobValue, &block, sizeof(u8), chunk_offset / CHAR_BIT); | |
| 8015 | - break; | |
| 8016 | - } | |
| 8017 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | |
| 8018 | - i64 value = sqlite3_value_int64(v); | |
| 8019 | - rc = sqlite3_blob_write(blobValue, &value, sizeof(value), chunk_offset * sizeof(i64)); | |
| 8020 | - break; | |
| 8021 | - } | |
| 8022 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | |
| 8023 | - double value = sqlite3_value_double(v); | |
| 8024 | - rc = sqlite3_blob_write(blobValue, &value, sizeof(value), chunk_offset * sizeof(double)); | |
| 8025 | - break; | |
| 8026 | - } | |
| 8027 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | |
| 8028 | - int prev_n; | |
| 8029 | - rc = sqlite3_blob_read(blobValue, &prev_n, sizeof(int), chunk_offset * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | |
| 8030 | - if(rc != SQLITE_OK) { | |
| 8031 | - goto done; | |
| 8032 | - } | |
| 8033 | - | |
| 8034 | - const char * s = (const char *) sqlite3_value_text(v); | |
| 8035 | - int n = sqlite3_value_bytes(v); | |
| 8036 | - u8 view[VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | |
| 8037 | - memset(view, 0, VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | |
| 8038 | - memcpy(view, &n, sizeof(int)); | |
| 8039 | - memcpy(view+4, s, min(n, VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH-4)); | |
| 8040 | - | |
| 8041 | - rc = sqlite3_blob_write(blobValue, &view, VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH, chunk_offset * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | |
| 8042 | - if(n > VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | |
| 8043 | - const char * zSql; | |
| 8044 | - | |
| 8045 | - if(isupdate && (prev_n > VEC0_METADATA_TEXT_VIEW_DATA_LENGTH)) { | |
| 8046 | - zSql = sqlite3_mprintf("UPDATE " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " SET data = ?2 WHERE rowid = ?1", p->schemaName, p->tableName, metadata_column_idx); | |
| 8047 | - }else { | |
| 8048 | - zSql = sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " (rowid, data) VALUES (?1, ?2)", p->schemaName, p->tableName, metadata_column_idx); | |
| 8049 | - } | |
| 8050 | - if(!zSql) { | |
| 8051 | - rc = SQLITE_NOMEM; | |
| 8052 | - goto done; | |
| 8053 | - } | |
| 8054 | - sqlite3_stmt * stmt; | |
| 8055 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 8056 | - if(rc != SQLITE_OK) { | |
| 8057 | - goto done; | |
| 8058 | - } | |
| 8059 | - sqlite3_bind_int64(stmt, 1, rowid); | |
| 8060 | - sqlite3_bind_text(stmt, 2, s, n, SQLITE_STATIC); | |
| 8061 | - rc = sqlite3_step(stmt); | |
| 8062 | - sqlite3_finalize(stmt); | |
| 8063 | - | |
| 8064 | - if(rc != SQLITE_DONE) { | |
| 8065 | - rc = SQLITE_ERROR; | |
| 8066 | - goto done; | |
| 8067 | - } | |
| 8068 | - } | |
| 8069 | - else if(prev_n > VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | |
| 8070 | - const char * zSql = sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " WHERE rowid = ?", p->schemaName, p->tableName, metadata_column_idx); | |
| 8071 | - if(!zSql) { | |
| 8072 | - rc = SQLITE_NOMEM; | |
| 8073 | - goto done; | |
| 8074 | - } | |
| 8075 | - sqlite3_stmt * stmt; | |
| 8076 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 8077 | - if(rc != SQLITE_OK) { | |
| 8078 | - goto done; | |
| 8079 | - } | |
| 8080 | - sqlite3_bind_int64(stmt, 1, rowid); | |
| 8081 | - rc = sqlite3_step(stmt); | |
| 8082 | - sqlite3_finalize(stmt); | |
| 8083 | - | |
| 8084 | - if(rc != SQLITE_DONE) { | |
| 8085 | - rc = SQLITE_ERROR; | |
| 8086 | - goto done; | |
| 8087 | - } | |
| 8088 | - } | |
| 8089 | - break; | |
| 8090 | - } | |
| 8091 | - } | |
| 8092 | - | |
| 8093 | - if(rc != SQLITE_OK) { | |
| 8094 | - | |
| 8095 | - } | |
| 8096 | - rc = sqlite3_blob_close(blobValue); | |
| 8097 | - if(rc != SQLITE_OK) { | |
| 8098 | - goto done; | |
| 8099 | - } | |
| 8100 | - | |
| 8101 | - done: | |
| 8102 | - return rc; | |
| 8103 | -} | |
| 8104 | - | |
| 8105 | - | |
| 8106 | -/** | |
| 8107 | - * @brief Handles INSERT INTO operations on a vec0 table. | |
| 8108 | - * | |
| 8109 | - * @return int SQLITE_OK on success, otherwise error code on failure | |
| 8110 | - */ | |
| 8111 | -int vec0Update_Insert(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv, | |
| 8112 | - sqlite_int64 *pRowid) { | |
| 8113 | - UNUSED_PARAMETER(argc); | |
| 8114 | - vec0_vtab *p = (vec0_vtab *)pVTab; | |
| 8115 | - int rc; | |
| 8116 | - // Rowid for the inserted row, deterimined by the inserted ID + _rowids shadow | |
| 8117 | - // table | |
| 8118 | - i64 rowid; | |
| 8119 | - | |
| 8120 | - // Array to hold the vector data of the inserted row. Individual elements will | |
| 8121 | - // have a lifetime bound to the argv[..] values. | |
| 8122 | - void *vectorDatas[VEC0_MAX_VECTOR_COLUMNS]; | |
| 8123 | - // Array to hold cleanup functions for vectorDatas[] | |
| 8124 | - vector_cleanup cleanups[VEC0_MAX_VECTOR_COLUMNS]; | |
| 8125 | - | |
| 8126 | - sqlite3_value * partitionKeyValues[VEC0_MAX_PARTITION_COLUMNS]; | |
| 8127 | - | |
| 8128 | - // Rowid of the chunk in the _chunks shadow table that the row will be a part | |
| 8129 | - // of. | |
| 8130 | - i64 chunk_rowid; | |
| 8131 | - // offset within the chunk where the rowid belongs | |
| 8132 | - i64 chunk_offset; | |
| 8133 | - | |
| 8134 | - // a write-able blob of the validity column for the given chunk. Used to mark | |
| 8135 | - // validity bit | |
| 8136 | - sqlite3_blob *blobChunksValidity = NULL; | |
| 8137 | - // buffer for the valididty column for the given chunk. Maybe not needed here? | |
| 8138 | - const unsigned char *bufferChunksValidity = NULL; | |
| 8139 | - int numReadVectors = 0; | |
| 8140 | - | |
| 8141 | - // Read all provided partition key values into partitionKeyValues | |
| 8142 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | |
| 8143 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_PARTITION) { | |
| 8144 | - continue; | |
| 8145 | - } | |
| 8146 | - int partition_key_idx = p->user_column_idxs[i]; | |
| 8147 | - partitionKeyValues[partition_key_idx] = argv[2+VEC0_COLUMN_USERN_START + i]; | |
| 8148 | - | |
| 8149 | - int new_value_type = sqlite3_value_type(partitionKeyValues[partition_key_idx]); | |
| 8150 | - if((new_value_type != SQLITE_NULL) && (new_value_type != p->paritition_columns[partition_key_idx].type)) { | |
| 8151 | - // IMP: V11454_28292 | |
| 8152 | - vtab_set_error( | |
| 8153 | - pVTab, | |
| 8154 | - "Parition key type mismatch: The partition key column %.*s has type %s, but %s was provided.", | |
| 8155 | - p->paritition_columns[partition_key_idx].name_length, | |
| 8156 | - p->paritition_columns[partition_key_idx].name, | |
| 8157 | - type_name(p->paritition_columns[partition_key_idx].type), | |
| 8158 | - type_name(new_value_type) | |
| 8159 | - ); | |
| 8160 | - rc = SQLITE_ERROR; | |
| 8161 | - goto cleanup; | |
| 8162 | - } | |
| 8163 | - } | |
| 8164 | - | |
| 8165 | - // read all the inserted vectors into vectorDatas, validate their lengths. | |
| 8166 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | |
| 8167 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_VECTOR) { | |
| 8168 | - continue; | |
| 8169 | - } | |
| 8170 | - int vector_column_idx = p->user_column_idxs[i]; | |
| 8171 | - sqlite3_value *valueVector = argv[2 + VEC0_COLUMN_USERN_START + i]; | |
| 8172 | - size_t dimensions; | |
| 8173 | - | |
| 8174 | - char *pzError; | |
| 8175 | - enum VectorElementType elementType; | |
| 8176 | - rc = vector_from_value(valueVector, &vectorDatas[vector_column_idx], &dimensions, | |
| 8177 | - &elementType, &cleanups[vector_column_idx], &pzError); | |
| 8178 | - if (rc != SQLITE_OK) { | |
| 8179 | - // IMP: V06519_23358 | |
| 8180 | - vtab_set_error( | |
| 8181 | - pVTab, "Inserted vector for the \"%.*s\" column is invalid: %z", | |
| 8182 | - p->vector_columns[vector_column_idx].name_length, p->vector_columns[vector_column_idx].name, pzError); | |
| 8183 | - rc = SQLITE_ERROR; | |
| 8184 | - goto cleanup; | |
| 8185 | - } | |
| 8186 | - | |
| 8187 | - numReadVectors++; | |
| 8188 | - if (elementType != p->vector_columns[vector_column_idx].element_type) { | |
| 8189 | - // IMP: V08221_25059 | |
| 8190 | - vtab_set_error( | |
| 8191 | - pVTab, | |
| 8192 | - "Inserted vector for the \"%.*s\" column is expected to be of type " | |
| 8193 | - "%s, but a %s vector was provided.", | |
| 8194 | - p->vector_columns[i].name_length, p->vector_columns[i].name, | |
| 8195 | - vector_subtype_name(p->vector_columns[i].element_type), | |
| 8196 | - vector_subtype_name(elementType)); | |
| 8197 | - rc = SQLITE_ERROR; | |
| 8198 | - goto cleanup; | |
| 8199 | - } | |
| 8200 | - | |
| 8201 | - if (dimensions != p->vector_columns[vector_column_idx].dimensions) { | |
| 8202 | - // IMP: V01145_17984 | |
| 8203 | - vtab_set_error( | |
| 8204 | - pVTab, | |
| 8205 | - "Dimension mismatch for inserted vector for the \"%.*s\" column. " | |
| 8206 | - "Expected %d dimensions but received %d.", | |
| 8207 | - p->vector_columns[vector_column_idx].name_length, p->vector_columns[vector_column_idx].name, | |
| 8208 | - p->vector_columns[vector_column_idx].dimensions, dimensions); | |
| 8209 | - rc = SQLITE_ERROR; | |
| 8210 | - goto cleanup; | |
| 8211 | - } | |
| 8212 | - } | |
| 8213 | - | |
| 8214 | - // Cannot insert a value in the hidden "distance" column | |
| 8215 | - if (sqlite3_value_type(argv[2 + vec0_column_distance_idx(p)]) != | |
| 8216 | - SQLITE_NULL) { | |
| 8217 | - // IMP: V24228_08298 | |
| 8218 | - vtab_set_error(pVTab, | |
| 8219 | - "A value was provided for the hidden \"distance\" column."); | |
| 8220 | - rc = SQLITE_ERROR; | |
| 8221 | - goto cleanup; | |
| 8222 | - } | |
| 8223 | - // Cannot insert a value in the hidden "k" column | |
| 8224 | - if (sqlite3_value_type(argv[2 + vec0_column_k_idx(p)]) != SQLITE_NULL) { | |
| 8225 | - // IMP: V11875_28713 | |
| 8226 | - vtab_set_error(pVTab, "A value was provided for the hidden \"k\" column."); | |
| 8227 | - rc = SQLITE_ERROR; | |
| 8228 | - goto cleanup; | |
| 8229 | - } | |
| 8230 | - | |
| 8231 | - // Step #1: Insert/get a rowid for this row, from the _rowids table. | |
| 8232 | - rc = vec0Update_InsertRowidStep(p, argv[2 + VEC0_COLUMN_ID], &rowid); | |
| 8233 | - if (rc != SQLITE_OK) { | |
| 8234 | - goto cleanup; | |
| 8235 | - } | |
| 8236 | - | |
| 8237 | - // Step #2: Find the next "available" position in the _chunks table for this | |
| 8238 | - // row. | |
| 8239 | - rc = vec0Update_InsertNextAvailableStep(p, partitionKeyValues, | |
| 8240 | - &chunk_rowid, &chunk_offset, | |
| 8241 | - &blobChunksValidity, | |
| 8242 | - &bufferChunksValidity); | |
| 8243 | - if (rc != SQLITE_OK) { | |
| 8244 | - goto cleanup; | |
| 8245 | - } | |
| 8246 | - | |
| 8247 | - // Step #3: With the next available chunk position, write out all the vectors | |
| 8248 | - // to their specified location. | |
| 8249 | - rc = vec0Update_InsertWriteFinalStep(p, chunk_rowid, chunk_offset, rowid, | |
| 8250 | - vectorDatas, blobChunksValidity, | |
| 8251 | - bufferChunksValidity); | |
| 8252 | - if (rc != SQLITE_OK) { | |
| 8253 | - goto cleanup; | |
| 8254 | - } | |
| 8255 | - | |
| 8256 | - if(p->numAuxiliaryColumns > 0) { | |
| 8257 | - sqlite3_stmt *stmt; | |
| 8258 | - sqlite3_str * s = sqlite3_str_new(NULL); | |
| 8259 | - sqlite3_str_appendf(s, "INSERT INTO " VEC0_SHADOW_AUXILIARY_NAME "(rowid ", p->schemaName, p->tableName); | |
| 8260 | - for(int i = 0; i < p->numAuxiliaryColumns; i++) { | |
| 8261 | - sqlite3_str_appendf(s, ", value%02d", i); | |
| 8262 | - } | |
| 8263 | - sqlite3_str_appendall(s, ") VALUES (? "); | |
| 8264 | - for(int i = 0; i < p->numAuxiliaryColumns; i++) { | |
| 8265 | - sqlite3_str_appendall(s, ", ?"); | |
| 8266 | - } | |
| 8267 | - sqlite3_str_appendall(s, ")"); | |
| 8268 | - char * zSql = sqlite3_str_finish(s); | |
| 8269 | - // TODO double check error handling ehre | |
| 8270 | - if(!zSql) { | |
| 8271 | - rc = SQLITE_NOMEM; | |
| 8272 | - goto cleanup; | |
| 8273 | - } | |
| 8274 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 8275 | - if(rc != SQLITE_OK) { | |
| 8276 | - goto cleanup; | |
| 8277 | - } | |
| 8278 | - sqlite3_bind_int64(stmt, 1, rowid); | |
| 8279 | - | |
| 8280 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | |
| 8281 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY) { | |
| 8282 | - continue; | |
| 8283 | - } | |
| 8284 | - int auxiliary_key_idx = p->user_column_idxs[i]; | |
| 8285 | - sqlite3_value * v = argv[2+VEC0_COLUMN_USERN_START + i]; | |
| 8286 | - int v_type = sqlite3_value_type(v); | |
| 8287 | - if(v_type != SQLITE_NULL && (v_type != p->auxiliary_columns[auxiliary_key_idx].type)) { | |
| 8288 | - sqlite3_finalize(stmt); | |
| 8289 | - rc = SQLITE_CONSTRAINT; | |
| 8290 | - vtab_set_error( | |
| 8291 | - pVTab, | |
| 8292 | - "Auxiliary column type mismatch: The auxiliary column %.*s has type %s, but %s was provided.", | |
| 8293 | - p->auxiliary_columns[auxiliary_key_idx].name_length, | |
| 8294 | - p->auxiliary_columns[auxiliary_key_idx].name, | |
| 8295 | - type_name(p->auxiliary_columns[auxiliary_key_idx].type), | |
| 8296 | - type_name(v_type) | |
| 8297 | - ); | |
| 8298 | - goto cleanup; | |
| 8299 | - } | |
| 8300 | - // first 1 is for 1-based indexing on sqlite3_bind_*, second 1 is to account for initial rowid parameter | |
| 8301 | - sqlite3_bind_value(stmt, 1 + 1 + auxiliary_key_idx, v); | |
| 8302 | - } | |
| 8303 | - | |
| 8304 | - rc = sqlite3_step(stmt); | |
| 8305 | - if(rc != SQLITE_DONE) { | |
| 8306 | - sqlite3_finalize(stmt); | |
| 8307 | - rc = SQLITE_ERROR; | |
| 8308 | - goto cleanup; | |
| 8309 | - } | |
| 8310 | - sqlite3_finalize(stmt); | |
| 8311 | - } | |
| 8312 | - | |
| 8313 | - | |
| 8314 | - for(int i = 0; i < vec0_num_defined_user_columns(p); i++) { | |
| 8315 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_METADATA) { | |
| 8316 | - continue; | |
| 8317 | - } | |
| 8318 | - int metadata_idx = p->user_column_idxs[i]; | |
| 8319 | - sqlite3_value *v = argv[2 + VEC0_COLUMN_USERN_START + i]; | |
| 8320 | - rc = vec0_write_metadata_value(p, metadata_idx, rowid, chunk_rowid, chunk_offset, v, 0); | |
| 8321 | - if(rc != SQLITE_OK) { | |
| 8322 | - goto cleanup; | |
| 8323 | - } | |
| 8324 | - } | |
| 8325 | - | |
| 8326 | - *pRowid = rowid; | |
| 8327 | - rc = SQLITE_OK; | |
| 8328 | - | |
| 8329 | -cleanup: | |
| 8330 | - for (int i = 0; i < numReadVectors; i++) { | |
| 8331 | - cleanups[i](vectorDatas[i]); | |
| 8332 | - } | |
| 8333 | - sqlite3_free((void *)bufferChunksValidity); | |
| 8334 | - int brc = sqlite3_blob_close(blobChunksValidity); | |
| 8335 | - if ((rc == SQLITE_OK) && (brc != SQLITE_OK)) { | |
| 8336 | - vtab_set_error(&p->base, | |
| 8337 | - VEC_INTERAL_ERROR "unknown error, blobChunksValidity could " | |
| 8338 | - "not be closed, please file an issue"); | |
| 8339 | - return brc; | |
| 8340 | - } | |
| 8341 | - return rc; | |
| 8342 | -} | |
| 8343 | - | |
| 8344 | -int vec0Update_Delete_ClearValidity(vec0_vtab *p, i64 chunk_id, | |
| 8345 | - u64 chunk_offset) { | |
| 8346 | - int rc, brc; | |
| 8347 | - sqlite3_blob *blobChunksValidity = NULL; | |
| 8348 | - char unsigned bx; | |
| 8349 | - int validityOffset = chunk_offset / CHAR_BIT; | |
| 8350 | - | |
| 8351 | - // 2. ensure chunks.validity bit is 1, then set to 0 | |
| 8352 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowChunksName, "validity", | |
| 8353 | - chunk_id, 1, &blobChunksValidity); | |
| 8354 | - if (rc != SQLITE_OK) { | |
| 8355 | - // IMP: V26002_10073 | |
| 8356 | - vtab_set_error(&p->base, "could not open validity blob for %s.%s.%lld", | |
| 8357 | - p->schemaName, p->shadowChunksName, chunk_id); | |
| 8358 | - return SQLITE_ERROR; | |
| 8359 | - } | |
| 8360 | - // will skip the sqlite3_blob_bytes(blobChunksValidity) check for now, | |
| 8361 | - // the read below would catch it | |
| 8362 | - | |
| 8363 | - rc = sqlite3_blob_read(blobChunksValidity, &bx, sizeof(bx), validityOffset); | |
| 8364 | - if (rc != SQLITE_OK) { | |
| 8365 | - // IMP: V21193_05263 | |
| 8366 | - vtab_set_error( | |
| 8367 | - &p->base, "could not read validity blob for %s.%s.%lld at %d", | |
| 8368 | - p->schemaName, p->shadowChunksName, chunk_id, validityOffset); | |
| 8369 | - goto cleanup; | |
| 8370 | - } | |
| 8371 | - if (!(bx >> (chunk_offset % CHAR_BIT))) { | |
| 8372 | - // IMP: V21193_05263 | |
| 8373 | - rc = SQLITE_ERROR; | |
| 8374 | - vtab_set_error( | |
| 8375 | - &p->base, | |
| 8376 | - "vec0 deletion error: validity bit is not set for %s.%s.%lld at %d", | |
| 8377 | - p->schemaName, p->shadowChunksName, chunk_id, validityOffset); | |
| 8378 | - goto cleanup; | |
| 8379 | - } | |
| 8380 | - char unsigned mask = ~(1 << (chunk_offset % CHAR_BIT)); | |
| 8381 | - char result = bx & mask; | |
| 8382 | - rc = sqlite3_blob_write(blobChunksValidity, &result, sizeof(bx), | |
| 8383 | - validityOffset); | |
| 8384 | - if (rc != SQLITE_OK) { | |
| 8385 | - vtab_set_error( | |
| 8386 | - &p->base, "could not write to validity blob for %s.%s.%lld at %d", | |
| 8387 | - p->schemaName, p->shadowChunksName, chunk_id, validityOffset); | |
| 8388 | - goto cleanup; | |
| 8389 | - } | |
| 8390 | - | |
| 8391 | -cleanup: | |
| 8392 | - | |
| 8393 | - brc = sqlite3_blob_close(blobChunksValidity); | |
| 8394 | - if (rc != SQLITE_OK) | |
| 8395 | - return rc; | |
| 8396 | - if (brc != SQLITE_OK) { | |
| 8397 | - vtab_set_error(&p->base, | |
| 8398 | - "vec0 deletion error: Error commiting validity blob " | |
| 8399 | - "transaction on %s.%s.%lld at %d", | |
| 8400 | - p->schemaName, p->shadowChunksName, chunk_id, | |
| 8401 | - validityOffset); | |
| 8402 | - return brc; | |
| 8403 | - } | |
| 8404 | - return SQLITE_OK; | |
| 8405 | -} | |
| 8406 | - | |
| 8407 | -int vec0Update_Delete_DeleteRowids(vec0_vtab *p, i64 rowid) { | |
| 8408 | - int rc; | |
| 8409 | - sqlite3_stmt *stmt = NULL; | |
| 8410 | - | |
| 8411 | - char *zSql = | |
| 8412 | - sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_ROWIDS_NAME " WHERE rowid = ?", | |
| 8413 | - p->schemaName, p->tableName); | |
| 8414 | - if (!zSql) { | |
| 8415 | - return SQLITE_NOMEM; | |
| 8416 | - } | |
| 8417 | - | |
| 8418 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 8419 | - sqlite3_free(zSql); | |
| 8420 | - if (rc != SQLITE_OK) { | |
| 8421 | - goto cleanup; | |
| 8422 | - } | |
| 8423 | - sqlite3_bind_int64(stmt, 1, rowid); | |
| 8424 | - rc = sqlite3_step(stmt); | |
| 8425 | - if (rc != SQLITE_DONE) { | |
| 8426 | - goto cleanup; | |
| 8427 | - } | |
| 8428 | - rc = SQLITE_OK; | |
| 8429 | - | |
| 8430 | -cleanup: | |
| 8431 | - sqlite3_finalize(stmt); | |
| 8432 | - return rc; | |
| 8433 | -} | |
| 8434 | - | |
| 8435 | -int vec0Update_Delete_DeleteAux(vec0_vtab *p, i64 rowid) { | |
| 8436 | - int rc; | |
| 8437 | - sqlite3_stmt *stmt = NULL; | |
| 8438 | - | |
| 8439 | - char *zSql = | |
| 8440 | - sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_AUXILIARY_NAME " WHERE rowid = ?", | |
| 8441 | - p->schemaName, p->tableName); | |
| 8442 | - if (!zSql) { | |
| 8443 | - return SQLITE_NOMEM; | |
| 8444 | - } | |
| 8445 | - | |
| 8446 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 8447 | - sqlite3_free(zSql); | |
| 8448 | - if (rc != SQLITE_OK) { | |
| 8449 | - goto cleanup; | |
| 8450 | - } | |
| 8451 | - sqlite3_bind_int64(stmt, 1, rowid); | |
| 8452 | - rc = sqlite3_step(stmt); | |
| 8453 | - if (rc != SQLITE_DONE) { | |
| 8454 | - goto cleanup; | |
| 8455 | - } | |
| 8456 | - rc = SQLITE_OK; | |
| 8457 | - | |
| 8458 | -cleanup: | |
| 8459 | - sqlite3_finalize(stmt); | |
| 8460 | - return rc; | |
| 8461 | -} | |
| 8462 | - | |
| 8463 | -int vec0Update_Delete_ClearMetadata(vec0_vtab *p, int metadata_idx, i64 rowid, i64 chunk_id, | |
| 8464 | - u64 chunk_offset) { | |
| 8465 | - int rc; | |
| 8466 | - sqlite3_blob * blobValue; | |
| 8467 | - vec0_metadata_column_kind kind = p->metadata_columns[metadata_idx].kind; | |
| 8468 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowMetadataChunksNames[metadata_idx], "data", chunk_id, 1, &blobValue); | |
| 8469 | - if(rc != SQLITE_OK) { | |
| 8470 | - return rc; | |
| 8471 | - } | |
| 8472 | - | |
| 8473 | - switch(kind) { | |
| 8474 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | |
| 8475 | - u8 block; | |
| 8476 | - rc = sqlite3_blob_read(blobValue, &block, sizeof(u8), (int) (chunk_offset / CHAR_BIT)); | |
| 8477 | - if(rc != SQLITE_OK) { | |
| 8478 | - goto done; | |
| 8479 | - } | |
| 8480 | - | |
| 8481 | - block &= ~(1 << (chunk_offset % CHAR_BIT)); | |
| 8482 | - rc = sqlite3_blob_write(blobValue, &block, sizeof(u8), chunk_offset / CHAR_BIT); | |
| 8483 | - break; | |
| 8484 | - } | |
| 8485 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | |
| 8486 | - i64 v = 0; | |
| 8487 | - rc = sqlite3_blob_write(blobValue, &v, sizeof(v), chunk_offset * sizeof(i64)); | |
| 8488 | - break; | |
| 8489 | - } | |
| 8490 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | |
| 8491 | - double v = 0; | |
| 8492 | - rc = sqlite3_blob_write(blobValue, &v, sizeof(v), chunk_offset * sizeof(double)); | |
| 8493 | - break; | |
| 8494 | - } | |
| 8495 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | |
| 8496 | - int n; | |
| 8497 | - rc = sqlite3_blob_read(blobValue, &n, sizeof(int), chunk_offset * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | |
| 8498 | - if(rc != SQLITE_OK) { | |
| 8499 | - goto done; | |
| 8500 | - } | |
| 8501 | - | |
| 8502 | - u8 view[VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | |
| 8503 | - memset(view, 0, VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | |
| 8504 | - rc = sqlite3_blob_write(blobValue, &view, sizeof(view), chunk_offset * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | |
| 8505 | - if(rc != SQLITE_OK) { | |
| 8506 | - goto done; | |
| 8507 | - } | |
| 8508 | - | |
| 8509 | - if(n > VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | |
| 8510 | - const char * zSql = sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " WHERE rowid = ?", p->schemaName, p->tableName, metadata_idx); | |
| 8511 | - if(!zSql) { | |
| 8512 | - rc = SQLITE_NOMEM; | |
| 8513 | - goto done; | |
| 8514 | - } | |
| 8515 | - sqlite3_stmt * stmt; | |
| 8516 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 8517 | - if(rc != SQLITE_OK) { | |
| 8518 | - goto done; | |
| 8519 | - } | |
| 8520 | - sqlite3_bind_int64(stmt, 1, rowid); | |
| 8521 | - rc = sqlite3_step(stmt); | |
| 8522 | - if(rc != SQLITE_DONE) { | |
| 8523 | - rc = SQLITE_ERROR; | |
| 8524 | - goto done; | |
| 8525 | - } | |
| 8526 | - sqlite3_finalize(stmt); | |
| 8527 | - } | |
| 8528 | - break; | |
| 8529 | - } | |
| 8530 | - } | |
| 8531 | - int rc2; | |
| 8532 | - done: | |
| 8533 | - rc2 = sqlite3_blob_close(blobValue); | |
| 8534 | - if(rc == SQLITE_OK) { | |
| 8535 | - return rc2; | |
| 8536 | - } | |
| 8537 | - return rc; | |
| 8538 | -} | |
| 8539 | - | |
| 8540 | -int vec0Update_Delete(sqlite3_vtab *pVTab, sqlite3_value *idValue) { | |
| 8541 | - vec0_vtab *p = (vec0_vtab *)pVTab; | |
| 8542 | - int rc; | |
| 8543 | - i64 rowid; | |
| 8544 | - i64 chunk_id; | |
| 8545 | - i64 chunk_offset; | |
| 8546 | - | |
| 8547 | - if (p->pkIsText) { | |
| 8548 | - rc = vec0_rowid_from_id(p, idValue, &rowid); | |
| 8549 | - if (rc != SQLITE_OK) { | |
| 8550 | - return rc; | |
| 8551 | - } | |
| 8552 | - } else { | |
| 8553 | - rowid = sqlite3_value_int64(idValue); | |
| 8554 | - } | |
| 8555 | - | |
| 8556 | - // 1. Find chunk position for given rowid | |
| 8557 | - // 2. Ensure that validity bit for position is 1, then set to 0 | |
| 8558 | - // 3. Zero out rowid in chunks.rowid | |
| 8559 | - // 4. Zero out vector data in all vector column chunks | |
| 8560 | - // 5. Delete value in _rowids table | |
| 8561 | - | |
| 8562 | - // 1. get chunk_id and chunk_offset from _rowids | |
| 8563 | - rc = vec0_get_chunk_position(p, rowid, NULL, &chunk_id, &chunk_offset); | |
| 8564 | - if (rc != SQLITE_OK) { | |
| 8565 | - return rc; | |
| 8566 | - } | |
| 8567 | - | |
| 8568 | - rc = vec0Update_Delete_ClearValidity(p, chunk_id, chunk_offset); | |
| 8569 | - if (rc != SQLITE_OK) { | |
| 8570 | - return rc; | |
| 8571 | - } | |
| 8572 | - | |
| 8573 | - // 3. zero out rowid in chunks.rowids | |
| 8574 | - // https://github.com/asg017/sqlite-vec/issues/54 | |
| 8575 | - | |
| 8576 | - // 4. zero out any data in vector chunks tables | |
| 8577 | - // https://github.com/asg017/sqlite-vec/issues/54 | |
| 8578 | - | |
| 8579 | - // 5. delete from _rowids table | |
| 8580 | - rc = vec0Update_Delete_DeleteRowids(p, rowid); | |
| 8581 | - if (rc != SQLITE_OK) { | |
| 8582 | - return rc; | |
| 8583 | - } | |
| 8584 | - | |
| 8585 | - // 6. delete any auxiliary rows | |
| 8586 | - if(p->numAuxiliaryColumns > 0) { | |
| 8587 | - rc = vec0Update_Delete_DeleteAux(p, rowid); | |
| 8588 | - if (rc != SQLITE_OK) { | |
| 8589 | - return rc; | |
| 8590 | - } | |
| 8591 | - } | |
| 8592 | - | |
| 8593 | - // 6. delete metadata | |
| 8594 | - for(int i = 0; i < p->numMetadataColumns; i++) { | |
| 8595 | - rc = vec0Update_Delete_ClearMetadata(p, i, rowid, chunk_id, chunk_offset); | |
| 8596 | - } | |
| 8597 | - | |
| 8598 | - return SQLITE_OK; | |
| 8599 | -} | |
| 8600 | - | |
| 8601 | -int vec0Update_UpdateAuxColumn(vec0_vtab *p, int auxiliary_column_idx, sqlite3_value * value, i64 rowid) { | |
| 8602 | - int rc; | |
| 8603 | - sqlite3_stmt *stmt; | |
| 8604 | - const char * zSql = sqlite3_mprintf("UPDATE " VEC0_SHADOW_AUXILIARY_NAME " SET value%02d = ? WHERE rowid = ?", p->schemaName, p->tableName, auxiliary_column_idx); | |
| 8605 | - if(!zSql) { | |
| 8606 | - return SQLITE_NOMEM; | |
| 8607 | - } | |
| 8608 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | |
| 8609 | - if(rc != SQLITE_OK) { | |
| 8610 | - return rc; | |
| 8611 | - } | |
| 8612 | - sqlite3_bind_value(stmt, 1, value); | |
| 8613 | - sqlite3_bind_int64(stmt, 2, rowid); | |
| 8614 | - rc = sqlite3_step(stmt); | |
| 8615 | - if(rc != SQLITE_DONE) { | |
| 8616 | - sqlite3_finalize(stmt); | |
| 8617 | - return SQLITE_ERROR; | |
| 8618 | - } | |
| 8619 | - sqlite3_finalize(stmt); | |
| 8620 | - return SQLITE_OK; | |
| 8621 | -} | |
| 8622 | - | |
| 8623 | -int vec0Update_UpdateVectorColumn(vec0_vtab *p, i64 chunk_id, i64 chunk_offset, | |
| 8624 | - int i, sqlite3_value *valueVector) { | |
| 8625 | - int rc; | |
| 8626 | - | |
| 8627 | - sqlite3_blob *blobVectors = NULL; | |
| 8628 | - | |
| 8629 | - char *pzError; | |
| 8630 | - size_t dimensions; | |
| 8631 | - enum VectorElementType elementType; | |
| 8632 | - void *vector; | |
| 8633 | - vector_cleanup cleanup = vector_cleanup_noop; | |
| 8634 | - // https://github.com/asg017/sqlite-vec/issues/53 | |
| 8635 | - rc = vector_from_value(valueVector, &vector, &dimensions, &elementType, | |
| 8636 | - &cleanup, &pzError); | |
| 8637 | - if (rc != SQLITE_OK) { | |
| 8638 | - // IMP: V15203_32042 | |
| 8639 | - vtab_set_error( | |
| 8640 | - &p->base, "Updated vector for the \"%.*s\" column is invalid: %z", | |
| 8641 | - p->vector_columns[i].name_length, p->vector_columns[i].name, pzError); | |
| 8642 | - rc = SQLITE_ERROR; | |
| 8643 | - goto cleanup; | |
| 8644 | - } | |
| 8645 | - if (elementType != p->vector_columns[i].element_type) { | |
| 8646 | - // IMP: V03643_20481 | |
| 8647 | - vtab_set_error( | |
| 8648 | - &p->base, | |
| 8649 | - "Updated vector for the \"%.*s\" column is expected to be of type " | |
| 8650 | - "%s, but a %s vector was provided.", | |
| 8651 | - p->vector_columns[i].name_length, p->vector_columns[i].name, | |
| 8652 | - vector_subtype_name(p->vector_columns[i].element_type), | |
| 8653 | - vector_subtype_name(elementType)); | |
| 8654 | - rc = SQLITE_ERROR; | |
| 8655 | - goto cleanup; | |
| 8656 | - } | |
| 8657 | - if (dimensions != p->vector_columns[i].dimensions) { | |
| 8658 | - // IMP: V25739_09810 | |
| 8659 | - vtab_set_error( | |
| 8660 | - &p->base, | |
| 8661 | - "Dimension mismatch for new updated vector for the \"%.*s\" column. " | |
| 8662 | - "Expected %d dimensions but received %d.", | |
| 8663 | - p->vector_columns[i].name_length, p->vector_columns[i].name, | |
| 8664 | - p->vector_columns[i].dimensions, dimensions); | |
| 8665 | - rc = SQLITE_ERROR; | |
| 8666 | - goto cleanup; | |
| 8667 | - } | |
| 8668 | - | |
| 8669 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowVectorChunksNames[i], | |
| 8670 | - "vectors", chunk_id, 1, &blobVectors); | |
| 8671 | - if (rc != SQLITE_OK) { | |
| 8672 | - vtab_set_error(&p->base, "Could not open vectors blob for %s.%s.%lld", | |
| 8673 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_id); | |
| 8674 | - goto cleanup; | |
| 8675 | - } | |
| 8676 | - rc = vec0_write_vector_to_vector_blob(blobVectors, chunk_offset, vector, | |
| 8677 | - p->vector_columns[i].dimensions, | |
| 8678 | - p->vector_columns[i].element_type); | |
| 8679 | - if (rc != SQLITE_OK) { | |
| 8680 | - vtab_set_error(&p->base, "Could not write to vectors blob for %s.%s.%lld", | |
| 8681 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_id); | |
| 8682 | - goto cleanup; | |
| 8683 | - } | |
| 8684 | - | |
| 8685 | -cleanup: | |
| 8686 | - cleanup(vector); | |
| 8687 | - int brc = sqlite3_blob_close(blobVectors); | |
| 8688 | - if (rc != SQLITE_OK) { | |
| 8689 | - return rc; | |
| 8690 | - } | |
| 8691 | - if (brc != SQLITE_OK) { | |
| 8692 | - vtab_set_error( | |
| 8693 | - &p->base, | |
| 8694 | - "Could not commit blob transaction for vectors blob for %s.%s.%lld", | |
| 8695 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_id); | |
| 8696 | - return brc; | |
| 8697 | - } | |
| 8698 | - return SQLITE_OK; | |
| 8699 | -} | |
| 8700 | - | |
| 8701 | -int vec0Update_Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv) { | |
| 8702 | - UNUSED_PARAMETER(argc); | |
| 8703 | - vec0_vtab *p = (vec0_vtab *)pVTab; | |
| 8704 | - int rc; | |
| 8705 | - i64 chunk_id; | |
| 8706 | - i64 chunk_offset; | |
| 8707 | - | |
| 8708 | - i64 rowid; | |
| 8709 | - if (p->pkIsText) { | |
| 8710 | - const char *a = (const char *)sqlite3_value_text(argv[0]); | |
| 8711 | - const char *b = (const char *)sqlite3_value_text(argv[1]); | |
| 8712 | - // IMP: V08886_25725 | |
| 8713 | - if ((sqlite3_value_bytes(argv[0]) != sqlite3_value_bytes(argv[1])) || | |
| 8714 | - strncmp(a, b, sqlite3_value_bytes(argv[0])) != 0) { | |
| 8715 | - vtab_set_error(pVTab, | |
| 8716 | - "UPDATEs on vec0 primary key values are not allowed."); | |
| 8717 | - return SQLITE_ERROR; | |
| 8718 | - } | |
| 8719 | - rc = vec0_rowid_from_id(p, argv[0], &rowid); | |
| 8720 | - if (rc != SQLITE_OK) { | |
| 8721 | - return rc; | |
| 8722 | - } | |
| 8723 | - } else { | |
| 8724 | - rowid = sqlite3_value_int64(argv[0]); | |
| 8725 | - } | |
| 8726 | - | |
| 8727 | - // 1) get chunk_id and chunk_offset from _rowids | |
| 8728 | - rc = vec0_get_chunk_position(p, rowid, NULL, &chunk_id, &chunk_offset); | |
| 8729 | - if (rc != SQLITE_OK) { | |
| 8730 | - return rc; | |
| 8731 | - } | |
| 8732 | - | |
| 8733 | - // 2) update any partition key values | |
| 8734 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | |
| 8735 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_PARTITION) { | |
| 8736 | - continue; | |
| 8737 | - } | |
| 8738 | - sqlite3_value * value = argv[2+VEC0_COLUMN_USERN_START + i]; | |
| 8739 | - if(sqlite3_value_nochange(value)) { | |
| 8740 | - continue; | |
| 8741 | - } | |
| 8742 | - vtab_set_error(pVTab, "UPDATE on partition key columns are not supported yet. "); | |
| 8743 | - return SQLITE_ERROR; | |
| 8744 | - } | |
| 8745 | - | |
| 8746 | - // 3) handle auxiliary column updates | |
| 8747 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | |
| 8748 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY) { | |
| 8749 | - continue; | |
| 8750 | - } | |
| 8751 | - int auxiliary_column_idx = p->user_column_idxs[i]; | |
| 8752 | - sqlite3_value * value = argv[2+VEC0_COLUMN_USERN_START + i]; | |
| 8753 | - if(sqlite3_value_nochange(value)) { | |
| 8754 | - continue; | |
| 8755 | - } | |
| 8756 | - rc = vec0Update_UpdateAuxColumn(p, auxiliary_column_idx, value, rowid); | |
| 8757 | - if(rc != SQLITE_OK) { | |
| 8758 | - return SQLITE_ERROR; | |
| 8759 | - } | |
| 8760 | - } | |
| 8761 | - | |
| 8762 | - // 4) handle metadata column updates | |
| 8763 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | |
| 8764 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_METADATA) { | |
| 8765 | - continue; | |
| 8766 | - } | |
| 8767 | - int metadata_column_idx = p->user_column_idxs[i]; | |
| 8768 | - sqlite3_value * value = argv[2+VEC0_COLUMN_USERN_START + i]; | |
| 8769 | - if(sqlite3_value_nochange(value)) { | |
| 8770 | - continue; | |
| 8771 | - } | |
| 8772 | - rc = vec0_write_metadata_value(p, metadata_column_idx, rowid, chunk_id, chunk_offset, value, 1); | |
| 8773 | - if(rc != SQLITE_OK) { | |
| 8774 | - return rc; | |
| 8775 | - } | |
| 8776 | - } | |
| 8777 | - | |
| 8778 | - // 5) iterate over all new vectors, update the vectors | |
| 8779 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | |
| 8780 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_VECTOR) { | |
| 8781 | - continue; | |
| 8782 | - } | |
| 8783 | - int vector_idx = p->user_column_idxs[i]; | |
| 8784 | - sqlite3_value *valueVector = argv[2 + VEC0_COLUMN_USERN_START + i]; | |
| 8785 | - // in vec0Column, we check sqlite3_vtab_nochange() on vector columns. | |
| 8786 | - // If the vector column isn't being changed, we return NULL; | |
| 8787 | - // That's not great, that means vector columns can never be NULLABLE | |
| 8788 | - // (bc we cant distinguish if an updated vector is truly NULL or nochange). | |
| 8789 | - // Also it means that if someone tries to run `UPDATE v SET X = NULL`, | |
| 8790 | - // we can't effectively detect and raise an error. | |
| 8791 | - // A better solution would be to use a custom result_type for "empty", | |
| 8792 | - // but subtypes don't appear to survive xColumn -> xUpdate, it's always 0. | |
| 8793 | - // So for now, we'll just use NULL and warn people to not SET X = NULL | |
| 8794 | - // in the docs. | |
| 8795 | - if (sqlite3_value_type(valueVector) == SQLITE_NULL) { | |
| 8796 | - continue; | |
| 8797 | - } | |
| 8798 | - | |
| 8799 | - rc = vec0Update_UpdateVectorColumn(p, chunk_id, chunk_offset, vector_idx, | |
| 8800 | - valueVector); | |
| 8801 | - if (rc != SQLITE_OK) { | |
| 8802 | - return SQLITE_ERROR; | |
| 8803 | - } | |
| 8804 | - } | |
| 8805 | - | |
| 8806 | - return SQLITE_OK; | |
| 8807 | -} | |
| 8808 | - | |
| 8809 | -static int vec0Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv, | |
| 8810 | - sqlite_int64 *pRowid) { | |
| 8811 | - // DELETE operation | |
| 8812 | - if (argc == 1 && sqlite3_value_type(argv[0]) != SQLITE_NULL) { | |
| 8813 | - return vec0Update_Delete(pVTab, argv[0]); | |
| 8814 | - } | |
| 8815 | - // INSERT operation | |
| 8816 | - else if (argc > 1 && sqlite3_value_type(argv[0]) == SQLITE_NULL) { | |
| 8817 | - return vec0Update_Insert(pVTab, argc, argv, pRowid); | |
| 8818 | - } | |
| 8819 | - // UPDATE operation | |
| 8820 | - else if (argc > 1 && sqlite3_value_type(argv[0]) != SQLITE_NULL) { | |
| 8821 | - return vec0Update_Update(pVTab, argc, argv); | |
| 8822 | - } else { | |
| 8823 | - vtab_set_error(pVTab, "Unrecognized xUpdate operation provided for vec0."); | |
| 8824 | - return SQLITE_ERROR; | |
| 8825 | - } | |
| 8826 | -} | |
| 8827 | - | |
| 8828 | -static int vec0ShadowName(const char *zName) { | |
| 8829 | - static const char *azName[] = { | |
| 8830 | - "rowids", "chunks", "auxiliary", "info", | |
| 8831 | - | |
| 8832 | - // Up to VEC0_MAX_METADATA_COLUMNS | |
| 8833 | - // TODO be smarter about this man | |
| 8834 | - "metadatachunks00", | |
| 8835 | - "metadatachunks01", | |
| 8836 | - "metadatachunks02", | |
| 8837 | - "metadatachunks03", | |
| 8838 | - "metadatachunks04", | |
| 8839 | - "metadatachunks05", | |
| 8840 | - "metadatachunks06", | |
| 8841 | - "metadatachunks07", | |
| 8842 | - "metadatachunks08", | |
| 8843 | - "metadatachunks09", | |
| 8844 | - "metadatachunks10", | |
| 8845 | - "metadatachunks11", | |
| 8846 | - "metadatachunks12", | |
| 8847 | - "metadatachunks13", | |
| 8848 | - "metadatachunks14", | |
| 8849 | - "metadatachunks15", | |
| 8850 | - | |
| 8851 | - // Up to | |
| 8852 | - "metadatatext00", | |
| 8853 | - "metadatatext01", | |
| 8854 | - "metadatatext02", | |
| 8855 | - "metadatatext03", | |
| 8856 | - "metadatatext04", | |
| 8857 | - "metadatatext05", | |
| 8858 | - "metadatatext06", | |
| 8859 | - "metadatatext07", | |
| 8860 | - "metadatatext08", | |
| 8861 | - "metadatatext09", | |
| 8862 | - "metadatatext10", | |
| 8863 | - "metadatatext11", | |
| 8864 | - "metadatatext12", | |
| 8865 | - "metadatatext13", | |
| 8866 | - "metadatatext14", | |
| 8867 | - "metadatatext15", | |
| 8868 | - }; | |
| 8869 | - | |
| 8870 | - for (size_t i = 0; i < sizeof(azName) / sizeof(azName[0]); i++) { | |
| 8871 | - if (sqlite3_stricmp(zName, azName[i]) == 0) | |
| 8872 | - return 1; | |
| 8873 | - } | |
| 8874 | - //for(size_t i = 0; i < )"vector_chunks", "metadatachunks" | |
| 8875 | - return 0; | |
| 8876 | -} | |
| 8877 | - | |
| 8878 | -static int vec0Begin(sqlite3_vtab *pVTab) { | |
| 8879 | - UNUSED_PARAMETER(pVTab); | |
| 8880 | - return SQLITE_OK; | |
| 8881 | -} | |
| 8882 | -static int vec0Sync(sqlite3_vtab *pVTab) { | |
| 8883 | - UNUSED_PARAMETER(pVTab); | |
| 8884 | - vec0_vtab *p = (vec0_vtab *)pVTab; | |
| 8885 | - if (p->stmtLatestChunk) { | |
| 8886 | - sqlite3_finalize(p->stmtLatestChunk); | |
| 8887 | - p->stmtLatestChunk = NULL; | |
| 8888 | - } | |
| 8889 | - if (p->stmtRowidsInsertRowid) { | |
| 8890 | - sqlite3_finalize(p->stmtRowidsInsertRowid); | |
| 8891 | - p->stmtRowidsInsertRowid = NULL; | |
| 8892 | - } | |
| 8893 | - if (p->stmtRowidsInsertId) { | |
| 8894 | - sqlite3_finalize(p->stmtRowidsInsertId); | |
| 8895 | - p->stmtRowidsInsertId = NULL; | |
| 8896 | - } | |
| 8897 | - if (p->stmtRowidsUpdatePosition) { | |
| 8898 | - sqlite3_finalize(p->stmtRowidsUpdatePosition); | |
| 8899 | - p->stmtRowidsUpdatePosition = NULL; | |
| 8900 | - } | |
| 8901 | - if (p->stmtRowidsGetChunkPosition) { | |
| 8902 | - sqlite3_finalize(p->stmtRowidsGetChunkPosition); | |
| 8903 | - p->stmtRowidsGetChunkPosition = NULL; | |
| 8904 | - } | |
| 8905 | - return SQLITE_OK; | |
| 8906 | -} | |
| 8907 | -static int vec0Commit(sqlite3_vtab *pVTab) { | |
| 8908 | - UNUSED_PARAMETER(pVTab); | |
| 8909 | - return SQLITE_OK; | |
| 8910 | -} | |
| 8911 | -static int vec0Rollback(sqlite3_vtab *pVTab) { | |
| 8912 | - UNUSED_PARAMETER(pVTab); | |
| 8913 | - return SQLITE_OK; | |
| 8914 | -} | |
| 8915 | - | |
| 8916 | -static sqlite3_module vec0Module = { | |
| 8917 | - /* iVersion */ 3, | |
| 8918 | - /* xCreate */ vec0Create, | |
| 8919 | - /* xConnect */ vec0Connect, | |
| 8920 | - /* xBestIndex */ vec0BestIndex, | |
| 8921 | - /* xDisconnect */ vec0Disconnect, | |
| 8922 | - /* xDestroy */ vec0Destroy, | |
| 8923 | - /* xOpen */ vec0Open, | |
| 8924 | - /* xClose */ vec0Close, | |
| 8925 | - /* xFilter */ vec0Filter, | |
| 8926 | - /* xNext */ vec0Next, | |
| 8927 | - /* xEof */ vec0Eof, | |
| 8928 | - /* xColumn */ vec0Column, | |
| 8929 | - /* xRowid */ vec0Rowid, | |
| 8930 | - /* xUpdate */ vec0Update, | |
| 8931 | - /* xBegin */ vec0Begin, | |
| 8932 | - /* xSync */ vec0Sync, | |
| 8933 | - /* xCommit */ vec0Commit, | |
| 8934 | - /* xRollback */ vec0Rollback, | |
| 8935 | - /* xFindFunction */ 0, | |
| 8936 | - /* xRename */ 0, // https://github.com/asg017/sqlite-vec/issues/43 | |
| 8937 | - /* xSavepoint */ 0, | |
| 8938 | - /* xRelease */ 0, | |
| 8939 | - /* xRollbackTo */ 0, | |
| 8940 | - /* xShadowName */ vec0ShadowName, | |
| 8941 | -#if SQLITE_VERSION_NUMBER >= 3044000 | |
| 8942 | - /* xIntegrity */ 0, // https://github.com/asg017/sqlite-vec/issues/44 | |
| 8943 | -#endif | |
| 8944 | -}; | |
| 8945 | -#pragma endregion | |
| 8946 | - | |
| 8947 | -static char *POINTER_NAME_STATIC_BLOB_DEF = "vec0-static_blob_def"; | |
| 8948 | -struct static_blob_definition { | |
| 8949 | - void *p; | |
| 8950 | - size_t dimensions; | |
| 8951 | - size_t nvectors; | |
| 8952 | - enum VectorElementType element_type; | |
| 8953 | -}; | |
| 8954 | -static void vec_static_blob_from_raw(sqlite3_context *context, int argc, | |
| 8955 | - sqlite3_value **argv) { | |
| 8956 | - | |
| 8957 | - assert(argc == 4); | |
| 8958 | - struct static_blob_definition *p; | |
| 8959 | - p = sqlite3_malloc(sizeof(*p)); | |
| 8960 | - if (!p) { | |
| 8961 | - sqlite3_result_error_nomem(context); | |
| 8962 | - return; | |
| 8963 | - } | |
| 8964 | - memset(p, 0, sizeof(*p)); | |
| 8965 | - p->p = (void *)sqlite3_value_int64(argv[0]); | |
| 8966 | - p->element_type = SQLITE_VEC_ELEMENT_TYPE_FLOAT32; | |
| 8967 | - p->dimensions = sqlite3_value_int64(argv[2]); | |
| 8968 | - p->nvectors = sqlite3_value_int64(argv[3]); | |
| 8969 | - sqlite3_result_pointer(context, p, POINTER_NAME_STATIC_BLOB_DEF, | |
| 8970 | - sqlite3_free); | |
| 8971 | -} | |
| 8972 | -#pragma region vec_static_blobs() table function | |
| 8973 | - | |
| 8974 | -#define MAX_STATIC_BLOBS 16 | |
| 8975 | - | |
| 8976 | -typedef struct static_blob static_blob; | |
| 8977 | -struct static_blob { | |
| 8978 | - char *name; | |
| 8979 | - void *p; | |
| 8980 | - size_t dimensions; | |
| 8981 | - size_t nvectors; | |
| 8982 | - enum VectorElementType element_type; | |
| 8983 | -}; | |
| 8984 | - | |
| 8985 | -typedef struct vec_static_blob_data vec_static_blob_data; | |
| 8986 | -struct vec_static_blob_data { | |
| 8987 | - static_blob static_blobs[MAX_STATIC_BLOBS]; | |
| 8988 | -}; | |
| 8989 | - | |
| 8990 | -typedef struct vec_static_blobs_vtab vec_static_blobs_vtab; | |
| 8991 | -struct vec_static_blobs_vtab { | |
| 8992 | - sqlite3_vtab base; | |
| 8993 | - vec_static_blob_data *data; | |
| 8994 | -}; | |
| 8995 | - | |
| 8996 | -typedef struct vec_static_blobs_cursor vec_static_blobs_cursor; | |
| 8997 | -struct vec_static_blobs_cursor { | |
| 8998 | - sqlite3_vtab_cursor base; | |
| 8999 | - sqlite3_int64 iRowid; | |
| 9000 | -}; | |
| 9001 | - | |
| 9002 | -static int vec_static_blobsConnect(sqlite3 *db, void *pAux, int argc, | |
| 9003 | - const char *const *argv, | |
| 9004 | - sqlite3_vtab **ppVtab, char **pzErr) { | |
| 9005 | - UNUSED_PARAMETER(argc); | |
| 9006 | - UNUSED_PARAMETER(argv); | |
| 9007 | - UNUSED_PARAMETER(pzErr); | |
| 9008 | - | |
| 9009 | - vec_static_blobs_vtab *pNew; | |
| 9010 | -#define VEC_STATIC_BLOBS_NAME 0 | |
| 9011 | -#define VEC_STATIC_BLOBS_DATA 1 | |
| 9012 | -#define VEC_STATIC_BLOBS_DIMENSIONS 2 | |
| 9013 | -#define VEC_STATIC_BLOBS_COUNT 3 | |
| 9014 | - int rc = sqlite3_declare_vtab( | |
| 9015 | - db, "CREATE TABLE x(name, data, dimensions hidden, count hidden)"); | |
| 9016 | - if (rc == SQLITE_OK) { | |
| 9017 | - pNew = sqlite3_malloc(sizeof(*pNew)); | |
| 9018 | - *ppVtab = (sqlite3_vtab *)pNew; | |
| 9019 | - if (pNew == 0) | |
| 9020 | - return SQLITE_NOMEM; | |
| 9021 | - memset(pNew, 0, sizeof(*pNew)); | |
| 9022 | - pNew->data = pAux; | |
| 9023 | - } | |
| 9024 | - return rc; | |
| 9025 | -} | |
| 9026 | - | |
| 9027 | -static int vec_static_blobsDisconnect(sqlite3_vtab *pVtab) { | |
| 9028 | - vec_static_blobs_vtab *p = (vec_static_blobs_vtab *)pVtab; | |
| 9029 | - sqlite3_free(p); | |
| 9030 | - return SQLITE_OK; | |
| 9031 | -} | |
| 9032 | - | |
| 9033 | -static int vec_static_blobsUpdate(sqlite3_vtab *pVTab, int argc, | |
| 9034 | - sqlite3_value **argv, sqlite_int64 *pRowid) { | |
| 9035 | - UNUSED_PARAMETER(pRowid); | |
| 9036 | - vec_static_blobs_vtab *p = (vec_static_blobs_vtab *)pVTab; | |
| 9037 | - // DELETE operation | |
| 9038 | - if (argc == 1 && sqlite3_value_type(argv[0]) != SQLITE_NULL) { | |
| 9039 | - return SQLITE_ERROR; | |
| 9040 | - } | |
| 9041 | - // INSERT operation | |
| 9042 | - else if (argc > 1 && sqlite3_value_type(argv[0]) == SQLITE_NULL) { | |
| 9043 | - const char *key = | |
| 9044 | - (const char *)sqlite3_value_text(argv[2 + VEC_STATIC_BLOBS_NAME]); | |
| 9045 | - int idx = -1; | |
| 9046 | - for (int i = 0; i < MAX_STATIC_BLOBS; i++) { | |
| 9047 | - if (!p->data->static_blobs[i].name) { | |
| 9048 | - p->data->static_blobs[i].name = sqlite3_mprintf("%s", key); | |
| 9049 | - idx = i; | |
| 9050 | - break; | |
| 9051 | - } | |
| 9052 | - } | |
| 9053 | - if (idx < 0) | |
| 9054 | - abort(); | |
| 9055 | - struct static_blob_definition *def = sqlite3_value_pointer( | |
| 9056 | - argv[2 + VEC_STATIC_BLOBS_DATA], POINTER_NAME_STATIC_BLOB_DEF); | |
| 9057 | - p->data->static_blobs[idx].p = def->p; | |
| 9058 | - p->data->static_blobs[idx].dimensions = def->dimensions; | |
| 9059 | - p->data->static_blobs[idx].nvectors = def->nvectors; | |
| 9060 | - p->data->static_blobs[idx].element_type = def->element_type; | |
| 9061 | - | |
| 9062 | - return SQLITE_OK; | |
| 9063 | - } | |
| 9064 | - // UPDATE operation | |
| 9065 | - else if (argc > 1 && sqlite3_value_type(argv[0]) != SQLITE_NULL) { | |
| 9066 | - return SQLITE_ERROR; | |
| 9067 | - } | |
| 9068 | - return SQLITE_ERROR; | |
| 9069 | -} | |
| 9070 | - | |
| 9071 | -static int vec_static_blobsOpen(sqlite3_vtab *p, | |
| 9072 | - sqlite3_vtab_cursor **ppCursor) { | |
| 9073 | - UNUSED_PARAMETER(p); | |
| 9074 | - vec_static_blobs_cursor *pCur; | |
| 9075 | - pCur = sqlite3_malloc(sizeof(*pCur)); | |
| 9076 | - if (pCur == 0) | |
| 9077 | - return SQLITE_NOMEM; | |
| 9078 | - memset(pCur, 0, sizeof(*pCur)); | |
| 9079 | - *ppCursor = &pCur->base; | |
| 9080 | - return SQLITE_OK; | |
| 9081 | -} | |
| 9082 | - | |
| 9083 | -static int vec_static_blobsClose(sqlite3_vtab_cursor *cur) { | |
| 9084 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)cur; | |
| 9085 | - sqlite3_free(pCur); | |
| 9086 | - return SQLITE_OK; | |
| 9087 | -} | |
| 9088 | - | |
| 9089 | -static int vec_static_blobsBestIndex(sqlite3_vtab *pVTab, | |
| 9090 | - sqlite3_index_info *pIdxInfo) { | |
| 9091 | - UNUSED_PARAMETER(pVTab); | |
| 9092 | - pIdxInfo->idxNum = 1; | |
| 9093 | - pIdxInfo->estimatedCost = (double)10; | |
| 9094 | - pIdxInfo->estimatedRows = 10; | |
| 9095 | - return SQLITE_OK; | |
| 9096 | -} | |
| 9097 | - | |
| 9098 | -static int vec_static_blobsNext(sqlite3_vtab_cursor *cur); | |
| 9099 | -static int vec_static_blobsFilter(sqlite3_vtab_cursor *pVtabCursor, int idxNum, | |
| 9100 | - const char *idxStr, int argc, | |
| 9101 | - sqlite3_value **argv) { | |
| 9102 | - UNUSED_PARAMETER(idxNum); | |
| 9103 | - UNUSED_PARAMETER(idxStr); | |
| 9104 | - UNUSED_PARAMETER(argc); | |
| 9105 | - UNUSED_PARAMETER(argv); | |
| 9106 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)pVtabCursor; | |
| 9107 | - pCur->iRowid = -1; | |
| 9108 | - vec_static_blobsNext(pVtabCursor); | |
| 9109 | - return SQLITE_OK; | |
| 9110 | -} | |
| 9111 | - | |
| 9112 | -static int vec_static_blobsRowid(sqlite3_vtab_cursor *cur, | |
| 9113 | - sqlite_int64 *pRowid) { | |
| 9114 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)cur; | |
| 9115 | - *pRowid = pCur->iRowid; | |
| 9116 | - return SQLITE_OK; | |
| 9117 | -} | |
| 9118 | - | |
| 9119 | -static int vec_static_blobsNext(sqlite3_vtab_cursor *cur) { | |
| 9120 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)cur; | |
| 9121 | - vec_static_blobs_vtab *p = (vec_static_blobs_vtab *)pCur->base.pVtab; | |
| 9122 | - pCur->iRowid++; | |
| 9123 | - while (pCur->iRowid < MAX_STATIC_BLOBS) { | |
| 9124 | - if (p->data->static_blobs[pCur->iRowid].name) { | |
| 9125 | - return SQLITE_OK; | |
| 9126 | - } | |
| 9127 | - pCur->iRowid++; | |
| 9128 | - } | |
| 9129 | - return SQLITE_OK; | |
| 9130 | -} | |
| 9131 | - | |
| 9132 | -static int vec_static_blobsEof(sqlite3_vtab_cursor *cur) { | |
| 9133 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)cur; | |
| 9134 | - return pCur->iRowid >= MAX_STATIC_BLOBS; | |
| 9135 | -} | |
| 9136 | - | |
| 9137 | -static int vec_static_blobsColumn(sqlite3_vtab_cursor *cur, | |
| 9138 | - sqlite3_context *context, int i) { | |
| 9139 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)cur; | |
| 9140 | - vec_static_blobs_vtab *p = (vec_static_blobs_vtab *)cur->pVtab; | |
| 9141 | - switch (i) { | |
| 9142 | - case VEC_STATIC_BLOBS_NAME: | |
| 9143 | - sqlite3_result_text(context, p->data->static_blobs[pCur->iRowid].name, -1, | |
| 9144 | - SQLITE_TRANSIENT); | |
| 9145 | - break; | |
| 9146 | - case VEC_STATIC_BLOBS_DATA: | |
| 9147 | - sqlite3_result_null(context); | |
| 9148 | - break; | |
| 9149 | - case VEC_STATIC_BLOBS_DIMENSIONS: | |
| 9150 | - sqlite3_result_int64(context, | |
| 9151 | - p->data->static_blobs[pCur->iRowid].dimensions); | |
| 9152 | - break; | |
| 9153 | - case VEC_STATIC_BLOBS_COUNT: | |
| 9154 | - sqlite3_result_int64(context, p->data->static_blobs[pCur->iRowid].nvectors); | |
| 9155 | - break; | |
| 9156 | - } | |
| 9157 | - return SQLITE_OK; | |
| 9158 | -} | |
| 9159 | - | |
| 9160 | -static sqlite3_module vec_static_blobsModule = { | |
| 9161 | - /* iVersion */ 3, | |
| 9162 | - /* xCreate */ 0, | |
| 9163 | - /* xConnect */ vec_static_blobsConnect, | |
| 9164 | - /* xBestIndex */ vec_static_blobsBestIndex, | |
| 9165 | - /* xDisconnect */ vec_static_blobsDisconnect, | |
| 9166 | - /* xDestroy */ 0, | |
| 9167 | - /* xOpen */ vec_static_blobsOpen, | |
| 9168 | - /* xClose */ vec_static_blobsClose, | |
| 9169 | - /* xFilter */ vec_static_blobsFilter, | |
| 9170 | - /* xNext */ vec_static_blobsNext, | |
| 9171 | - /* xEof */ vec_static_blobsEof, | |
| 9172 | - /* xColumn */ vec_static_blobsColumn, | |
| 9173 | - /* xRowid */ vec_static_blobsRowid, | |
| 9174 | - /* xUpdate */ vec_static_blobsUpdate, | |
| 9175 | - /* xBegin */ 0, | |
| 9176 | - /* xSync */ 0, | |
| 9177 | - /* xCommit */ 0, | |
| 9178 | - /* xRollback */ 0, | |
| 9179 | - /* xFindMethod */ 0, | |
| 9180 | - /* xRename */ 0, | |
| 9181 | - /* xSavepoint */ 0, | |
| 9182 | - /* xRelease */ 0, | |
| 9183 | - /* xRollbackTo */ 0, | |
| 9184 | - /* xShadowName */ 0, | |
| 9185 | -#if SQLITE_VERSION_NUMBER >= 3044000 | |
| 9186 | - /* xIntegrity */ 0 | |
| 9187 | -#endif | |
| 9188 | -}; | |
| 9189 | -#pragma endregion | |
| 9190 | - | |
| 9191 | -#pragma region vec_static_blob_entries() table function | |
| 9192 | - | |
| 9193 | -typedef struct vec_static_blob_entries_vtab vec_static_blob_entries_vtab; | |
| 9194 | -struct vec_static_blob_entries_vtab { | |
| 9195 | - sqlite3_vtab base; | |
| 9196 | - static_blob *blob; | |
| 9197 | -}; | |
| 9198 | -typedef enum { | |
| 9199 | - VEC_SBE__QUERYPLAN_FULLSCAN = 1, | |
| 9200 | - VEC_SBE__QUERYPLAN_KNN = 2 | |
| 9201 | -} vec_sbe_query_plan; | |
| 9202 | - | |
| 9203 | -struct sbe_query_knn_data { | |
| 9204 | - i64 k; | |
| 9205 | - i64 k_used; | |
| 9206 | - // Array of rowids of size k. Must be freed with sqlite3_free(). | |
| 9207 | - i32 *rowids; | |
| 9208 | - // Array of distances of size k. Must be freed with sqlite3_free(). | |
| 9209 | - f32 *distances; | |
| 9210 | - i64 current_idx; | |
| 9211 | -}; | |
| 9212 | -void sbe_query_knn_data_clear(struct sbe_query_knn_data *knn_data) { | |
| 9213 | - if (!knn_data) | |
| 9214 | - return; | |
| 9215 | - | |
| 9216 | - if (knn_data->rowids) { | |
| 9217 | - sqlite3_free(knn_data->rowids); | |
| 9218 | - knn_data->rowids = NULL; | |
| 9219 | - } | |
| 9220 | - if (knn_data->distances) { | |
| 9221 | - sqlite3_free(knn_data->distances); | |
| 9222 | - knn_data->distances = NULL; | |
| 9223 | - } | |
| 9224 | -} | |
| 9225 | - | |
| 9226 | -typedef struct vec_static_blob_entries_cursor vec_static_blob_entries_cursor; | |
| 9227 | -struct vec_static_blob_entries_cursor { | |
| 9228 | - sqlite3_vtab_cursor base; | |
| 9229 | - sqlite3_int64 iRowid; | |
| 9230 | - vec_sbe_query_plan query_plan; | |
| 9231 | - struct sbe_query_knn_data *knn_data; | |
| 9232 | -}; | |
| 9233 | - | |
| 9234 | -static int vec_static_blob_entriesConnect(sqlite3 *db, void *pAux, int argc, | |
| 9235 | - const char *const *argv, | |
| 9236 | - sqlite3_vtab **ppVtab, char **pzErr) { | |
| 9237 | - UNUSED_PARAMETER(argc); | |
| 9238 | - UNUSED_PARAMETER(argv); | |
| 9239 | - UNUSED_PARAMETER(pzErr); | |
| 9240 | - vec_static_blob_data *blob_data = pAux; | |
| 9241 | - int idx = -1; | |
| 9242 | - for (int i = 0; i < MAX_STATIC_BLOBS; i++) { | |
| 9243 | - if (!blob_data->static_blobs[i].name) | |
| 9244 | - continue; | |
| 9245 | - if (strncmp(blob_data->static_blobs[i].name, argv[3], | |
| 9246 | - strlen(blob_data->static_blobs[i].name)) == 0) { | |
| 9247 | - idx = i; | |
| 9248 | - break; | |
| 9249 | - } | |
| 9250 | - } | |
| 9251 | - if (idx < 0) | |
| 9252 | - abort(); | |
| 9253 | - vec_static_blob_entries_vtab *pNew; | |
| 9254 | -#define VEC_STATIC_BLOB_ENTRIES_VECTOR 0 | |
| 9255 | -#define VEC_STATIC_BLOB_ENTRIES_DISTANCE 1 | |
| 9256 | -#define VEC_STATIC_BLOB_ENTRIES_K 2 | |
| 9257 | - int rc = sqlite3_declare_vtab( | |
| 9258 | - db, "CREATE TABLE x(vector, distance hidden, k hidden)"); | |
| 9259 | - if (rc == SQLITE_OK) { | |
| 9260 | - pNew = sqlite3_malloc(sizeof(*pNew)); | |
| 9261 | - *ppVtab = (sqlite3_vtab *)pNew; | |
| 9262 | - if (pNew == 0) | |
| 9263 | - return SQLITE_NOMEM; | |
| 9264 | - memset(pNew, 0, sizeof(*pNew)); | |
| 9265 | - pNew->blob = &blob_data->static_blobs[idx]; | |
| 9266 | - } | |
| 9267 | - return rc; | |
| 9268 | -} | |
| 9269 | - | |
| 9270 | -static int vec_static_blob_entriesCreate(sqlite3 *db, void *pAux, int argc, | |
| 9271 | - const char *const *argv, | |
| 9272 | - sqlite3_vtab **ppVtab, char **pzErr) { | |
| 9273 | - return vec_static_blob_entriesConnect(db, pAux, argc, argv, ppVtab, pzErr); | |
| 9274 | -} | |
| 9275 | - | |
| 9276 | -static int vec_static_blob_entriesDisconnect(sqlite3_vtab *pVtab) { | |
| 9277 | - vec_static_blob_entries_vtab *p = (vec_static_blob_entries_vtab *)pVtab; | |
| 9278 | - sqlite3_free(p); | |
| 9279 | - return SQLITE_OK; | |
| 9280 | -} | |
| 9281 | - | |
| 9282 | -static int vec_static_blob_entriesOpen(sqlite3_vtab *p, | |
| 9283 | - sqlite3_vtab_cursor **ppCursor) { | |
| 9284 | - UNUSED_PARAMETER(p); | |
| 9285 | - vec_static_blob_entries_cursor *pCur; | |
| 9286 | - pCur = sqlite3_malloc(sizeof(*pCur)); | |
| 9287 | - if (pCur == 0) | |
| 9288 | - return SQLITE_NOMEM; | |
| 9289 | - memset(pCur, 0, sizeof(*pCur)); | |
| 9290 | - *ppCursor = &pCur->base; | |
| 9291 | - return SQLITE_OK; | |
| 9292 | -} | |
| 9293 | - | |
| 9294 | -static int vec_static_blob_entriesClose(sqlite3_vtab_cursor *cur) { | |
| 9295 | - vec_static_blob_entries_cursor *pCur = (vec_static_blob_entries_cursor *)cur; | |
| 9296 | - sqlite3_free(pCur->knn_data); | |
| 9297 | - sqlite3_free(pCur); | |
| 9298 | - return SQLITE_OK; | |
| 9299 | -} | |
| 9300 | - | |
| 9301 | -static int vec_static_blob_entriesBestIndex(sqlite3_vtab *pVTab, | |
| 9302 | - sqlite3_index_info *pIdxInfo) { | |
| 9303 | - vec_static_blob_entries_vtab *p = (vec_static_blob_entries_vtab *)pVTab; | |
| 9304 | - int iMatchTerm = -1; | |
| 9305 | - int iLimitTerm = -1; | |
| 9306 | - // int iRowidTerm = -1; // https://github.com/asg017/sqlite-vec/issues/47 | |
| 9307 | - int iKTerm = -1; | |
| 9308 | - | |
| 9309 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | |
| 9310 | - if (!pIdxInfo->aConstraint[i].usable) | |
| 9311 | - continue; | |
| 9312 | - | |
| 9313 | - int iColumn = pIdxInfo->aConstraint[i].iColumn; | |
| 9314 | - int op = pIdxInfo->aConstraint[i].op; | |
| 9315 | - if (op == SQLITE_INDEX_CONSTRAINT_MATCH && | |
| 9316 | - iColumn == VEC_STATIC_BLOB_ENTRIES_VECTOR) { | |
| 9317 | - if (iMatchTerm > -1) { | |
| 9318 | - // https://github.com/asg017/sqlite-vec/issues/51 | |
| 9319 | - return SQLITE_ERROR; | |
| 9320 | - } | |
| 9321 | - iMatchTerm = i; | |
| 9322 | - } | |
| 9323 | - if (op == SQLITE_INDEX_CONSTRAINT_LIMIT) { | |
| 9324 | - iLimitTerm = i; | |
| 9325 | - } | |
| 9326 | - if (op == SQLITE_INDEX_CONSTRAINT_EQ && | |
| 9327 | - iColumn == VEC_STATIC_BLOB_ENTRIES_K) { | |
| 9328 | - iKTerm = i; | |
| 9329 | - } | |
| 9330 | - } | |
| 9331 | - if (iMatchTerm >= 0) { | |
| 9332 | - if (iLimitTerm < 0 && iKTerm < 0) { | |
| 9333 | - // https://github.com/asg017/sqlite-vec/issues/51 | |
| 9334 | - return SQLITE_ERROR; | |
| 9335 | - } | |
| 9336 | - if (iLimitTerm >= 0 && iKTerm >= 0) { | |
| 9337 | - return SQLITE_ERROR; // limit or k, not both | |
| 9338 | - } | |
| 9339 | - if (pIdxInfo->nOrderBy < 1) { | |
| 9340 | - vtab_set_error(pVTab, "ORDER BY distance required"); | |
| 9341 | - return SQLITE_CONSTRAINT; | |
| 9342 | - } | |
| 9343 | - if (pIdxInfo->nOrderBy > 1) { | |
| 9344 | - // https://github.com/asg017/sqlite-vec/issues/51 | |
| 9345 | - vtab_set_error(pVTab, "more than 1 ORDER BY clause provided"); | |
| 9346 | - return SQLITE_CONSTRAINT; | |
| 9347 | - } | |
| 9348 | - if (pIdxInfo->aOrderBy[0].iColumn != VEC_STATIC_BLOB_ENTRIES_DISTANCE) { | |
| 9349 | - vtab_set_error(pVTab, "ORDER BY must be on the distance column"); | |
| 9350 | - return SQLITE_CONSTRAINT; | |
| 9351 | - } | |
| 9352 | - if (pIdxInfo->aOrderBy[0].desc) { | |
| 9353 | - vtab_set_error(pVTab, | |
| 9354 | - "Only ascending in ORDER BY distance clause is supported, " | |
| 9355 | - "DESC is not supported yet."); | |
| 9356 | - return SQLITE_CONSTRAINT; | |
| 9357 | - } | |
| 9358 | - | |
| 9359 | - pIdxInfo->idxNum = VEC_SBE__QUERYPLAN_KNN; | |
| 9360 | - pIdxInfo->estimatedCost = (double)10; | |
| 9361 | - pIdxInfo->estimatedRows = 10; | |
| 9362 | - | |
| 9363 | - pIdxInfo->orderByConsumed = 1; | |
| 9364 | - pIdxInfo->aConstraintUsage[iMatchTerm].argvIndex = 1; | |
| 9365 | - pIdxInfo->aConstraintUsage[iMatchTerm].omit = 1; | |
| 9366 | - if (iLimitTerm >= 0) { | |
| 9367 | - pIdxInfo->aConstraintUsage[iLimitTerm].argvIndex = 2; | |
| 9368 | - pIdxInfo->aConstraintUsage[iLimitTerm].omit = 1; | |
| 9369 | - } else { | |
| 9370 | - pIdxInfo->aConstraintUsage[iKTerm].argvIndex = 2; | |
| 9371 | - pIdxInfo->aConstraintUsage[iKTerm].omit = 1; | |
| 9372 | - } | |
| 9373 | - | |
| 9374 | - } else { | |
| 9375 | - pIdxInfo->idxNum = VEC_SBE__QUERYPLAN_FULLSCAN; | |
| 9376 | - pIdxInfo->estimatedCost = (double)p->blob->nvectors; | |
| 9377 | - pIdxInfo->estimatedRows = p->blob->nvectors; | |
| 9378 | - } | |
| 9379 | - return SQLITE_OK; | |
| 9380 | -} | |
| 9381 | - | |
| 9382 | -static int vec_static_blob_entriesFilter(sqlite3_vtab_cursor *pVtabCursor, | |
| 9383 | - int idxNum, const char *idxStr, | |
| 9384 | - int argc, sqlite3_value **argv) { | |
| 9385 | - UNUSED_PARAMETER(idxStr); | |
| 9386 | - assert(argc >= 0 && argc <= 3); | |
| 9387 | - vec_static_blob_entries_cursor *pCur = | |
| 9388 | - (vec_static_blob_entries_cursor *)pVtabCursor; | |
| 9389 | - vec_static_blob_entries_vtab *p = | |
| 9390 | - (vec_static_blob_entries_vtab *)pCur->base.pVtab; | |
| 9391 | - | |
| 9392 | - if (idxNum == VEC_SBE__QUERYPLAN_KNN) { | |
| 9393 | - assert(argc == 2); | |
| 9394 | - pCur->query_plan = VEC_SBE__QUERYPLAN_KNN; | |
| 9395 | - struct sbe_query_knn_data *knn_data; | |
| 9396 | - knn_data = sqlite3_malloc(sizeof(*knn_data)); | |
| 9397 | - if (!knn_data) { | |
| 9398 | - return SQLITE_NOMEM; | |
| 9399 | - } | |
| 9400 | - memset(knn_data, 0, sizeof(*knn_data)); | |
| 9401 | - | |
| 9402 | - void *queryVector; | |
| 9403 | - size_t dimensions; | |
| 9404 | - enum VectorElementType elementType; | |
| 9405 | - vector_cleanup cleanup; | |
| 9406 | - char *err; | |
| 9407 | - int rc = vector_from_value(argv[0], &queryVector, &dimensions, &elementType, | |
| 9408 | - &cleanup, &err); | |
| 9409 | - if (rc != SQLITE_OK) { | |
| 9410 | - return SQLITE_ERROR; | |
| 9411 | - } | |
| 9412 | - if (elementType != p->blob->element_type) { | |
| 9413 | - return SQLITE_ERROR; | |
| 9414 | - } | |
| 9415 | - if (dimensions != p->blob->dimensions) { | |
| 9416 | - return SQLITE_ERROR; | |
| 9417 | - } | |
| 9418 | - | |
| 9419 | - i64 k = min(sqlite3_value_int64(argv[1]), (i64)p->blob->nvectors); | |
| 9420 | - if (k < 0) { | |
| 9421 | - // HANDLE https://github.com/asg017/sqlite-vec/issues/55 | |
| 9422 | - return SQLITE_ERROR; | |
| 9423 | - } | |
| 9424 | - if (k == 0) { | |
| 9425 | - knn_data->k = 0; | |
| 9426 | - pCur->knn_data = knn_data; | |
| 9427 | - return SQLITE_OK; | |
| 9428 | - } | |
| 9429 | - | |
| 9430 | - size_t bsize = (p->blob->nvectors + 7) & ~7; | |
| 9431 | - | |
| 9432 | - i32 *topk_rowids = sqlite3_malloc(k * sizeof(i32)); | |
| 9433 | - if (!topk_rowids) { | |
| 9434 | - // HANDLE https://github.com/asg017/sqlite-vec/issues/55 | |
| 9435 | - return SQLITE_ERROR; | |
| 9436 | - } | |
| 9437 | - f32 *distances = sqlite3_malloc(bsize * sizeof(f32)); | |
| 9438 | - if (!distances) { | |
| 9439 | - // HANDLE https://github.com/asg017/sqlite-vec/issues/55 | |
| 9440 | - return SQLITE_ERROR; | |
| 9441 | - } | |
| 9442 | - | |
| 9443 | - for (size_t i = 0; i < p->blob->nvectors; i++) { | |
| 9444 | - // https://github.com/asg017/sqlite-vec/issues/52 | |
| 9445 | - float *v = ((float *)p->blob->p) + (i * p->blob->dimensions); | |
| 9446 | - distances[i] = | |
| 9447 | - distance_l2_sqr_float(v, (float *)queryVector, &p->blob->dimensions); | |
| 9448 | - } | |
| 9449 | - u8 *candidates = bitmap_new(bsize); | |
| 9450 | - assert(candidates); | |
| 9451 | - | |
| 9452 | - u8 *taken = bitmap_new(bsize); | |
| 9453 | - assert(taken); | |
| 9454 | - | |
| 9455 | - bitmap_fill(candidates, bsize); | |
| 9456 | - for (size_t i = bsize; i >= p->blob->nvectors; i--) { | |
| 9457 | - bitmap_set(candidates, i, 0); | |
| 9458 | - } | |
| 9459 | - i32 k_used = 0; | |
| 9460 | - min_idx(distances, bsize, candidates, topk_rowids, k, taken, &k_used); | |
| 9461 | - knn_data->current_idx = 0; | |
| 9462 | - knn_data->distances = distances; | |
| 9463 | - knn_data->k = k; | |
| 9464 | - knn_data->rowids = topk_rowids; | |
| 9465 | - | |
| 9466 | - pCur->knn_data = knn_data; | |
| 9467 | - } else { | |
| 9468 | - pCur->query_plan = VEC_SBE__QUERYPLAN_FULLSCAN; | |
| 9469 | - pCur->iRowid = 0; | |
| 9470 | - } | |
| 9471 | - | |
| 9472 | - return SQLITE_OK; | |
| 9473 | -} | |
| 9474 | - | |
| 9475 | -static int vec_static_blob_entriesRowid(sqlite3_vtab_cursor *cur, | |
| 9476 | - sqlite_int64 *pRowid) { | |
| 9477 | - vec_static_blob_entries_cursor *pCur = (vec_static_blob_entries_cursor *)cur; | |
| 9478 | - switch (pCur->query_plan) { | |
| 9479 | - case VEC_SBE__QUERYPLAN_FULLSCAN: { | |
| 9480 | - *pRowid = pCur->iRowid; | |
| 9481 | - return SQLITE_OK; | |
| 9482 | - } | |
| 9483 | - case VEC_SBE__QUERYPLAN_KNN: { | |
| 9484 | - i32 rowid = ((i32 *)pCur->knn_data->rowids)[pCur->knn_data->current_idx]; | |
| 9485 | - *pRowid = (sqlite3_int64)rowid; | |
| 9486 | - return SQLITE_OK; | |
| 9487 | - } | |
| 9488 | - } | |
| 9489 | - return SQLITE_ERROR; | |
| 9490 | -} | |
| 9491 | - | |
| 9492 | -static int vec_static_blob_entriesNext(sqlite3_vtab_cursor *cur) { | |
| 9493 | - vec_static_blob_entries_cursor *pCur = (vec_static_blob_entries_cursor *)cur; | |
| 9494 | - switch (pCur->query_plan) { | |
| 9495 | - case VEC_SBE__QUERYPLAN_FULLSCAN: { | |
| 9496 | - pCur->iRowid++; | |
| 9497 | - return SQLITE_OK; | |
| 9498 | - } | |
| 9499 | - case VEC_SBE__QUERYPLAN_KNN: { | |
| 9500 | - pCur->knn_data->current_idx++; | |
| 9501 | - return SQLITE_OK; | |
| 9502 | - } | |
| 9503 | - } | |
| 9504 | - return SQLITE_ERROR; | |
| 9505 | -} | |
| 9506 | - | |
| 9507 | -static int vec_static_blob_entriesEof(sqlite3_vtab_cursor *cur) { | |
| 9508 | - vec_static_blob_entries_cursor *pCur = (vec_static_blob_entries_cursor *)cur; | |
| 9509 | - vec_static_blob_entries_vtab *p = | |
| 9510 | - (vec_static_blob_entries_vtab *)pCur->base.pVtab; | |
| 9511 | - switch (pCur->query_plan) { | |
| 9512 | - case VEC_SBE__QUERYPLAN_FULLSCAN: { | |
| 9513 | - return (size_t)pCur->iRowid >= p->blob->nvectors; | |
| 9514 | - } | |
| 9515 | - case VEC_SBE__QUERYPLAN_KNN: { | |
| 9516 | - return pCur->knn_data->current_idx >= pCur->knn_data->k; | |
| 9517 | - } | |
| 9518 | - } | |
| 9519 | - return SQLITE_ERROR; | |
| 9520 | -} | |
| 9521 | - | |
| 9522 | -static int vec_static_blob_entriesColumn(sqlite3_vtab_cursor *cur, | |
| 9523 | - sqlite3_context *context, int i) { | |
| 9524 | - vec_static_blob_entries_cursor *pCur = (vec_static_blob_entries_cursor *)cur; | |
| 9525 | - vec_static_blob_entries_vtab *p = (vec_static_blob_entries_vtab *)cur->pVtab; | |
| 9526 | - | |
| 9527 | - switch (pCur->query_plan) { | |
| 9528 | - case VEC_SBE__QUERYPLAN_FULLSCAN: { | |
| 9529 | - switch (i) { | |
| 9530 | - case VEC_STATIC_BLOB_ENTRIES_VECTOR: | |
| 9531 | - | |
| 9532 | - sqlite3_result_blob( | |
| 9533 | - context, | |
| 9534 | - ((unsigned char *)p->blob->p) + | |
| 9535 | - (pCur->iRowid * p->blob->dimensions * sizeof(float)), | |
| 9536 | - p->blob->dimensions * sizeof(float), SQLITE_TRANSIENT); | |
| 9537 | - sqlite3_result_subtype(context, p->blob->element_type); | |
| 9538 | - break; | |
| 9539 | - } | |
| 9540 | - return SQLITE_OK; | |
| 9541 | - } | |
| 9542 | - case VEC_SBE__QUERYPLAN_KNN: { | |
| 9543 | - switch (i) { | |
| 9544 | - case VEC_STATIC_BLOB_ENTRIES_VECTOR: { | |
| 9545 | - i32 rowid = ((i32 *)pCur->knn_data->rowids)[pCur->knn_data->current_idx]; | |
| 9546 | - sqlite3_result_blob(context, | |
| 9547 | - ((unsigned char *)p->blob->p) + | |
| 9548 | - (rowid * p->blob->dimensions * sizeof(float)), | |
| 9549 | - p->blob->dimensions * sizeof(float), | |
| 9550 | - SQLITE_TRANSIENT); | |
| 9551 | - sqlite3_result_subtype(context, p->blob->element_type); | |
| 9552 | - break; | |
| 9553 | - } | |
| 9554 | - } | |
| 9555 | - return SQLITE_OK; | |
| 9556 | - } | |
| 9557 | - } | |
| 9558 | - return SQLITE_ERROR; | |
| 9559 | -} | |
| 9560 | - | |
| 9561 | -static sqlite3_module vec_static_blob_entriesModule = { | |
| 9562 | - /* iVersion */ 3, | |
| 9563 | - /* xCreate */ | |
| 9564 | - vec_static_blob_entriesCreate, // handle rm? | |
| 9565 | - // https://github.com/asg017/sqlite-vec/issues/55 | |
| 9566 | - /* xConnect */ vec_static_blob_entriesConnect, | |
| 9567 | - /* xBestIndex */ vec_static_blob_entriesBestIndex, | |
| 9568 | - /* xDisconnect */ vec_static_blob_entriesDisconnect, | |
| 9569 | - /* xDestroy */ vec_static_blob_entriesDisconnect, | |
| 9570 | - /* xOpen */ vec_static_blob_entriesOpen, | |
| 9571 | - /* xClose */ vec_static_blob_entriesClose, | |
| 9572 | - /* xFilter */ vec_static_blob_entriesFilter, | |
| 9573 | - /* xNext */ vec_static_blob_entriesNext, | |
| 9574 | - /* xEof */ vec_static_blob_entriesEof, | |
| 9575 | - /* xColumn */ vec_static_blob_entriesColumn, | |
| 9576 | - /* xRowid */ vec_static_blob_entriesRowid, | |
| 9577 | - /* xUpdate */ 0, | |
| 9578 | - /* xBegin */ 0, | |
| 9579 | - /* xSync */ 0, | |
| 9580 | - /* xCommit */ 0, | |
| 9581 | - /* xRollback */ 0, | |
| 9582 | - /* xFindMethod */ 0, | |
| 9583 | - /* xRename */ 0, | |
| 9584 | - /* xSavepoint */ 0, | |
| 9585 | - /* xRelease */ 0, | |
| 9586 | - /* xRollbackTo */ 0, | |
| 9587 | - /* xShadowName */ 0, | |
| 9588 | -#if SQLITE_VERSION_NUMBER >= 3044000 | |
| 9589 | - /* xIntegrity */ 0 | |
| 9590 | -#endif | |
| 9591 | -}; | |
| 9592 | -#pragma endregion | |
| 9593 | - | |
| 9594 | -#ifdef SQLITE_VEC_ENABLE_AVX | |
| 9595 | -#define SQLITE_VEC_DEBUG_BUILD_AVX "avx" | |
| 9596 | -#else | |
| 9597 | -#define SQLITE_VEC_DEBUG_BUILD_AVX "" | |
| 9598 | -#endif | |
| 9599 | -#ifdef SQLITE_VEC_ENABLE_NEON | |
| 9600 | -#define SQLITE_VEC_DEBUG_BUILD_NEON "neon" | |
| 9601 | -#else | |
| 9602 | -#define SQLITE_VEC_DEBUG_BUILD_NEON "" | |
| 9603 | -#endif | |
| 9604 | - | |
| 9605 | -#define SQLITE_VEC_DEBUG_BUILD \ | |
| 9606 | - SQLITE_VEC_DEBUG_BUILD_AVX " " SQLITE_VEC_DEBUG_BUILD_NEON | |
| 9607 | - | |
| 9608 | -#define SQLITE_VEC_DEBUG_STRING \ | |
| 9609 | - "Version: " SQLITE_VEC_VERSION "\n" \ | |
| 9610 | - "Date: " SQLITE_VEC_DATE "\n" \ | |
| 9611 | - "Commit: " SQLITE_VEC_SOURCE "\n" \ | |
| 9612 | - "Build flags: " SQLITE_VEC_DEBUG_BUILD | |
| 9613 | - | |
| 9614 | -SQLITE_VEC_API int sqlite3_vec_init(sqlite3 *db, char **pzErrMsg, | |
| 9615 | - const sqlite3_api_routines *pApi) { | |
| 9616 | -#ifndef SQLITE_CORE | |
| 9617 | - SQLITE_EXTENSION_INIT2(pApi); | |
| 9618 | -#endif | |
| 9619 | - int rc = SQLITE_OK; | |
| 9620 | - | |
| 9621 | -#define DEFAULT_FLAGS (SQLITE_UTF8 | SQLITE_INNOCUOUS | SQLITE_DETERMINISTIC) | |
| 9622 | - | |
| 9623 | - rc = sqlite3_create_function_v2(db, "vec_version", 0, DEFAULT_FLAGS, | |
| 9624 | - SQLITE_VEC_VERSION, _static_text_func, NULL, | |
| 9625 | - NULL, NULL); | |
| 9626 | - if (rc != SQLITE_OK) { | |
| 9627 | - return rc; | |
| 9628 | - } | |
| 9629 | - rc = sqlite3_create_function_v2(db, "vec_debug", 0, DEFAULT_FLAGS, | |
| 9630 | - SQLITE_VEC_DEBUG_STRING, _static_text_func, | |
| 9631 | - NULL, NULL, NULL); | |
| 9632 | - if (rc != SQLITE_OK) { | |
| 9633 | - return rc; | |
| 9634 | - } | |
| 9635 | - static struct { | |
| 9636 | - const char *zFName; | |
| 9637 | - void (*xFunc)(sqlite3_context *, int, sqlite3_value **); | |
| 9638 | - int nArg; | |
| 9639 | - int flags; | |
| 9640 | - } aFunc[] = { | |
| 9641 | - // clang-format off | |
| 9642 | - //{"vec_version", _static_text_func, 0, DEFAULT_FLAGS, (void *) SQLITE_VEC_VERSION }, | |
| 9643 | - //{"vec_debug", _static_text_func, 0, DEFAULT_FLAGS, (void *) SQLITE_VEC_DEBUG_STRING }, | |
| 9644 | - {"vec_distance_l2", vec_distance_l2, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE, }, | |
| 9645 | - {"vec_distance_l1", vec_distance_l1, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE, }, | |
| 9646 | - {"vec_distance_hamming",vec_distance_hamming, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE, }, | |
| 9647 | - {"vec_distance_cosine", vec_distance_cosine, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE, }, | |
| 9648 | - {"vec_length", vec_length, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE, }, | |
| 9649 | - {"vec_type", vec_type, 1, DEFAULT_FLAGS, }, | |
| 9650 | - {"vec_to_json", vec_to_json, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | |
| 9651 | - {"vec_add", vec_add, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | |
| 9652 | - {"vec_sub", vec_sub, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | |
| 9653 | - {"vec_slice", vec_slice, 3, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | |
| 9654 | - {"vec_normalize", vec_normalize, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | |
| 9655 | - {"vec_f32", vec_f32, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | |
| 9656 | - {"vec_bit", vec_bit, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | |
| 9657 | - {"vec_int8", vec_int8, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | |
| 9658 | - {"vec_quantize_int8", vec_quantize_int8, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | |
| 9659 | - {"vec_quantize_binary", vec_quantize_binary, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | |
| 9660 | - // clang-format on | |
| 9661 | - }; | |
| 9662 | - | |
| 9663 | - static struct { | |
| 9664 | - char *name; | |
| 9665 | - const sqlite3_module *module; | |
| 9666 | - void *p; | |
| 9667 | - void (*xDestroy)(void *); | |
| 9668 | - } aMod[] = { | |
| 9669 | - // clang-format off | |
| 9670 | - {"vec0", &vec0Module, NULL, NULL}, | |
| 9671 | - {"vec_each", &vec_eachModule, NULL, NULL}, | |
| 9672 | - // clang-format on | |
| 9673 | - }; | |
| 9674 | - | |
| 9675 | - for (unsigned long i = 0; i < countof(aFunc) && rc == SQLITE_OK; i++) { | |
| 9676 | - rc = sqlite3_create_function_v2(db, aFunc[i].zFName, aFunc[i].nArg, | |
| 9677 | - aFunc[i].flags, NULL, aFunc[i].xFunc, NULL, | |
| 9678 | - NULL, NULL); | |
| 9679 | - if (rc != SQLITE_OK) { | |
| 9680 | - *pzErrMsg = sqlite3_mprintf("Error creating function %s: %s", | |
| 9681 | - aFunc[i].zFName, sqlite3_errmsg(db)); | |
| 9682 | - return rc; | |
| 9683 | - } | |
| 9684 | - } | |
| 9685 | - | |
| 9686 | - for (unsigned long i = 0; i < countof(aMod) && rc == SQLITE_OK; i++) { | |
| 9687 | - rc = sqlite3_create_module_v2(db, aMod[i].name, aMod[i].module, NULL, NULL); | |
| 9688 | - if (rc != SQLITE_OK) { | |
| 9689 | - *pzErrMsg = sqlite3_mprintf("Error creating module %s: %s", aMod[i].name, | |
| 9690 | - sqlite3_errmsg(db)); | |
| 9691 | - return rc; | |
| 9692 | - } | |
| 9693 | - } | |
| 9694 | - | |
| 9695 | - return SQLITE_OK; | |
| 9696 | -} | |
| 9697 | - | |
| 9698 | -#ifndef SQLITE_VEC_OMIT_FS | |
| 9699 | -SQLITE_VEC_API int sqlite3_vec_numpy_init(sqlite3 *db, char **pzErrMsg, | |
| 9700 | - const sqlite3_api_routines *pApi) { | |
| 9701 | - UNUSED_PARAMETER(pzErrMsg); | |
| 9702 | -#ifndef SQLITE_CORE | |
| 9703 | - SQLITE_EXTENSION_INIT2(pApi); | |
| 9704 | -#endif | |
| 9705 | - int rc = SQLITE_OK; | |
| 9706 | - rc = sqlite3_create_function_v2(db, "vec_npy_file", 1, SQLITE_RESULT_SUBTYPE, | |
| 9707 | - NULL, vec_npy_file, NULL, NULL, NULL); | |
| 9708 | - if(rc != SQLITE_OK) { | |
| 9709 | - return rc; | |
| 9710 | - } | |
| 9711 | - rc = sqlite3_create_module_v2(db, "vec_npy_each", &vec_npy_eachModule, NULL, NULL); | |
| 9712 | - return rc; | |
| 9713 | -} | |
| 9714 | -#endif | |
| 9715 | - | |
| 9716 | -SQLITE_VEC_API int | |
| 9717 | -sqlite3_vec_static_blobs_init(sqlite3 *db, char **pzErrMsg, | |
| 9718 | - const sqlite3_api_routines *pApi) { | |
| 9719 | - UNUSED_PARAMETER(pzErrMsg); | |
| 9720 | -#ifndef SQLITE_CORE | |
| 9721 | - SQLITE_EXTENSION_INIT2(pApi); | |
| 9722 | -#endif | |
| 9723 | - | |
| 9724 | - int rc = SQLITE_OK; | |
| 9725 | - vec_static_blob_data *static_blob_data; | |
| 9726 | - static_blob_data = sqlite3_malloc(sizeof(*static_blob_data)); | |
| 9727 | - if (!static_blob_data) { | |
| 9728 | - return SQLITE_NOMEM; | |
| 9729 | - } | |
| 9730 | - memset(static_blob_data, 0, sizeof(*static_blob_data)); | |
| 9731 | - | |
| 9732 | - rc = sqlite3_create_function_v2( | |
| 9733 | - db, "vec_static_blob_from_raw", 4, | |
| 9734 | - DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, NULL, | |
| 9735 | - vec_static_blob_from_raw, NULL, NULL, NULL); | |
| 9736 | - if (rc != SQLITE_OK) | |
| 9737 | - return rc; | |
| 9738 | - | |
| 9739 | - rc = sqlite3_create_module_v2(db, "vec_static_blobs", &vec_static_blobsModule, | |
| 9740 | - static_blob_data, sqlite3_free); | |
| 9741 | - if (rc != SQLITE_OK) | |
| 9742 | - return rc; | |
| 9743 | - rc = sqlite3_create_module_v2(db, "vec_static_blob_entries", | |
| 9744 | - &vec_static_blob_entriesModule, | |
| 9745 | - static_blob_data, NULL); | |
| 9746 | - if (rc != SQLITE_OK) | |
| 9747 | - return rc; | |
| 9748 | - return rc; | |
| 9749 | -} | |
| deleted file mode 100644 | |||
| @@ -1,9749 +0,0 @@ | |||
| 1 | -#include "sqlite-vec.h" | ||
| 2 | - | ||
| 3 | -#include <assert.h> | ||
| 4 | -#include <errno.h> | ||
| 5 | -#include <float.h> | ||
| 6 | -#include <inttypes.h> | ||
| 7 | -#include <limits.h> | ||
| 8 | -#include <math.h> | ||
| 9 | -#include <stdbool.h> | ||
| 10 | -#include <stdint.h> | ||
| 11 | -#include <stdlib.h> | ||
| 12 | -#include <string.h> | ||
| 13 | - | ||
| 14 | -#ifndef SQLITE_VEC_OMIT_FS | ||
| 15 | -#include <stdio.h> | ||
| 16 | -#endif | ||
| 17 | - | ||
| 18 | -#ifndef SQLITE_CORE | ||
| 19 | -#include "sqlite3ext.h" | ||
| 20 | -SQLITE_EXTENSION_INIT1 | ||
| 21 | -#else | ||
| 22 | -#include "sqlite3.h" | ||
| 23 | -#endif | ||
| 24 | - | ||
| 25 | -#ifndef UINT32_TYPE | ||
| 26 | -#ifdef HAVE_UINT32_T | ||
| 27 | -#define UINT32_TYPE uint32_t | ||
| 28 | -#else | ||
| 29 | -#define UINT32_TYPE unsigned int | ||
| 30 | -#endif | ||
| 31 | -#endif | ||
| 32 | -#ifndef UINT16_TYPE | ||
| 33 | -#ifdef HAVE_UINT16_T | ||
| 34 | -#define UINT16_TYPE uint16_t | ||
| 35 | -#else | ||
| 36 | -#define UINT16_TYPE unsigned short int | ||
| 37 | -#endif | ||
| 38 | -#endif | ||
| 39 | -#ifndef INT16_TYPE | ||
| 40 | -#ifdef HAVE_INT16_T | ||
| 41 | -#define INT16_TYPE int16_t | ||
| 42 | -#else | ||
| 43 | -#define INT16_TYPE short int | ||
| 44 | -#endif | ||
| 45 | -#endif | ||
| 46 | -#ifndef UINT8_TYPE | ||
| 47 | -#ifdef HAVE_UINT8_T | ||
| 48 | -#define UINT8_TYPE uint8_t | ||
| 49 | -#else | ||
| 50 | -#define UINT8_TYPE unsigned char | ||
| 51 | -#endif | ||
| 52 | -#endif | ||
| 53 | -#ifndef INT8_TYPE | ||
| 54 | -#ifdef HAVE_INT8_T | ||
| 55 | -#define INT8_TYPE int8_t | ||
| 56 | -#else | ||
| 57 | -#define INT8_TYPE signed char | ||
| 58 | -#endif | ||
| 59 | -#endif | ||
| 60 | -#ifndef LONGDOUBLE_TYPE | ||
| 61 | -#define LONGDOUBLE_TYPE long double | ||
| 62 | -#endif | ||
| 63 | - | ||
| 64 | -#ifndef _WIN32 | ||
| 65 | -#ifndef __EMSCRIPTEN__ | ||
| 66 | -#ifndef __COSMOPOLITAN__ | ||
| 67 | -#ifndef __wasi__ | ||
| 68 | -typedef u_int8_t uint8_t; | ||
| 69 | -typedef u_int16_t uint16_t; | ||
| 70 | -typedef u_int64_t uint64_t; | ||
| 71 | -#endif | ||
| 72 | -#endif | ||
| 73 | -#endif | ||
| 74 | -#endif | ||
| 75 | - | ||
| 76 | -typedef int8_t i8; | ||
| 77 | -typedef uint8_t u8; | ||
| 78 | -typedef int16_t i16; | ||
| 79 | -typedef int32_t i32; | ||
| 80 | -typedef sqlite3_int64 i64; | ||
| 81 | -typedef uint32_t u32; | ||
| 82 | -typedef uint64_t u64; | ||
| 83 | -typedef float f32; | ||
| 84 | -typedef size_t usize; | ||
| 85 | - | ||
| 86 | -#ifndef UNUSED_PARAMETER | ||
| 87 | -#define UNUSED_PARAMETER(X) (void)(X) | ||
| 88 | -#endif | ||
| 89 | - | ||
| 90 | -// sqlite3_vtab_in() was added in SQLite version 3.38 (2022-02-22) | ||
| 91 | -// https://www.sqlite.org/changes.html#version_3_38_0 | ||
| 92 | -#if SQLITE_VERSION_NUMBER >= 3038000 | ||
| 93 | -#define COMPILER_SUPPORTS_VTAB_IN 1 | ||
| 94 | -#endif | ||
| 95 | - | ||
| 96 | -#ifndef SQLITE_SUBTYPE | ||
| 97 | -#define SQLITE_SUBTYPE 0x000100000 | ||
| 98 | -#endif | ||
| 99 | - | ||
| 100 | -#ifndef SQLITE_RESULT_SUBTYPE | ||
| 101 | -#define SQLITE_RESULT_SUBTYPE 0x001000000 | ||
| 102 | -#endif | ||
| 103 | - | ||
| 104 | -#ifndef SQLITE_INDEX_CONSTRAINT_LIMIT | ||
| 105 | -#define SQLITE_INDEX_CONSTRAINT_LIMIT 73 | ||
| 106 | -#endif | ||
| 107 | - | ||
| 108 | -#ifndef SQLITE_INDEX_CONSTRAINT_OFFSET | ||
| 109 | -#define SQLITE_INDEX_CONSTRAINT_OFFSET 74 | ||
| 110 | -#endif | ||
| 111 | - | ||
| 112 | -#define countof(x) (sizeof(x) / sizeof((x)[0])) | ||
| 113 | -#define min(a, b) (((a) <= (b)) ? (a) : (b)) | ||
| 114 | - | ||
| 115 | -enum VectorElementType { | ||
| 116 | - // clang-format off | ||
| 117 | - SQLITE_VEC_ELEMENT_TYPE_FLOAT32 = 223 + 0, | ||
| 118 | - SQLITE_VEC_ELEMENT_TYPE_BIT = 223 + 1, | ||
| 119 | - SQLITE_VEC_ELEMENT_TYPE_INT8 = 223 + 2, | ||
| 120 | - // clang-format on | ||
| 121 | -}; | ||
| 122 | - | ||
| 123 | -#ifdef SQLITE_VEC_ENABLE_AVX | ||
| 124 | -#include <immintrin.h> | ||
| 125 | -#define PORTABLE_ALIGN32 __attribute__((aligned(32))) | ||
| 126 | -#define PORTABLE_ALIGN64 __attribute__((aligned(64))) | ||
| 127 | - | ||
| 128 | -static f32 l2_sqr_float_avx(const void *pVect1v, const void *pVect2v, | ||
| 129 | - const void *qty_ptr) { | ||
| 130 | - f32 *pVect1 = (f32 *)pVect1v; | ||
| 131 | - f32 *pVect2 = (f32 *)pVect2v; | ||
| 132 | - size_t qty = *((size_t *)qty_ptr); | ||
| 133 | - f32 PORTABLE_ALIGN32 TmpRes[8]; | ||
| 134 | - size_t qty16 = qty >> 4; | ||
| 135 | - | ||
| 136 | - const f32 *pEnd1 = pVect1 + (qty16 << 4); | ||
| 137 | - | ||
| 138 | - __m256 diff, v1, v2; | ||
| 139 | - __m256 sum = _mm256_set1_ps(0); | ||
| 140 | - | ||
| 141 | - while (pVect1 < pEnd1) { | ||
| 142 | - v1 = _mm256_loadu_ps(pVect1); | ||
| 143 | - pVect1 += 8; | ||
| 144 | - v2 = _mm256_loadu_ps(pVect2); | ||
| 145 | - pVect2 += 8; | ||
| 146 | - diff = _mm256_sub_ps(v1, v2); | ||
| 147 | - sum = _mm256_add_ps(sum, _mm256_mul_ps(diff, diff)); | ||
| 148 | - | ||
| 149 | - v1 = _mm256_loadu_ps(pVect1); | ||
| 150 | - pVect1 += 8; | ||
| 151 | - v2 = _mm256_loadu_ps(pVect2); | ||
| 152 | - pVect2 += 8; | ||
| 153 | - diff = _mm256_sub_ps(v1, v2); | ||
| 154 | - sum = _mm256_add_ps(sum, _mm256_mul_ps(diff, diff)); | ||
| 155 | - } | ||
| 156 | - | ||
| 157 | - _mm256_store_ps(TmpRes, sum); | ||
| 158 | - return sqrt(TmpRes[0] + TmpRes[1] + TmpRes[2] + TmpRes[3] + TmpRes[4] + | ||
| 159 | - TmpRes[5] + TmpRes[6] + TmpRes[7]); | ||
| 160 | -} | ||
| 161 | -#endif | ||
| 162 | - | ||
| 163 | -#ifdef SQLITE_VEC_ENABLE_NEON | ||
| 164 | -#include <arm_neon.h> | ||
| 165 | - | ||
| 166 | -#define PORTABLE_ALIGN32 __attribute__((aligned(32))) | ||
| 167 | - | ||
| 168 | -// thx https://github.com/nmslib/hnswlib/pull/299/files | ||
| 169 | -static f32 l2_sqr_float_neon(const void *pVect1v, const void *pVect2v, | ||
| 170 | - const void *qty_ptr) { | ||
| 171 | - f32 *pVect1 = (f32 *)pVect1v; | ||
| 172 | - f32 *pVect2 = (f32 *)pVect2v; | ||
| 173 | - size_t qty = *((size_t *)qty_ptr); | ||
| 174 | - size_t qty16 = qty >> 4; | ||
| 175 | - | ||
| 176 | - const f32 *pEnd1 = pVect1 + (qty16 << 4); | ||
| 177 | - | ||
| 178 | - float32x4_t diff, v1, v2; | ||
| 179 | - float32x4_t sum0 = vdupq_n_f32(0); | ||
| 180 | - float32x4_t sum1 = vdupq_n_f32(0); | ||
| 181 | - float32x4_t sum2 = vdupq_n_f32(0); | ||
| 182 | - float32x4_t sum3 = vdupq_n_f32(0); | ||
| 183 | - | ||
| 184 | - while (pVect1 < pEnd1) { | ||
| 185 | - v1 = vld1q_f32(pVect1); | ||
| 186 | - pVect1 += 4; | ||
| 187 | - v2 = vld1q_f32(pVect2); | ||
| 188 | - pVect2 += 4; | ||
| 189 | - diff = vsubq_f32(v1, v2); | ||
| 190 | - sum0 = vfmaq_f32(sum0, diff, diff); | ||
| 191 | - | ||
| 192 | - v1 = vld1q_f32(pVect1); | ||
| 193 | - pVect1 += 4; | ||
| 194 | - v2 = vld1q_f32(pVect2); | ||
| 195 | - pVect2 += 4; | ||
| 196 | - diff = vsubq_f32(v1, v2); | ||
| 197 | - sum1 = vfmaq_f32(sum1, diff, diff); | ||
| 198 | - | ||
| 199 | - v1 = vld1q_f32(pVect1); | ||
| 200 | - pVect1 += 4; | ||
| 201 | - v2 = vld1q_f32(pVect2); | ||
| 202 | - pVect2 += 4; | ||
| 203 | - diff = vsubq_f32(v1, v2); | ||
| 204 | - sum2 = vfmaq_f32(sum2, diff, diff); | ||
| 205 | - | ||
| 206 | - v1 = vld1q_f32(pVect1); | ||
| 207 | - pVect1 += 4; | ||
| 208 | - v2 = vld1q_f32(pVect2); | ||
| 209 | - pVect2 += 4; | ||
| 210 | - diff = vsubq_f32(v1, v2); | ||
| 211 | - sum3 = vfmaq_f32(sum3, diff, diff); | ||
| 212 | - } | ||
| 213 | - | ||
| 214 | - f32 sum_scalar = | ||
| 215 | - vaddvq_f32(vaddq_f32(vaddq_f32(sum0, sum1), vaddq_f32(sum2, sum3))); | ||
| 216 | - const f32 *pEnd2 = pVect1 + (qty - (qty16 << 4)); | ||
| 217 | - while (pVect1 < pEnd2) { | ||
| 218 | - f32 diff = *pVect1 - *pVect2; | ||
| 219 | - sum_scalar += diff * diff; | ||
| 220 | - pVect1++; | ||
| 221 | - pVect2++; | ||
| 222 | - } | ||
| 223 | - | ||
| 224 | - return sqrt(sum_scalar); | ||
| 225 | -} | ||
| 226 | - | ||
| 227 | -static f32 l2_sqr_int8_neon(const void *pVect1v, const void *pVect2v, | ||
| 228 | - const void *qty_ptr) { | ||
| 229 | - i8 *pVect1 = (i8 *)pVect1v; | ||
| 230 | - i8 *pVect2 = (i8 *)pVect2v; | ||
| 231 | - size_t qty = *((size_t *)qty_ptr); | ||
| 232 | - | ||
| 233 | - const i8 *pEnd1 = pVect1 + qty; | ||
| 234 | - i32 sum_scalar = 0; | ||
| 235 | - | ||
| 236 | - while (pVect1 < pEnd1 - 7) { | ||
| 237 | - // loading 8 at a time | ||
| 238 | - int8x8_t v1 = vld1_s8(pVect1); | ||
| 239 | - int8x8_t v2 = vld1_s8(pVect2); | ||
| 240 | - pVect1 += 8; | ||
| 241 | - pVect2 += 8; | ||
| 242 | - | ||
| 243 | - // widen to protect against overflow | ||
| 244 | - int16x8_t v1_wide = vmovl_s8(v1); | ||
| 245 | - int16x8_t v2_wide = vmovl_s8(v2); | ||
| 246 | - | ||
| 247 | - int16x8_t diff = vsubq_s16(v1_wide, v2_wide); | ||
| 248 | - int16x8_t squared_diff = vmulq_s16(diff, diff); | ||
| 249 | - int32x4_t sum = vpaddlq_s16(squared_diff); | ||
| 250 | - | ||
| 251 | - sum_scalar += vgetq_lane_s32(sum, 0) + vgetq_lane_s32(sum, 1) + | ||
| 252 | - vgetq_lane_s32(sum, 2) + vgetq_lane_s32(sum, 3); | ||
| 253 | - } | ||
| 254 | - | ||
| 255 | - // handle leftovers | ||
| 256 | - while (pVect1 < pEnd1) { | ||
| 257 | - i16 diff = (i16)*pVect1 - (i16)*pVect2; | ||
| 258 | - sum_scalar += diff * diff; | ||
| 259 | - pVect1++; | ||
| 260 | - pVect2++; | ||
| 261 | - } | ||
| 262 | - | ||
| 263 | - return sqrtf(sum_scalar); | ||
| 264 | -} | ||
| 265 | - | ||
| 266 | -static i32 l1_int8_neon(const void *pVect1v, const void *pVect2v, | ||
| 267 | - const void *qty_ptr) { | ||
| 268 | - i8 *pVect1 = (i8 *)pVect1v; | ||
| 269 | - i8 *pVect2 = (i8 *)pVect2v; | ||
| 270 | - size_t qty = *((size_t *)qty_ptr); | ||
| 271 | - | ||
| 272 | - const int8_t *pEnd1 = pVect1 + qty; | ||
| 273 | - | ||
| 274 | - int32x4_t acc1 = vdupq_n_s32(0); | ||
| 275 | - int32x4_t acc2 = vdupq_n_s32(0); | ||
| 276 | - int32x4_t acc3 = vdupq_n_s32(0); | ||
| 277 | - int32x4_t acc4 = vdupq_n_s32(0); | ||
| 278 | - | ||
| 279 | - while (pVect1 < pEnd1 - 63) { | ||
| 280 | - int8x16_t v1 = vld1q_s8(pVect1); | ||
| 281 | - int8x16_t v2 = vld1q_s8(pVect2); | ||
| 282 | - int8x16_t diff1 = vabdq_s8(v1, v2); | ||
| 283 | - acc1 = vaddq_s32(acc1, vpaddlq_u16(vpaddlq_u8(diff1))); | ||
| 284 | - | ||
| 285 | - v1 = vld1q_s8(pVect1 + 16); | ||
| 286 | - v2 = vld1q_s8(pVect2 + 16); | ||
| 287 | - int8x16_t diff2 = vabdq_s8(v1, v2); | ||
| 288 | - acc2 = vaddq_s32(acc2, vpaddlq_u16(vpaddlq_u8(diff2))); | ||
| 289 | - | ||
| 290 | - v1 = vld1q_s8(pVect1 + 32); | ||
| 291 | - v2 = vld1q_s8(pVect2 + 32); | ||
| 292 | - int8x16_t diff3 = vabdq_s8(v1, v2); | ||
| 293 | - acc3 = vaddq_s32(acc3, vpaddlq_u16(vpaddlq_u8(diff3))); | ||
| 294 | - | ||
| 295 | - v1 = vld1q_s8(pVect1 + 48); | ||
| 296 | - v2 = vld1q_s8(pVect2 + 48); | ||
| 297 | - int8x16_t diff4 = vabdq_s8(v1, v2); | ||
| 298 | - acc4 = vaddq_s32(acc4, vpaddlq_u16(vpaddlq_u8(diff4))); | ||
| 299 | - | ||
| 300 | - pVect1 += 64; | ||
| 301 | - pVect2 += 64; | ||
| 302 | - } | ||
| 303 | - | ||
| 304 | - while (pVect1 < pEnd1 - 15) { | ||
| 305 | - int8x16_t v1 = vld1q_s8(pVect1); | ||
| 306 | - int8x16_t v2 = vld1q_s8(pVect2); | ||
| 307 | - int8x16_t diff = vabdq_s8(v1, v2); | ||
| 308 | - acc1 = vaddq_s32(acc1, vpaddlq_u16(vpaddlq_u8(diff))); | ||
| 309 | - pVect1 += 16; | ||
| 310 | - pVect2 += 16; | ||
| 311 | - } | ||
| 312 | - | ||
| 313 | - int32x4_t acc = vaddq_s32(vaddq_s32(acc1, acc2), vaddq_s32(acc3, acc4)); | ||
| 314 | - | ||
| 315 | - int32_t sum = 0; | ||
| 316 | - while (pVect1 < pEnd1) { | ||
| 317 | - int32_t diff = abs((int32_t)*pVect1 - (int32_t)*pVect2); | ||
| 318 | - sum += diff; | ||
| 319 | - pVect1++; | ||
| 320 | - pVect2++; | ||
| 321 | - } | ||
| 322 | - | ||
| 323 | - return vaddvq_s32(acc) + sum; | ||
| 324 | -} | ||
| 325 | - | ||
| 326 | -static double l1_f32_neon(const void *pVect1v, const void *pVect2v, | ||
| 327 | - const void *qty_ptr) { | ||
| 328 | - f32 *pVect1 = (f32 *)pVect1v; | ||
| 329 | - f32 *pVect2 = (f32 *)pVect2v; | ||
| 330 | - size_t qty = *((size_t *)qty_ptr); | ||
| 331 | - | ||
| 332 | - const f32 *pEnd1 = pVect1 + qty; | ||
| 333 | - float64x2_t acc = vdupq_n_f64(0); | ||
| 334 | - | ||
| 335 | - while (pVect1 < pEnd1 - 3) { | ||
| 336 | - float32x4_t v1 = vld1q_f32(pVect1); | ||
| 337 | - float32x4_t v2 = vld1q_f32(pVect2); | ||
| 338 | - pVect1 += 4; | ||
| 339 | - pVect2 += 4; | ||
| 340 | - | ||
| 341 | - // f32x4 -> f64x2 pad for overflow | ||
| 342 | - float64x2_t low_diff = vabdq_f64(vcvt_f64_f32(vget_low_f32(v1)), | ||
| 343 | - vcvt_f64_f32(vget_low_f32(v2))); | ||
| 344 | - float64x2_t high_diff = | ||
| 345 | - vabdq_f64(vcvt_high_f64_f32(v1), vcvt_high_f64_f32(v2)); | ||
| 346 | - | ||
| 347 | - acc = vaddq_f64(acc, vaddq_f64(low_diff, high_diff)); | ||
| 348 | - } | ||
| 349 | - | ||
| 350 | - double sum = 0; | ||
| 351 | - while (pVect1 < pEnd1) { | ||
| 352 | - sum += fabs((double)*pVect1 - (double)*pVect2); | ||
| 353 | - pVect1++; | ||
| 354 | - pVect2++; | ||
| 355 | - } | ||
| 356 | - | ||
| 357 | - return vaddvq_f64(acc) + sum; | ||
| 358 | -} | ||
| 359 | -#endif | ||
| 360 | - | ||
| 361 | -static f32 l2_sqr_float(const void *pVect1v, const void *pVect2v, | ||
| 362 | - const void *qty_ptr) { | ||
| 363 | - f32 *pVect1 = (f32 *)pVect1v; | ||
| 364 | - f32 *pVect2 = (f32 *)pVect2v; | ||
| 365 | - size_t qty = *((size_t *)qty_ptr); | ||
| 366 | - | ||
| 367 | - f32 res = 0; | ||
| 368 | - for (size_t i = 0; i < qty; i++) { | ||
| 369 | - f32 t = *pVect1 - *pVect2; | ||
| 370 | - pVect1++; | ||
| 371 | - pVect2++; | ||
| 372 | - res += t * t; | ||
| 373 | - } | ||
| 374 | - return sqrt(res); | ||
| 375 | -} | ||
| 376 | - | ||
| 377 | -static f32 l2_sqr_int8(const void *pA, const void *pB, const void *pD) { | ||
| 378 | - i8 *a = (i8 *)pA; | ||
| 379 | - i8 *b = (i8 *)pB; | ||
| 380 | - size_t d = *((size_t *)pD); | ||
| 381 | - | ||
| 382 | - f32 res = 0; | ||
| 383 | - for (size_t i = 0; i < d; i++) { | ||
| 384 | - f32 t = *a - *b; | ||
| 385 | - a++; | ||
| 386 | - b++; | ||
| 387 | - res += t * t; | ||
| 388 | - } | ||
| 389 | - return sqrt(res); | ||
| 390 | -} | ||
| 391 | - | ||
| 392 | -static f32 distance_l2_sqr_float(const void *a, const void *b, const void *d) { | ||
| 393 | -#ifdef SQLITE_VEC_ENABLE_NEON | ||
| 394 | - if ((*(const size_t *)d) > 16) { | ||
| 395 | - return l2_sqr_float_neon(a, b, d); | ||
| 396 | - } | ||
| 397 | -#endif | ||
| 398 | -#ifdef SQLITE_VEC_ENABLE_AVX | ||
| 399 | - if (((*(const size_t *)d) % 16 == 0)) { | ||
| 400 | - return l2_sqr_float_avx(a, b, d); | ||
| 401 | - } | ||
| 402 | -#endif | ||
| 403 | - return l2_sqr_float(a, b, d); | ||
| 404 | -} | ||
| 405 | - | ||
| 406 | -static f32 distance_l2_sqr_int8(const void *a, const void *b, const void *d) { | ||
| 407 | -#ifdef SQLITE_VEC_ENABLE_NEON | ||
| 408 | - if ((*(const size_t *)d) > 7) { | ||
| 409 | - return l2_sqr_int8_neon(a, b, d); | ||
| 410 | - } | ||
| 411 | -#endif | ||
| 412 | - return l2_sqr_int8(a, b, d); | ||
| 413 | -} | ||
| 414 | - | ||
| 415 | -static i32 l1_int8(const void *pA, const void *pB, const void *pD) { | ||
| 416 | - i8 *a = (i8 *)pA; | ||
| 417 | - i8 *b = (i8 *)pB; | ||
| 418 | - size_t d = *((size_t *)pD); | ||
| 419 | - | ||
| 420 | - i32 res = 0; | ||
| 421 | - for (size_t i = 0; i < d; i++) { | ||
| 422 | - res += abs(*a - *b); | ||
| 423 | - a++; | ||
| 424 | - b++; | ||
| 425 | - } | ||
| 426 | - | ||
| 427 | - return res; | ||
| 428 | -} | ||
| 429 | - | ||
| 430 | -static i32 distance_l1_int8(const void *a, const void *b, const void *d) { | ||
| 431 | -#ifdef SQLITE_VEC_ENABLE_NEON | ||
| 432 | - if ((*(const size_t *)d) > 15) { | ||
| 433 | - return l1_int8_neon(a, b, d); | ||
| 434 | - } | ||
| 435 | -#endif | ||
| 436 | - return l1_int8(a, b, d); | ||
| 437 | -} | ||
| 438 | - | ||
| 439 | -static double l1_f32(const void *pA, const void *pB, const void *pD) { | ||
| 440 | - f32 *a = (f32 *)pA; | ||
| 441 | - f32 *b = (f32 *)pB; | ||
| 442 | - size_t d = *((size_t *)pD); | ||
| 443 | - | ||
| 444 | - double res = 0; | ||
| 445 | - for (size_t i = 0; i < d; i++) { | ||
| 446 | - res += fabs((double)*a - (double)*b); | ||
| 447 | - a++; | ||
| 448 | - b++; | ||
| 449 | - } | ||
| 450 | - | ||
| 451 | - return res; | ||
| 452 | -} | ||
| 453 | - | ||
| 454 | -static double distance_l1_f32(const void *a, const void *b, const void *d) { | ||
| 455 | -#ifdef SQLITE_VEC_ENABLE_NEON | ||
| 456 | - if ((*(const size_t *)d) > 3) { | ||
| 457 | - return l1_f32_neon(a, b, d); | ||
| 458 | - } | ||
| 459 | -#endif | ||
| 460 | - return l1_f32(a, b, d); | ||
| 461 | -} | ||
| 462 | - | ||
| 463 | -static f32 distance_cosine_float(const void *pVect1v, const void *pVect2v, | ||
| 464 | - const void *qty_ptr) { | ||
| 465 | - f32 *pVect1 = (f32 *)pVect1v; | ||
| 466 | - f32 *pVect2 = (f32 *)pVect2v; | ||
| 467 | - size_t qty = *((size_t *)qty_ptr); | ||
| 468 | - | ||
| 469 | - f32 dot = 0; | ||
| 470 | - f32 aMag = 0; | ||
| 471 | - f32 bMag = 0; | ||
| 472 | - for (size_t i = 0; i < qty; i++) { | ||
| 473 | - dot += *pVect1 * *pVect2; | ||
| 474 | - aMag += *pVect1 * *pVect1; | ||
| 475 | - bMag += *pVect2 * *pVect2; | ||
| 476 | - pVect1++; | ||
| 477 | - pVect2++; | ||
| 478 | - } | ||
| 479 | - return 1 - (dot / (sqrt(aMag) * sqrt(bMag))); | ||
| 480 | -} | ||
| 481 | -static f32 distance_cosine_int8(const void *pA, const void *pB, | ||
| 482 | - const void *pD) { | ||
| 483 | - i8 *a = (i8 *)pA; | ||
| 484 | - i8 *b = (i8 *)pB; | ||
| 485 | - size_t d = *((size_t *)pD); | ||
| 486 | - | ||
| 487 | - f32 dot = 0; | ||
| 488 | - f32 aMag = 0; | ||
| 489 | - f32 bMag = 0; | ||
| 490 | - for (size_t i = 0; i < d; i++) { | ||
| 491 | - dot += *a * *b; | ||
| 492 | - aMag += *a * *a; | ||
| 493 | - bMag += *b * *b; | ||
| 494 | - a++; | ||
| 495 | - b++; | ||
| 496 | - } | ||
| 497 | - return 1 - (dot / (sqrt(aMag) * sqrt(bMag))); | ||
| 498 | -} | ||
| 499 | - | ||
| 500 | -// https://github.com/facebookresearch/faiss/blob/77e2e79cd0a680adc343b9840dd865da724c579e/faiss/utils/hamming_distance/common.h#L34 | ||
| 501 | -static u8 hamdist_table[256] = { | ||
| 502 | - 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, | ||
| 503 | - 2, 3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, | ||
| 504 | - 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, | ||
| 505 | - 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, | ||
| 506 | - 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, | ||
| 507 | - 4, 5, 5, 6, 5, 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, | ||
| 508 | - 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, | ||
| 509 | - 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, | ||
| 510 | - 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, | ||
| 511 | - 4, 5, 5, 6, 5, 6, 6, 7, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, | ||
| 512 | - 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8}; | ||
| 513 | - | ||
| 514 | -static f32 distance_hamming_u8(u8 *a, u8 *b, size_t n) { | ||
| 515 | - int same = 0; | ||
| 516 | - for (unsigned long i = 0; i < n; i++) { | ||
| 517 | - same += hamdist_table[a[i] ^ b[i]]; | ||
| 518 | - } | ||
| 519 | - return (f32)same; | ||
| 520 | -} | ||
| 521 | - | ||
| 522 | -#ifdef _MSC_VER | ||
| 523 | -#if !defined(__clang__) && (defined(_M_ARM) || defined(_M_ARM64)) | ||
| 524 | -// From | ||
| 525 | -// https://github.com/ngtcp2/ngtcp2/blob/b64f1e77b5e0d880b93d31f474147fae4a1d17cc/lib/ngtcp2_ringbuf.c, | ||
| 526 | -// line 34-43 | ||
| 527 | -static unsigned int __builtin_popcountl(unsigned int x) { | ||
| 528 | - unsigned int c = 0; | ||
| 529 | - for (; x; ++c) { | ||
| 530 | - x &= x - 1; | ||
| 531 | - } | ||
| 532 | - return c; | ||
| 533 | -} | ||
| 534 | -#else | ||
| 535 | -#include <intrin.h> | ||
| 536 | -#define __builtin_popcountl __popcnt64 | ||
| 537 | -#endif | ||
| 538 | -#endif | ||
| 539 | - | ||
| 540 | -static f32 distance_hamming_u64(u64 *a, u64 *b, size_t n) { | ||
| 541 | - int same = 0; | ||
| 542 | - for (unsigned long i = 0; i < n; i++) { | ||
| 543 | - same += __builtin_popcountl(a[i] ^ b[i]); | ||
| 544 | - } | ||
| 545 | - return (f32)same; | ||
| 546 | -} | ||
| 547 | - | ||
| 548 | -/** | ||
| 549 | - * @brief Calculate the hamming distance between two bitvectors. | ||
| 550 | - * | ||
| 551 | - * @param a - first bitvector, MUST have d dimensions | ||
| 552 | - * @param b - second bitvector, MUST have d dimensions | ||
| 553 | - * @param d - pointer to size_t, MUST be divisible by CHAR_BIT | ||
| 554 | - * @return f32 | ||
| 555 | - */ | ||
| 556 | -static f32 distance_hamming(const void *a, const void *b, const void *d) { | ||
| 557 | - size_t dimensions = *((size_t *)d); | ||
| 558 | - | ||
| 559 | - if ((dimensions % 64) == 0) { | ||
| 560 | - return distance_hamming_u64((u64 *)a, (u64 *)b, dimensions / 8 / CHAR_BIT); | ||
| 561 | - } | ||
| 562 | - return distance_hamming_u8((u8 *)a, (u8 *)b, dimensions / CHAR_BIT); | ||
| 563 | -} | ||
| 564 | - | ||
| 565 | -// from SQLite source: | ||
| 566 | -// https://github.com/sqlite/sqlite/blob/a509a90958ddb234d1785ed7801880ccb18b497e/src/json.c#L153 | ||
| 567 | -static const char vecJsonIsSpaceX[] = { | ||
| 568 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 569 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 570 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 571 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 572 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 573 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 574 | - | ||
| 575 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 576 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 577 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 578 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 579 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 580 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
| 581 | -}; | ||
| 582 | - | ||
| 583 | -#define vecJsonIsspace(x) (vecJsonIsSpaceX[(unsigned char)x]) | ||
| 584 | - | ||
| 585 | -typedef void (*vector_cleanup)(void *p); | ||
| 586 | - | ||
| 587 | -void vector_cleanup_noop(void *_) { UNUSED_PARAMETER(_); } | ||
| 588 | - | ||
| 589 | -#define JSON_SUBTYPE 74 | ||
| 590 | - | ||
| 591 | -void vtab_set_error(sqlite3_vtab *pVTab, const char *zFormat, ...) { | ||
| 592 | - va_list args; | ||
| 593 | - sqlite3_free(pVTab->zErrMsg); | ||
| 594 | - va_start(args, zFormat); | ||
| 595 | - pVTab->zErrMsg = sqlite3_vmprintf(zFormat, args); | ||
| 596 | - va_end(args); | ||
| 597 | -} | ||
| 598 | -struct Array { | ||
| 599 | - size_t element_size; | ||
| 600 | - size_t length; | ||
| 601 | - size_t capacity; | ||
| 602 | - void *z; | ||
| 603 | -}; | ||
| 604 | - | ||
| 605 | -/** | ||
| 606 | - * @brief Initial an array with the given element size and capacity. | ||
| 607 | - * | ||
| 608 | - * @param array | ||
| 609 | - * @param element_size | ||
| 610 | - * @param init_capacity | ||
| 611 | - * @return SQLITE_OK on success, error code on failure. Only error is | ||
| 612 | - * SQLITE_NOMEM | ||
| 613 | - */ | ||
| 614 | -int array_init(struct Array *array, size_t element_size, size_t init_capacity) { | ||
| 615 | - int sz = element_size * init_capacity; | ||
| 616 | - void *z = sqlite3_malloc(sz); | ||
| 617 | - if (!z) { | ||
| 618 | - return SQLITE_NOMEM; | ||
| 619 | - } | ||
| 620 | - memset(z, 0, sz); | ||
| 621 | - | ||
| 622 | - array->element_size = element_size; | ||
| 623 | - array->length = 0; | ||
| 624 | - array->capacity = init_capacity; | ||
| 625 | - array->z = z; | ||
| 626 | - return SQLITE_OK; | ||
| 627 | -} | ||
| 628 | - | ||
| 629 | -int array_append(struct Array *array, const void *element) { | ||
| 630 | - if (array->length == array->capacity) { | ||
| 631 | - size_t new_capacity = array->capacity * 2 + 100; | ||
| 632 | - void *z = sqlite3_realloc64(array->z, array->element_size * new_capacity); | ||
| 633 | - if (z) { | ||
| 634 | - array->capacity = new_capacity; | ||
| 635 | - array->z = z; | ||
| 636 | - } else { | ||
| 637 | - return SQLITE_NOMEM; | ||
| 638 | - } | ||
| 639 | - } | ||
| 640 | - memcpy(&((unsigned char *)array->z)[array->length * array->element_size], | ||
| 641 | - element, array->element_size); | ||
| 642 | - array->length++; | ||
| 643 | - return SQLITE_OK; | ||
| 644 | -} | ||
| 645 | - | ||
| 646 | -void array_cleanup(struct Array *array) { | ||
| 647 | - if (!array) | ||
| 648 | - return; | ||
| 649 | - array->element_size = 0; | ||
| 650 | - array->length = 0; | ||
| 651 | - array->capacity = 0; | ||
| 652 | - sqlite3_free(array->z); | ||
| 653 | - array->z = NULL; | ||
| 654 | -} | ||
| 655 | - | ||
| 656 | -char *vector_subtype_name(int subtype) { | ||
| 657 | - switch (subtype) { | ||
| 658 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: | ||
| 659 | - return "float32"; | ||
| 660 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | ||
| 661 | - return "int8"; | ||
| 662 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: | ||
| 663 | - return "bit"; | ||
| 664 | - } | ||
| 665 | - return ""; | ||
| 666 | -} | ||
| 667 | -char *type_name(int type) { | ||
| 668 | - switch (type) { | ||
| 669 | - case SQLITE_INTEGER: | ||
| 670 | - return "INTEGER"; | ||
| 671 | - case SQLITE_BLOB: | ||
| 672 | - return "BLOB"; | ||
| 673 | - case SQLITE_TEXT: | ||
| 674 | - return "TEXT"; | ||
| 675 | - case SQLITE_FLOAT: | ||
| 676 | - return "FLOAT"; | ||
| 677 | - case SQLITE_NULL: | ||
| 678 | - return "NULL"; | ||
| 679 | - } | ||
| 680 | - return ""; | ||
| 681 | -} | ||
| 682 | - | ||
| 683 | -typedef void (*fvec_cleanup)(f32 *vector); | ||
| 684 | - | ||
| 685 | -void fvec_cleanup_noop(f32 *_) { UNUSED_PARAMETER(_); } | ||
| 686 | - | ||
| 687 | -static int fvec_from_value(sqlite3_value *value, f32 **vector, | ||
| 688 | - size_t *dimensions, fvec_cleanup *cleanup, | ||
| 689 | - char **pzErr) { | ||
| 690 | - int value_type = sqlite3_value_type(value); | ||
| 691 | - | ||
| 692 | - if (value_type == SQLITE_BLOB) { | ||
| 693 | - const void *blob = sqlite3_value_blob(value); | ||
| 694 | - int bytes = sqlite3_value_bytes(value); | ||
| 695 | - if (bytes == 0) { | ||
| 696 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | ||
| 697 | - return SQLITE_ERROR; | ||
| 698 | - } | ||
| 699 | - if ((bytes % sizeof(f32)) != 0) { | ||
| 700 | - *pzErr = sqlite3_mprintf("invalid float32 vector BLOB length. Must be " | ||
| 701 | - "divisible by %d, found %d", | ||
| 702 | - sizeof(f32), bytes); | ||
| 703 | - return SQLITE_ERROR; | ||
| 704 | - } | ||
| 705 | - *vector = (f32 *)blob; | ||
| 706 | - *dimensions = bytes / sizeof(f32); | ||
| 707 | - *cleanup = fvec_cleanup_noop; | ||
| 708 | - return SQLITE_OK; | ||
| 709 | - } | ||
| 710 | - | ||
| 711 | - if (value_type == SQLITE_TEXT) { | ||
| 712 | - const char *source = (const char *)sqlite3_value_text(value); | ||
| 713 | - int source_len = sqlite3_value_bytes(value); | ||
| 714 | - if (source_len == 0) { | ||
| 715 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | ||
| 716 | - return SQLITE_ERROR; | ||
| 717 | - } | ||
| 718 | - int i = 0; | ||
| 719 | - | ||
| 720 | - struct Array x; | ||
| 721 | - int rc = array_init(&x, sizeof(f32), ceil(source_len / 2.0)); | ||
| 722 | - if (rc != SQLITE_OK) { | ||
| 723 | - return rc; | ||
| 724 | - } | ||
| 725 | - | ||
| 726 | - // advance leading whitespace to first '[' | ||
| 727 | - while (i < source_len) { | ||
| 728 | - if (vecJsonIsspace(source[i])) { | ||
| 729 | - i++; | ||
| 730 | - continue; | ||
| 731 | - } | ||
| 732 | - if (source[i] == '[') { | ||
| 733 | - break; | ||
| 734 | - } | ||
| 735 | - | ||
| 736 | - *pzErr = sqlite3_mprintf( | ||
| 737 | - "JSON array parsing error: Input does not start with '['"); | ||
| 738 | - array_cleanup(&x); | ||
| 739 | - return SQLITE_ERROR; | ||
| 740 | - } | ||
| 741 | - if (source[i] != '[') { | ||
| 742 | - *pzErr = sqlite3_mprintf( | ||
| 743 | - "JSON array parsing error: Input does not start with '['"); | ||
| 744 | - array_cleanup(&x); | ||
| 745 | - return SQLITE_ERROR; | ||
| 746 | - } | ||
| 747 | - int offset = i + 1; | ||
| 748 | - | ||
| 749 | - while (offset < source_len) { | ||
| 750 | - char *ptr = (char *)&source[offset]; | ||
| 751 | - char *endptr; | ||
| 752 | - | ||
| 753 | - errno = 0; | ||
| 754 | - double result = strtod(ptr, &endptr); | ||
| 755 | - if ((errno != 0 && result == 0) // some interval error? | ||
| 756 | - || (errno == ERANGE && | ||
| 757 | - (result == HUGE_VAL || result == -HUGE_VAL)) // too big / smalls | ||
| 758 | - ) { | ||
| 759 | - sqlite3_free(x.z); | ||
| 760 | - *pzErr = sqlite3_mprintf("JSON parsing error"); | ||
| 761 | - return SQLITE_ERROR; | ||
| 762 | - } | ||
| 763 | - | ||
| 764 | - if (endptr == ptr) { | ||
| 765 | - if (*ptr != ']') { | ||
| 766 | - sqlite3_free(x.z); | ||
| 767 | - *pzErr = sqlite3_mprintf("JSON parsing error"); | ||
| 768 | - return SQLITE_ERROR; | ||
| 769 | - } | ||
| 770 | - goto done; | ||
| 771 | - } | ||
| 772 | - | ||
| 773 | - f32 res = (f32)result; | ||
| 774 | - array_append(&x, (const void *)&res); | ||
| 775 | - | ||
| 776 | - offset += (endptr - ptr); | ||
| 777 | - while (offset < source_len) { | ||
| 778 | - if (vecJsonIsspace(source[offset])) { | ||
| 779 | - offset++; | ||
| 780 | - continue; | ||
| 781 | - } | ||
| 782 | - if (source[offset] == ',') { | ||
| 783 | - offset++; | ||
| 784 | - continue; | ||
| 785 | - } | ||
| 786 | - if (source[offset] == ']') | ||
| 787 | - goto done; | ||
| 788 | - break; | ||
| 789 | - } | ||
| 790 | - } | ||
| 791 | - | ||
| 792 | - done: | ||
| 793 | - | ||
| 794 | - if (x.length > 0) { | ||
| 795 | - *vector = (f32 *)x.z; | ||
| 796 | - *dimensions = x.length; | ||
| 797 | - *cleanup = (fvec_cleanup)sqlite3_free; | ||
| 798 | - return SQLITE_OK; | ||
| 799 | - } | ||
| 800 | - sqlite3_free(x.z); | ||
| 801 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | ||
| 802 | - return SQLITE_ERROR; | ||
| 803 | - } | ||
| 804 | - | ||
| 805 | - *pzErr = sqlite3_mprintf( | ||
| 806 | - "Input must have type BLOB (compact format) or TEXT (JSON), found %s", | ||
| 807 | - type_name(value_type)); | ||
| 808 | - return SQLITE_ERROR; | ||
| 809 | -} | ||
| 810 | - | ||
| 811 | -static int bitvec_from_value(sqlite3_value *value, u8 **vector, | ||
| 812 | - size_t *dimensions, vector_cleanup *cleanup, | ||
| 813 | - char **pzErr) { | ||
| 814 | - int value_type = sqlite3_value_type(value); | ||
| 815 | - if (value_type == SQLITE_BLOB) { | ||
| 816 | - const void *blob = sqlite3_value_blob(value); | ||
| 817 | - int bytes = sqlite3_value_bytes(value); | ||
| 818 | - if (bytes == 0) { | ||
| 819 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | ||
| 820 | - return SQLITE_ERROR; | ||
| 821 | - } | ||
| 822 | - *vector = (u8 *)blob; | ||
| 823 | - *dimensions = bytes * CHAR_BIT; | ||
| 824 | - *cleanup = vector_cleanup_noop; | ||
| 825 | - return SQLITE_OK; | ||
| 826 | - } | ||
| 827 | - *pzErr = sqlite3_mprintf("Unknown type for bitvector."); | ||
| 828 | - return SQLITE_ERROR; | ||
| 829 | -} | ||
| 830 | - | ||
| 831 | -static int int8_vec_from_value(sqlite3_value *value, i8 **vector, | ||
| 832 | - size_t *dimensions, vector_cleanup *cleanup, | ||
| 833 | - char **pzErr) { | ||
| 834 | - int value_type = sqlite3_value_type(value); | ||
| 835 | - if (value_type == SQLITE_BLOB) { | ||
| 836 | - const void *blob = sqlite3_value_blob(value); | ||
| 837 | - int bytes = sqlite3_value_bytes(value); | ||
| 838 | - if (bytes == 0) { | ||
| 839 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | ||
| 840 | - return SQLITE_ERROR; | ||
| 841 | - } | ||
| 842 | - *vector = (i8 *)blob; | ||
| 843 | - *dimensions = bytes; | ||
| 844 | - *cleanup = vector_cleanup_noop; | ||
| 845 | - return SQLITE_OK; | ||
| 846 | - } | ||
| 847 | - | ||
| 848 | - if (value_type == SQLITE_TEXT) { | ||
| 849 | - const char *source = (const char *)sqlite3_value_text(value); | ||
| 850 | - int source_len = sqlite3_value_bytes(value); | ||
| 851 | - int i = 0; | ||
| 852 | - | ||
| 853 | - if (source_len == 0) { | ||
| 854 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | ||
| 855 | - return SQLITE_ERROR; | ||
| 856 | - } | ||
| 857 | - | ||
| 858 | - struct Array x; | ||
| 859 | - int rc = array_init(&x, sizeof(i8), ceil(source_len / 2.0)); | ||
| 860 | - if (rc != SQLITE_OK) { | ||
| 861 | - return rc; | ||
| 862 | - } | ||
| 863 | - | ||
| 864 | - // advance leading whitespace to first '[' | ||
| 865 | - while (i < source_len) { | ||
| 866 | - if (vecJsonIsspace(source[i])) { | ||
| 867 | - i++; | ||
| 868 | - continue; | ||
| 869 | - } | ||
| 870 | - if (source[i] == '[') { | ||
| 871 | - break; | ||
| 872 | - } | ||
| 873 | - | ||
| 874 | - *pzErr = sqlite3_mprintf( | ||
| 875 | - "JSON array parsing error: Input does not start with '['"); | ||
| 876 | - array_cleanup(&x); | ||
| 877 | - return SQLITE_ERROR; | ||
| 878 | - } | ||
| 879 | - if (source[i] != '[') { | ||
| 880 | - *pzErr = sqlite3_mprintf( | ||
| 881 | - "JSON array parsing error: Input does not start with '['"); | ||
| 882 | - array_cleanup(&x); | ||
| 883 | - return SQLITE_ERROR; | ||
| 884 | - } | ||
| 885 | - int offset = i + 1; | ||
| 886 | - | ||
| 887 | - while (offset < source_len) { | ||
| 888 | - char *ptr = (char *)&source[offset]; | ||
| 889 | - char *endptr; | ||
| 890 | - | ||
| 891 | - errno = 0; | ||
| 892 | - long result = strtol(ptr, &endptr, 10); | ||
| 893 | - if ((errno != 0 && result == 0) || | ||
| 894 | - (errno == ERANGE && (result == LONG_MAX || result == LONG_MIN))) { | ||
| 895 | - sqlite3_free(x.z); | ||
| 896 | - *pzErr = sqlite3_mprintf("JSON parsing error"); | ||
| 897 | - return SQLITE_ERROR; | ||
| 898 | - } | ||
| 899 | - | ||
| 900 | - if (endptr == ptr) { | ||
| 901 | - if (*ptr != ']') { | ||
| 902 | - sqlite3_free(x.z); | ||
| 903 | - *pzErr = sqlite3_mprintf("JSON parsing error"); | ||
| 904 | - return SQLITE_ERROR; | ||
| 905 | - } | ||
| 906 | - goto done; | ||
| 907 | - } | ||
| 908 | - | ||
| 909 | - if (result < INT8_MIN || result > INT8_MAX) { | ||
| 910 | - sqlite3_free(x.z); | ||
| 911 | - *pzErr = | ||
| 912 | - sqlite3_mprintf("JSON parsing error: value out of range for int8"); | ||
| 913 | - return SQLITE_ERROR; | ||
| 914 | - } | ||
| 915 | - | ||
| 916 | - i8 res = (i8)result; | ||
| 917 | - array_append(&x, (const void *)&res); | ||
| 918 | - | ||
| 919 | - offset += (endptr - ptr); | ||
| 920 | - while (offset < source_len) { | ||
| 921 | - if (vecJsonIsspace(source[offset])) { | ||
| 922 | - offset++; | ||
| 923 | - continue; | ||
| 924 | - } | ||
| 925 | - if (source[offset] == ',') { | ||
| 926 | - offset++; | ||
| 927 | - continue; | ||
| 928 | - } | ||
| 929 | - if (source[offset] == ']') | ||
| 930 | - goto done; | ||
| 931 | - break; | ||
| 932 | - } | ||
| 933 | - } | ||
| 934 | - | ||
| 935 | - done: | ||
| 936 | - | ||
| 937 | - if (x.length > 0) { | ||
| 938 | - *vector = (i8 *)x.z; | ||
| 939 | - *dimensions = x.length; | ||
| 940 | - *cleanup = (vector_cleanup)sqlite3_free; | ||
| 941 | - return SQLITE_OK; | ||
| 942 | - } | ||
| 943 | - sqlite3_free(x.z); | ||
| 944 | - *pzErr = sqlite3_mprintf("zero-length vectors are not supported."); | ||
| 945 | - return SQLITE_ERROR; | ||
| 946 | - } | ||
| 947 | - | ||
| 948 | - *pzErr = sqlite3_mprintf("Unknown type for int8 vector."); | ||
| 949 | - return SQLITE_ERROR; | ||
| 950 | -} | ||
| 951 | - | ||
| 952 | -/** | ||
| 953 | - * @brief Extract a vector from a sqlite3_value. Can be a float32, int8, or bit | ||
| 954 | - * vector. | ||
| 955 | - * | ||
| 956 | - * @param value: the sqlite3_value to read from. | ||
| 957 | - * @param vector: Output pointer to vector data. | ||
| 958 | - * @param dimensions: Output number of dimensions | ||
| 959 | - * @param dimensions: Output vector element type | ||
| 960 | - * @param cleanup | ||
| 961 | - * @param pzErrorMessage | ||
| 962 | - * @return int SQLITE_OK on success, error code otherwise | ||
| 963 | - */ | ||
| 964 | -int vector_from_value(sqlite3_value *value, void **vector, size_t *dimensions, | ||
| 965 | - enum VectorElementType *element_type, | ||
| 966 | - vector_cleanup *cleanup, char **pzErrorMessage) { | ||
| 967 | - int subtype = sqlite3_value_subtype(value); | ||
| 968 | - if (!subtype || (subtype == SQLITE_VEC_ELEMENT_TYPE_FLOAT32) || | ||
| 969 | - (subtype == JSON_SUBTYPE)) { | ||
| 970 | - int rc = fvec_from_value(value, (f32 **)vector, dimensions, | ||
| 971 | - (fvec_cleanup *)cleanup, pzErrorMessage); | ||
| 972 | - if (rc == SQLITE_OK) { | ||
| 973 | - *element_type = SQLITE_VEC_ELEMENT_TYPE_FLOAT32; | ||
| 974 | - } | ||
| 975 | - return rc; | ||
| 976 | - } | ||
| 977 | - | ||
| 978 | - if (subtype == SQLITE_VEC_ELEMENT_TYPE_BIT) { | ||
| 979 | - int rc = bitvec_from_value(value, (u8 **)vector, dimensions, cleanup, | ||
| 980 | - pzErrorMessage); | ||
| 981 | - if (rc == SQLITE_OK) { | ||
| 982 | - *element_type = SQLITE_VEC_ELEMENT_TYPE_BIT; | ||
| 983 | - } | ||
| 984 | - return rc; | ||
| 985 | - } | ||
| 986 | - if (subtype == SQLITE_VEC_ELEMENT_TYPE_INT8) { | ||
| 987 | - int rc = int8_vec_from_value(value, (i8 **)vector, dimensions, cleanup, | ||
| 988 | - pzErrorMessage); | ||
| 989 | - if (rc == SQLITE_OK) { | ||
| 990 | - *element_type = SQLITE_VEC_ELEMENT_TYPE_INT8; | ||
| 991 | - } | ||
| 992 | - return rc; | ||
| 993 | - } | ||
| 994 | - *pzErrorMessage = sqlite3_mprintf("Unknown subtype: %d", subtype); | ||
| 995 | - return SQLITE_ERROR; | ||
| 996 | -} | ||
| 997 | - | ||
| 998 | -int ensure_vector_match(sqlite3_value *aValue, sqlite3_value *bValue, void **a, | ||
| 999 | - void **b, enum VectorElementType *element_type, | ||
| 1000 | - size_t *dimensions, vector_cleanup *outACleanup, | ||
| 1001 | - vector_cleanup *outBCleanup, char **outError) { | ||
| 1002 | - int rc; | ||
| 1003 | - enum VectorElementType aType, bType; | ||
| 1004 | - size_t aDims, bDims; | ||
| 1005 | - char *error = NULL; | ||
| 1006 | - vector_cleanup aCleanup, bCleanup; | ||
| 1007 | - | ||
| 1008 | - rc = vector_from_value(aValue, a, &aDims, &aType, &aCleanup, &error); | ||
| 1009 | - if (rc != SQLITE_OK) { | ||
| 1010 | - *outError = sqlite3_mprintf("Error reading 1st vector: %s", error); | ||
| 1011 | - sqlite3_free(error); | ||
| 1012 | - return SQLITE_ERROR; | ||
| 1013 | - } | ||
| 1014 | - | ||
| 1015 | - rc = vector_from_value(bValue, b, &bDims, &bType, &bCleanup, &error); | ||
| 1016 | - if (rc != SQLITE_OK) { | ||
| 1017 | - *outError = sqlite3_mprintf("Error reading 2nd vector: %s", error); | ||
| 1018 | - sqlite3_free(error); | ||
| 1019 | - aCleanup(a); | ||
| 1020 | - return SQLITE_ERROR; | ||
| 1021 | - } | ||
| 1022 | - | ||
| 1023 | - if (aType != bType) { | ||
| 1024 | - *outError = | ||
| 1025 | - sqlite3_mprintf("Vector type mistmatch. First vector has type %s, " | ||
| 1026 | - "while the second has type %s.", | ||
| 1027 | - vector_subtype_name(aType), vector_subtype_name(bType)); | ||
| 1028 | - aCleanup(*a); | ||
| 1029 | - bCleanup(*b); | ||
| 1030 | - return SQLITE_ERROR; | ||
| 1031 | - } | ||
| 1032 | - if (aDims != bDims) { | ||
| 1033 | - *outError = sqlite3_mprintf( | ||
| 1034 | - "Vector dimension mistmatch. First vector has %ld dimensions, " | ||
| 1035 | - "while the second has %ld dimensions.", | ||
| 1036 | - aDims, bDims); | ||
| 1037 | - aCleanup(*a); | ||
| 1038 | - bCleanup(*b); | ||
| 1039 | - return SQLITE_ERROR; | ||
| 1040 | - } | ||
| 1041 | - *element_type = aType; | ||
| 1042 | - *dimensions = aDims; | ||
| 1043 | - *outACleanup = aCleanup; | ||
| 1044 | - *outBCleanup = bCleanup; | ||
| 1045 | - return SQLITE_OK; | ||
| 1046 | -} | ||
| 1047 | - | ||
| 1048 | -int _cmp(const void *a, const void *b) { return (*(i64 *)a - *(i64 *)b); } | ||
| 1049 | - | ||
| 1050 | -struct VecNpyFile { | ||
| 1051 | - char *path; | ||
| 1052 | - size_t pathLength; | ||
| 1053 | -}; | ||
| 1054 | -#define SQLITE_VEC_NPY_FILE_NAME "vec0-npy-file" | ||
| 1055 | - | ||
| 1056 | -#ifndef SQLITE_VEC_OMIT_FS | ||
| 1057 | -static void vec_npy_file(sqlite3_context *context, int argc, | ||
| 1058 | - sqlite3_value **argv) { | ||
| 1059 | - assert(argc == 1); | ||
| 1060 | - char *path = (char *)sqlite3_value_text(argv[0]); | ||
| 1061 | - size_t pathLength = sqlite3_value_bytes(argv[0]); | ||
| 1062 | - struct VecNpyFile *f; | ||
| 1063 | - | ||
| 1064 | - f = sqlite3_malloc(sizeof(*f)); | ||
| 1065 | - if (!f) { | ||
| 1066 | - sqlite3_result_error_nomem(context); | ||
| 1067 | - return; | ||
| 1068 | - } | ||
| 1069 | - memset(f, 0, sizeof(*f)); | ||
| 1070 | - | ||
| 1071 | - f->path = path; | ||
| 1072 | - f->pathLength = pathLength; | ||
| 1073 | - sqlite3_result_pointer(context, f, SQLITE_VEC_NPY_FILE_NAME, sqlite3_free); | ||
| 1074 | -} | ||
| 1075 | -#endif | ||
| 1076 | - | ||
| 1077 | -#pragma region scalar functions | ||
| 1078 | -static void vec_f32(sqlite3_context *context, int argc, sqlite3_value **argv) { | ||
| 1079 | - assert(argc == 1); | ||
| 1080 | - int rc; | ||
| 1081 | - f32 *vector = NULL; | ||
| 1082 | - size_t dimensions; | ||
| 1083 | - fvec_cleanup cleanup; | ||
| 1084 | - char *errmsg; | ||
| 1085 | - rc = fvec_from_value(argv[0], &vector, &dimensions, &cleanup, &errmsg); | ||
| 1086 | - if (rc != SQLITE_OK) { | ||
| 1087 | - sqlite3_result_error(context, errmsg, -1); | ||
| 1088 | - sqlite3_free(errmsg); | ||
| 1089 | - return; | ||
| 1090 | - } | ||
| 1091 | - sqlite3_result_blob(context, vector, dimensions * sizeof(f32), | ||
| 1092 | - (void (*)(void *))cleanup); | ||
| 1093 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_FLOAT32); | ||
| 1094 | -} | ||
| 1095 | - | ||
| 1096 | -static void vec_bit(sqlite3_context *context, int argc, sqlite3_value **argv) { | ||
| 1097 | - assert(argc == 1); | ||
| 1098 | - int rc; | ||
| 1099 | - u8 *vector; | ||
| 1100 | - size_t dimensions; | ||
| 1101 | - vector_cleanup cleanup; | ||
| 1102 | - char *errmsg; | ||
| 1103 | - rc = bitvec_from_value(argv[0], &vector, &dimensions, &cleanup, &errmsg); | ||
| 1104 | - if (rc != SQLITE_OK) { | ||
| 1105 | - sqlite3_result_error(context, errmsg, -1); | ||
| 1106 | - sqlite3_free(errmsg); | ||
| 1107 | - return; | ||
| 1108 | - } | ||
| 1109 | - sqlite3_result_blob(context, vector, dimensions / CHAR_BIT, SQLITE_TRANSIENT); | ||
| 1110 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_BIT); | ||
| 1111 | - cleanup(vector); | ||
| 1112 | -} | ||
| 1113 | -static void vec_int8(sqlite3_context *context, int argc, sqlite3_value **argv) { | ||
| 1114 | - assert(argc == 1); | ||
| 1115 | - int rc; | ||
| 1116 | - i8 *vector; | ||
| 1117 | - size_t dimensions; | ||
| 1118 | - vector_cleanup cleanup; | ||
| 1119 | - char *errmsg; | ||
| 1120 | - rc = int8_vec_from_value(argv[0], &vector, &dimensions, &cleanup, &errmsg); | ||
| 1121 | - if (rc != SQLITE_OK) { | ||
| 1122 | - sqlite3_result_error(context, errmsg, -1); | ||
| 1123 | - sqlite3_free(errmsg); | ||
| 1124 | - return; | ||
| 1125 | - } | ||
| 1126 | - sqlite3_result_blob(context, vector, dimensions, SQLITE_TRANSIENT); | ||
| 1127 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_INT8); | ||
| 1128 | - cleanup(vector); | ||
| 1129 | -} | ||
| 1130 | - | ||
| 1131 | -static void vec_length(sqlite3_context *context, int argc, | ||
| 1132 | - sqlite3_value **argv) { | ||
| 1133 | - assert(argc == 1); | ||
| 1134 | - int rc; | ||
| 1135 | - void *vector; | ||
| 1136 | - size_t dimensions; | ||
| 1137 | - vector_cleanup cleanup; | ||
| 1138 | - char *errmsg; | ||
| 1139 | - enum VectorElementType elementType; | ||
| 1140 | - rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, &cleanup, | ||
| 1141 | - &errmsg); | ||
| 1142 | - if (rc != SQLITE_OK) { | ||
| 1143 | - sqlite3_result_error(context, errmsg, -1); | ||
| 1144 | - sqlite3_free(errmsg); | ||
| 1145 | - return; | ||
| 1146 | - } | ||
| 1147 | - sqlite3_result_int64(context, dimensions); | ||
| 1148 | - cleanup(vector); | ||
| 1149 | -} | ||
| 1150 | - | ||
| 1151 | -static void vec_distance_cosine(sqlite3_context *context, int argc, | ||
| 1152 | - sqlite3_value **argv) { | ||
| 1153 | - assert(argc == 2); | ||
| 1154 | - int rc; | ||
| 1155 | - void *a = NULL, *b = NULL; | ||
| 1156 | - size_t dimensions; | ||
| 1157 | - vector_cleanup aCleanup, bCleanup; | ||
| 1158 | - char *error; | ||
| 1159 | - enum VectorElementType elementType; | ||
| 1160 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | ||
| 1161 | - &aCleanup, &bCleanup, &error); | ||
| 1162 | - if (rc != SQLITE_OK) { | ||
| 1163 | - sqlite3_result_error(context, error, -1); | ||
| 1164 | - sqlite3_free(error); | ||
| 1165 | - return; | ||
| 1166 | - } | ||
| 1167 | - | ||
| 1168 | - switch (elementType) { | ||
| 1169 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 1170 | - sqlite3_result_error( | ||
| 1171 | - context, "Cannot calculate cosine distance between two bitvectors.", | ||
| 1172 | - -1); | ||
| 1173 | - goto finish; | ||
| 1174 | - } | ||
| 1175 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 1176 | - f32 result = distance_cosine_float(a, b, &dimensions); | ||
| 1177 | - sqlite3_result_double(context, result); | ||
| 1178 | - goto finish; | ||
| 1179 | - } | ||
| 1180 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | ||
| 1181 | - f32 result = distance_cosine_int8(a, b, &dimensions); | ||
| 1182 | - sqlite3_result_double(context, result); | ||
| 1183 | - goto finish; | ||
| 1184 | - } | ||
| 1185 | - } | ||
| 1186 | - | ||
| 1187 | -finish: | ||
| 1188 | - aCleanup(a); | ||
| 1189 | - bCleanup(b); | ||
| 1190 | - return; | ||
| 1191 | -} | ||
| 1192 | - | ||
| 1193 | -static void vec_distance_l2(sqlite3_context *context, int argc, | ||
| 1194 | - sqlite3_value **argv) { | ||
| 1195 | - assert(argc == 2); | ||
| 1196 | - int rc; | ||
| 1197 | - void *a = NULL, *b = NULL; | ||
| 1198 | - size_t dimensions; | ||
| 1199 | - vector_cleanup aCleanup, bCleanup; | ||
| 1200 | - char *error; | ||
| 1201 | - enum VectorElementType elementType; | ||
| 1202 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | ||
| 1203 | - &aCleanup, &bCleanup, &error); | ||
| 1204 | - if (rc != SQLITE_OK) { | ||
| 1205 | - sqlite3_result_error(context, error, -1); | ||
| 1206 | - sqlite3_free(error); | ||
| 1207 | - return; | ||
| 1208 | - } | ||
| 1209 | - | ||
| 1210 | - switch (elementType) { | ||
| 1211 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 1212 | - sqlite3_result_error( | ||
| 1213 | - context, "Cannot calculate L2 distance between two bitvectors.", -1); | ||
| 1214 | - goto finish; | ||
| 1215 | - } | ||
| 1216 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 1217 | - f32 result = distance_l2_sqr_float(a, b, &dimensions); | ||
| 1218 | - sqlite3_result_double(context, result); | ||
| 1219 | - goto finish; | ||
| 1220 | - } | ||
| 1221 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | ||
| 1222 | - f32 result = distance_l2_sqr_int8(a, b, &dimensions); | ||
| 1223 | - sqlite3_result_double(context, result); | ||
| 1224 | - goto finish; | ||
| 1225 | - } | ||
| 1226 | - } | ||
| 1227 | - | ||
| 1228 | -finish: | ||
| 1229 | - aCleanup(a); | ||
| 1230 | - bCleanup(b); | ||
| 1231 | - return; | ||
| 1232 | -} | ||
| 1233 | - | ||
| 1234 | -static void vec_distance_l1(sqlite3_context *context, int argc, | ||
| 1235 | - sqlite3_value **argv) { | ||
| 1236 | - assert(argc == 2); | ||
| 1237 | - int rc; | ||
| 1238 | - void *a, *b; | ||
| 1239 | - size_t dimensions; | ||
| 1240 | - vector_cleanup aCleanup, bCleanup; | ||
| 1241 | - char *error; | ||
| 1242 | - enum VectorElementType elementType; | ||
| 1243 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | ||
| 1244 | - &aCleanup, &bCleanup, &error); | ||
| 1245 | - if (rc != SQLITE_OK) { | ||
| 1246 | - sqlite3_result_error(context, error, -1); | ||
| 1247 | - sqlite3_free(error); | ||
| 1248 | - return; | ||
| 1249 | - } | ||
| 1250 | - | ||
| 1251 | - switch (elementType) { | ||
| 1252 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 1253 | - sqlite3_result_error( | ||
| 1254 | - context, "Cannot calculate L1 distance between two bitvectors.", -1); | ||
| 1255 | - goto finish; | ||
| 1256 | - } | ||
| 1257 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 1258 | - double result = distance_l1_f32(a, b, &dimensions); | ||
| 1259 | - sqlite3_result_double(context, result); | ||
| 1260 | - goto finish; | ||
| 1261 | - } | ||
| 1262 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | ||
| 1263 | - i64 result = distance_l1_int8(a, b, &dimensions); | ||
| 1264 | - sqlite3_result_int(context, result); | ||
| 1265 | - goto finish; | ||
| 1266 | - } | ||
| 1267 | - } | ||
| 1268 | - | ||
| 1269 | -finish: | ||
| 1270 | - aCleanup(a); | ||
| 1271 | - bCleanup(b); | ||
| 1272 | - return; | ||
| 1273 | -} | ||
| 1274 | - | ||
| 1275 | -static void vec_distance_hamming(sqlite3_context *context, int argc, | ||
| 1276 | - sqlite3_value **argv) { | ||
| 1277 | - assert(argc == 2); | ||
| 1278 | - int rc; | ||
| 1279 | - void *a = NULL, *b = NULL; | ||
| 1280 | - size_t dimensions; | ||
| 1281 | - vector_cleanup aCleanup, bCleanup; | ||
| 1282 | - char *error; | ||
| 1283 | - enum VectorElementType elementType; | ||
| 1284 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | ||
| 1285 | - &aCleanup, &bCleanup, &error); | ||
| 1286 | - if (rc != SQLITE_OK) { | ||
| 1287 | - sqlite3_result_error(context, error, -1); | ||
| 1288 | - sqlite3_free(error); | ||
| 1289 | - return; | ||
| 1290 | - } | ||
| 1291 | - | ||
| 1292 | - switch (elementType) { | ||
| 1293 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 1294 | - sqlite3_result_double(context, distance_hamming(a, b, &dimensions)); | ||
| 1295 | - goto finish; | ||
| 1296 | - } | ||
| 1297 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 1298 | - sqlite3_result_error( | ||
| 1299 | - context, | ||
| 1300 | - "Cannot calculate hamming distance between two float32 vectors.", -1); | ||
| 1301 | - goto finish; | ||
| 1302 | - } | ||
| 1303 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | ||
| 1304 | - sqlite3_result_error( | ||
| 1305 | - context, "Cannot calculate hamming distance between two int8 vectors.", | ||
| 1306 | - -1); | ||
| 1307 | - goto finish; | ||
| 1308 | - } | ||
| 1309 | - } | ||
| 1310 | - | ||
| 1311 | -finish: | ||
| 1312 | - aCleanup(a); | ||
| 1313 | - bCleanup(b); | ||
| 1314 | - return; | ||
| 1315 | -} | ||
| 1316 | - | ||
| 1317 | -char *vec_type_name(enum VectorElementType elementType) { | ||
| 1318 | - switch (elementType) { | ||
| 1319 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: | ||
| 1320 | - return "float32"; | ||
| 1321 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | ||
| 1322 | - return "int8"; | ||
| 1323 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: | ||
| 1324 | - return "bit"; | ||
| 1325 | - } | ||
| 1326 | - return ""; | ||
| 1327 | -} | ||
| 1328 | - | ||
| 1329 | -static void vec_type(sqlite3_context *context, int argc, sqlite3_value **argv) { | ||
| 1330 | - assert(argc == 1); | ||
| 1331 | - void *vector; | ||
| 1332 | - size_t dimensions; | ||
| 1333 | - vector_cleanup cleanup; | ||
| 1334 | - char *pzError; | ||
| 1335 | - enum VectorElementType elementType; | ||
| 1336 | - int rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, | ||
| 1337 | - &cleanup, &pzError); | ||
| 1338 | - if (rc != SQLITE_OK) { | ||
| 1339 | - sqlite3_result_error(context, pzError, -1); | ||
| 1340 | - sqlite3_free(pzError); | ||
| 1341 | - return; | ||
| 1342 | - } | ||
| 1343 | - sqlite3_result_text(context, vec_type_name(elementType), -1, SQLITE_STATIC); | ||
| 1344 | - cleanup(vector); | ||
| 1345 | -} | ||
| 1346 | -static void vec_quantize_binary(sqlite3_context *context, int argc, | ||
| 1347 | - sqlite3_value **argv) { | ||
| 1348 | - assert(argc == 1); | ||
| 1349 | - void *vector; | ||
| 1350 | - size_t dimensions; | ||
| 1351 | - vector_cleanup vectorCleanup; | ||
| 1352 | - char *pzError; | ||
| 1353 | - enum VectorElementType elementType; | ||
| 1354 | - int rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, | ||
| 1355 | - &vectorCleanup, &pzError); | ||
| 1356 | - if (rc != SQLITE_OK) { | ||
| 1357 | - sqlite3_result_error(context, pzError, -1); | ||
| 1358 | - sqlite3_free(pzError); | ||
| 1359 | - return; | ||
| 1360 | - } | ||
| 1361 | - | ||
| 1362 | - if (dimensions <= 0) { | ||
| 1363 | - sqlite3_result_error(context, "Zero length vectors are not supported.", -1); | ||
| 1364 | - goto cleanup; | ||
| 1365 | - return; | ||
| 1366 | - } | ||
| 1367 | - if ((dimensions % CHAR_BIT) != 0) { | ||
| 1368 | - sqlite3_result_error( | ||
| 1369 | - context, | ||
| 1370 | - "Binary quantization requires vectors with a length divisible by 8", | ||
| 1371 | - -1); | ||
| 1372 | - goto cleanup; | ||
| 1373 | - return; | ||
| 1374 | - } | ||
| 1375 | - | ||
| 1376 | - int sz = dimensions / CHAR_BIT; | ||
| 1377 | - u8 *out = sqlite3_malloc(sz); | ||
| 1378 | - if (!out) { | ||
| 1379 | - sqlite3_result_error_code(context, SQLITE_NOMEM); | ||
| 1380 | - goto cleanup; | ||
| 1381 | - return; | ||
| 1382 | - } | ||
| 1383 | - memset(out, 0, sz); | ||
| 1384 | - | ||
| 1385 | - switch (elementType) { | ||
| 1386 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 1387 | - | ||
| 1388 | - for (size_t i = 0; i < dimensions; i++) { | ||
| 1389 | - int res = ((f32 *)vector)[i] > 0.0; | ||
| 1390 | - out[i / 8] |= (res << (i % 8)); | ||
| 1391 | - } | ||
| 1392 | - break; | ||
| 1393 | - } | ||
| 1394 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | ||
| 1395 | - for (size_t i = 0; i < dimensions; i++) { | ||
| 1396 | - int res = ((i8 *)vector)[i] > 0; | ||
| 1397 | - out[i / 8] |= (res << (i % 8)); | ||
| 1398 | - } | ||
| 1399 | - break; | ||
| 1400 | - } | ||
| 1401 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 1402 | - sqlite3_result_error(context, | ||
| 1403 | - "Can only binary quantize float or int8 vectors", -1); | ||
| 1404 | - sqlite3_free(out); | ||
| 1405 | - return; | ||
| 1406 | - } | ||
| 1407 | - } | ||
| 1408 | - sqlite3_result_blob(context, out, sz, sqlite3_free); | ||
| 1409 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_BIT); | ||
| 1410 | - | ||
| 1411 | -cleanup: | ||
| 1412 | - vectorCleanup(vector); | ||
| 1413 | -} | ||
| 1414 | - | ||
| 1415 | -static void vec_quantize_int8(sqlite3_context *context, int argc, | ||
| 1416 | - sqlite3_value **argv) { | ||
| 1417 | - assert(argc == 2); | ||
| 1418 | - f32 *srcVector; | ||
| 1419 | - size_t dimensions; | ||
| 1420 | - fvec_cleanup srcCleanup; | ||
| 1421 | - char *err; | ||
| 1422 | - i8 *out = NULL; | ||
| 1423 | - int rc = fvec_from_value(argv[0], &srcVector, &dimensions, &srcCleanup, &err); | ||
| 1424 | - if (rc != SQLITE_OK) { | ||
| 1425 | - sqlite3_result_error(context, err, -1); | ||
| 1426 | - sqlite3_free(err); | ||
| 1427 | - return; | ||
| 1428 | - } | ||
| 1429 | - | ||
| 1430 | - int sz = dimensions * sizeof(i8); | ||
| 1431 | - out = sqlite3_malloc(sz); | ||
| 1432 | - if (!out) { | ||
| 1433 | - sqlite3_result_error_nomem(context); | ||
| 1434 | - goto cleanup; | ||
| 1435 | - } | ||
| 1436 | - memset(out, 0, sz); | ||
| 1437 | - | ||
| 1438 | - if ((sqlite3_value_type(argv[1]) != SQLITE_TEXT) || | ||
| 1439 | - (sqlite3_value_bytes(argv[1]) != strlen("unit")) || | ||
| 1440 | - (sqlite3_stricmp((const char *)sqlite3_value_text(argv[1]), "unit") != | ||
| 1441 | - 0)) { | ||
| 1442 | - sqlite3_result_error( | ||
| 1443 | - context, "2nd argument to vec_quantize_int8() must be 'unit'.", -1); | ||
| 1444 | - sqlite3_free(out); | ||
| 1445 | - goto cleanup; | ||
| 1446 | - } | ||
| 1447 | - f32 step = (1.0 - (-1.0)) / 255; | ||
| 1448 | - for (size_t i = 0; i < dimensions; i++) { | ||
| 1449 | - out[i] = ((srcVector[i] - (-1.0)) / step) - 128; | ||
| 1450 | - } | ||
| 1451 | - | ||
| 1452 | - sqlite3_result_blob(context, out, dimensions * sizeof(i8), sqlite3_free); | ||
| 1453 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_INT8); | ||
| 1454 | - | ||
| 1455 | -cleanup: | ||
| 1456 | - srcCleanup(srcVector); | ||
| 1457 | -} | ||
| 1458 | - | ||
| 1459 | -static void vec_add(sqlite3_context *context, int argc, sqlite3_value **argv) { | ||
| 1460 | - assert(argc == 2); | ||
| 1461 | - int rc; | ||
| 1462 | - void *a = NULL, *b = NULL; | ||
| 1463 | - size_t dimensions; | ||
| 1464 | - vector_cleanup aCleanup, bCleanup; | ||
| 1465 | - char *error; | ||
| 1466 | - enum VectorElementType elementType; | ||
| 1467 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | ||
| 1468 | - &aCleanup, &bCleanup, &error); | ||
| 1469 | - if (rc != SQLITE_OK) { | ||
| 1470 | - sqlite3_result_error(context, error, -1); | ||
| 1471 | - sqlite3_free(error); | ||
| 1472 | - return; | ||
| 1473 | - } | ||
| 1474 | - | ||
| 1475 | - switch (elementType) { | ||
| 1476 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 1477 | - sqlite3_result_error(context, "Cannot add two bitvectors together.", -1); | ||
| 1478 | - goto finish; | ||
| 1479 | - } | ||
| 1480 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 1481 | - size_t outSize = dimensions * sizeof(f32); | ||
| 1482 | - f32 *out = sqlite3_malloc(outSize); | ||
| 1483 | - if (!out) { | ||
| 1484 | - sqlite3_result_error_nomem(context); | ||
| 1485 | - goto finish; | ||
| 1486 | - } | ||
| 1487 | - memset(out, 0, outSize); | ||
| 1488 | - for (size_t i = 0; i < dimensions; i++) { | ||
| 1489 | - out[i] = ((f32 *)a)[i] + ((f32 *)b)[i]; | ||
| 1490 | - } | ||
| 1491 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | ||
| 1492 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_FLOAT32); | ||
| 1493 | - goto finish; | ||
| 1494 | - } | ||
| 1495 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | ||
| 1496 | - size_t outSize = dimensions * sizeof(i8); | ||
| 1497 | - i8 *out = sqlite3_malloc(outSize); | ||
| 1498 | - if (!out) { | ||
| 1499 | - sqlite3_result_error_nomem(context); | ||
| 1500 | - goto finish; | ||
| 1501 | - } | ||
| 1502 | - memset(out, 0, outSize); | ||
| 1503 | - for (size_t i = 0; i < dimensions; i++) { | ||
| 1504 | - out[i] = ((i8 *)a)[i] + ((i8 *)b)[i]; | ||
| 1505 | - } | ||
| 1506 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | ||
| 1507 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_INT8); | ||
| 1508 | - goto finish; | ||
| 1509 | - } | ||
| 1510 | - } | ||
| 1511 | -finish: | ||
| 1512 | - aCleanup(a); | ||
| 1513 | - bCleanup(b); | ||
| 1514 | - return; | ||
| 1515 | -} | ||
| 1516 | -static void vec_sub(sqlite3_context *context, int argc, sqlite3_value **argv) { | ||
| 1517 | - assert(argc == 2); | ||
| 1518 | - int rc; | ||
| 1519 | - void *a = NULL, *b = NULL; | ||
| 1520 | - size_t dimensions; | ||
| 1521 | - vector_cleanup aCleanup, bCleanup; | ||
| 1522 | - char *error; | ||
| 1523 | - enum VectorElementType elementType; | ||
| 1524 | - rc = ensure_vector_match(argv[0], argv[1], &a, &b, &elementType, &dimensions, | ||
| 1525 | - &aCleanup, &bCleanup, &error); | ||
| 1526 | - if (rc != SQLITE_OK) { | ||
| 1527 | - sqlite3_result_error(context, error, -1); | ||
| 1528 | - sqlite3_free(error); | ||
| 1529 | - return; | ||
| 1530 | - } | ||
| 1531 | - | ||
| 1532 | - switch (elementType) { | ||
| 1533 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 1534 | - sqlite3_result_error(context, "Cannot subtract two bitvectors together.", | ||
| 1535 | - -1); | ||
| 1536 | - goto finish; | ||
| 1537 | - } | ||
| 1538 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 1539 | - size_t outSize = dimensions * sizeof(f32); | ||
| 1540 | - f32 *out = sqlite3_malloc(outSize); | ||
| 1541 | - if (!out) { | ||
| 1542 | - sqlite3_result_error_nomem(context); | ||
| 1543 | - goto finish; | ||
| 1544 | - } | ||
| 1545 | - memset(out, 0, outSize); | ||
| 1546 | - for (size_t i = 0; i < dimensions; i++) { | ||
| 1547 | - out[i] = ((f32 *)a)[i] - ((f32 *)b)[i]; | ||
| 1548 | - } | ||
| 1549 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | ||
| 1550 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_FLOAT32); | ||
| 1551 | - goto finish; | ||
| 1552 | - } | ||
| 1553 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | ||
| 1554 | - size_t outSize = dimensions * sizeof(i8); | ||
| 1555 | - i8 *out = sqlite3_malloc(outSize); | ||
| 1556 | - if (!out) { | ||
| 1557 | - sqlite3_result_error_nomem(context); | ||
| 1558 | - goto finish; | ||
| 1559 | - } | ||
| 1560 | - memset(out, 0, outSize); | ||
| 1561 | - for (size_t i = 0; i < dimensions; i++) { | ||
| 1562 | - out[i] = ((i8 *)a)[i] - ((i8 *)b)[i]; | ||
| 1563 | - } | ||
| 1564 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | ||
| 1565 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_INT8); | ||
| 1566 | - goto finish; | ||
| 1567 | - } | ||
| 1568 | - } | ||
| 1569 | -finish: | ||
| 1570 | - aCleanup(a); | ||
| 1571 | - bCleanup(b); | ||
| 1572 | - return; | ||
| 1573 | -} | ||
| 1574 | -static void vec_slice(sqlite3_context *context, int argc, | ||
| 1575 | - sqlite3_value **argv) { | ||
| 1576 | - assert(argc == 3); | ||
| 1577 | - | ||
| 1578 | - void *vector; | ||
| 1579 | - size_t dimensions; | ||
| 1580 | - vector_cleanup cleanup; | ||
| 1581 | - char *err; | ||
| 1582 | - enum VectorElementType elementType; | ||
| 1583 | - | ||
| 1584 | - int rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, | ||
| 1585 | - &cleanup, &err); | ||
| 1586 | - if (rc != SQLITE_OK) { | ||
| 1587 | - sqlite3_result_error(context, err, -1); | ||
| 1588 | - sqlite3_free(err); | ||
| 1589 | - return; | ||
| 1590 | - } | ||
| 1591 | - | ||
| 1592 | - int start = sqlite3_value_int(argv[1]); | ||
| 1593 | - int end = sqlite3_value_int(argv[2]); | ||
| 1594 | - | ||
| 1595 | - if (start < 0) { | ||
| 1596 | - sqlite3_result_error(context, | ||
| 1597 | - "slice 'start' index must be a postive number.", -1); | ||
| 1598 | - goto done; | ||
| 1599 | - } | ||
| 1600 | - if (end < 0) { | ||
| 1601 | - sqlite3_result_error(context, "slice 'end' index must be a postive number.", | ||
| 1602 | - -1); | ||
| 1603 | - goto done; | ||
| 1604 | - } | ||
| 1605 | - if (((size_t)start) > dimensions) { | ||
| 1606 | - sqlite3_result_error( | ||
| 1607 | - context, "slice 'start' index is greater than the number of dimensions", | ||
| 1608 | - -1); | ||
| 1609 | - goto done; | ||
| 1610 | - } | ||
| 1611 | - if (((size_t)end) > dimensions) { | ||
| 1612 | - sqlite3_result_error( | ||
| 1613 | - context, "slice 'end' index is greater than the number of dimensions", | ||
| 1614 | - -1); | ||
| 1615 | - goto done; | ||
| 1616 | - } | ||
| 1617 | - if (start > end) { | ||
| 1618 | - sqlite3_result_error(context, | ||
| 1619 | - "slice 'start' index is greater than 'end' index", -1); | ||
| 1620 | - goto done; | ||
| 1621 | - } | ||
| 1622 | - if (start == end) { | ||
| 1623 | - sqlite3_result_error(context, | ||
| 1624 | - "slice 'start' index is equal to the 'end' index, " | ||
| 1625 | - "vectors must have non-zero length", | ||
| 1626 | - -1); | ||
| 1627 | - goto done; | ||
| 1628 | - } | ||
| 1629 | - size_t n = end - start; | ||
| 1630 | - | ||
| 1631 | - switch (elementType) { | ||
| 1632 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 1633 | - int outSize = n * sizeof(f32); | ||
| 1634 | - f32 *out = sqlite3_malloc(outSize); | ||
| 1635 | - if (!out) { | ||
| 1636 | - sqlite3_result_error_nomem(context); | ||
| 1637 | - goto done; | ||
| 1638 | - } | ||
| 1639 | - memset(out, 0, outSize); | ||
| 1640 | - for (size_t i = 0; i < n; i++) { | ||
| 1641 | - out[i] = ((f32 *)vector)[start + i]; | ||
| 1642 | - } | ||
| 1643 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | ||
| 1644 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_FLOAT32); | ||
| 1645 | - goto done; | ||
| 1646 | - } | ||
| 1647 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | ||
| 1648 | - int outSize = n * sizeof(i8); | ||
| 1649 | - i8 *out = sqlite3_malloc(outSize); | ||
| 1650 | - if (!out) { | ||
| 1651 | - sqlite3_result_error_nomem(context); | ||
| 1652 | - return; | ||
| 1653 | - } | ||
| 1654 | - memset(out, 0, outSize); | ||
| 1655 | - for (size_t i = 0; i < n; i++) { | ||
| 1656 | - out[i] = ((i8 *)vector)[start + i]; | ||
| 1657 | - } | ||
| 1658 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | ||
| 1659 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_INT8); | ||
| 1660 | - goto done; | ||
| 1661 | - } | ||
| 1662 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 1663 | - if ((start % CHAR_BIT) != 0) { | ||
| 1664 | - sqlite3_result_error(context, "start index must be divisible by 8.", -1); | ||
| 1665 | - goto done; | ||
| 1666 | - } | ||
| 1667 | - if ((end % CHAR_BIT) != 0) { | ||
| 1668 | - sqlite3_result_error(context, "end index must be divisible by 8.", -1); | ||
| 1669 | - goto done; | ||
| 1670 | - } | ||
| 1671 | - int outSize = n / CHAR_BIT; | ||
| 1672 | - u8 *out = sqlite3_malloc(outSize); | ||
| 1673 | - if (!out) { | ||
| 1674 | - sqlite3_result_error_nomem(context); | ||
| 1675 | - return; | ||
| 1676 | - } | ||
| 1677 | - memset(out, 0, outSize); | ||
| 1678 | - for (size_t i = 0; i < n / CHAR_BIT; i++) { | ||
| 1679 | - out[i] = ((u8 *)vector)[(start / CHAR_BIT) + i]; | ||
| 1680 | - } | ||
| 1681 | - sqlite3_result_blob(context, out, outSize, sqlite3_free); | ||
| 1682 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_BIT); | ||
| 1683 | - goto done; | ||
| 1684 | - } | ||
| 1685 | - } | ||
| 1686 | -done: | ||
| 1687 | - cleanup(vector); | ||
| 1688 | -} | ||
| 1689 | - | ||
| 1690 | -static void vec_to_json(sqlite3_context *context, int argc, | ||
| 1691 | - sqlite3_value **argv) { | ||
| 1692 | - assert(argc == 1); | ||
| 1693 | - void *vector; | ||
| 1694 | - size_t dimensions; | ||
| 1695 | - vector_cleanup cleanup; | ||
| 1696 | - char *err; | ||
| 1697 | - enum VectorElementType elementType; | ||
| 1698 | - | ||
| 1699 | - int rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, | ||
| 1700 | - &cleanup, &err); | ||
| 1701 | - if (rc != SQLITE_OK) { | ||
| 1702 | - sqlite3_result_error(context, err, -1); | ||
| 1703 | - sqlite3_free(err); | ||
| 1704 | - return; | ||
| 1705 | - } | ||
| 1706 | - | ||
| 1707 | - sqlite3_str *str = sqlite3_str_new(sqlite3_context_db_handle(context)); | ||
| 1708 | - sqlite3_str_appendall(str, "["); | ||
| 1709 | - for (size_t i = 0; i < dimensions; i++) { | ||
| 1710 | - if (i != 0) { | ||
| 1711 | - sqlite3_str_appendall(str, ","); | ||
| 1712 | - } | ||
| 1713 | - if (elementType == SQLITE_VEC_ELEMENT_TYPE_FLOAT32) { | ||
| 1714 | - f32 value = ((f32 *)vector)[i]; | ||
| 1715 | - if (isnan(value)) { | ||
| 1716 | - sqlite3_str_appendall(str, "null"); | ||
| 1717 | - } else { | ||
| 1718 | - sqlite3_str_appendf(str, "%f", value); | ||
| 1719 | - } | ||
| 1720 | - | ||
| 1721 | - } else if (elementType == SQLITE_VEC_ELEMENT_TYPE_INT8) { | ||
| 1722 | - sqlite3_str_appendf(str, "%d", ((i8 *)vector)[i]); | ||
| 1723 | - } else if (elementType == SQLITE_VEC_ELEMENT_TYPE_BIT) { | ||
| 1724 | - u8 b = (((u8 *)vector)[i / 8] >> (i % CHAR_BIT)) & 1; | ||
| 1725 | - sqlite3_str_appendf(str, "%d", b); | ||
| 1726 | - } | ||
| 1727 | - } | ||
| 1728 | - sqlite3_str_appendall(str, "]"); | ||
| 1729 | - int len = sqlite3_str_length(str); | ||
| 1730 | - char *s = sqlite3_str_finish(str); | ||
| 1731 | - if (s) { | ||
| 1732 | - sqlite3_result_text(context, s, len, sqlite3_free); | ||
| 1733 | - sqlite3_result_subtype(context, JSON_SUBTYPE); | ||
| 1734 | - } else { | ||
| 1735 | - sqlite3_result_error_nomem(context); | ||
| 1736 | - } | ||
| 1737 | - cleanup(vector); | ||
| 1738 | -} | ||
| 1739 | - | ||
| 1740 | -static void vec_normalize(sqlite3_context *context, int argc, | ||
| 1741 | - sqlite3_value **argv) { | ||
| 1742 | - assert(argc == 1); | ||
| 1743 | - void *vector; | ||
| 1744 | - size_t dimensions; | ||
| 1745 | - vector_cleanup cleanup; | ||
| 1746 | - char *err; | ||
| 1747 | - enum VectorElementType elementType; | ||
| 1748 | - | ||
| 1749 | - int rc = vector_from_value(argv[0], &vector, &dimensions, &elementType, | ||
| 1750 | - &cleanup, &err); | ||
| 1751 | - if (rc != SQLITE_OK) { | ||
| 1752 | - sqlite3_result_error(context, err, -1); | ||
| 1753 | - sqlite3_free(err); | ||
| 1754 | - return; | ||
| 1755 | - } | ||
| 1756 | - | ||
| 1757 | - if (elementType != SQLITE_VEC_ELEMENT_TYPE_FLOAT32) { | ||
| 1758 | - sqlite3_result_error( | ||
| 1759 | - context, "only float32 vectors are supported when normalizing", -1); | ||
| 1760 | - cleanup(vector); | ||
| 1761 | - return; | ||
| 1762 | - } | ||
| 1763 | - | ||
| 1764 | - int outSize = dimensions * sizeof(f32); | ||
| 1765 | - f32 *out = sqlite3_malloc(outSize); | ||
| 1766 | - if (!out) { | ||
| 1767 | - cleanup(vector); | ||
| 1768 | - sqlite3_result_error_code(context, SQLITE_NOMEM); | ||
| 1769 | - return; | ||
| 1770 | - } | ||
| 1771 | - memset(out, 0, outSize); | ||
| 1772 | - | ||
| 1773 | - f32 *v = (f32 *)vector; | ||
| 1774 | - | ||
| 1775 | - f32 norm = 0; | ||
| 1776 | - for (size_t i = 0; i < dimensions; i++) { | ||
| 1777 | - norm += v[i] * v[i]; | ||
| 1778 | - } | ||
| 1779 | - norm = sqrt(norm); | ||
| 1780 | - for (size_t i = 0; i < dimensions; i++) { | ||
| 1781 | - out[i] = v[i] / norm; | ||
| 1782 | - } | ||
| 1783 | - | ||
| 1784 | - sqlite3_result_blob(context, out, dimensions * sizeof(f32), sqlite3_free); | ||
| 1785 | - sqlite3_result_subtype(context, SQLITE_VEC_ELEMENT_TYPE_FLOAT32); | ||
| 1786 | - cleanup(vector); | ||
| 1787 | -} | ||
| 1788 | - | ||
| 1789 | -static void _static_text_func(sqlite3_context *context, int argc, | ||
| 1790 | - sqlite3_value **argv) { | ||
| 1791 | - UNUSED_PARAMETER(argc); | ||
| 1792 | - UNUSED_PARAMETER(argv); | ||
| 1793 | - sqlite3_result_text(context, sqlite3_user_data(context), -1, SQLITE_STATIC); | ||
| 1794 | -} | ||
| 1795 | - | ||
| 1796 | -#pragma endregion | ||
| 1797 | - | ||
| 1798 | -enum Vec0TokenType { | ||
| 1799 | - TOKEN_TYPE_IDENTIFIER, | ||
| 1800 | - TOKEN_TYPE_DIGIT, | ||
| 1801 | - TOKEN_TYPE_LBRACKET, | ||
| 1802 | - TOKEN_TYPE_RBRACKET, | ||
| 1803 | - TOKEN_TYPE_PLUS, | ||
| 1804 | - TOKEN_TYPE_EQ, | ||
| 1805 | -}; | ||
| 1806 | -struct Vec0Token { | ||
| 1807 | - enum Vec0TokenType token_type; | ||
| 1808 | - char *start; | ||
| 1809 | - char *end; | ||
| 1810 | -}; | ||
| 1811 | - | ||
| 1812 | -int is_alpha(char x) { | ||
| 1813 | - return (x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z'); | ||
| 1814 | -} | ||
| 1815 | -int is_digit(char x) { return (x >= '0' && x <= '9'); } | ||
| 1816 | -int is_whitespace(char x) { | ||
| 1817 | - return x == ' ' || x == '\t' || x == '\n' || x == '\r'; | ||
| 1818 | -} | ||
| 1819 | - | ||
| 1820 | -#define VEC0_TOKEN_RESULT_EOF 1 | ||
| 1821 | -#define VEC0_TOKEN_RESULT_SOME 2 | ||
| 1822 | -#define VEC0_TOKEN_RESULT_ERROR 3 | ||
| 1823 | - | ||
| 1824 | -int vec0_token_next(char *start, char *end, struct Vec0Token *out) { | ||
| 1825 | - char *ptr = start; | ||
| 1826 | - while (ptr < end) { | ||
| 1827 | - char curr = *ptr; | ||
| 1828 | - if (is_whitespace(curr)) { | ||
| 1829 | - ptr++; | ||
| 1830 | - continue; | ||
| 1831 | - } else if (curr == '+') { | ||
| 1832 | - ptr++; | ||
| 1833 | - out->start = ptr; | ||
| 1834 | - out->end = ptr; | ||
| 1835 | - out->token_type = TOKEN_TYPE_PLUS; | ||
| 1836 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 1837 | - } else if (curr == '[') { | ||
| 1838 | - ptr++; | ||
| 1839 | - out->start = ptr; | ||
| 1840 | - out->end = ptr; | ||
| 1841 | - out->token_type = TOKEN_TYPE_LBRACKET; | ||
| 1842 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 1843 | - } else if (curr == ']') { | ||
| 1844 | - ptr++; | ||
| 1845 | - out->start = ptr; | ||
| 1846 | - out->end = ptr; | ||
| 1847 | - out->token_type = TOKEN_TYPE_RBRACKET; | ||
| 1848 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 1849 | - } else if (curr == '=') { | ||
| 1850 | - ptr++; | ||
| 1851 | - out->start = ptr; | ||
| 1852 | - out->end = ptr; | ||
| 1853 | - out->token_type = TOKEN_TYPE_EQ; | ||
| 1854 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 1855 | - } else if (is_alpha(curr)) { | ||
| 1856 | - char *start = ptr; | ||
| 1857 | - while (ptr < end && (is_alpha(*ptr) || is_digit(*ptr) || *ptr == '_')) { | ||
| 1858 | - ptr++; | ||
| 1859 | - } | ||
| 1860 | - out->start = start; | ||
| 1861 | - out->end = ptr; | ||
| 1862 | - out->token_type = TOKEN_TYPE_IDENTIFIER; | ||
| 1863 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 1864 | - } else if (is_digit(curr)) { | ||
| 1865 | - char *start = ptr; | ||
| 1866 | - while (ptr < end && (is_digit(*ptr))) { | ||
| 1867 | - ptr++; | ||
| 1868 | - } | ||
| 1869 | - out->start = start; | ||
| 1870 | - out->end = ptr; | ||
| 1871 | - out->token_type = TOKEN_TYPE_DIGIT; | ||
| 1872 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 1873 | - } else { | ||
| 1874 | - return VEC0_TOKEN_RESULT_ERROR; | ||
| 1875 | - } | ||
| 1876 | - } | ||
| 1877 | - return VEC0_TOKEN_RESULT_EOF; | ||
| 1878 | -} | ||
| 1879 | - | ||
| 1880 | -struct Vec0Scanner { | ||
| 1881 | - char *start; | ||
| 1882 | - char *end; | ||
| 1883 | - char *ptr; | ||
| 1884 | -}; | ||
| 1885 | - | ||
| 1886 | -void vec0_scanner_init(struct Vec0Scanner *scanner, const char *source, | ||
| 1887 | - int source_length) { | ||
| 1888 | - scanner->start = (char *)source; | ||
| 1889 | - scanner->end = (char *)source + source_length; | ||
| 1890 | - scanner->ptr = (char *)source; | ||
| 1891 | -} | ||
| 1892 | -int vec0_scanner_next(struct Vec0Scanner *scanner, struct Vec0Token *out) { | ||
| 1893 | - int rc = vec0_token_next(scanner->start, scanner->end, out); | ||
| 1894 | - if (rc == VEC0_TOKEN_RESULT_SOME) { | ||
| 1895 | - scanner->start = out->end; | ||
| 1896 | - } | ||
| 1897 | - return rc; | ||
| 1898 | -} | ||
| 1899 | - | ||
| 1900 | -int vec0_parse_table_option(const char *source, int source_length, | ||
| 1901 | - char **out_key, int *out_key_length, | ||
| 1902 | - char **out_value, int *out_value_length) { | ||
| 1903 | - int rc; | ||
| 1904 | - struct Vec0Scanner scanner; | ||
| 1905 | - struct Vec0Token token; | ||
| 1906 | - char *key; | ||
| 1907 | - char *value; | ||
| 1908 | - int keyLength, valueLength; | ||
| 1909 | - | ||
| 1910 | - vec0_scanner_init(&scanner, source, source_length); | ||
| 1911 | - | ||
| 1912 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 1913 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 1914 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 1915 | - return SQLITE_EMPTY; | ||
| 1916 | - } | ||
| 1917 | - key = token.start; | ||
| 1918 | - keyLength = token.end - token.start; | ||
| 1919 | - | ||
| 1920 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 1921 | - if (rc != VEC0_TOKEN_RESULT_SOME && token.token_type != TOKEN_TYPE_EQ) { | ||
| 1922 | - return SQLITE_EMPTY; | ||
| 1923 | - } | ||
| 1924 | - | ||
| 1925 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 1926 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 1927 | - !((token.token_type == TOKEN_TYPE_IDENTIFIER) || | ||
| 1928 | - (token.token_type == TOKEN_TYPE_DIGIT))) { | ||
| 1929 | - return SQLITE_ERROR; | ||
| 1930 | - } | ||
| 1931 | - value = token.start; | ||
| 1932 | - valueLength = token.end - token.start; | ||
| 1933 | - | ||
| 1934 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 1935 | - if (rc == VEC0_TOKEN_RESULT_EOF) { | ||
| 1936 | - *out_key = key; | ||
| 1937 | - *out_key_length = keyLength; | ||
| 1938 | - *out_value = value; | ||
| 1939 | - *out_value_length = valueLength; | ||
| 1940 | - return SQLITE_OK; | ||
| 1941 | - } | ||
| 1942 | - return SQLITE_ERROR; | ||
| 1943 | -} | ||
| 1944 | -/** | ||
| 1945 | - * @brief Parse an argv[i] entry of a vec0 virtual table definition, and see if | ||
| 1946 | - * it's a PARTITION KEY definition. | ||
| 1947 | - * | ||
| 1948 | - * @param source: argv[i] source string | ||
| 1949 | - * @param source_length: length of the source string | ||
| 1950 | - * @param out_column_name: If it is a partition key, the output column name. Same lifetime | ||
| 1951 | - * as source, points to specific char * | ||
| 1952 | - * @param out_column_name_length: Length of out_column_name in bytes | ||
| 1953 | - * @param out_column_type: SQLITE_TEXT or SQLITE_INTEGER. | ||
| 1954 | - * @return int: SQLITE_EMPTY if not a PK, SQLITE_OK if it is. | ||
| 1955 | - */ | ||
| 1956 | -int vec0_parse_partition_key_definition(const char *source, int source_length, | ||
| 1957 | - char **out_column_name, | ||
| 1958 | - int *out_column_name_length, | ||
| 1959 | - int *out_column_type) { | ||
| 1960 | - struct Vec0Scanner scanner; | ||
| 1961 | - struct Vec0Token token; | ||
| 1962 | - char *column_name; | ||
| 1963 | - int column_name_length; | ||
| 1964 | - int column_type; | ||
| 1965 | - vec0_scanner_init(&scanner, source, source_length); | ||
| 1966 | - | ||
| 1967 | - // Check first token is identifier, will be the column name | ||
| 1968 | - int rc = vec0_scanner_next(&scanner, &token); | ||
| 1969 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 1970 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 1971 | - return SQLITE_EMPTY; | ||
| 1972 | - } | ||
| 1973 | - | ||
| 1974 | - column_name = token.start; | ||
| 1975 | - column_name_length = token.end - token.start; | ||
| 1976 | - | ||
| 1977 | - // Check the next token matches "text" or "integer", as column type | ||
| 1978 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 1979 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 1980 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 1981 | - return SQLITE_EMPTY; | ||
| 1982 | - } | ||
| 1983 | - if (sqlite3_strnicmp(token.start, "text", token.end - token.start) == 0) { | ||
| 1984 | - column_type = SQLITE_TEXT; | ||
| 1985 | - } else if (sqlite3_strnicmp(token.start, "int", token.end - token.start) == | ||
| 1986 | - 0 || | ||
| 1987 | - sqlite3_strnicmp(token.start, "integer", | ||
| 1988 | - token.end - token.start) == 0) { | ||
| 1989 | - column_type = SQLITE_INTEGER; | ||
| 1990 | - } else { | ||
| 1991 | - return SQLITE_EMPTY; | ||
| 1992 | - } | ||
| 1993 | - | ||
| 1994 | - // Check the next token is identifier and matches "partition" | ||
| 1995 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 1996 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 1997 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 1998 | - return SQLITE_EMPTY; | ||
| 1999 | - } | ||
| 2000 | - if (sqlite3_strnicmp(token.start, "partition", token.end - token.start) != 0) { | ||
| 2001 | - return SQLITE_EMPTY; | ||
| 2002 | - } | ||
| 2003 | - | ||
| 2004 | - // Check the next token is identifier and matches "key" | ||
| 2005 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2006 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 2007 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2008 | - return SQLITE_EMPTY; | ||
| 2009 | - } | ||
| 2010 | - if (sqlite3_strnicmp(token.start, "key", token.end - token.start) != 0) { | ||
| 2011 | - return SQLITE_EMPTY; | ||
| 2012 | - } | ||
| 2013 | - | ||
| 2014 | - *out_column_name = column_name; | ||
| 2015 | - *out_column_name_length = column_name_length; | ||
| 2016 | - *out_column_type = column_type; | ||
| 2017 | - | ||
| 2018 | - return SQLITE_OK; | ||
| 2019 | -} | ||
| 2020 | - | ||
| 2021 | -/** | ||
| 2022 | - * @brief Parse an argv[i] entry of a vec0 virtual table definition, and see if | ||
| 2023 | - * it's an auxiliar column definition, ie `+[name] [type]` like `+contents text` | ||
| 2024 | - * | ||
| 2025 | - * @param source: argv[i] source string | ||
| 2026 | - * @param source_length: length of the source string | ||
| 2027 | - * @param out_column_name: If it is a partition key, the output column name. Same lifetime | ||
| 2028 | - * as source, points to specific char * | ||
| 2029 | - * @param out_column_name_length: Length of out_column_name in bytes | ||
| 2030 | - * @param out_column_type: SQLITE_TEXT, SQLITE_INTEGER, SQLITE_FLOAT, or SQLITE_BLOB. | ||
| 2031 | - * @return int: SQLITE_EMPTY if not an aux column, SQLITE_OK if it is. | ||
| 2032 | - */ | ||
| 2033 | -int vec0_parse_auxiliary_column_definition(const char *source, int source_length, | ||
| 2034 | - char **out_column_name, | ||
| 2035 | - int *out_column_name_length, | ||
| 2036 | - int *out_column_type) { | ||
| 2037 | - struct Vec0Scanner scanner; | ||
| 2038 | - struct Vec0Token token; | ||
| 2039 | - char *column_name; | ||
| 2040 | - int column_name_length; | ||
| 2041 | - int column_type; | ||
| 2042 | - vec0_scanner_init(&scanner, source, source_length); | ||
| 2043 | - | ||
| 2044 | - // Check first token is '+', which denotes aux columns | ||
| 2045 | - int rc = vec0_scanner_next(&scanner, &token); | ||
| 2046 | - if (rc != VEC0_TOKEN_RESULT_SOME || | ||
| 2047 | - token.token_type != TOKEN_TYPE_PLUS) { | ||
| 2048 | - return SQLITE_EMPTY; | ||
| 2049 | - } | ||
| 2050 | - | ||
| 2051 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2052 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 2053 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2054 | - return SQLITE_EMPTY; | ||
| 2055 | - } | ||
| 2056 | - | ||
| 2057 | - column_name = token.start; | ||
| 2058 | - column_name_length = token.end - token.start; | ||
| 2059 | - | ||
| 2060 | - // Check the next token matches "text" or "integer", as column type | ||
| 2061 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2062 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 2063 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2064 | - return SQLITE_EMPTY; | ||
| 2065 | - } | ||
| 2066 | - if (sqlite3_strnicmp(token.start, "text", token.end - token.start) == 0) { | ||
| 2067 | - column_type = SQLITE_TEXT; | ||
| 2068 | - } else if (sqlite3_strnicmp(token.start, "int", token.end - token.start) == | ||
| 2069 | - 0 || | ||
| 2070 | - sqlite3_strnicmp(token.start, "integer", | ||
| 2071 | - token.end - token.start) == 0) { | ||
| 2072 | - column_type = SQLITE_INTEGER; | ||
| 2073 | - } else if (sqlite3_strnicmp(token.start, "float", token.end - token.start) == | ||
| 2074 | - 0 || | ||
| 2075 | - sqlite3_strnicmp(token.start, "double", | ||
| 2076 | - token.end - token.start) == 0) { | ||
| 2077 | - column_type = SQLITE_FLOAT; | ||
| 2078 | - } else if (sqlite3_strnicmp(token.start, "blob", token.end - token.start) ==0) { | ||
| 2079 | - column_type = SQLITE_BLOB; | ||
| 2080 | - } else { | ||
| 2081 | - return SQLITE_EMPTY; | ||
| 2082 | - } | ||
| 2083 | - | ||
| 2084 | - *out_column_name = column_name; | ||
| 2085 | - *out_column_name_length = column_name_length; | ||
| 2086 | - *out_column_type = column_type; | ||
| 2087 | - | ||
| 2088 | - return SQLITE_OK; | ||
| 2089 | -} | ||
| 2090 | - | ||
| 2091 | -typedef enum { | ||
| 2092 | - VEC0_METADATA_COLUMN_KIND_BOOLEAN, | ||
| 2093 | - VEC0_METADATA_COLUMN_KIND_INTEGER, | ||
| 2094 | - VEC0_METADATA_COLUMN_KIND_FLOAT, | ||
| 2095 | - VEC0_METADATA_COLUMN_KIND_TEXT, | ||
| 2096 | - // future: blob, date, datetime | ||
| 2097 | -} vec0_metadata_column_kind; | ||
| 2098 | - | ||
| 2099 | -/** | ||
| 2100 | - * @brief Parse an argv[i] entry of a vec0 virtual table definition, and see if | ||
| 2101 | - * it's an metadata column definition, ie `[name] [type]` like `is_released boolean` | ||
| 2102 | - * | ||
| 2103 | - * @param source: argv[i] source string | ||
| 2104 | - * @param source_length: length of the source string | ||
| 2105 | - * @param out_column_name: If it is a metadata column, the output column name. Same lifetime | ||
| 2106 | - * as source, points to specific char * | ||
| 2107 | - * @param out_column_name_length: Length of out_column_name in bytes | ||
| 2108 | - * @param out_column_type: one of vec0_metadata_column_kind | ||
| 2109 | - * @return int: SQLITE_EMPTY if not an metadata column, SQLITE_OK if it is. | ||
| 2110 | - */ | ||
| 2111 | -int vec0_parse_metadata_column_definition(const char *source, int source_length, | ||
| 2112 | - char **out_column_name, | ||
| 2113 | - int *out_column_name_length, | ||
| 2114 | - vec0_metadata_column_kind *out_column_type) { | ||
| 2115 | - struct Vec0Scanner scanner; | ||
| 2116 | - struct Vec0Token token; | ||
| 2117 | - char *column_name; | ||
| 2118 | - int column_name_length; | ||
| 2119 | - vec0_metadata_column_kind column_type; | ||
| 2120 | - int rc; | ||
| 2121 | - vec0_scanner_init(&scanner, source, source_length); | ||
| 2122 | - | ||
| 2123 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2124 | - if (rc != VEC0_TOKEN_RESULT_SOME || | ||
| 2125 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2126 | - return SQLITE_EMPTY; | ||
| 2127 | - } | ||
| 2128 | - | ||
| 2129 | - column_name = token.start; | ||
| 2130 | - column_name_length = token.end - token.start; | ||
| 2131 | - | ||
| 2132 | - // Check the next token matches a valid metadata type | ||
| 2133 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2134 | - if (rc != VEC0_TOKEN_RESULT_SOME || | ||
| 2135 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2136 | - return SQLITE_EMPTY; | ||
| 2137 | - } | ||
| 2138 | - char * t = token.start; | ||
| 2139 | - int n = token.end - token.start; | ||
| 2140 | - if (sqlite3_strnicmp(t, "boolean", n) == 0 || sqlite3_strnicmp(t, "bool", n) == 0) { | ||
| 2141 | - column_type = VEC0_METADATA_COLUMN_KIND_BOOLEAN; | ||
| 2142 | - }else if (sqlite3_strnicmp(t, "int64", n) == 0 || sqlite3_strnicmp(t, "integer64", n) == 0 || sqlite3_strnicmp(t, "integer", n) == 0 || sqlite3_strnicmp(t, "int", n) == 0) { | ||
| 2143 | - column_type = VEC0_METADATA_COLUMN_KIND_INTEGER; | ||
| 2144 | - }else if (sqlite3_strnicmp(t, "float", n) == 0 || sqlite3_strnicmp(t, "double", n) == 0 || sqlite3_strnicmp(t, "float64", n) == 0 || sqlite3_strnicmp(t, "f64", n) == 0) { | ||
| 2145 | - column_type = VEC0_METADATA_COLUMN_KIND_FLOAT; | ||
| 2146 | - } else if (sqlite3_strnicmp(t, "text", n) == 0) { | ||
| 2147 | - column_type = VEC0_METADATA_COLUMN_KIND_TEXT; | ||
| 2148 | - } else { | ||
| 2149 | - return SQLITE_EMPTY; | ||
| 2150 | - } | ||
| 2151 | - | ||
| 2152 | - *out_column_name = column_name; | ||
| 2153 | - *out_column_name_length = column_name_length; | ||
| 2154 | - *out_column_type = column_type; | ||
| 2155 | - | ||
| 2156 | - return SQLITE_OK; | ||
| 2157 | -} | ||
| 2158 | - | ||
| 2159 | -/** | ||
| 2160 | - * @brief Parse an argv[i] entry of a vec0 virtual table definition, and see if | ||
| 2161 | - * it's a PRIMARY KEY definition. | ||
| 2162 | - * | ||
| 2163 | - * @param source: argv[i] source string | ||
| 2164 | - * @param source_length: length of the source string | ||
| 2165 | - * @param out_column_name: If it is a PK, the output column name. Same lifetime | ||
| 2166 | - * as source, points to specific char * | ||
| 2167 | - * @param out_column_name_length: Length of out_column_name in bytes | ||
| 2168 | - * @param out_column_type: SQLITE_TEXT or SQLITE_INTEGER. | ||
| 2169 | - * @return int: SQLITE_EMPTY if not a PK, SQLITE_OK if it is. | ||
| 2170 | - */ | ||
| 2171 | -int vec0_parse_primary_key_definition(const char *source, int source_length, | ||
| 2172 | - char **out_column_name, | ||
| 2173 | - int *out_column_name_length, | ||
| 2174 | - int *out_column_type) { | ||
| 2175 | - struct Vec0Scanner scanner; | ||
| 2176 | - struct Vec0Token token; | ||
| 2177 | - char *column_name; | ||
| 2178 | - int column_name_length; | ||
| 2179 | - int column_type; | ||
| 2180 | - vec0_scanner_init(&scanner, source, source_length); | ||
| 2181 | - | ||
| 2182 | - // Check first token is identifier, will be the column name | ||
| 2183 | - int rc = vec0_scanner_next(&scanner, &token); | ||
| 2184 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 2185 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2186 | - return SQLITE_EMPTY; | ||
| 2187 | - } | ||
| 2188 | - | ||
| 2189 | - column_name = token.start; | ||
| 2190 | - column_name_length = token.end - token.start; | ||
| 2191 | - | ||
| 2192 | - // Check the next token matches "text" or "integer", as column type | ||
| 2193 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2194 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 2195 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2196 | - return SQLITE_EMPTY; | ||
| 2197 | - } | ||
| 2198 | - if (sqlite3_strnicmp(token.start, "text", token.end - token.start) == 0) { | ||
| 2199 | - column_type = SQLITE_TEXT; | ||
| 2200 | - } else if (sqlite3_strnicmp(token.start, "int", token.end - token.start) == | ||
| 2201 | - 0 || | ||
| 2202 | - sqlite3_strnicmp(token.start, "integer", | ||
| 2203 | - token.end - token.start) == 0) { | ||
| 2204 | - column_type = SQLITE_INTEGER; | ||
| 2205 | - } else { | ||
| 2206 | - return SQLITE_EMPTY; | ||
| 2207 | - } | ||
| 2208 | - | ||
| 2209 | - // Check the next token is identifier and matches "primary" | ||
| 2210 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2211 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 2212 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2213 | - return SQLITE_EMPTY; | ||
| 2214 | - } | ||
| 2215 | - if (sqlite3_strnicmp(token.start, "primary", token.end - token.start) != 0) { | ||
| 2216 | - return SQLITE_EMPTY; | ||
| 2217 | - } | ||
| 2218 | - | ||
| 2219 | - // Check the next token is identifier and matches "key" | ||
| 2220 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2221 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 2222 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2223 | - return SQLITE_EMPTY; | ||
| 2224 | - } | ||
| 2225 | - if (sqlite3_strnicmp(token.start, "key", token.end - token.start) != 0) { | ||
| 2226 | - return SQLITE_EMPTY; | ||
| 2227 | - } | ||
| 2228 | - | ||
| 2229 | - *out_column_name = column_name; | ||
| 2230 | - *out_column_name_length = column_name_length; | ||
| 2231 | - *out_column_type = column_type; | ||
| 2232 | - | ||
| 2233 | - return SQLITE_OK; | ||
| 2234 | -} | ||
| 2235 | - | ||
| 2236 | -enum Vec0DistanceMetrics { | ||
| 2237 | - VEC0_DISTANCE_METRIC_L2 = 1, | ||
| 2238 | - VEC0_DISTANCE_METRIC_COSINE = 2, | ||
| 2239 | - VEC0_DISTANCE_METRIC_L1 = 3, | ||
| 2240 | -}; | ||
| 2241 | - | ||
| 2242 | -struct VectorColumnDefinition { | ||
| 2243 | - char *name; | ||
| 2244 | - int name_length; | ||
| 2245 | - size_t dimensions; | ||
| 2246 | - enum VectorElementType element_type; | ||
| 2247 | - enum Vec0DistanceMetrics distance_metric; | ||
| 2248 | -}; | ||
| 2249 | - | ||
| 2250 | -struct Vec0PartitionColumnDefinition { | ||
| 2251 | - int type; | ||
| 2252 | - char * name; | ||
| 2253 | - int name_length; | ||
| 2254 | -}; | ||
| 2255 | - | ||
| 2256 | -struct Vec0AuxiliaryColumnDefinition { | ||
| 2257 | - int type; | ||
| 2258 | - char * name; | ||
| 2259 | - int name_length; | ||
| 2260 | -}; | ||
| 2261 | -struct Vec0MetadataColumnDefinition { | ||
| 2262 | - vec0_metadata_column_kind kind; | ||
| 2263 | - char * name; | ||
| 2264 | - int name_length; | ||
| 2265 | -}; | ||
| 2266 | - | ||
| 2267 | -size_t vector_byte_size(enum VectorElementType element_type, | ||
| 2268 | - size_t dimensions) { | ||
| 2269 | - switch (element_type) { | ||
| 2270 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: | ||
| 2271 | - return dimensions * sizeof(f32); | ||
| 2272 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | ||
| 2273 | - return dimensions * sizeof(i8); | ||
| 2274 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: | ||
| 2275 | - return dimensions / CHAR_BIT; | ||
| 2276 | - } | ||
| 2277 | - return 0; | ||
| 2278 | -} | ||
| 2279 | - | ||
| 2280 | -size_t vector_column_byte_size(struct VectorColumnDefinition column) { | ||
| 2281 | - return vector_byte_size(column.element_type, column.dimensions); | ||
| 2282 | -} | ||
| 2283 | - | ||
| 2284 | -/** | ||
| 2285 | - * @brief Parse an vec0 vtab argv[i] column definition and see if | ||
| 2286 | - * it's a vector column defintion, ex `contents_embedding float[768]`. | ||
| 2287 | - * | ||
| 2288 | - * @param source vec0 argv[i] item | ||
| 2289 | - * @param source_length length of source in bytes | ||
| 2290 | - * @param outColumn Output the parse vector column to this struct, if success | ||
| 2291 | - * @return int SQLITE_OK on success, SQLITE_EMPTY is it's not a vector column | ||
| 2292 | - * definition, SQLITE_ERROR on error. | ||
| 2293 | - */ | ||
| 2294 | -int vec0_parse_vector_column(const char *source, int source_length, | ||
| 2295 | - struct VectorColumnDefinition *outColumn) { | ||
| 2296 | - // parses a vector column definition like so: | ||
| 2297 | - // "abc float[123]", "abc_123 bit[1234]", eetc. | ||
| 2298 | - // https://github.com/asg017/sqlite-vec/issues/46 | ||
| 2299 | - int rc; | ||
| 2300 | - struct Vec0Scanner scanner; | ||
| 2301 | - struct Vec0Token token; | ||
| 2302 | - | ||
| 2303 | - char *name; | ||
| 2304 | - int nameLength; | ||
| 2305 | - enum VectorElementType elementType; | ||
| 2306 | - enum Vec0DistanceMetrics distanceMetric = VEC0_DISTANCE_METRIC_L2; | ||
| 2307 | - int dimensions; | ||
| 2308 | - | ||
| 2309 | - vec0_scanner_init(&scanner, source, source_length); | ||
| 2310 | - | ||
| 2311 | - // starts with an identifier | ||
| 2312 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2313 | - | ||
| 2314 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 2315 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2316 | - return SQLITE_EMPTY; | ||
| 2317 | - } | ||
| 2318 | - | ||
| 2319 | - name = token.start; | ||
| 2320 | - nameLength = token.end - token.start; | ||
| 2321 | - | ||
| 2322 | - // vector column type comes next: float, int, or bit | ||
| 2323 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2324 | - | ||
| 2325 | - if (rc != VEC0_TOKEN_RESULT_SOME || | ||
| 2326 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2327 | - return SQLITE_EMPTY; | ||
| 2328 | - } | ||
| 2329 | - if (sqlite3_strnicmp(token.start, "float", 5) == 0 || | ||
| 2330 | - sqlite3_strnicmp(token.start, "f32", 3) == 0) { | ||
| 2331 | - elementType = SQLITE_VEC_ELEMENT_TYPE_FLOAT32; | ||
| 2332 | - } else if (sqlite3_strnicmp(token.start, "int8", 4) == 0 || | ||
| 2333 | - sqlite3_strnicmp(token.start, "i8", 2) == 0) { | ||
| 2334 | - elementType = SQLITE_VEC_ELEMENT_TYPE_INT8; | ||
| 2335 | - } else if (sqlite3_strnicmp(token.start, "bit", 3) == 0) { | ||
| 2336 | - elementType = SQLITE_VEC_ELEMENT_TYPE_BIT; | ||
| 2337 | - } else { | ||
| 2338 | - return SQLITE_EMPTY; | ||
| 2339 | - } | ||
| 2340 | - | ||
| 2341 | - // left '[' bracket | ||
| 2342 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2343 | - if (rc != VEC0_TOKEN_RESULT_SOME && token.token_type != TOKEN_TYPE_LBRACKET) { | ||
| 2344 | - return SQLITE_EMPTY; | ||
| 2345 | - } | ||
| 2346 | - | ||
| 2347 | - // digit, for vector dimension length | ||
| 2348 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2349 | - if (rc != VEC0_TOKEN_RESULT_SOME && token.token_type != TOKEN_TYPE_DIGIT) { | ||
| 2350 | - return SQLITE_ERROR; | ||
| 2351 | - } | ||
| 2352 | - dimensions = atoi(token.start); | ||
| 2353 | - if (dimensions <= 0) { | ||
| 2354 | - return SQLITE_ERROR; | ||
| 2355 | - } | ||
| 2356 | - | ||
| 2357 | - // // right ']' bracket | ||
| 2358 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2359 | - if (rc != VEC0_TOKEN_RESULT_SOME && token.token_type != TOKEN_TYPE_RBRACKET) { | ||
| 2360 | - return SQLITE_ERROR; | ||
| 2361 | - } | ||
| 2362 | - | ||
| 2363 | - // any other tokens left should be column-level options , ex `key=value` | ||
| 2364 | - // ex `distance_metric=L2 distance_metric=cosine` should error | ||
| 2365 | - while (1) { | ||
| 2366 | - // should be EOF or identifier (option key) | ||
| 2367 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2368 | - if (rc == VEC0_TOKEN_RESULT_EOF) { | ||
| 2369 | - break; | ||
| 2370 | - } | ||
| 2371 | - | ||
| 2372 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 2373 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2374 | - return SQLITE_ERROR; | ||
| 2375 | - } | ||
| 2376 | - | ||
| 2377 | - char *key = token.start; | ||
| 2378 | - int keyLength = token.end - token.start; | ||
| 2379 | - | ||
| 2380 | - if (sqlite3_strnicmp(key, "distance_metric", keyLength) == 0) { | ||
| 2381 | - | ||
| 2382 | - if (elementType == SQLITE_VEC_ELEMENT_TYPE_BIT) { | ||
| 2383 | - return SQLITE_ERROR; | ||
| 2384 | - } | ||
| 2385 | - // ensure equal sign after distance_metric | ||
| 2386 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2387 | - if (rc != VEC0_TOKEN_RESULT_SOME && token.token_type != TOKEN_TYPE_EQ) { | ||
| 2388 | - return SQLITE_ERROR; | ||
| 2389 | - } | ||
| 2390 | - | ||
| 2391 | - // distance_metric value, an identifier (L2, cosine, etc) | ||
| 2392 | - rc = vec0_scanner_next(&scanner, &token); | ||
| 2393 | - if (rc != VEC0_TOKEN_RESULT_SOME && | ||
| 2394 | - token.token_type != TOKEN_TYPE_IDENTIFIER) { | ||
| 2395 | - return SQLITE_ERROR; | ||
| 2396 | - } | ||
| 2397 | - | ||
| 2398 | - char *value = token.start; | ||
| 2399 | - int valueLength = token.end - token.start; | ||
| 2400 | - if (sqlite3_strnicmp(value, "l2", valueLength) == 0) { | ||
| 2401 | - distanceMetric = VEC0_DISTANCE_METRIC_L2; | ||
| 2402 | - } else if (sqlite3_strnicmp(value, "l1", valueLength) == 0) { | ||
| 2403 | - distanceMetric = VEC0_DISTANCE_METRIC_L1; | ||
| 2404 | - } else if (sqlite3_strnicmp(value, "cosine", valueLength) == 0) { | ||
| 2405 | - distanceMetric = VEC0_DISTANCE_METRIC_COSINE; | ||
| 2406 | - } else { | ||
| 2407 | - return SQLITE_ERROR; | ||
| 2408 | - } | ||
| 2409 | - } | ||
| 2410 | - // unknown key | ||
| 2411 | - else { | ||
| 2412 | - return SQLITE_ERROR; | ||
| 2413 | - } | ||
| 2414 | - } | ||
| 2415 | - | ||
| 2416 | - outColumn->name = sqlite3_mprintf("%.*s", nameLength, name); | ||
| 2417 | - if (!outColumn->name) { | ||
| 2418 | - return SQLITE_ERROR; | ||
| 2419 | - } | ||
| 2420 | - outColumn->name_length = nameLength; | ||
| 2421 | - outColumn->distance_metric = distanceMetric; | ||
| 2422 | - outColumn->element_type = elementType; | ||
| 2423 | - outColumn->dimensions = dimensions; | ||
| 2424 | - return SQLITE_OK; | ||
| 2425 | -} | ||
| 2426 | - | ||
| 2427 | -#pragma region vec_each table function | ||
| 2428 | - | ||
| 2429 | -typedef struct vec_each_vtab vec_each_vtab; | ||
| 2430 | -struct vec_each_vtab { | ||
| 2431 | - sqlite3_vtab base; | ||
| 2432 | -}; | ||
| 2433 | - | ||
| 2434 | -typedef struct vec_each_cursor vec_each_cursor; | ||
| 2435 | -struct vec_each_cursor { | ||
| 2436 | - sqlite3_vtab_cursor base; | ||
| 2437 | - i64 iRowid; | ||
| 2438 | - enum VectorElementType vector_type; | ||
| 2439 | - void *vector; | ||
| 2440 | - size_t dimensions; | ||
| 2441 | - vector_cleanup cleanup; | ||
| 2442 | -}; | ||
| 2443 | - | ||
| 2444 | -static int vec_eachConnect(sqlite3 *db, void *pAux, int argc, | ||
| 2445 | - const char *const *argv, sqlite3_vtab **ppVtab, | ||
| 2446 | - char **pzErr) { | ||
| 2447 | - UNUSED_PARAMETER(pAux); | ||
| 2448 | - UNUSED_PARAMETER(argc); | ||
| 2449 | - UNUSED_PARAMETER(argv); | ||
| 2450 | - UNUSED_PARAMETER(pzErr); | ||
| 2451 | - vec_each_vtab *pNew; | ||
| 2452 | - int rc; | ||
| 2453 | - | ||
| 2454 | - rc = sqlite3_declare_vtab(db, "CREATE TABLE x(value, vector hidden)"); | ||
| 2455 | -#define VEC_EACH_COLUMN_VALUE 0 | ||
| 2456 | -#define VEC_EACH_COLUMN_VECTOR 1 | ||
| 2457 | - if (rc == SQLITE_OK) { | ||
| 2458 | - pNew = sqlite3_malloc(sizeof(*pNew)); | ||
| 2459 | - *ppVtab = (sqlite3_vtab *)pNew; | ||
| 2460 | - if (pNew == 0) | ||
| 2461 | - return SQLITE_NOMEM; | ||
| 2462 | - memset(pNew, 0, sizeof(*pNew)); | ||
| 2463 | - } | ||
| 2464 | - return rc; | ||
| 2465 | -} | ||
| 2466 | - | ||
| 2467 | -static int vec_eachDisconnect(sqlite3_vtab *pVtab) { | ||
| 2468 | - vec_each_vtab *p = (vec_each_vtab *)pVtab; | ||
| 2469 | - sqlite3_free(p); | ||
| 2470 | - return SQLITE_OK; | ||
| 2471 | -} | ||
| 2472 | - | ||
| 2473 | -static int vec_eachOpen(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor) { | ||
| 2474 | - UNUSED_PARAMETER(p); | ||
| 2475 | - vec_each_cursor *pCur; | ||
| 2476 | - pCur = sqlite3_malloc(sizeof(*pCur)); | ||
| 2477 | - if (pCur == 0) | ||
| 2478 | - return SQLITE_NOMEM; | ||
| 2479 | - memset(pCur, 0, sizeof(*pCur)); | ||
| 2480 | - *ppCursor = &pCur->base; | ||
| 2481 | - return SQLITE_OK; | ||
| 2482 | -} | ||
| 2483 | - | ||
| 2484 | -static int vec_eachClose(sqlite3_vtab_cursor *cur) { | ||
| 2485 | - vec_each_cursor *pCur = (vec_each_cursor *)cur; | ||
| 2486 | - pCur->cleanup(pCur->vector); | ||
| 2487 | - sqlite3_free(pCur); | ||
| 2488 | - return SQLITE_OK; | ||
| 2489 | -} | ||
| 2490 | - | ||
| 2491 | -static int vec_eachBestIndex(sqlite3_vtab *pVTab, | ||
| 2492 | - sqlite3_index_info *pIdxInfo) { | ||
| 2493 | - UNUSED_PARAMETER(pVTab); | ||
| 2494 | - int hasVector = 0; | ||
| 2495 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | ||
| 2496 | - const struct sqlite3_index_constraint *pCons = &pIdxInfo->aConstraint[i]; | ||
| 2497 | - // printf("i=%d iColumn=%d, op=%d, usable=%d\n", i, pCons->iColumn, | ||
| 2498 | - // pCons->op, pCons->usable); | ||
| 2499 | - switch (pCons->iColumn) { | ||
| 2500 | - case VEC_EACH_COLUMN_VECTOR: { | ||
| 2501 | - if (pCons->op == SQLITE_INDEX_CONSTRAINT_EQ && pCons->usable) { | ||
| 2502 | - hasVector = 1; | ||
| 2503 | - pIdxInfo->aConstraintUsage[i].argvIndex = 1; | ||
| 2504 | - pIdxInfo->aConstraintUsage[i].omit = 1; | ||
| 2505 | - } | ||
| 2506 | - break; | ||
| 2507 | - } | ||
| 2508 | - } | ||
| 2509 | - } | ||
| 2510 | - if (!hasVector) { | ||
| 2511 | - return SQLITE_CONSTRAINT; | ||
| 2512 | - } | ||
| 2513 | - | ||
| 2514 | - pIdxInfo->estimatedCost = (double)100000; | ||
| 2515 | - pIdxInfo->estimatedRows = 100000; | ||
| 2516 | - | ||
| 2517 | - return SQLITE_OK; | ||
| 2518 | -} | ||
| 2519 | - | ||
| 2520 | -static int vec_eachFilter(sqlite3_vtab_cursor *pVtabCursor, int idxNum, | ||
| 2521 | - const char *idxStr, int argc, sqlite3_value **argv) { | ||
| 2522 | - UNUSED_PARAMETER(idxNum); | ||
| 2523 | - UNUSED_PARAMETER(idxStr); | ||
| 2524 | - assert(argc == 1); | ||
| 2525 | - vec_each_cursor *pCur = (vec_each_cursor *)pVtabCursor; | ||
| 2526 | - | ||
| 2527 | - if (pCur->vector) { | ||
| 2528 | - pCur->cleanup(pCur->vector); | ||
| 2529 | - pCur->vector = NULL; | ||
| 2530 | - } | ||
| 2531 | - | ||
| 2532 | - char *pzErrMsg; | ||
| 2533 | - int rc = vector_from_value(argv[0], &pCur->vector, &pCur->dimensions, | ||
| 2534 | - &pCur->vector_type, &pCur->cleanup, &pzErrMsg); | ||
| 2535 | - if (rc != SQLITE_OK) { | ||
| 2536 | - return SQLITE_ERROR; | ||
| 2537 | - } | ||
| 2538 | - pCur->iRowid = 0; | ||
| 2539 | - return SQLITE_OK; | ||
| 2540 | -} | ||
| 2541 | - | ||
| 2542 | -static int vec_eachRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid) { | ||
| 2543 | - vec_each_cursor *pCur = (vec_each_cursor *)cur; | ||
| 2544 | - *pRowid = pCur->iRowid; | ||
| 2545 | - return SQLITE_OK; | ||
| 2546 | -} | ||
| 2547 | - | ||
| 2548 | -static int vec_eachEof(sqlite3_vtab_cursor *cur) { | ||
| 2549 | - vec_each_cursor *pCur = (vec_each_cursor *)cur; | ||
| 2550 | - return pCur->iRowid >= (i64)pCur->dimensions; | ||
| 2551 | -} | ||
| 2552 | - | ||
| 2553 | -static int vec_eachNext(sqlite3_vtab_cursor *cur) { | ||
| 2554 | - vec_each_cursor *pCur = (vec_each_cursor *)cur; | ||
| 2555 | - pCur->iRowid++; | ||
| 2556 | - return SQLITE_OK; | ||
| 2557 | -} | ||
| 2558 | - | ||
| 2559 | -static int vec_eachColumn(sqlite3_vtab_cursor *cur, sqlite3_context *context, | ||
| 2560 | - int i) { | ||
| 2561 | - vec_each_cursor *pCur = (vec_each_cursor *)cur; | ||
| 2562 | - switch (i) { | ||
| 2563 | - case VEC_EACH_COLUMN_VALUE: | ||
| 2564 | - switch (pCur->vector_type) { | ||
| 2565 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 2566 | - sqlite3_result_double(context, ((f32 *)pCur->vector)[pCur->iRowid]); | ||
| 2567 | - break; | ||
| 2568 | - } | ||
| 2569 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 2570 | - u8 x = ((u8 *)pCur->vector)[pCur->iRowid / CHAR_BIT]; | ||
| 2571 | - sqlite3_result_int(context, | ||
| 2572 | - (x & (0b10000000 >> ((pCur->iRowid % CHAR_BIT)))) > 0); | ||
| 2573 | - break; | ||
| 2574 | - } | ||
| 2575 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | ||
| 2576 | - sqlite3_result_int(context, ((i8 *)pCur->vector)[pCur->iRowid]); | ||
| 2577 | - break; | ||
| 2578 | - } | ||
| 2579 | - } | ||
| 2580 | - | ||
| 2581 | - break; | ||
| 2582 | - } | ||
| 2583 | - return SQLITE_OK; | ||
| 2584 | -} | ||
| 2585 | - | ||
| 2586 | -static sqlite3_module vec_eachModule = { | ||
| 2587 | - /* iVersion */ 0, | ||
| 2588 | - /* xCreate */ 0, | ||
| 2589 | - /* xConnect */ vec_eachConnect, | ||
| 2590 | - /* xBestIndex */ vec_eachBestIndex, | ||
| 2591 | - /* xDisconnect */ vec_eachDisconnect, | ||
| 2592 | - /* xDestroy */ 0, | ||
| 2593 | - /* xOpen */ vec_eachOpen, | ||
| 2594 | - /* xClose */ vec_eachClose, | ||
| 2595 | - /* xFilter */ vec_eachFilter, | ||
| 2596 | - /* xNext */ vec_eachNext, | ||
| 2597 | - /* xEof */ vec_eachEof, | ||
| 2598 | - /* xColumn */ vec_eachColumn, | ||
| 2599 | - /* xRowid */ vec_eachRowid, | ||
| 2600 | - /* xUpdate */ 0, | ||
| 2601 | - /* xBegin */ 0, | ||
| 2602 | - /* xSync */ 0, | ||
| 2603 | - /* xCommit */ 0, | ||
| 2604 | - /* xRollback */ 0, | ||
| 2605 | - /* xFindMethod */ 0, | ||
| 2606 | - /* xRename */ 0, | ||
| 2607 | - /* xSavepoint */ 0, | ||
| 2608 | - /* xRelease */ 0, | ||
| 2609 | - /* xRollbackTo */ 0, | ||
| 2610 | - /* xShadowName */ 0, | ||
| 2611 | -#if SQLITE_VERSION_NUMBER >= 3044000 | ||
| 2612 | - /* xIntegrity */ 0 | ||
| 2613 | -#endif | ||
| 2614 | -}; | ||
| 2615 | - | ||
| 2616 | -#pragma endregion | ||
| 2617 | - | ||
| 2618 | -#pragma region vec_npy_each table function | ||
| 2619 | - | ||
| 2620 | -enum NpyTokenType { | ||
| 2621 | - NPY_TOKEN_TYPE_IDENTIFIER, | ||
| 2622 | - NPY_TOKEN_TYPE_NUMBER, | ||
| 2623 | - NPY_TOKEN_TYPE_LPAREN, | ||
| 2624 | - NPY_TOKEN_TYPE_RPAREN, | ||
| 2625 | - NPY_TOKEN_TYPE_LBRACE, | ||
| 2626 | - NPY_TOKEN_TYPE_RBRACE, | ||
| 2627 | - NPY_TOKEN_TYPE_COLON, | ||
| 2628 | - NPY_TOKEN_TYPE_COMMA, | ||
| 2629 | - NPY_TOKEN_TYPE_STRING, | ||
| 2630 | - NPY_TOKEN_TYPE_FALSE, | ||
| 2631 | -}; | ||
| 2632 | - | ||
| 2633 | -struct NpyToken { | ||
| 2634 | - enum NpyTokenType token_type; | ||
| 2635 | - unsigned char *start; | ||
| 2636 | - unsigned char *end; | ||
| 2637 | -}; | ||
| 2638 | - | ||
| 2639 | -int npy_token_next(unsigned char *start, unsigned char *end, | ||
| 2640 | - struct NpyToken *out) { | ||
| 2641 | - unsigned char *ptr = start; | ||
| 2642 | - while (ptr < end) { | ||
| 2643 | - unsigned char curr = *ptr; | ||
| 2644 | - if (is_whitespace(curr)) { | ||
| 2645 | - ptr++; | ||
| 2646 | - continue; | ||
| 2647 | - } else if (curr == '(') { | ||
| 2648 | - out->start = ptr++; | ||
| 2649 | - out->end = ptr; | ||
| 2650 | - out->token_type = NPY_TOKEN_TYPE_LPAREN; | ||
| 2651 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 2652 | - } else if (curr == ')') { | ||
| 2653 | - out->start = ptr++; | ||
| 2654 | - out->end = ptr; | ||
| 2655 | - out->token_type = NPY_TOKEN_TYPE_RPAREN; | ||
| 2656 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 2657 | - } else if (curr == '{') { | ||
| 2658 | - out->start = ptr++; | ||
| 2659 | - out->end = ptr; | ||
| 2660 | - out->token_type = NPY_TOKEN_TYPE_LBRACE; | ||
| 2661 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 2662 | - } else if (curr == '}') { | ||
| 2663 | - out->start = ptr++; | ||
| 2664 | - out->end = ptr; | ||
| 2665 | - out->token_type = NPY_TOKEN_TYPE_RBRACE; | ||
| 2666 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 2667 | - } else if (curr == ':') { | ||
| 2668 | - out->start = ptr++; | ||
| 2669 | - out->end = ptr; | ||
| 2670 | - out->token_type = NPY_TOKEN_TYPE_COLON; | ||
| 2671 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 2672 | - } else if (curr == ',') { | ||
| 2673 | - out->start = ptr++; | ||
| 2674 | - out->end = ptr; | ||
| 2675 | - out->token_type = NPY_TOKEN_TYPE_COMMA; | ||
| 2676 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 2677 | - } else if (curr == '\'') { | ||
| 2678 | - unsigned char *start = ptr; | ||
| 2679 | - ptr++; | ||
| 2680 | - while (ptr < end) { | ||
| 2681 | - if ((*ptr) == '\'') { | ||
| 2682 | - break; | ||
| 2683 | - } | ||
| 2684 | - ptr++; | ||
| 2685 | - } | ||
| 2686 | - if ((*ptr) != '\'') { | ||
| 2687 | - return VEC0_TOKEN_RESULT_ERROR; | ||
| 2688 | - } | ||
| 2689 | - out->start = start; | ||
| 2690 | - out->end = ++ptr; | ||
| 2691 | - out->token_type = NPY_TOKEN_TYPE_STRING; | ||
| 2692 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 2693 | - } else if (curr == 'F' && | ||
| 2694 | - strncmp((char *)ptr, "False", strlen("False")) == 0) { | ||
| 2695 | - out->start = ptr; | ||
| 2696 | - out->end = (ptr + (int)strlen("False")); | ||
| 2697 | - ptr = out->end; | ||
| 2698 | - out->token_type = NPY_TOKEN_TYPE_FALSE; | ||
| 2699 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 2700 | - } else if (is_digit(curr)) { | ||
| 2701 | - unsigned char *start = ptr; | ||
| 2702 | - while (ptr < end && (is_digit(*ptr))) { | ||
| 2703 | - ptr++; | ||
| 2704 | - } | ||
| 2705 | - out->start = start; | ||
| 2706 | - out->end = ptr; | ||
| 2707 | - out->token_type = NPY_TOKEN_TYPE_NUMBER; | ||
| 2708 | - return VEC0_TOKEN_RESULT_SOME; | ||
| 2709 | - } else { | ||
| 2710 | - return VEC0_TOKEN_RESULT_ERROR; | ||
| 2711 | - } | ||
| 2712 | - } | ||
| 2713 | - return VEC0_TOKEN_RESULT_ERROR; | ||
| 2714 | -} | ||
| 2715 | - | ||
| 2716 | -struct NpyScanner { | ||
| 2717 | - unsigned char *start; | ||
| 2718 | - unsigned char *end; | ||
| 2719 | - unsigned char *ptr; | ||
| 2720 | -}; | ||
| 2721 | - | ||
| 2722 | -void npy_scanner_init(struct NpyScanner *scanner, const unsigned char *source, | ||
| 2723 | - int source_length) { | ||
| 2724 | - scanner->start = (unsigned char *)source; | ||
| 2725 | - scanner->end = (unsigned char *)source + source_length; | ||
| 2726 | - scanner->ptr = (unsigned char *)source; | ||
| 2727 | -} | ||
| 2728 | - | ||
| 2729 | -int npy_scanner_next(struct NpyScanner *scanner, struct NpyToken *out) { | ||
| 2730 | - int rc = npy_token_next(scanner->start, scanner->end, out); | ||
| 2731 | - if (rc == VEC0_TOKEN_RESULT_SOME) { | ||
| 2732 | - scanner->start = out->end; | ||
| 2733 | - } | ||
| 2734 | - return rc; | ||
| 2735 | -} | ||
| 2736 | - | ||
| 2737 | -#define NPY_PARSE_ERROR "Error parsing numpy array: " | ||
| 2738 | -int parse_npy_header(sqlite3_vtab *pVTab, const unsigned char *header, | ||
| 2739 | - size_t headerLength, | ||
| 2740 | - enum VectorElementType *out_element_type, | ||
| 2741 | - int *fortran_order, size_t *numElements, | ||
| 2742 | - size_t *numDimensions) { | ||
| 2743 | - | ||
| 2744 | - struct NpyScanner scanner; | ||
| 2745 | - struct NpyToken token; | ||
| 2746 | - int rc; | ||
| 2747 | - npy_scanner_init(&scanner, header, headerLength); | ||
| 2748 | - | ||
| 2749 | - if (npy_scanner_next(&scanner, &token) != VEC0_TOKEN_RESULT_SOME && | ||
| 2750 | - token.token_type != NPY_TOKEN_TYPE_LBRACE) { | ||
| 2751 | - vtab_set_error(pVTab, | ||
| 2752 | - NPY_PARSE_ERROR "numpy header did not start with '{'"); | ||
| 2753 | - return SQLITE_ERROR; | ||
| 2754 | - } | ||
| 2755 | - while (1) { | ||
| 2756 | - rc = npy_scanner_next(&scanner, &token); | ||
| 2757 | - if (rc != VEC0_TOKEN_RESULT_SOME) { | ||
| 2758 | - vtab_set_error(pVTab, NPY_PARSE_ERROR "expected key in numpy header"); | ||
| 2759 | - return SQLITE_ERROR; | ||
| 2760 | - } | ||
| 2761 | - | ||
| 2762 | - if (token.token_type == NPY_TOKEN_TYPE_RBRACE) { | ||
| 2763 | - break; | ||
| 2764 | - } | ||
| 2765 | - if (token.token_type != NPY_TOKEN_TYPE_STRING) { | ||
| 2766 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | ||
| 2767 | - "expected a string as key in numpy header"); | ||
| 2768 | - return SQLITE_ERROR; | ||
| 2769 | - } | ||
| 2770 | - unsigned char *key = token.start; | ||
| 2771 | - | ||
| 2772 | - rc = npy_scanner_next(&scanner, &token); | ||
| 2773 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | ||
| 2774 | - (token.token_type != NPY_TOKEN_TYPE_COLON)) { | ||
| 2775 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | ||
| 2776 | - "expected a ':' after key in numpy header"); | ||
| 2777 | - return SQLITE_ERROR; | ||
| 2778 | - } | ||
| 2779 | - | ||
| 2780 | - if (strncmp((char *)key, "'descr'", strlen("'descr'")) == 0) { | ||
| 2781 | - rc = npy_scanner_next(&scanner, &token); | ||
| 2782 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | ||
| 2783 | - (token.token_type != NPY_TOKEN_TYPE_STRING)) { | ||
| 2784 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | ||
| 2785 | - "expected a string value after 'descr' key"); | ||
| 2786 | - return SQLITE_ERROR; | ||
| 2787 | - } | ||
| 2788 | - if (strncmp((char *)token.start, "'<f4'", strlen("'<f4'")) != 0) { | ||
| 2789 | - vtab_set_error( | ||
| 2790 | - pVTab, NPY_PARSE_ERROR | ||
| 2791 | - "Only '<f4' values are supported in sqlite-vec numpy functions"); | ||
| 2792 | - return SQLITE_ERROR; | ||
| 2793 | - } | ||
| 2794 | - *out_element_type = SQLITE_VEC_ELEMENT_TYPE_FLOAT32; | ||
| 2795 | - } else if (strncmp((char *)key, "'fortran_order'", | ||
| 2796 | - strlen("'fortran_order'")) == 0) { | ||
| 2797 | - rc = npy_scanner_next(&scanner, &token); | ||
| 2798 | - if (rc != VEC0_TOKEN_RESULT_SOME || | ||
| 2799 | - token.token_type != NPY_TOKEN_TYPE_FALSE) { | ||
| 2800 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | ||
| 2801 | - "Only fortran_order = False is supported in sqlite-vec " | ||
| 2802 | - "numpy functions"); | ||
| 2803 | - return SQLITE_ERROR; | ||
| 2804 | - } | ||
| 2805 | - *fortran_order = 0; | ||
| 2806 | - } else if (strncmp((char *)key, "'shape'", strlen("'shape'")) == 0) { | ||
| 2807 | - // "(xxx, xxx)" OR (xxx,) | ||
| 2808 | - size_t first; | ||
| 2809 | - rc = npy_scanner_next(&scanner, &token); | ||
| 2810 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | ||
| 2811 | - (token.token_type != NPY_TOKEN_TYPE_LPAREN)) { | ||
| 2812 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | ||
| 2813 | - "Expected left parenthesis '(' after shape key"); | ||
| 2814 | - return SQLITE_ERROR; | ||
| 2815 | - } | ||
| 2816 | - | ||
| 2817 | - rc = npy_scanner_next(&scanner, &token); | ||
| 2818 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | ||
| 2819 | - (token.token_type != NPY_TOKEN_TYPE_NUMBER)) { | ||
| 2820 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | ||
| 2821 | - "Expected an initial number in shape value"); | ||
| 2822 | - return SQLITE_ERROR; | ||
| 2823 | - } | ||
| 2824 | - first = strtol((char *)token.start, NULL, 10); | ||
| 2825 | - | ||
| 2826 | - rc = npy_scanner_next(&scanner, &token); | ||
| 2827 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | ||
| 2828 | - (token.token_type != NPY_TOKEN_TYPE_COMMA)) { | ||
| 2829 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | ||
| 2830 | - "Expected comma after first shape value"); | ||
| 2831 | - return SQLITE_ERROR; | ||
| 2832 | - } | ||
| 2833 | - | ||
| 2834 | - rc = npy_scanner_next(&scanner, &token); | ||
| 2835 | - if (rc != VEC0_TOKEN_RESULT_SOME) { | ||
| 2836 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | ||
| 2837 | - "unexpected header EOF while parsing shape"); | ||
| 2838 | - return SQLITE_ERROR; | ||
| 2839 | - } | ||
| 2840 | - if (token.token_type == NPY_TOKEN_TYPE_NUMBER) { | ||
| 2841 | - *numElements = first; | ||
| 2842 | - *numDimensions = strtol((char *)token.start, NULL, 10); | ||
| 2843 | - rc = npy_scanner_next(&scanner, &token); | ||
| 2844 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | ||
| 2845 | - (token.token_type != NPY_TOKEN_TYPE_RPAREN)) { | ||
| 2846 | - vtab_set_error(pVTab, NPY_PARSE_ERROR | ||
| 2847 | - "expected right parenthesis after shape value"); | ||
| 2848 | - return SQLITE_ERROR; | ||
| 2849 | - } | ||
| 2850 | - } else if (token.token_type == NPY_TOKEN_TYPE_RPAREN) { | ||
| 2851 | - // '(0,)' means an empty array! | ||
| 2852 | - *numElements = first ? 1 : 0; | ||
| 2853 | - *numDimensions = first; | ||
| 2854 | - } else { | ||
| 2855 | - vtab_set_error(pVTab, NPY_PARSE_ERROR "unknown type in shape value"); | ||
| 2856 | - return SQLITE_ERROR; | ||
| 2857 | - } | ||
| 2858 | - } else { | ||
| 2859 | - vtab_set_error(pVTab, NPY_PARSE_ERROR "unknown key in numpy header"); | ||
| 2860 | - return SQLITE_ERROR; | ||
| 2861 | - } | ||
| 2862 | - | ||
| 2863 | - rc = npy_scanner_next(&scanner, &token); | ||
| 2864 | - if ((rc != VEC0_TOKEN_RESULT_SOME) || | ||
| 2865 | - (token.token_type != NPY_TOKEN_TYPE_COMMA)) { | ||
| 2866 | - vtab_set_error(pVTab, NPY_PARSE_ERROR "unknown extra token after value"); | ||
| 2867 | - return SQLITE_ERROR; | ||
| 2868 | - } | ||
| 2869 | - } | ||
| 2870 | - | ||
| 2871 | - return SQLITE_OK; | ||
| 2872 | -} | ||
| 2873 | - | ||
| 2874 | -typedef struct vec_npy_each_vtab vec_npy_each_vtab; | ||
| 2875 | -struct vec_npy_each_vtab { | ||
| 2876 | - sqlite3_vtab base; | ||
| 2877 | -}; | ||
| 2878 | - | ||
| 2879 | -typedef enum { | ||
| 2880 | - VEC_NPY_EACH_INPUT_BUFFER, | ||
| 2881 | - VEC_NPY_EACH_INPUT_FILE, | ||
| 2882 | -} vec_npy_each_input_type; | ||
| 2883 | - | ||
| 2884 | -typedef struct vec_npy_each_cursor vec_npy_each_cursor; | ||
| 2885 | -struct vec_npy_each_cursor { | ||
| 2886 | - sqlite3_vtab_cursor base; | ||
| 2887 | - i64 iRowid; | ||
| 2888 | - // sqlite-vec compatible type of vector | ||
| 2889 | - enum VectorElementType elementType; | ||
| 2890 | - // number of vectors in the npy array | ||
| 2891 | - size_t nElements; | ||
| 2892 | - // number of dimensions each vector has | ||
| 2893 | - size_t nDimensions; | ||
| 2894 | - | ||
| 2895 | - vec_npy_each_input_type input_type; | ||
| 2896 | - | ||
| 2897 | - // when input_type == VEC_NPY_EACH_INPUT_BUFFER | ||
| 2898 | - | ||
| 2899 | - // Buffer containing the vector data, when reading from an in-memory buffer. | ||
| 2900 | - // Size: nElements * nDimensions * element_size | ||
| 2901 | - // Clean up with sqlite3_free() once complete | ||
| 2902 | - void *vector; | ||
| 2903 | - | ||
| 2904 | - // when input_type == VEC_NPY_EACH_INPUT_FILE | ||
| 2905 | - | ||
| 2906 | - // Opened npy file, when reading from a file. | ||
| 2907 | - // fclose() when complete. | ||
| 2908 | -#ifndef SQLITE_VEC_OMIT_FS | ||
| 2909 | - FILE *file; | ||
| 2910 | -#endif | ||
| 2911 | - | ||
| 2912 | - // an in-memory buffer containing a portion of the npy array. | ||
| 2913 | - // Used for faster reading, instead of calling fread a lot. | ||
| 2914 | - // Will have a byte-size of fileBufferSize | ||
| 2915 | - void *chunksBuffer; | ||
| 2916 | - // size of allocated fileBuffer in bytes | ||
| 2917 | - size_t chunksBufferSize; | ||
| 2918 | - //// Maximum length of the buffer, in terms of number of vectors. | ||
| 2919 | - size_t maxChunks; | ||
| 2920 | - | ||
| 2921 | - // Counter index of the current vector into of fileBuffer to yield. | ||
| 2922 | - // Starts at 0 once fileBuffer is read, and iterates to bufferLength. | ||
| 2923 | - // Resets to 0 once that "buffer" is yielded and a new one is read. | ||
| 2924 | - size_t currentChunkIndex; | ||
| 2925 | - size_t currentChunkSize; | ||
| 2926 | - | ||
| 2927 | - // 0 when there are still more elements to read/yield, 1 when complete. | ||
| 2928 | - int eof; | ||
| 2929 | -}; | ||
| 2930 | - | ||
| 2931 | -static unsigned char NPY_MAGIC[6] = "\x93NUMPY"; | ||
| 2932 | - | ||
| 2933 | -#ifndef SQLITE_VEC_OMIT_FS | ||
| 2934 | -int parse_npy_file(sqlite3_vtab *pVTab, FILE *file, vec_npy_each_cursor *pCur) { | ||
| 2935 | - int n; | ||
| 2936 | - fseek(file, 0, SEEK_END); | ||
| 2937 | - long fileSize = ftell(file); | ||
| 2938 | - | ||
| 2939 | - fseek(file, 0L, SEEK_SET); | ||
| 2940 | - | ||
| 2941 | - unsigned char header[10]; | ||
| 2942 | - n = fread(&header, sizeof(unsigned char), 10, file); | ||
| 2943 | - if (n != 10) { | ||
| 2944 | - vtab_set_error(pVTab, "numpy array file too short"); | ||
| 2945 | - return SQLITE_ERROR; | ||
| 2946 | - } | ||
| 2947 | - | ||
| 2948 | - if (memcmp(NPY_MAGIC, header, sizeof(NPY_MAGIC)) != 0) { | ||
| 2949 | - vtab_set_error(pVTab, | ||
| 2950 | - "numpy array file does not contain the 'magic' header"); | ||
| 2951 | - return SQLITE_ERROR; | ||
| 2952 | - } | ||
| 2953 | - | ||
| 2954 | - u8 major = header[6]; | ||
| 2955 | - u8 minor = header[7]; | ||
| 2956 | - uint16_t headerLength = 0; | ||
| 2957 | - memcpy(&headerLength, &header[8], sizeof(uint16_t)); | ||
| 2958 | - | ||
| 2959 | - size_t totalHeaderLength = sizeof(NPY_MAGIC) + sizeof(major) + sizeof(minor) + | ||
| 2960 | - sizeof(headerLength) + headerLength; | ||
| 2961 | - i32 dataSize = fileSize - totalHeaderLength; | ||
| 2962 | - if (dataSize < 0) { | ||
| 2963 | - vtab_set_error(pVTab, "numpy array file header length is invalid"); | ||
| 2964 | - return SQLITE_ERROR; | ||
| 2965 | - } | ||
| 2966 | - | ||
| 2967 | - unsigned char *headerX = sqlite3_malloc(headerLength); | ||
| 2968 | - if (headerLength && !headerX) { | ||
| 2969 | - return SQLITE_NOMEM; | ||
| 2970 | - } | ||
| 2971 | - | ||
| 2972 | - n = fread(headerX, sizeof(char), headerLength, file); | ||
| 2973 | - if (n != headerLength) { | ||
| 2974 | - sqlite3_free(headerX); | ||
| 2975 | - vtab_set_error(pVTab, "numpy array file header length is invalid"); | ||
| 2976 | - return SQLITE_ERROR; | ||
| 2977 | - } | ||
| 2978 | - | ||
| 2979 | - int fortran_order; | ||
| 2980 | - enum VectorElementType element_type; | ||
| 2981 | - size_t numElements; | ||
| 2982 | - size_t numDimensions; | ||
| 2983 | - int rc = parse_npy_header(pVTab, headerX, headerLength, &element_type, | ||
| 2984 | - &fortran_order, &numElements, &numDimensions); | ||
| 2985 | - sqlite3_free(headerX); | ||
| 2986 | - if (rc != SQLITE_OK) { | ||
| 2987 | - // parse_npy_header already attackes an error emssage | ||
| 2988 | - return rc; | ||
| 2989 | - } | ||
| 2990 | - | ||
| 2991 | - i32 expectedDataSize = | ||
| 2992 | - numElements * vector_byte_size(element_type, numDimensions); | ||
| 2993 | - if (expectedDataSize != dataSize) { | ||
| 2994 | - vtab_set_error( | ||
| 2995 | - pVTab, "numpy array file error: Expected a data size of %d, found %d", | ||
| 2996 | - expectedDataSize, dataSize); | ||
| 2997 | - return SQLITE_ERROR; | ||
| 2998 | - } | ||
| 2999 | - | ||
| 3000 | - pCur->maxChunks = 1024; | ||
| 3001 | - pCur->chunksBufferSize = | ||
| 3002 | - (vector_byte_size(element_type, numDimensions)) * pCur->maxChunks; | ||
| 3003 | - pCur->chunksBuffer = sqlite3_malloc(pCur->chunksBufferSize); | ||
| 3004 | - if (pCur->chunksBufferSize && !pCur->chunksBuffer) { | ||
| 3005 | - return SQLITE_NOMEM; | ||
| 3006 | - } | ||
| 3007 | - | ||
| 3008 | - pCur->currentChunkSize = | ||
| 3009 | - fread(pCur->chunksBuffer, vector_byte_size(element_type, numDimensions), | ||
| 3010 | - pCur->maxChunks, file); | ||
| 3011 | - | ||
| 3012 | - pCur->currentChunkIndex = 0; | ||
| 3013 | - pCur->elementType = element_type; | ||
| 3014 | - pCur->nElements = numElements; | ||
| 3015 | - pCur->nDimensions = numDimensions; | ||
| 3016 | - pCur->input_type = VEC_NPY_EACH_INPUT_FILE; | ||
| 3017 | - | ||
| 3018 | - pCur->eof = pCur->currentChunkSize == 0; | ||
| 3019 | - pCur->file = file; | ||
| 3020 | - return SQLITE_OK; | ||
| 3021 | -} | ||
| 3022 | -#endif | ||
| 3023 | - | ||
| 3024 | -int parse_npy_buffer(sqlite3_vtab *pVTab, const unsigned char *buffer, | ||
| 3025 | - int bufferLength, void **data, size_t *numElements, | ||
| 3026 | - size_t *numDimensions, | ||
| 3027 | - enum VectorElementType *element_type) { | ||
| 3028 | - | ||
| 3029 | - if (bufferLength < 10) { | ||
| 3030 | - // IMP: V03312_20150 | ||
| 3031 | - vtab_set_error(pVTab, "numpy array too short"); | ||
| 3032 | - return SQLITE_ERROR; | ||
| 3033 | - } | ||
| 3034 | - if (memcmp(NPY_MAGIC, buffer, sizeof(NPY_MAGIC)) != 0) { | ||
| 3035 | - // V11954_28792 | ||
| 3036 | - vtab_set_error(pVTab, "numpy array does not contain the 'magic' header"); | ||
| 3037 | - return SQLITE_ERROR; | ||
| 3038 | - } | ||
| 3039 | - | ||
| 3040 | - u8 major = buffer[6]; | ||
| 3041 | - u8 minor = buffer[7]; | ||
| 3042 | - uint16_t headerLength = 0; | ||
| 3043 | - memcpy(&headerLength, &buffer[8], sizeof(uint16_t)); | ||
| 3044 | - | ||
| 3045 | - i32 totalHeaderLength = sizeof(NPY_MAGIC) + sizeof(major) + sizeof(minor) + | ||
| 3046 | - sizeof(headerLength) + headerLength; | ||
| 3047 | - i32 dataSize = bufferLength - totalHeaderLength; | ||
| 3048 | - | ||
| 3049 | - if (dataSize < 0) { | ||
| 3050 | - vtab_set_error(pVTab, "numpy array header length is invalid"); | ||
| 3051 | - return SQLITE_ERROR; | ||
| 3052 | - } | ||
| 3053 | - | ||
| 3054 | - const unsigned char *header = &buffer[10]; | ||
| 3055 | - int fortran_order; | ||
| 3056 | - | ||
| 3057 | - int rc = parse_npy_header(pVTab, header, headerLength, element_type, | ||
| 3058 | - &fortran_order, numElements, numDimensions); | ||
| 3059 | - if (rc != SQLITE_OK) { | ||
| 3060 | - return rc; | ||
| 3061 | - } | ||
| 3062 | - | ||
| 3063 | - i32 expectedDataSize = | ||
| 3064 | - (*numElements * vector_byte_size(*element_type, *numDimensions)); | ||
| 3065 | - if (expectedDataSize != dataSize) { | ||
| 3066 | - vtab_set_error(pVTab, | ||
| 3067 | - "numpy array error: Expected a data size of %d, found %d", | ||
| 3068 | - expectedDataSize, dataSize); | ||
| 3069 | - return SQLITE_ERROR; | ||
| 3070 | - } | ||
| 3071 | - | ||
| 3072 | - *data = (void *)&buffer[totalHeaderLength]; | ||
| 3073 | - return SQLITE_OK; | ||
| 3074 | -} | ||
| 3075 | - | ||
| 3076 | -static int vec_npy_eachConnect(sqlite3 *db, void *pAux, int argc, | ||
| 3077 | - const char *const *argv, sqlite3_vtab **ppVtab, | ||
| 3078 | - char **pzErr) { | ||
| 3079 | - UNUSED_PARAMETER(pAux); | ||
| 3080 | - UNUSED_PARAMETER(argc); | ||
| 3081 | - UNUSED_PARAMETER(argv); | ||
| 3082 | - UNUSED_PARAMETER(pzErr); | ||
| 3083 | - vec_npy_each_vtab *pNew; | ||
| 3084 | - int rc; | ||
| 3085 | - | ||
| 3086 | - rc = sqlite3_declare_vtab(db, "CREATE TABLE x(vector, input hidden)"); | ||
| 3087 | -#define VEC_NPY_EACH_COLUMN_VECTOR 0 | ||
| 3088 | -#define VEC_NPY_EACH_COLUMN_INPUT 1 | ||
| 3089 | - if (rc == SQLITE_OK) { | ||
| 3090 | - pNew = sqlite3_malloc(sizeof(*pNew)); | ||
| 3091 | - *ppVtab = (sqlite3_vtab *)pNew; | ||
| 3092 | - if (pNew == 0) | ||
| 3093 | - return SQLITE_NOMEM; | ||
| 3094 | - memset(pNew, 0, sizeof(*pNew)); | ||
| 3095 | - } | ||
| 3096 | - return rc; | ||
| 3097 | -} | ||
| 3098 | - | ||
| 3099 | -static int vec_npy_eachDisconnect(sqlite3_vtab *pVtab) { | ||
| 3100 | - vec_npy_each_vtab *p = (vec_npy_each_vtab *)pVtab; | ||
| 3101 | - sqlite3_free(p); | ||
| 3102 | - return SQLITE_OK; | ||
| 3103 | -} | ||
| 3104 | - | ||
| 3105 | -static int vec_npy_eachOpen(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor) { | ||
| 3106 | - UNUSED_PARAMETER(p); | ||
| 3107 | - vec_npy_each_cursor *pCur; | ||
| 3108 | - pCur = sqlite3_malloc(sizeof(*pCur)); | ||
| 3109 | - if (pCur == 0) | ||
| 3110 | - return SQLITE_NOMEM; | ||
| 3111 | - memset(pCur, 0, sizeof(*pCur)); | ||
| 3112 | - *ppCursor = &pCur->base; | ||
| 3113 | - return SQLITE_OK; | ||
| 3114 | -} | ||
| 3115 | - | ||
| 3116 | -static int vec_npy_eachClose(sqlite3_vtab_cursor *cur) { | ||
| 3117 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)cur; | ||
| 3118 | -#ifndef SQLITE_VEC_OMIT_FS | ||
| 3119 | - if (pCur->file) { | ||
| 3120 | - fclose(pCur->file); | ||
| 3121 | - pCur->file = NULL; | ||
| 3122 | - } | ||
| 3123 | -#endif | ||
| 3124 | - if (pCur->chunksBuffer) { | ||
| 3125 | - sqlite3_free(pCur->chunksBuffer); | ||
| 3126 | - pCur->chunksBuffer = NULL; | ||
| 3127 | - } | ||
| 3128 | - if (pCur->vector) { | ||
| 3129 | - pCur->vector = NULL; | ||
| 3130 | - } | ||
| 3131 | - sqlite3_free(pCur); | ||
| 3132 | - return SQLITE_OK; | ||
| 3133 | -} | ||
| 3134 | - | ||
| 3135 | -static int vec_npy_eachBestIndex(sqlite3_vtab *pVTab, | ||
| 3136 | - sqlite3_index_info *pIdxInfo) { | ||
| 3137 | - int hasInput; | ||
| 3138 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | ||
| 3139 | - const struct sqlite3_index_constraint *pCons = &pIdxInfo->aConstraint[i]; | ||
| 3140 | - // printf("i=%d iColumn=%d, op=%d, usable=%d\n", i, pCons->iColumn, | ||
| 3141 | - // pCons->op, pCons->usable); | ||
| 3142 | - switch (pCons->iColumn) { | ||
| 3143 | - case VEC_NPY_EACH_COLUMN_INPUT: { | ||
| 3144 | - if (pCons->op == SQLITE_INDEX_CONSTRAINT_EQ && pCons->usable) { | ||
| 3145 | - hasInput = 1; | ||
| 3146 | - pIdxInfo->aConstraintUsage[i].argvIndex = 1; | ||
| 3147 | - pIdxInfo->aConstraintUsage[i].omit = 1; | ||
| 3148 | - } | ||
| 3149 | - break; | ||
| 3150 | - } | ||
| 3151 | - } | ||
| 3152 | - } | ||
| 3153 | - if (!hasInput) { | ||
| 3154 | - pVTab->zErrMsg = sqlite3_mprintf("input argument is required"); | ||
| 3155 | - return SQLITE_ERROR; | ||
| 3156 | - } | ||
| 3157 | - | ||
| 3158 | - pIdxInfo->estimatedCost = (double)100000; | ||
| 3159 | - pIdxInfo->estimatedRows = 100000; | ||
| 3160 | - | ||
| 3161 | - return SQLITE_OK; | ||
| 3162 | -} | ||
| 3163 | - | ||
| 3164 | -static int vec_npy_eachFilter(sqlite3_vtab_cursor *pVtabCursor, int idxNum, | ||
| 3165 | - const char *idxStr, int argc, | ||
| 3166 | - sqlite3_value **argv) { | ||
| 3167 | - UNUSED_PARAMETER(idxNum); | ||
| 3168 | - UNUSED_PARAMETER(idxStr); | ||
| 3169 | - assert(argc == 1); | ||
| 3170 | - int rc; | ||
| 3171 | - | ||
| 3172 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)pVtabCursor; | ||
| 3173 | - | ||
| 3174 | -#ifndef SQLITE_VEC_OMIT_FS | ||
| 3175 | - if (pCur->file) { | ||
| 3176 | - fclose(pCur->file); | ||
| 3177 | - pCur->file = NULL; | ||
| 3178 | - } | ||
| 3179 | -#endif | ||
| 3180 | - if (pCur->chunksBuffer) { | ||
| 3181 | - sqlite3_free(pCur->chunksBuffer); | ||
| 3182 | - pCur->chunksBuffer = NULL; | ||
| 3183 | - } | ||
| 3184 | - if (pCur->vector) { | ||
| 3185 | - pCur->vector = NULL; | ||
| 3186 | - } | ||
| 3187 | - | ||
| 3188 | -#ifndef SQLITE_VEC_OMIT_FS | ||
| 3189 | - struct VecNpyFile *f = NULL; | ||
| 3190 | - if ((f = sqlite3_value_pointer(argv[0], SQLITE_VEC_NPY_FILE_NAME))) { | ||
| 3191 | - FILE *file = fopen(f->path, "r"); | ||
| 3192 | - if (!file) { | ||
| 3193 | - vtab_set_error(pVtabCursor->pVtab, "Could not open numpy file"); | ||
| 3194 | - return SQLITE_ERROR; | ||
| 3195 | - } | ||
| 3196 | - | ||
| 3197 | - rc = parse_npy_file(pVtabCursor->pVtab, file, pCur); | ||
| 3198 | - if (rc != SQLITE_OK) { | ||
| 3199 | -#ifndef SQLITE_VEC_OMIT_FS | ||
| 3200 | - fclose(file); | ||
| 3201 | -#endif | ||
| 3202 | - return rc; | ||
| 3203 | - } | ||
| 3204 | - | ||
| 3205 | - } else | ||
| 3206 | -#endif | ||
| 3207 | - { | ||
| 3208 | - | ||
| 3209 | - const unsigned char *input = sqlite3_value_blob(argv[0]); | ||
| 3210 | - int inputLength = sqlite3_value_bytes(argv[0]); | ||
| 3211 | - void *data; | ||
| 3212 | - size_t numElements; | ||
| 3213 | - size_t numDimensions; | ||
| 3214 | - enum VectorElementType element_type; | ||
| 3215 | - | ||
| 3216 | - rc = parse_npy_buffer(pVtabCursor->pVtab, input, inputLength, &data, | ||
| 3217 | - &numElements, &numDimensions, &element_type); | ||
| 3218 | - if (rc != SQLITE_OK) { | ||
| 3219 | - return rc; | ||
| 3220 | - } | ||
| 3221 | - | ||
| 3222 | - pCur->vector = data; | ||
| 3223 | - pCur->elementType = element_type; | ||
| 3224 | - pCur->nElements = numElements; | ||
| 3225 | - pCur->nDimensions = numDimensions; | ||
| 3226 | - pCur->input_type = VEC_NPY_EACH_INPUT_BUFFER; | ||
| 3227 | - } | ||
| 3228 | - | ||
| 3229 | - pCur->iRowid = 0; | ||
| 3230 | - return SQLITE_OK; | ||
| 3231 | -} | ||
| 3232 | - | ||
| 3233 | -static int vec_npy_eachRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid) { | ||
| 3234 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)cur; | ||
| 3235 | - *pRowid = pCur->iRowid; | ||
| 3236 | - return SQLITE_OK; | ||
| 3237 | -} | ||
| 3238 | - | ||
| 3239 | -static int vec_npy_eachEof(sqlite3_vtab_cursor *cur) { | ||
| 3240 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)cur; | ||
| 3241 | - if (pCur->input_type == VEC_NPY_EACH_INPUT_BUFFER) { | ||
| 3242 | - return (!pCur->nElements) || (size_t)pCur->iRowid >= pCur->nElements; | ||
| 3243 | - } | ||
| 3244 | - return pCur->eof; | ||
| 3245 | -} | ||
| 3246 | - | ||
| 3247 | -static int vec_npy_eachNext(sqlite3_vtab_cursor *cur) { | ||
| 3248 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)cur; | ||
| 3249 | - pCur->iRowid++; | ||
| 3250 | - if (pCur->input_type == VEC_NPY_EACH_INPUT_BUFFER) { | ||
| 3251 | - return SQLITE_OK; | ||
| 3252 | - } | ||
| 3253 | - | ||
| 3254 | -#ifndef SQLITE_VEC_OMIT_FS | ||
| 3255 | - // else: input is a file | ||
| 3256 | - pCur->currentChunkIndex++; | ||
| 3257 | - if (pCur->currentChunkIndex >= pCur->currentChunkSize) { | ||
| 3258 | - pCur->currentChunkSize = | ||
| 3259 | - fread(pCur->chunksBuffer, | ||
| 3260 | - vector_byte_size(pCur->elementType, pCur->nDimensions), | ||
| 3261 | - pCur->maxChunks, pCur->file); | ||
| 3262 | - if (!pCur->currentChunkSize) { | ||
| 3263 | - pCur->eof = 1; | ||
| 3264 | - } | ||
| 3265 | - pCur->currentChunkIndex = 0; | ||
| 3266 | - } | ||
| 3267 | -#endif | ||
| 3268 | - return SQLITE_OK; | ||
| 3269 | -} | ||
| 3270 | - | ||
| 3271 | -static int vec_npy_eachColumnBuffer(vec_npy_each_cursor *pCur, | ||
| 3272 | - sqlite3_context *context, int i) { | ||
| 3273 | - switch (i) { | ||
| 3274 | - case VEC_NPY_EACH_COLUMN_VECTOR: { | ||
| 3275 | - sqlite3_result_subtype(context, pCur->elementType); | ||
| 3276 | - switch (pCur->elementType) { | ||
| 3277 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 3278 | - sqlite3_result_blob( | ||
| 3279 | - context, | ||
| 3280 | - &((unsigned char *) | ||
| 3281 | - pCur->vector)[pCur->iRowid * pCur->nDimensions * sizeof(f32)], | ||
| 3282 | - pCur->nDimensions * sizeof(f32), SQLITE_TRANSIENT); | ||
| 3283 | - | ||
| 3284 | - break; | ||
| 3285 | - } | ||
| 3286 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | ||
| 3287 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 3288 | - // https://github.com/asg017/sqlite-vec/issues/42 | ||
| 3289 | - sqlite3_result_error(context, | ||
| 3290 | - "vec_npy_each only supports float32 vectors", -1); | ||
| 3291 | - break; | ||
| 3292 | - } | ||
| 3293 | - } | ||
| 3294 | - | ||
| 3295 | - break; | ||
| 3296 | - } | ||
| 3297 | - } | ||
| 3298 | - return SQLITE_OK; | ||
| 3299 | -} | ||
| 3300 | -static int vec_npy_eachColumnFile(vec_npy_each_cursor *pCur, | ||
| 3301 | - sqlite3_context *context, int i) { | ||
| 3302 | - switch (i) { | ||
| 3303 | - case VEC_NPY_EACH_COLUMN_VECTOR: { | ||
| 3304 | - switch (pCur->elementType) { | ||
| 3305 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 3306 | - sqlite3_result_blob( | ||
| 3307 | - context, | ||
| 3308 | - &((unsigned char *) | ||
| 3309 | - pCur->chunksBuffer)[pCur->currentChunkIndex * | ||
| 3310 | - pCur->nDimensions * sizeof(f32)], | ||
| 3311 | - pCur->nDimensions * sizeof(f32), SQLITE_TRANSIENT); | ||
| 3312 | - break; | ||
| 3313 | - } | ||
| 3314 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | ||
| 3315 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 3316 | - // https://github.com/asg017/sqlite-vec/issues/42 | ||
| 3317 | - sqlite3_result_error(context, | ||
| 3318 | - "vec_npy_each only supports float32 vectors", -1); | ||
| 3319 | - break; | ||
| 3320 | - } | ||
| 3321 | - } | ||
| 3322 | - break; | ||
| 3323 | - } | ||
| 3324 | - } | ||
| 3325 | - return SQLITE_OK; | ||
| 3326 | -} | ||
| 3327 | -static int vec_npy_eachColumn(sqlite3_vtab_cursor *cur, | ||
| 3328 | - sqlite3_context *context, int i) { | ||
| 3329 | - vec_npy_each_cursor *pCur = (vec_npy_each_cursor *)cur; | ||
| 3330 | - switch (pCur->input_type) { | ||
| 3331 | - case VEC_NPY_EACH_INPUT_BUFFER: | ||
| 3332 | - return vec_npy_eachColumnBuffer(pCur, context, i); | ||
| 3333 | - case VEC_NPY_EACH_INPUT_FILE: | ||
| 3334 | - return vec_npy_eachColumnFile(pCur, context, i); | ||
| 3335 | - } | ||
| 3336 | - return SQLITE_ERROR; | ||
| 3337 | -} | ||
| 3338 | - | ||
| 3339 | -static sqlite3_module vec_npy_eachModule = { | ||
| 3340 | - /* iVersion */ 0, | ||
| 3341 | - /* xCreate */ 0, | ||
| 3342 | - /* xConnect */ vec_npy_eachConnect, | ||
| 3343 | - /* xBestIndex */ vec_npy_eachBestIndex, | ||
| 3344 | - /* xDisconnect */ vec_npy_eachDisconnect, | ||
| 3345 | - /* xDestroy */ 0, | ||
| 3346 | - /* xOpen */ vec_npy_eachOpen, | ||
| 3347 | - /* xClose */ vec_npy_eachClose, | ||
| 3348 | - /* xFilter */ vec_npy_eachFilter, | ||
| 3349 | - /* xNext */ vec_npy_eachNext, | ||
| 3350 | - /* xEof */ vec_npy_eachEof, | ||
| 3351 | - /* xColumn */ vec_npy_eachColumn, | ||
| 3352 | - /* xRowid */ vec_npy_eachRowid, | ||
| 3353 | - /* xUpdate */ 0, | ||
| 3354 | - /* xBegin */ 0, | ||
| 3355 | - /* xSync */ 0, | ||
| 3356 | - /* xCommit */ 0, | ||
| 3357 | - /* xRollback */ 0, | ||
| 3358 | - /* xFindMethod */ 0, | ||
| 3359 | - /* xRename */ 0, | ||
| 3360 | - /* xSavepoint */ 0, | ||
| 3361 | - /* xRelease */ 0, | ||
| 3362 | - /* xRollbackTo */ 0, | ||
| 3363 | - /* xShadowName */ 0, | ||
| 3364 | -#if SQLITE_VERSION_NUMBER >= 3044000 | ||
| 3365 | - /* xIntegrity */ 0, | ||
| 3366 | -#endif | ||
| 3367 | -}; | ||
| 3368 | - | ||
| 3369 | -#pragma endregion | ||
| 3370 | - | ||
| 3371 | -#pragma region vec0 virtual table | ||
| 3372 | - | ||
| 3373 | -#define VEC0_COLUMN_ID 0 | ||
| 3374 | -#define VEC0_COLUMN_USERN_START 1 | ||
| 3375 | -#define VEC0_COLUMN_OFFSET_DISTANCE 1 | ||
| 3376 | -#define VEC0_COLUMN_OFFSET_K 2 | ||
| 3377 | - | ||
| 3378 | -#define VEC0_SHADOW_INFO_NAME "\"%w\".\"%w_info\"" | ||
| 3379 | - | ||
| 3380 | -#define VEC0_SHADOW_CHUNKS_NAME "\"%w\".\"%w_chunks\"" | ||
| 3381 | -/// 1) schema, 2) original vtab table name | ||
| 3382 | -#define VEC0_SHADOW_CHUNKS_CREATE \ | ||
| 3383 | - "CREATE TABLE " VEC0_SHADOW_CHUNKS_NAME "(" \ | ||
| 3384 | - "chunk_id INTEGER PRIMARY KEY AUTOINCREMENT," \ | ||
| 3385 | - "size INTEGER NOT NULL," \ | ||
| 3386 | - "validity BLOB NOT NULL," \ | ||
| 3387 | - "rowids BLOB NOT NULL" \ | ||
| 3388 | - ");" | ||
| 3389 | - | ||
| 3390 | -#define VEC0_SHADOW_ROWIDS_NAME "\"%w\".\"%w_rowids\"" | ||
| 3391 | -/// 1) schema, 2) original vtab table name | ||
| 3392 | -#define VEC0_SHADOW_ROWIDS_CREATE_BASIC \ | ||
| 3393 | - "CREATE TABLE " VEC0_SHADOW_ROWIDS_NAME "(" \ | ||
| 3394 | - "rowid INTEGER PRIMARY KEY AUTOINCREMENT," \ | ||
| 3395 | - "id," \ | ||
| 3396 | - "chunk_id INTEGER," \ | ||
| 3397 | - "chunk_offset INTEGER" \ | ||
| 3398 | - ");" | ||
| 3399 | - | ||
| 3400 | -// vec0 tables with a text primary keys are still backed by int64 primary keys, | ||
| 3401 | -// since a fixed-length rowid is required for vec0 chunks. But we add a new 'id | ||
| 3402 | -// text unique' column to emulate a text primary key interface. | ||
| 3403 | -#define VEC0_SHADOW_ROWIDS_CREATE_PK_TEXT \ | ||
| 3404 | - "CREATE TABLE " VEC0_SHADOW_ROWIDS_NAME "(" \ | ||
| 3405 | - "rowid INTEGER PRIMARY KEY AUTOINCREMENT," \ | ||
| 3406 | - "id TEXT UNIQUE NOT NULL," \ | ||
| 3407 | - "chunk_id INTEGER," \ | ||
| 3408 | - "chunk_offset INTEGER" \ | ||
| 3409 | - ");" | ||
| 3410 | - | ||
| 3411 | -/// 1) schema, 2) original vtab table name | ||
| 3412 | -#define VEC0_SHADOW_VECTOR_N_NAME "\"%w\".\"%w_vector_chunks%02d\"" | ||
| 3413 | - | ||
| 3414 | -/// 1) schema, 2) original vtab table name | ||
| 3415 | -#define VEC0_SHADOW_VECTOR_N_CREATE \ | ||
| 3416 | - "CREATE TABLE " VEC0_SHADOW_VECTOR_N_NAME "(" \ | ||
| 3417 | - "rowid PRIMARY KEY," \ | ||
| 3418 | - "vectors BLOB NOT NULL" \ | ||
| 3419 | - ");" | ||
| 3420 | - | ||
| 3421 | -#define VEC0_SHADOW_AUXILIARY_NAME "\"%w\".\"%w_auxiliary\"" | ||
| 3422 | - | ||
| 3423 | -#define VEC0_SHADOW_METADATA_N_NAME "\"%w\".\"%w_metadatachunks%02d\"" | ||
| 3424 | -#define VEC0_SHADOW_METADATA_TEXT_DATA_NAME "\"%w\".\"%w_metadatatext%02d\"" | ||
| 3425 | - | ||
| 3426 | -#define VEC_INTERAL_ERROR "Internal sqlite-vec error: " | ||
| 3427 | -#define REPORT_URL "https://github.com/asg017/sqlite-vec/issues/new" | ||
| 3428 | - | ||
| 3429 | -typedef struct vec0_vtab vec0_vtab; | ||
| 3430 | - | ||
| 3431 | -#define VEC0_MAX_VECTOR_COLUMNS 16 | ||
| 3432 | -#define VEC0_MAX_PARTITION_COLUMNS 4 | ||
| 3433 | -#define VEC0_MAX_AUXILIARY_COLUMNS 16 | ||
| 3434 | -#define VEC0_MAX_METADATA_COLUMNS 16 | ||
| 3435 | - | ||
| 3436 | -#define SQLITE_VEC_VEC0_MAX_DIMENSIONS 8192 | ||
| 3437 | -#define VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH 16 | ||
| 3438 | -#define VEC0_METADATA_TEXT_VIEW_DATA_LENGTH 12 | ||
| 3439 | - | ||
| 3440 | -typedef enum { | ||
| 3441 | - // vector column, ie "contents_embedding float[1024]" | ||
| 3442 | - SQLITE_VEC0_USER_COLUMN_KIND_VECTOR = 1, | ||
| 3443 | - | ||
| 3444 | - // partition key column, ie "user_id integer partition key" | ||
| 3445 | - SQLITE_VEC0_USER_COLUMN_KIND_PARTITION = 2, | ||
| 3446 | - | ||
| 3447 | - // | ||
| 3448 | - SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY = 3, | ||
| 3449 | - | ||
| 3450 | - // metadata column that can be filtered, ie "genre text" | ||
| 3451 | - SQLITE_VEC0_USER_COLUMN_KIND_METADATA = 4, | ||
| 3452 | -} vec0_user_column_kind; | ||
| 3453 | - | ||
| 3454 | -struct vec0_vtab { | ||
| 3455 | - sqlite3_vtab base; | ||
| 3456 | - | ||
| 3457 | - // the SQLite connection of the host database | ||
| 3458 | - sqlite3 *db; | ||
| 3459 | - | ||
| 3460 | - // True if the primary key of the vec0 table has a column type TEXT. | ||
| 3461 | - // Will change the schema of the _rowids table, and insert/query logic. | ||
| 3462 | - int pkIsText; | ||
| 3463 | - | ||
| 3464 | - // number of defined vector columns. | ||
| 3465 | - int numVectorColumns; | ||
| 3466 | - | ||
| 3467 | - // number of defined PARTITION KEY columns. | ||
| 3468 | - int numPartitionColumns; | ||
| 3469 | - | ||
| 3470 | - // number of defined auxiliary columns | ||
| 3471 | - int numAuxiliaryColumns; | ||
| 3472 | - | ||
| 3473 | - // number of defined metadata columns | ||
| 3474 | - int numMetadataColumns; | ||
| 3475 | - | ||
| 3476 | - | ||
| 3477 | - // Name of the schema the table exists on. | ||
| 3478 | - // Must be freed with sqlite3_free() | ||
| 3479 | - char *schemaName; | ||
| 3480 | - | ||
| 3481 | - // Name of the table the table exists on. | ||
| 3482 | - // Must be freed with sqlite3_free() | ||
| 3483 | - char *tableName; | ||
| 3484 | - | ||
| 3485 | - // Name of the _rowids shadow table. | ||
| 3486 | - // Must be freed with sqlite3_free() | ||
| 3487 | - char *shadowRowidsName; | ||
| 3488 | - | ||
| 3489 | - // Name of the _chunks shadow table. | ||
| 3490 | - // Must be freed with sqlite3_free() | ||
| 3491 | - char *shadowChunksName; | ||
| 3492 | - | ||
| 3493 | - // contains enum vec0_user_column_kind values for up to | ||
| 3494 | - // numVectorColumns + numPartitionColumns entries | ||
| 3495 | - vec0_user_column_kind user_column_kinds[VEC0_MAX_VECTOR_COLUMNS + VEC0_MAX_PARTITION_COLUMNS + VEC0_MAX_AUXILIARY_COLUMNS + VEC0_MAX_METADATA_COLUMNS]; | ||
| 3496 | - | ||
| 3497 | - uint8_t user_column_idxs[VEC0_MAX_VECTOR_COLUMNS + VEC0_MAX_PARTITION_COLUMNS + VEC0_MAX_AUXILIARY_COLUMNS + VEC0_MAX_METADATA_COLUMNS]; | ||
| 3498 | - | ||
| 3499 | - | ||
| 3500 | - // Name of all the vector chunk shadow tables. | ||
| 3501 | - // Ex '_vector_chunks00' | ||
| 3502 | - // Only the first numVectorColumns entries will be available. | ||
| 3503 | - // The first numVectorColumns entries must be freed with sqlite3_free() | ||
| 3504 | - char *shadowVectorChunksNames[VEC0_MAX_VECTOR_COLUMNS]; | ||
| 3505 | - | ||
| 3506 | - // Name of all metadata chunk shadow tables, ie `_metadatachunks00` | ||
| 3507 | - // Only the first numMetadataColumns entries will be available. | ||
| 3508 | - // The first numMetadataColumns entries must be freed with sqlite3_free() | ||
| 3509 | - char *shadowMetadataChunksNames[VEC0_MAX_METADATA_COLUMNS]; | ||
| 3510 | - | ||
| 3511 | - struct VectorColumnDefinition vector_columns[VEC0_MAX_VECTOR_COLUMNS]; | ||
| 3512 | - struct Vec0PartitionColumnDefinition paritition_columns[VEC0_MAX_PARTITION_COLUMNS]; | ||
| 3513 | - struct Vec0AuxiliaryColumnDefinition auxiliary_columns[VEC0_MAX_AUXILIARY_COLUMNS]; | ||
| 3514 | - struct Vec0MetadataColumnDefinition metadata_columns[VEC0_MAX_METADATA_COLUMNS]; | ||
| 3515 | - | ||
| 3516 | - int chunk_size; | ||
| 3517 | - | ||
| 3518 | - // select latest chunk from _chunks, getting chunk_id | ||
| 3519 | - sqlite3_stmt *stmtLatestChunk; | ||
| 3520 | - | ||
| 3521 | - /** | ||
| 3522 | - * Statement to insert a row into the _rowids table, with a rowid. | ||
| 3523 | - * Parameters: | ||
| 3524 | - * 1: int64, rowid to insert | ||
| 3525 | - * Result columns: none | ||
| 3526 | - * SQL: "INSERT INTO _rowids(rowid) VALUES (?)" | ||
| 3527 | - * | ||
| 3528 | - * Must be cleaned up with sqlite3_finalize(). | ||
| 3529 | - */ | ||
| 3530 | - sqlite3_stmt *stmtRowidsInsertRowid; | ||
| 3531 | - | ||
| 3532 | - /** | ||
| 3533 | - * Statement to insert a row into the _rowids table, with an id. | ||
| 3534 | - * The id column isn't a tradition primary key, but instead a unique | ||
| 3535 | - * column to handle "text primary key" vec0 tables. The true int64 rowid | ||
| 3536 | - * can be retrieved after inserting with sqlite3_last_rowid(). | ||
| 3537 | - * | ||
| 3538 | - * Parameters: | ||
| 3539 | - * 1: text or null, id to insert | ||
| 3540 | - * Result columns: none | ||
| 3541 | - * | ||
| 3542 | - * Must be cleaned up with sqlite3_finalize(). | ||
| 3543 | - */ | ||
| 3544 | - sqlite3_stmt *stmtRowidsInsertId; | ||
| 3545 | - | ||
| 3546 | - /** | ||
| 3547 | - * Statement to update the "position" columns chunk_id and chunk_offset for | ||
| 3548 | - * a given _rowids row. Used when the "next available" chunk position is found | ||
| 3549 | - * for a vector. | ||
| 3550 | - * | ||
| 3551 | - * Parameters: | ||
| 3552 | - * 1: int64, chunk_id value | ||
| 3553 | - * 2: int64, chunk_offset value | ||
| 3554 | - * 3: int64, rowid value | ||
| 3555 | - * Result columns: none | ||
| 3556 | - * | ||
| 3557 | - * Must be cleaned up with sqlite3_finalize(). | ||
| 3558 | - */ | ||
| 3559 | - sqlite3_stmt *stmtRowidsUpdatePosition; | ||
| 3560 | - | ||
| 3561 | - /** | ||
| 3562 | - * Statement to quickly find the chunk_id + chunk_offset of a given row. | ||
| 3563 | - * Parameters: | ||
| 3564 | - * 1: rowid of the row/vector to lookup | ||
| 3565 | - * Result columns: | ||
| 3566 | - * 0: chunk_id (i64) | ||
| 3567 | - * 1: chunk_offset (i64) | ||
| 3568 | - * SQL: "SELECT id, chunk_id, chunk_offset FROM _rowids WHERE rowid = ?"" | ||
| 3569 | - * | ||
| 3570 | - * Must be cleaned up with sqlite3_finalize(). | ||
| 3571 | - */ | ||
| 3572 | - sqlite3_stmt *stmtRowidsGetChunkPosition; | ||
| 3573 | -}; | ||
| 3574 | - | ||
| 3575 | -/** | ||
| 3576 | - * @brief Finalize all the sqlite3_stmt members in a vec0_vtab. | ||
| 3577 | - * | ||
| 3578 | - * @param p vec0_vtab pointer | ||
| 3579 | - */ | ||
| 3580 | -void vec0_free_resources(vec0_vtab *p) { | ||
| 3581 | - sqlite3_finalize(p->stmtLatestChunk); | ||
| 3582 | - p->stmtLatestChunk = NULL; | ||
| 3583 | - sqlite3_finalize(p->stmtRowidsInsertRowid); | ||
| 3584 | - p->stmtRowidsInsertRowid = NULL; | ||
| 3585 | - sqlite3_finalize(p->stmtRowidsInsertId); | ||
| 3586 | - p->stmtRowidsInsertId = NULL; | ||
| 3587 | - sqlite3_finalize(p->stmtRowidsUpdatePosition); | ||
| 3588 | - p->stmtRowidsUpdatePosition = NULL; | ||
| 3589 | - sqlite3_finalize(p->stmtRowidsGetChunkPosition); | ||
| 3590 | - p->stmtRowidsGetChunkPosition = NULL; | ||
| 3591 | -} | ||
| 3592 | - | ||
| 3593 | -/** | ||
| 3594 | - * @brief Free all memory and sqlite3_stmt members of a vec0_vtab | ||
| 3595 | - * | ||
| 3596 | - * @param p vec0_vtab pointer | ||
| 3597 | - */ | ||
| 3598 | -void vec0_free(vec0_vtab *p) { | ||
| 3599 | - vec0_free_resources(p); | ||
| 3600 | - | ||
| 3601 | - sqlite3_free(p->schemaName); | ||
| 3602 | - p->schemaName = NULL; | ||
| 3603 | - sqlite3_free(p->tableName); | ||
| 3604 | - p->tableName = NULL; | ||
| 3605 | - sqlite3_free(p->shadowChunksName); | ||
| 3606 | - p->shadowChunksName = NULL; | ||
| 3607 | - sqlite3_free(p->shadowRowidsName); | ||
| 3608 | - p->shadowRowidsName = NULL; | ||
| 3609 | - | ||
| 3610 | - for (int i = 0; i < p->numVectorColumns; i++) { | ||
| 3611 | - sqlite3_free(p->shadowVectorChunksNames[i]); | ||
| 3612 | - p->shadowVectorChunksNames[i] = NULL; | ||
| 3613 | - | ||
| 3614 | - sqlite3_free(p->vector_columns[i].name); | ||
| 3615 | - p->vector_columns[i].name = NULL; | ||
| 3616 | - } | ||
| 3617 | -} | ||
| 3618 | - | ||
| 3619 | -int vec0_num_defined_user_columns(vec0_vtab *p) { | ||
| 3620 | - return p->numVectorColumns + p->numPartitionColumns + p->numAuxiliaryColumns + p->numMetadataColumns; | ||
| 3621 | -} | ||
| 3622 | - | ||
| 3623 | -/** | ||
| 3624 | - * @brief Returns the index of the distance hidden column for the given vec0 | ||
| 3625 | - * table. | ||
| 3626 | - * | ||
| 3627 | - * @param p vec0 table | ||
| 3628 | - * @return int | ||
| 3629 | - */ | ||
| 3630 | -int vec0_column_distance_idx(vec0_vtab *p) { | ||
| 3631 | - return VEC0_COLUMN_USERN_START + (vec0_num_defined_user_columns(p) - 1) + | ||
| 3632 | - VEC0_COLUMN_OFFSET_DISTANCE; | ||
| 3633 | -} | ||
| 3634 | - | ||
| 3635 | -/** | ||
| 3636 | - * @brief Returns the index of the k hidden column for the given vec0 table. | ||
| 3637 | - * | ||
| 3638 | - * @param p vec0 table | ||
| 3639 | - * @return int k column index | ||
| 3640 | - */ | ||
| 3641 | -int vec0_column_k_idx(vec0_vtab *p) { | ||
| 3642 | - return VEC0_COLUMN_USERN_START + (vec0_num_defined_user_columns(p) - 1) + | ||
| 3643 | - VEC0_COLUMN_OFFSET_K; | ||
| 3644 | -} | ||
| 3645 | - | ||
| 3646 | -/** | ||
| 3647 | - * Returns 1 if the given column-based index is a valid vector column, | ||
| 3648 | - * 0 otherwise. | ||
| 3649 | - */ | ||
| 3650 | -int vec0_column_idx_is_vector(vec0_vtab *pVtab, int column_idx) { | ||
| 3651 | - return column_idx >= VEC0_COLUMN_USERN_START && | ||
| 3652 | - column_idx <= (VEC0_COLUMN_USERN_START + vec0_num_defined_user_columns(pVtab) - 1) && | ||
| 3653 | - pVtab->user_column_kinds[column_idx - VEC0_COLUMN_USERN_START] == SQLITE_VEC0_USER_COLUMN_KIND_VECTOR; | ||
| 3654 | -} | ||
| 3655 | - | ||
| 3656 | -/** | ||
| 3657 | - * Returns the vector index of the given user column index. | ||
| 3658 | - * ONLY call if validated with vec0_column_idx_is_vector before | ||
| 3659 | - */ | ||
| 3660 | -int vec0_column_idx_to_vector_idx(vec0_vtab *pVtab, int column_idx) { | ||
| 3661 | - UNUSED_PARAMETER(pVtab); | ||
| 3662 | - return pVtab->user_column_idxs[column_idx - VEC0_COLUMN_USERN_START]; | ||
| 3663 | -} | ||
| 3664 | -/** | ||
| 3665 | - * Returns 1 if the given column-based index is a "partition key" column, | ||
| 3666 | - * 0 otherwise. | ||
| 3667 | - */ | ||
| 3668 | -int vec0_column_idx_is_partition(vec0_vtab *pVtab, int column_idx) { | ||
| 3669 | - return column_idx >= VEC0_COLUMN_USERN_START && | ||
| 3670 | - column_idx <= (VEC0_COLUMN_USERN_START + vec0_num_defined_user_columns(pVtab) - 1) && | ||
| 3671 | - pVtab->user_column_kinds[column_idx - VEC0_COLUMN_USERN_START] == SQLITE_VEC0_USER_COLUMN_KIND_PARTITION; | ||
| 3672 | -} | ||
| 3673 | - | ||
| 3674 | -/** | ||
| 3675 | - * Returns the partition column index of the given user column index. | ||
| 3676 | - * ONLY call if validated with vec0_column_idx_is_vector before | ||
| 3677 | - */ | ||
| 3678 | -int vec0_column_idx_to_partition_idx(vec0_vtab *pVtab, int column_idx) { | ||
| 3679 | - UNUSED_PARAMETER(pVtab); | ||
| 3680 | - return pVtab->user_column_idxs[column_idx - VEC0_COLUMN_USERN_START]; | ||
| 3681 | -} | ||
| 3682 | - | ||
| 3683 | -/** | ||
| 3684 | - * Returns 1 if the given column-based index is a auxiliary column, | ||
| 3685 | - * 0 otherwise. | ||
| 3686 | - */ | ||
| 3687 | -int vec0_column_idx_is_auxiliary(vec0_vtab *pVtab, int column_idx) { | ||
| 3688 | - return column_idx >= VEC0_COLUMN_USERN_START && | ||
| 3689 | - column_idx <= (VEC0_COLUMN_USERN_START + vec0_num_defined_user_columns(pVtab) - 1) && | ||
| 3690 | - pVtab->user_column_kinds[column_idx - VEC0_COLUMN_USERN_START] == SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY; | ||
| 3691 | -} | ||
| 3692 | - | ||
| 3693 | -/** | ||
| 3694 | - * Returns the auxiliary column index of the given user column index. | ||
| 3695 | - * ONLY call if validated with vec0_column_idx_to_partition_idx before | ||
| 3696 | - */ | ||
| 3697 | -int vec0_column_idx_to_auxiliary_idx(vec0_vtab *pVtab, int column_idx) { | ||
| 3698 | - UNUSED_PARAMETER(pVtab); | ||
| 3699 | - return pVtab->user_column_idxs[column_idx - VEC0_COLUMN_USERN_START]; | ||
| 3700 | -} | ||
| 3701 | - | ||
| 3702 | -/** | ||
| 3703 | - * Returns 1 if the given column-based index is a metadata column, | ||
| 3704 | - * 0 otherwise. | ||
| 3705 | - */ | ||
| 3706 | -int vec0_column_idx_is_metadata(vec0_vtab *pVtab, int column_idx) { | ||
| 3707 | - return column_idx >= VEC0_COLUMN_USERN_START && | ||
| 3708 | - column_idx <= (VEC0_COLUMN_USERN_START + vec0_num_defined_user_columns(pVtab) - 1) && | ||
| 3709 | - pVtab->user_column_kinds[column_idx - VEC0_COLUMN_USERN_START] == SQLITE_VEC0_USER_COLUMN_KIND_METADATA; | ||
| 3710 | -} | ||
| 3711 | - | ||
| 3712 | -/** | ||
| 3713 | - * Returns the metadata column index of the given user column index. | ||
| 3714 | - * ONLY call if validated with vec0_column_idx_is_metadata before | ||
| 3715 | - */ | ||
| 3716 | -int vec0_column_idx_to_metadata_idx(vec0_vtab *pVtab, int column_idx) { | ||
| 3717 | - UNUSED_PARAMETER(pVtab); | ||
| 3718 | - return pVtab->user_column_idxs[column_idx - VEC0_COLUMN_USERN_START]; | ||
| 3719 | -} | ||
| 3720 | - | ||
| 3721 | -/** | ||
| 3722 | - * @brief Retrieve the chunk_id, chunk_offset, and possible "id" value | ||
| 3723 | - * of a vec0_vtab row with the provided rowid | ||
| 3724 | - * | ||
| 3725 | - * @param p vec0_vtab | ||
| 3726 | - * @param rowid the rowid of the row to query | ||
| 3727 | - * @param id output, optional sqlite3_value to provide the id. | ||
| 3728 | - * Useful for text PK rows. Must be freed with sqlite3_value_free() | ||
| 3729 | - * @param chunk_id output, the chunk_id the row belongs to | ||
| 3730 | - * @param chunk_offset output, the offset within the chunk the row belongs to | ||
| 3731 | - * @return SQLITE_ROW on success, error code otherwise. SQLITE_EMPTY if row DNE | ||
| 3732 | - */ | ||
| 3733 | -int vec0_get_chunk_position(vec0_vtab *p, i64 rowid, sqlite3_value **id, | ||
| 3734 | - i64 *chunk_id, i64 *chunk_offset) { | ||
| 3735 | - int rc; | ||
| 3736 | - | ||
| 3737 | - if (!p->stmtRowidsGetChunkPosition) { | ||
| 3738 | - const char *zSql = | ||
| 3739 | - sqlite3_mprintf("SELECT id, chunk_id, chunk_offset " | ||
| 3740 | - "FROM " VEC0_SHADOW_ROWIDS_NAME " WHERE rowid = ?", | ||
| 3741 | - p->schemaName, p->tableName); | ||
| 3742 | - if (!zSql) { | ||
| 3743 | - rc = SQLITE_NOMEM; | ||
| 3744 | - goto cleanup; | ||
| 3745 | - } | ||
| 3746 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &p->stmtRowidsGetChunkPosition, 0); | ||
| 3747 | - sqlite3_free((void *)zSql); | ||
| 3748 | - if (rc != SQLITE_OK) { | ||
| 3749 | - vtab_set_error( | ||
| 3750 | - &p->base, VEC_INTERAL_ERROR | ||
| 3751 | - "could not initialize 'rowids get chunk position' statement"); | ||
| 3752 | - goto cleanup; | ||
| 3753 | - } | ||
| 3754 | - } | ||
| 3755 | - | ||
| 3756 | - sqlite3_bind_int64(p->stmtRowidsGetChunkPosition, 1, rowid); | ||
| 3757 | - rc = sqlite3_step(p->stmtRowidsGetChunkPosition); | ||
| 3758 | - // special case: when no results, return SQLITE_EMPTY to convey "that chunk | ||
| 3759 | - // position doesnt exist" | ||
| 3760 | - if (rc == SQLITE_DONE) { | ||
| 3761 | - rc = SQLITE_EMPTY; | ||
| 3762 | - goto cleanup; | ||
| 3763 | - } | ||
| 3764 | - if (rc != SQLITE_ROW) { | ||
| 3765 | - goto cleanup; | ||
| 3766 | - } | ||
| 3767 | - | ||
| 3768 | - if (id) { | ||
| 3769 | - sqlite3_value *value = | ||
| 3770 | - sqlite3_column_value(p->stmtRowidsGetChunkPosition, 0); | ||
| 3771 | - *id = sqlite3_value_dup(value); | ||
| 3772 | - if (!*id) { | ||
| 3773 | - rc = SQLITE_NOMEM; | ||
| 3774 | - goto cleanup; | ||
| 3775 | - } | ||
| 3776 | - } | ||
| 3777 | - | ||
| 3778 | - if (chunk_id) { | ||
| 3779 | - *chunk_id = sqlite3_column_int64(p->stmtRowidsGetChunkPosition, 1); | ||
| 3780 | - } | ||
| 3781 | - if (chunk_offset) { | ||
| 3782 | - *chunk_offset = sqlite3_column_int64(p->stmtRowidsGetChunkPosition, 2); | ||
| 3783 | - } | ||
| 3784 | - | ||
| 3785 | - rc = SQLITE_OK; | ||
| 3786 | - | ||
| 3787 | -cleanup: | ||
| 3788 | - sqlite3_reset(p->stmtRowidsGetChunkPosition); | ||
| 3789 | - sqlite3_clear_bindings(p->stmtRowidsGetChunkPosition); | ||
| 3790 | - return rc; | ||
| 3791 | -} | ||
| 3792 | - | ||
| 3793 | -/** | ||
| 3794 | - * @brief Return the id value from the _rowids table where _rowids.rowid = | ||
| 3795 | - * rowid. | ||
| 3796 | - * | ||
| 3797 | - * @param pVtab: vec0 table to query | ||
| 3798 | - * @param rowid: rowid of the row to query. | ||
| 3799 | - * @param out: A dup'ed sqlite3_value of the id column. Might be null. | ||
| 3800 | - * Must be cleaned up with sqlite3_value_free(). | ||
| 3801 | - * @returns SQLITE_OK on success, error code on failure | ||
| 3802 | - */ | ||
| 3803 | -int vec0_get_id_value_from_rowid(vec0_vtab *pVtab, i64 rowid, | ||
| 3804 | - sqlite3_value **out) { | ||
| 3805 | - // PERF: different strategy than get_chunk_position? | ||
| 3806 | - return vec0_get_chunk_position((vec0_vtab *)pVtab, rowid, out, NULL, NULL); | ||
| 3807 | -} | ||
| 3808 | - | ||
| 3809 | -int vec0_rowid_from_id(vec0_vtab *p, sqlite3_value *valueId, i64 *rowid) { | ||
| 3810 | - sqlite3_stmt *stmt = NULL; | ||
| 3811 | - int rc; | ||
| 3812 | - char *zSql; | ||
| 3813 | - zSql = sqlite3_mprintf("SELECT rowid" | ||
| 3814 | - " FROM " VEC0_SHADOW_ROWIDS_NAME " WHERE id = ?", | ||
| 3815 | - p->schemaName, p->tableName); | ||
| 3816 | - if (!zSql) { | ||
| 3817 | - rc = SQLITE_NOMEM; | ||
| 3818 | - goto cleanup; | ||
| 3819 | - } | ||
| 3820 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 3821 | - sqlite3_free(zSql); | ||
| 3822 | - if (rc != SQLITE_OK) { | ||
| 3823 | - goto cleanup; | ||
| 3824 | - } | ||
| 3825 | - sqlite3_bind_value(stmt, 1, valueId); | ||
| 3826 | - rc = sqlite3_step(stmt); | ||
| 3827 | - if (rc == SQLITE_DONE) { | ||
| 3828 | - rc = SQLITE_EMPTY; | ||
| 3829 | - goto cleanup; | ||
| 3830 | - } | ||
| 3831 | - if (rc != SQLITE_ROW) { | ||
| 3832 | - goto cleanup; | ||
| 3833 | - } | ||
| 3834 | - *rowid = sqlite3_column_int64(stmt, 0); | ||
| 3835 | - rc = sqlite3_step(stmt); | ||
| 3836 | - if (rc != SQLITE_DONE) { | ||
| 3837 | - goto cleanup; | ||
| 3838 | - } | ||
| 3839 | - | ||
| 3840 | - rc = SQLITE_OK; | ||
| 3841 | - | ||
| 3842 | -cleanup: | ||
| 3843 | - sqlite3_finalize(stmt); | ||
| 3844 | - return rc; | ||
| 3845 | -} | ||
| 3846 | - | ||
| 3847 | -int vec0_result_id(vec0_vtab *p, sqlite3_context *context, i64 rowid) { | ||
| 3848 | - if (!p->pkIsText) { | ||
| 3849 | - sqlite3_result_int64(context, rowid); | ||
| 3850 | - return SQLITE_OK; | ||
| 3851 | - } | ||
| 3852 | - sqlite3_value *valueId; | ||
| 3853 | - int rc = vec0_get_id_value_from_rowid(p, rowid, &valueId); | ||
| 3854 | - if (rc != SQLITE_OK) { | ||
| 3855 | - return rc; | ||
| 3856 | - } | ||
| 3857 | - if (!valueId) { | ||
| 3858 | - sqlite3_result_error_nomem(context); | ||
| 3859 | - } else { | ||
| 3860 | - sqlite3_result_value(context, valueId); | ||
| 3861 | - sqlite3_value_free(valueId); | ||
| 3862 | - } | ||
| 3863 | - return SQLITE_OK; | ||
| 3864 | -} | ||
| 3865 | - | ||
| 3866 | -/** | ||
| 3867 | - * @brief | ||
| 3868 | - * | ||
| 3869 | - * @param pVtab: virtual table to query | ||
| 3870 | - * @param rowid: row to lookup | ||
| 3871 | - * @param vector_column_idx: which vector column to query | ||
| 3872 | - * @param outVector: Output pointer to the vector buffer. | ||
| 3873 | - * Must be sqlite3_free()'ed. | ||
| 3874 | - * @param outVectorSize: Pointer to a int where the size of outVector | ||
| 3875 | - * will be stored. | ||
| 3876 | - * @return int SQLITE_OK on success. | ||
| 3877 | - */ | ||
| 3878 | -int vec0_get_vector_data(vec0_vtab *pVtab, i64 rowid, int vector_column_idx, | ||
| 3879 | - void **outVector, int *outVectorSize) { | ||
| 3880 | - vec0_vtab *p = pVtab; | ||
| 3881 | - int rc, brc; | ||
| 3882 | - i64 chunk_id; | ||
| 3883 | - i64 chunk_offset; | ||
| 3884 | - size_t size; | ||
| 3885 | - void *buf = NULL; | ||
| 3886 | - int blobOffset; | ||
| 3887 | - sqlite3_blob *vectorBlob = NULL; | ||
| 3888 | - assert((vector_column_idx >= 0) && | ||
| 3889 | - (vector_column_idx < pVtab->numVectorColumns)); | ||
| 3890 | - | ||
| 3891 | - rc = vec0_get_chunk_position(pVtab, rowid, NULL, &chunk_id, &chunk_offset); | ||
| 3892 | - if (rc == SQLITE_EMPTY) { | ||
| 3893 | - vtab_set_error(&pVtab->base, "Could not find a row with rowid %lld", rowid); | ||
| 3894 | - goto cleanup; | ||
| 3895 | - } | ||
| 3896 | - if (rc != SQLITE_OK) { | ||
| 3897 | - goto cleanup; | ||
| 3898 | - } | ||
| 3899 | - | ||
| 3900 | - rc = sqlite3_blob_open(p->db, p->schemaName, | ||
| 3901 | - p->shadowVectorChunksNames[vector_column_idx], | ||
| 3902 | - "vectors", chunk_id, 0, &vectorBlob); | ||
| 3903 | - | ||
| 3904 | - if (rc != SQLITE_OK) { | ||
| 3905 | - vtab_set_error(&pVtab->base, | ||
| 3906 | - "Could not fetch vector data for %lld, opening blob failed", | ||
| 3907 | - rowid); | ||
| 3908 | - rc = SQLITE_ERROR; | ||
| 3909 | - goto cleanup; | ||
| 3910 | - } | ||
| 3911 | - | ||
| 3912 | - size = vector_column_byte_size(pVtab->vector_columns[vector_column_idx]); | ||
| 3913 | - blobOffset = chunk_offset * size; | ||
| 3914 | - | ||
| 3915 | - buf = sqlite3_malloc(size); | ||
| 3916 | - if (!buf) { | ||
| 3917 | - rc = SQLITE_NOMEM; | ||
| 3918 | - goto cleanup; | ||
| 3919 | - } | ||
| 3920 | - | ||
| 3921 | - rc = sqlite3_blob_read(vectorBlob, buf, size, blobOffset); | ||
| 3922 | - if (rc != SQLITE_OK) { | ||
| 3923 | - sqlite3_free(buf); | ||
| 3924 | - buf = NULL; | ||
| 3925 | - vtab_set_error( | ||
| 3926 | - &pVtab->base, | ||
| 3927 | - "Could not fetch vector data for %lld, reading from blob failed", | ||
| 3928 | - rowid); | ||
| 3929 | - rc = SQLITE_ERROR; | ||
| 3930 | - goto cleanup; | ||
| 3931 | - } | ||
| 3932 | - | ||
| 3933 | - *outVector = buf; | ||
| 3934 | - if (outVectorSize) { | ||
| 3935 | - *outVectorSize = size; | ||
| 3936 | - } | ||
| 3937 | - rc = SQLITE_OK; | ||
| 3938 | - | ||
| 3939 | -cleanup: | ||
| 3940 | - brc = sqlite3_blob_close(vectorBlob); | ||
| 3941 | - if ((rc == SQLITE_OK) && (brc != SQLITE_OK)) { | ||
| 3942 | - vtab_set_error( | ||
| 3943 | - &p->base, VEC_INTERAL_ERROR | ||
| 3944 | - "unknown error, could not close vector blob, please file an issue"); | ||
| 3945 | - return brc; | ||
| 3946 | - } | ||
| 3947 | - | ||
| 3948 | - return rc; | ||
| 3949 | -} | ||
| 3950 | - | ||
| 3951 | -/** | ||
| 3952 | - * @brief Retrieve the sqlite3_value of the i'th partition value for the given row. | ||
| 3953 | - * | ||
| 3954 | - * @param pVtab - the vec0_vtab in questions | ||
| 3955 | - * @param rowid - rowid of target row | ||
| 3956 | - * @param partition_idx - which partition column to retrieve | ||
| 3957 | - * @param outValue - output sqlite3_value | ||
| 3958 | - * @return int - SQLITE_OK on success, otherwise error code | ||
| 3959 | - */ | ||
| 3960 | -int vec0_get_partition_value_for_rowid(vec0_vtab *pVtab, i64 rowid, int partition_idx, sqlite3_value ** outValue) { | ||
| 3961 | - int rc; | ||
| 3962 | - i64 chunk_id; | ||
| 3963 | - i64 chunk_offset; | ||
| 3964 | - rc = vec0_get_chunk_position(pVtab, rowid, NULL, &chunk_id, &chunk_offset); | ||
| 3965 | - if(rc != SQLITE_OK) { | ||
| 3966 | - return rc; | ||
| 3967 | - } | ||
| 3968 | - sqlite3_stmt * stmt = NULL; | ||
| 3969 | - char * zSql = sqlite3_mprintf("SELECT partition%02d FROM " VEC0_SHADOW_CHUNKS_NAME " WHERE chunk_id = ?", partition_idx, pVtab->schemaName, pVtab->tableName); | ||
| 3970 | - if(!zSql) { | ||
| 3971 | - return SQLITE_NOMEM; | ||
| 3972 | - } | ||
| 3973 | - rc = sqlite3_prepare_v2(pVtab->db, zSql, -1, &stmt, NULL); | ||
| 3974 | - sqlite3_free(zSql); | ||
| 3975 | - if(rc != SQLITE_OK) { | ||
| 3976 | - return rc; | ||
| 3977 | - } | ||
| 3978 | - sqlite3_bind_int64(stmt, 1, chunk_id); | ||
| 3979 | - rc = sqlite3_step(stmt); | ||
| 3980 | - if(rc != SQLITE_ROW) { | ||
| 3981 | - rc = SQLITE_ERROR; | ||
| 3982 | - goto done; | ||
| 3983 | - } | ||
| 3984 | - *outValue = sqlite3_value_dup(sqlite3_column_value(stmt, 0)); | ||
| 3985 | - if(!*outValue) { | ||
| 3986 | - rc = SQLITE_NOMEM; | ||
| 3987 | - goto done; | ||
| 3988 | - } | ||
| 3989 | - rc = SQLITE_OK; | ||
| 3990 | - | ||
| 3991 | - done: | ||
| 3992 | - sqlite3_finalize(stmt); | ||
| 3993 | - return rc; | ||
| 3994 | - | ||
| 3995 | -} | ||
| 3996 | - | ||
| 3997 | -/** | ||
| 3998 | - * @brief Get the value of an auxiliary column for the given rowid | ||
| 3999 | - * | ||
| 4000 | - * @param pVtab vec0_vtab | ||
| 4001 | - * @param rowid the rowid of the row to lookup | ||
| 4002 | - * @param auxiliary_idx aux index of the column we care about | ||
| 4003 | - * @param outValue Output sqlite3_value to store | ||
| 4004 | - * @return int SQLITE_OK on success, error code otherwise | ||
| 4005 | - */ | ||
| 4006 | -int vec0_get_auxiliary_value_for_rowid(vec0_vtab *pVtab, i64 rowid, int auxiliary_idx, sqlite3_value ** outValue) { | ||
| 4007 | - int rc; | ||
| 4008 | - sqlite3_stmt * stmt = NULL; | ||
| 4009 | - char * zSql = sqlite3_mprintf("SELECT value%02d FROM " VEC0_SHADOW_AUXILIARY_NAME " WHERE rowid = ?", auxiliary_idx, pVtab->schemaName, pVtab->tableName); | ||
| 4010 | - if(!zSql) { | ||
| 4011 | - return SQLITE_NOMEM; | ||
| 4012 | - } | ||
| 4013 | - rc = sqlite3_prepare_v2(pVtab->db, zSql, -1, &stmt, NULL); | ||
| 4014 | - sqlite3_free(zSql); | ||
| 4015 | - if(rc != SQLITE_OK) { | ||
| 4016 | - return rc; | ||
| 4017 | - } | ||
| 4018 | - sqlite3_bind_int64(stmt, 1, rowid); | ||
| 4019 | - rc = sqlite3_step(stmt); | ||
| 4020 | - if(rc != SQLITE_ROW) { | ||
| 4021 | - rc = SQLITE_ERROR; | ||
| 4022 | - goto done; | ||
| 4023 | - } | ||
| 4024 | - *outValue = sqlite3_value_dup(sqlite3_column_value(stmt, 0)); | ||
| 4025 | - if(!*outValue) { | ||
| 4026 | - rc = SQLITE_NOMEM; | ||
| 4027 | - goto done; | ||
| 4028 | - } | ||
| 4029 | - rc = SQLITE_OK; | ||
| 4030 | - | ||
| 4031 | - done: | ||
| 4032 | - sqlite3_finalize(stmt); | ||
| 4033 | - return rc; | ||
| 4034 | -} | ||
| 4035 | - | ||
| 4036 | -/** | ||
| 4037 | - * @brief Result the given metadata value for the given row and metadata column index. | ||
| 4038 | - * Will traverse the metadatachunksNN table with BLOB I/0 for the given rowid. | ||
| 4039 | - * | ||
| 4040 | - * @param p | ||
| 4041 | - * @param rowid | ||
| 4042 | - * @param metadata_idx | ||
| 4043 | - * @param context | ||
| 4044 | - * @return int | ||
| 4045 | - */ | ||
| 4046 | -int vec0_result_metadata_value_for_rowid(vec0_vtab *p, i64 rowid, int metadata_idx, sqlite3_context * context) { | ||
| 4047 | - int rc; | ||
| 4048 | - i64 chunk_id; | ||
| 4049 | - i64 chunk_offset; | ||
| 4050 | - rc = vec0_get_chunk_position(p, rowid, NULL, &chunk_id, &chunk_offset); | ||
| 4051 | - if(rc != SQLITE_OK) { | ||
| 4052 | - return rc; | ||
| 4053 | - } | ||
| 4054 | - sqlite3_blob * blobValue; | ||
| 4055 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowMetadataChunksNames[metadata_idx], "data", chunk_id, 0, &blobValue); | ||
| 4056 | - if(rc != SQLITE_OK) { | ||
| 4057 | - return rc; | ||
| 4058 | - } | ||
| 4059 | - | ||
| 4060 | - switch(p->metadata_columns[metadata_idx].kind) { | ||
| 4061 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | ||
| 4062 | - u8 block; | ||
| 4063 | - rc = sqlite3_blob_read(blobValue, &block, sizeof(block), chunk_offset / CHAR_BIT); | ||
| 4064 | - if(rc != SQLITE_OK) { | ||
| 4065 | - goto done; | ||
| 4066 | - } | ||
| 4067 | - int value = block >> ((chunk_offset % CHAR_BIT)) & 1; | ||
| 4068 | - sqlite3_result_int(context, value); | ||
| 4069 | - break; | ||
| 4070 | - } | ||
| 4071 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | ||
| 4072 | - i64 value; | ||
| 4073 | - rc = sqlite3_blob_read(blobValue, &value, sizeof(value), chunk_offset * sizeof(i64)); | ||
| 4074 | - if(rc != SQLITE_OK) { | ||
| 4075 | - goto done; | ||
| 4076 | - } | ||
| 4077 | - sqlite3_result_int64(context, value); | ||
| 4078 | - break; | ||
| 4079 | - } | ||
| 4080 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | ||
| 4081 | - double value; | ||
| 4082 | - rc = sqlite3_blob_read(blobValue, &value, sizeof(value), chunk_offset * sizeof(double)); | ||
| 4083 | - if(rc != SQLITE_OK) { | ||
| 4084 | - goto done; | ||
| 4085 | - } | ||
| 4086 | - sqlite3_result_double(context, value); | ||
| 4087 | - break; | ||
| 4088 | - } | ||
| 4089 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | ||
| 4090 | - u8 view[VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | ||
| 4091 | - rc = sqlite3_blob_read(blobValue, &view, VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH, chunk_offset * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | ||
| 4092 | - if(rc != SQLITE_OK) { | ||
| 4093 | - goto done; | ||
| 4094 | - } | ||
| 4095 | - int length = ((int *)view)[0]; | ||
| 4096 | - if(length <= VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | ||
| 4097 | - sqlite3_result_text(context, (const char*) (view + 4), length, SQLITE_TRANSIENT); | ||
| 4098 | - } | ||
| 4099 | - else { | ||
| 4100 | - sqlite3_stmt * stmt; | ||
| 4101 | - const char * zSql = sqlite3_mprintf("SELECT data FROM " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " WHERE rowid = ?", p->schemaName, p->tableName, metadata_idx); | ||
| 4102 | - if(!zSql) { | ||
| 4103 | - rc = SQLITE_ERROR; | ||
| 4104 | - goto done; | ||
| 4105 | - } | ||
| 4106 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 4107 | - sqlite3_free((void *) zSql); | ||
| 4108 | - if(rc != SQLITE_OK) { | ||
| 4109 | - goto done; | ||
| 4110 | - } | ||
| 4111 | - sqlite3_bind_int64(stmt, 1, rowid); | ||
| 4112 | - rc = sqlite3_step(stmt); | ||
| 4113 | - if(rc != SQLITE_ROW) { | ||
| 4114 | - sqlite3_finalize(stmt); | ||
| 4115 | - rc = SQLITE_ERROR; | ||
| 4116 | - goto done; | ||
| 4117 | - } | ||
| 4118 | - sqlite3_result_value(context, sqlite3_column_value(stmt, 0)); | ||
| 4119 | - sqlite3_finalize(stmt); | ||
| 4120 | - rc = SQLITE_OK; | ||
| 4121 | - } | ||
| 4122 | - break; | ||
| 4123 | - } | ||
| 4124 | - } | ||
| 4125 | - done: | ||
| 4126 | - // blobValue is read-only, will not fail on close | ||
| 4127 | - sqlite3_blob_close(blobValue); | ||
| 4128 | - return rc; | ||
| 4129 | - | ||
| 4130 | -} | ||
| 4131 | - | ||
| 4132 | -int vec0_get_latest_chunk_rowid(vec0_vtab *p, i64 *chunk_rowid, sqlite3_value ** partitionKeyValues) { | ||
| 4133 | - int rc; | ||
| 4134 | - const char *zSql; | ||
| 4135 | - // lazy initialize stmtLatestChunk when needed. May be cleared during xSync() | ||
| 4136 | - if (!p->stmtLatestChunk) { | ||
| 4137 | - if(p->numPartitionColumns > 0) { | ||
| 4138 | - sqlite3_str * s = sqlite3_str_new(NULL); | ||
| 4139 | - sqlite3_str_appendf(s, "SELECT max(rowid) FROM " VEC0_SHADOW_CHUNKS_NAME " WHERE ", | ||
| 4140 | - p->schemaName, p->tableName); | ||
| 4141 | - | ||
| 4142 | - for(int i = 0; i < p->numPartitionColumns; i++) { | ||
| 4143 | - if(i != 0) { | ||
| 4144 | - sqlite3_str_appendall(s, " AND "); | ||
| 4145 | - } | ||
| 4146 | - sqlite3_str_appendf(s, " partition%02d = ? ", i); | ||
| 4147 | - } | ||
| 4148 | - zSql = sqlite3_str_finish(s); | ||
| 4149 | - }else { | ||
| 4150 | - zSql = sqlite3_mprintf("SELECT max(rowid) FROM " VEC0_SHADOW_CHUNKS_NAME, | ||
| 4151 | - p->schemaName, p->tableName); | ||
| 4152 | - } | ||
| 4153 | - | ||
| 4154 | - if (!zSql) { | ||
| 4155 | - rc = SQLITE_NOMEM; | ||
| 4156 | - goto cleanup; | ||
| 4157 | - } | ||
| 4158 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &p->stmtLatestChunk, 0); | ||
| 4159 | - sqlite3_free((void *)zSql); | ||
| 4160 | - if (rc != SQLITE_OK) { | ||
| 4161 | - // IMP: V21406_05476 | ||
| 4162 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | ||
| 4163 | - "could not initialize 'latest chunk' statement"); | ||
| 4164 | - goto cleanup; | ||
| 4165 | - } | ||
| 4166 | - } | ||
| 4167 | - | ||
| 4168 | - for(int i = 0; i < p->numPartitionColumns; i++) { | ||
| 4169 | - sqlite3_bind_value(p->stmtLatestChunk, i+1, (partitionKeyValues[i])); | ||
| 4170 | - } | ||
| 4171 | - | ||
| 4172 | - rc = sqlite3_step(p->stmtLatestChunk); | ||
| 4173 | - if (rc != SQLITE_ROW) { | ||
| 4174 | - // IMP: V31559_15629 | ||
| 4175 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR "Could not find latest chunk"); | ||
| 4176 | - rc = SQLITE_ERROR; | ||
| 4177 | - goto cleanup; | ||
| 4178 | - } | ||
| 4179 | - if(sqlite3_column_type(p->stmtLatestChunk, 0) == SQLITE_NULL){ | ||
| 4180 | - rc = SQLITE_EMPTY; | ||
| 4181 | - goto cleanup; | ||
| 4182 | - } | ||
| 4183 | - *chunk_rowid = sqlite3_column_int64(p->stmtLatestChunk, 0); | ||
| 4184 | - rc = sqlite3_step(p->stmtLatestChunk); | ||
| 4185 | - if (rc != SQLITE_DONE) { | ||
| 4186 | - vtab_set_error(&p->base, | ||
| 4187 | - VEC_INTERAL_ERROR | ||
| 4188 | - "unknown result code when closing out stmtLatestChunk. " | ||
| 4189 | - "Please file an issue: " REPORT_URL, | ||
| 4190 | - p->schemaName, p->shadowChunksName); | ||
| 4191 | - goto cleanup; | ||
| 4192 | - } | ||
| 4193 | - rc = SQLITE_OK; | ||
| 4194 | - | ||
| 4195 | -cleanup: | ||
| 4196 | - if (p->stmtLatestChunk) { | ||
| 4197 | - sqlite3_reset(p->stmtLatestChunk); | ||
| 4198 | - sqlite3_clear_bindings(p->stmtLatestChunk); | ||
| 4199 | - } | ||
| 4200 | - return rc; | ||
| 4201 | -} | ||
| 4202 | - | ||
| 4203 | -int vec0_rowids_insert_rowid(vec0_vtab *p, i64 rowid) { | ||
| 4204 | - int rc = SQLITE_OK; | ||
| 4205 | - int entered = 0; | ||
| 4206 | - UNUSED_PARAMETER(entered); // temporary | ||
| 4207 | - if (!p->stmtRowidsInsertRowid) { | ||
| 4208 | - const char *zSql = | ||
| 4209 | - sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_ROWIDS_NAME "(rowid)" | ||
| 4210 | - "VALUES (?);", | ||
| 4211 | - p->schemaName, p->tableName); | ||
| 4212 | - if (!zSql) { | ||
| 4213 | - rc = SQLITE_NOMEM; | ||
| 4214 | - goto cleanup; | ||
| 4215 | - } | ||
| 4216 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &p->stmtRowidsInsertRowid, 0); | ||
| 4217 | - sqlite3_free((void *)zSql); | ||
| 4218 | - if (rc != SQLITE_OK) { | ||
| 4219 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | ||
| 4220 | - "could not initialize 'insert rowids' statement"); | ||
| 4221 | - goto cleanup; | ||
| 4222 | - } | ||
| 4223 | - } | ||
| 4224 | - | ||
| 4225 | -#if SQLITE_THREADSAFE | ||
| 4226 | - if (sqlite3_mutex_enter) { | ||
| 4227 | - sqlite3_mutex_enter(sqlite3_db_mutex(p->db)); | ||
| 4228 | - entered = 1; | ||
| 4229 | - } | ||
| 4230 | -#endif | ||
| 4231 | - sqlite3_bind_int64(p->stmtRowidsInsertRowid, 1, rowid); | ||
| 4232 | - rc = sqlite3_step(p->stmtRowidsInsertRowid); | ||
| 4233 | - | ||
| 4234 | - if (rc != SQLITE_DONE) { | ||
| 4235 | - if (sqlite3_extended_errcode(p->db) == SQLITE_CONSTRAINT_PRIMARYKEY) { | ||
| 4236 | - // IMP: V17090_01160 | ||
| 4237 | - vtab_set_error(&p->base, "UNIQUE constraint failed on %s primary key", | ||
| 4238 | - p->tableName); | ||
| 4239 | - } else { | ||
| 4240 | - // IMP: V04679_21517 | ||
| 4241 | - vtab_set_error(&p->base, | ||
| 4242 | - "Error inserting rowid into rowids shadow table: %s", | ||
| 4243 | - sqlite3_errmsg(sqlite3_db_handle(p->stmtRowidsInsertId))); | ||
| 4244 | - } | ||
| 4245 | - rc = SQLITE_ERROR; | ||
| 4246 | - goto cleanup; | ||
| 4247 | - } | ||
| 4248 | - | ||
| 4249 | - rc = SQLITE_OK; | ||
| 4250 | - | ||
| 4251 | -cleanup: | ||
| 4252 | - if (p->stmtRowidsInsertRowid) { | ||
| 4253 | - sqlite3_reset(p->stmtRowidsInsertRowid); | ||
| 4254 | - sqlite3_clear_bindings(p->stmtRowidsInsertRowid); | ||
| 4255 | - } | ||
| 4256 | - | ||
| 4257 | -#if SQLITE_THREADSAFE | ||
| 4258 | - if (sqlite3_mutex_leave && entered) { | ||
| 4259 | - sqlite3_mutex_leave(sqlite3_db_mutex(p->db)); | ||
| 4260 | - } | ||
| 4261 | -#endif | ||
| 4262 | - return rc; | ||
| 4263 | -} | ||
| 4264 | - | ||
| 4265 | -int vec0_rowids_insert_id(vec0_vtab *p, sqlite3_value *idValue, i64 *rowid) { | ||
| 4266 | - int rc = SQLITE_OK; | ||
| 4267 | - int entered = 0; | ||
| 4268 | - UNUSED_PARAMETER(entered); // temporary | ||
| 4269 | - if (!p->stmtRowidsInsertId) { | ||
| 4270 | - const char *zSql = | ||
| 4271 | - sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_ROWIDS_NAME "(id)" | ||
| 4272 | - "VALUES (?);", | ||
| 4273 | - p->schemaName, p->tableName); | ||
| 4274 | - if (!zSql) { | ||
| 4275 | - rc = SQLITE_NOMEM; | ||
| 4276 | - goto complete; | ||
| 4277 | - } | ||
| 4278 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &p->stmtRowidsInsertId, 0); | ||
| 4279 | - sqlite3_free((void *)zSql); | ||
| 4280 | - if (rc != SQLITE_OK) { | ||
| 4281 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | ||
| 4282 | - "could not initialize 'insert rowids id' statement"); | ||
| 4283 | - goto complete; | ||
| 4284 | - } | ||
| 4285 | - } | ||
| 4286 | - | ||
| 4287 | -#if SQLITE_THREADSAFE | ||
| 4288 | - if (sqlite3_mutex_enter) { | ||
| 4289 | - sqlite3_mutex_enter(sqlite3_db_mutex(p->db)); | ||
| 4290 | - entered = 1; | ||
| 4291 | - } | ||
| 4292 | -#endif | ||
| 4293 | - | ||
| 4294 | - if (idValue) { | ||
| 4295 | - sqlite3_bind_value(p->stmtRowidsInsertId, 1, idValue); | ||
| 4296 | - } | ||
| 4297 | - rc = sqlite3_step(p->stmtRowidsInsertId); | ||
| 4298 | - | ||
| 4299 | - if (rc != SQLITE_DONE) { | ||
| 4300 | - if (sqlite3_extended_errcode(p->db) == SQLITE_CONSTRAINT_UNIQUE) { | ||
| 4301 | - // IMP: V20497_04568 | ||
| 4302 | - vtab_set_error(&p->base, "UNIQUE constraint failed on %s primary key", | ||
| 4303 | - p->tableName); | ||
| 4304 | - } else { | ||
| 4305 | - // IMP: V24016_08086 | ||
| 4306 | - // IMP: V15177_32015 | ||
| 4307 | - vtab_set_error(&p->base, | ||
| 4308 | - "Error inserting id into rowids shadow table: %s", | ||
| 4309 | - sqlite3_errmsg(sqlite3_db_handle(p->stmtRowidsInsertId))); | ||
| 4310 | - } | ||
| 4311 | - rc = SQLITE_ERROR; | ||
| 4312 | - goto complete; | ||
| 4313 | - } | ||
| 4314 | - | ||
| 4315 | - *rowid = sqlite3_last_insert_rowid(p->db); | ||
| 4316 | - rc = SQLITE_OK; | ||
| 4317 | - | ||
| 4318 | -complete: | ||
| 4319 | - if (p->stmtRowidsInsertId) { | ||
| 4320 | - sqlite3_reset(p->stmtRowidsInsertId); | ||
| 4321 | - sqlite3_clear_bindings(p->stmtRowidsInsertId); | ||
| 4322 | - } | ||
| 4323 | - | ||
| 4324 | -#if SQLITE_THREADSAFE | ||
| 4325 | - if (sqlite3_mutex_leave && entered) { | ||
| 4326 | - sqlite3_mutex_leave(sqlite3_db_mutex(p->db)); | ||
| 4327 | - } | ||
| 4328 | -#endif | ||
| 4329 | - return rc; | ||
| 4330 | -} | ||
| 4331 | - | ||
| 4332 | -int vec0_metadata_chunk_size(vec0_metadata_column_kind kind, int chunk_size) { | ||
| 4333 | - switch(kind) { | ||
| 4334 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: | ||
| 4335 | - return chunk_size / 8; | ||
| 4336 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: | ||
| 4337 | - return chunk_size * sizeof(i64); | ||
| 4338 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: | ||
| 4339 | - return chunk_size * sizeof(double); | ||
| 4340 | - case VEC0_METADATA_COLUMN_KIND_TEXT: | ||
| 4341 | - return chunk_size * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH; | ||
| 4342 | - } | ||
| 4343 | - return 0; | ||
| 4344 | -} | ||
| 4345 | - | ||
| 4346 | -int vec0_rowids_update_position(vec0_vtab *p, i64 rowid, i64 chunk_rowid, | ||
| 4347 | - i64 chunk_offset) { | ||
| 4348 | - int rc = SQLITE_OK; | ||
| 4349 | - | ||
| 4350 | - if (!p->stmtRowidsUpdatePosition) { | ||
| 4351 | - const char *zSql = sqlite3_mprintf(" UPDATE " VEC0_SHADOW_ROWIDS_NAME | ||
| 4352 | - " SET chunk_id = ?, chunk_offset = ?" | ||
| 4353 | - " WHERE rowid = ?", | ||
| 4354 | - p->schemaName, p->tableName); | ||
| 4355 | - if (!zSql) { | ||
| 4356 | - rc = SQLITE_NOMEM; | ||
| 4357 | - goto cleanup; | ||
| 4358 | - } | ||
| 4359 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &p->stmtRowidsUpdatePosition, 0); | ||
| 4360 | - sqlite3_free((void *)zSql); | ||
| 4361 | - if (rc != SQLITE_OK) { | ||
| 4362 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | ||
| 4363 | - "could not initialize 'update rowids position' statement"); | ||
| 4364 | - goto cleanup; | ||
| 4365 | - } | ||
| 4366 | - } | ||
| 4367 | - | ||
| 4368 | - sqlite3_bind_int64(p->stmtRowidsUpdatePosition, 1, chunk_rowid); | ||
| 4369 | - sqlite3_bind_int64(p->stmtRowidsUpdatePosition, 2, chunk_offset); | ||
| 4370 | - sqlite3_bind_int64(p->stmtRowidsUpdatePosition, 3, rowid); | ||
| 4371 | - | ||
| 4372 | - rc = sqlite3_step(p->stmtRowidsUpdatePosition); | ||
| 4373 | - if (rc != SQLITE_DONE) { | ||
| 4374 | - // IMP: V21925_05995 | ||
| 4375 | - vtab_set_error(&p->base, | ||
| 4376 | - VEC_INTERAL_ERROR | ||
| 4377 | - "could not update rowids position for rowid=%lld, " | ||
| 4378 | - "chunk_rowid=%lld, chunk_offset=%lld", | ||
| 4379 | - rowid, chunk_rowid, chunk_offset); | ||
| 4380 | - rc = SQLITE_ERROR; | ||
| 4381 | - goto cleanup; | ||
| 4382 | - } | ||
| 4383 | - rc = SQLITE_OK; | ||
| 4384 | - | ||
| 4385 | -cleanup: | ||
| 4386 | - if (p->stmtRowidsUpdatePosition) { | ||
| 4387 | - sqlite3_reset(p->stmtRowidsUpdatePosition); | ||
| 4388 | - sqlite3_clear_bindings(p->stmtRowidsUpdatePosition); | ||
| 4389 | - } | ||
| 4390 | - | ||
| 4391 | - return rc; | ||
| 4392 | -} | ||
| 4393 | - | ||
| 4394 | -/** | ||
| 4395 | - * @brief Adds a new chunk for the vec0 table, and the corresponding vector | ||
| 4396 | - * chunks. | ||
| 4397 | - * | ||
| 4398 | - * Inserts a new row into the _chunks table, with blank data, and uses that new | ||
| 4399 | - * rowid to insert new blank rows into _vector_chunksXX tables. | ||
| 4400 | - * | ||
| 4401 | - * @param p: vec0 table to add new chunk | ||
| 4402 | - * @param paritionKeyValues: Array of partition key valeus for the new chunk, if available | ||
| 4403 | - * @param chunk_rowid: Output pointer, if not NULL, then will be filled with the | ||
| 4404 | - * new chunk rowid. | ||
| 4405 | - * @return int SQLITE_OK on success, error code otherwise. | ||
| 4406 | - */ | ||
| 4407 | -int vec0_new_chunk(vec0_vtab *p, sqlite3_value ** partitionKeyValues, i64 *chunk_rowid) { | ||
| 4408 | - int rc; | ||
| 4409 | - char *zSql; | ||
| 4410 | - sqlite3_stmt *stmt; | ||
| 4411 | - i64 rowid; | ||
| 4412 | - | ||
| 4413 | - // Step 1: Insert a new row in _chunks, capture that new rowid | ||
| 4414 | - if(p->numPartitionColumns > 0) { | ||
| 4415 | - sqlite3_str * s = sqlite3_str_new(NULL); | ||
| 4416 | - sqlite3_str_appendf(s, "INSERT INTO " VEC0_SHADOW_CHUNKS_NAME, p->schemaName, p->tableName); | ||
| 4417 | - sqlite3_str_appendall(s, "(size, validity, rowids"); | ||
| 4418 | - for(int i = 0; i < p->numPartitionColumns; i++) { | ||
| 4419 | - sqlite3_str_appendf(s, ", partition%02d", i); | ||
| 4420 | - } | ||
| 4421 | - sqlite3_str_appendall(s, ") VALUES (?, ?, ?"); | ||
| 4422 | - for(int i = 0; i < p->numPartitionColumns; i++) { | ||
| 4423 | - sqlite3_str_appendall(s, ", ?"); | ||
| 4424 | - } | ||
| 4425 | - sqlite3_str_appendall(s, ")"); | ||
| 4426 | - | ||
| 4427 | - zSql = sqlite3_str_finish(s); | ||
| 4428 | - }else { | ||
| 4429 | - zSql = sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_CHUNKS_NAME | ||
| 4430 | - "(size, validity, rowids) " | ||
| 4431 | - "VALUES (?, ?, ?);", | ||
| 4432 | - p->schemaName, p->tableName); | ||
| 4433 | - } | ||
| 4434 | - | ||
| 4435 | - if (!zSql) { | ||
| 4436 | - return SQLITE_NOMEM; | ||
| 4437 | - } | ||
| 4438 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 4439 | - sqlite3_free(zSql); | ||
| 4440 | - if (rc != SQLITE_OK) { | ||
| 4441 | - sqlite3_finalize(stmt); | ||
| 4442 | - return rc; | ||
| 4443 | - } | ||
| 4444 | - | ||
| 4445 | -#if SQLITE_THREADSAFE | ||
| 4446 | - if (sqlite3_mutex_enter) { | ||
| 4447 | - sqlite3_mutex_enter(sqlite3_db_mutex(p->db)); | ||
| 4448 | - } | ||
| 4449 | -#endif | ||
| 4450 | - | ||
| 4451 | - sqlite3_bind_int64(stmt, 1, p->chunk_size); // size | ||
| 4452 | - sqlite3_bind_zeroblob(stmt, 2, p->chunk_size / CHAR_BIT); // validity bitmap | ||
| 4453 | - sqlite3_bind_zeroblob(stmt, 3, p->chunk_size * sizeof(i64)); // rowids | ||
| 4454 | - | ||
| 4455 | - for(int i = 0; i < p->numPartitionColumns; i++) { | ||
| 4456 | - sqlite3_bind_value(stmt, 4 + i, partitionKeyValues[i]); | ||
| 4457 | - } | ||
| 4458 | - | ||
| 4459 | - rc = sqlite3_step(stmt); | ||
| 4460 | - int failed = rc != SQLITE_DONE; | ||
| 4461 | - rowid = sqlite3_last_insert_rowid(p->db); | ||
| 4462 | -#if SQLITE_THREADSAFE | ||
| 4463 | - if (sqlite3_mutex_leave) { | ||
| 4464 | - sqlite3_mutex_leave(sqlite3_db_mutex(p->db)); | ||
| 4465 | - } | ||
| 4466 | -#endif | ||
| 4467 | - sqlite3_finalize(stmt); | ||
| 4468 | - if (failed) { | ||
| 4469 | - return SQLITE_ERROR; | ||
| 4470 | - } | ||
| 4471 | - | ||
| 4472 | - // Step 2: Create new vector chunks for each vector column, with | ||
| 4473 | - // that new chunk_rowid. | ||
| 4474 | - | ||
| 4475 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | ||
| 4476 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_VECTOR) { | ||
| 4477 | - continue; | ||
| 4478 | - } | ||
| 4479 | - int vector_column_idx = p->user_column_idxs[i]; | ||
| 4480 | - i64 vectorsSize = | ||
| 4481 | - p->chunk_size * vector_column_byte_size(p->vector_columns[vector_column_idx]); | ||
| 4482 | - | ||
| 4483 | - zSql = sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_VECTOR_N_NAME | ||
| 4484 | - "(rowid, vectors)" | ||
| 4485 | - "VALUES (?, ?)", | ||
| 4486 | - p->schemaName, p->tableName, vector_column_idx); | ||
| 4487 | - if (!zSql) { | ||
| 4488 | - return SQLITE_NOMEM; | ||
| 4489 | - } | ||
| 4490 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 4491 | - sqlite3_free(zSql); | ||
| 4492 | - | ||
| 4493 | - if (rc != SQLITE_OK) { | ||
| 4494 | - sqlite3_finalize(stmt); | ||
| 4495 | - return rc; | ||
| 4496 | - } | ||
| 4497 | - | ||
| 4498 | - sqlite3_bind_int64(stmt, 1, rowid); | ||
| 4499 | - sqlite3_bind_zeroblob64(stmt, 2, vectorsSize); | ||
| 4500 | - | ||
| 4501 | - rc = sqlite3_step(stmt); | ||
| 4502 | - sqlite3_finalize(stmt); | ||
| 4503 | - if (rc != SQLITE_DONE) { | ||
| 4504 | - return rc; | ||
| 4505 | - } | ||
| 4506 | - } | ||
| 4507 | - | ||
| 4508 | - // Step 3: Create new metadata chunks for each metadata column | ||
| 4509 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | ||
| 4510 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_METADATA) { | ||
| 4511 | - continue; | ||
| 4512 | - } | ||
| 4513 | - int metadata_column_idx = p->user_column_idxs[i]; | ||
| 4514 | - zSql = sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_METADATA_N_NAME | ||
| 4515 | - "(rowid, data)" | ||
| 4516 | - "VALUES (?, ?)", | ||
| 4517 | - p->schemaName, p->tableName, metadata_column_idx); | ||
| 4518 | - if (!zSql) { | ||
| 4519 | - return SQLITE_NOMEM; | ||
| 4520 | - } | ||
| 4521 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 4522 | - sqlite3_free(zSql); | ||
| 4523 | - | ||
| 4524 | - if (rc != SQLITE_OK) { | ||
| 4525 | - sqlite3_finalize(stmt); | ||
| 4526 | - return rc; | ||
| 4527 | - } | ||
| 4528 | - | ||
| 4529 | - sqlite3_bind_int64(stmt, 1, rowid); | ||
| 4530 | - sqlite3_bind_zeroblob64(stmt, 2, vec0_metadata_chunk_size(p->metadata_columns[metadata_column_idx].kind, p->chunk_size)); | ||
| 4531 | - | ||
| 4532 | - rc = sqlite3_step(stmt); | ||
| 4533 | - sqlite3_finalize(stmt); | ||
| 4534 | - if (rc != SQLITE_DONE) { | ||
| 4535 | - return rc; | ||
| 4536 | - } | ||
| 4537 | - } | ||
| 4538 | - | ||
| 4539 | - | ||
| 4540 | - if (chunk_rowid) { | ||
| 4541 | - *chunk_rowid = rowid; | ||
| 4542 | - } | ||
| 4543 | - | ||
| 4544 | - return SQLITE_OK; | ||
| 4545 | -} | ||
| 4546 | - | ||
| 4547 | -struct vec0_query_fullscan_data { | ||
| 4548 | - sqlite3_stmt *rowids_stmt; | ||
| 4549 | - i8 done; | ||
| 4550 | -}; | ||
| 4551 | -void vec0_query_fullscan_data_clear( | ||
| 4552 | - struct vec0_query_fullscan_data *fullscan_data) { | ||
| 4553 | - if (!fullscan_data) | ||
| 4554 | - return; | ||
| 4555 | - | ||
| 4556 | - if (fullscan_data->rowids_stmt) { | ||
| 4557 | - sqlite3_finalize(fullscan_data->rowids_stmt); | ||
| 4558 | - fullscan_data->rowids_stmt = NULL; | ||
| 4559 | - } | ||
| 4560 | -} | ||
| 4561 | - | ||
| 4562 | -struct vec0_query_knn_data { | ||
| 4563 | - i64 k; | ||
| 4564 | - i64 k_used; | ||
| 4565 | - // Array of rowids of size k. Must be freed with sqlite3_free(). | ||
| 4566 | - i64 *rowids; | ||
| 4567 | - // Array of distances of size k. Must be freed with sqlite3_free(). | ||
| 4568 | - f32 *distances; | ||
| 4569 | - i64 current_idx; | ||
| 4570 | -}; | ||
| 4571 | -void vec0_query_knn_data_clear(struct vec0_query_knn_data *knn_data) { | ||
| 4572 | - if (!knn_data) | ||
| 4573 | - return; | ||
| 4574 | - | ||
| 4575 | - if (knn_data->rowids) { | ||
| 4576 | - sqlite3_free(knn_data->rowids); | ||
| 4577 | - knn_data->rowids = NULL; | ||
| 4578 | - } | ||
| 4579 | - if (knn_data->distances) { | ||
| 4580 | - sqlite3_free(knn_data->distances); | ||
| 4581 | - knn_data->distances = NULL; | ||
| 4582 | - } | ||
| 4583 | -} | ||
| 4584 | - | ||
| 4585 | -struct vec0_query_point_data { | ||
| 4586 | - i64 rowid; | ||
| 4587 | - void *vectors[VEC0_MAX_VECTOR_COLUMNS]; | ||
| 4588 | - int done; | ||
| 4589 | -}; | ||
| 4590 | -void vec0_query_point_data_clear(struct vec0_query_point_data *point_data) { | ||
| 4591 | - if (!point_data) | ||
| 4592 | - return; | ||
| 4593 | - for (int i = 0; i < VEC0_MAX_VECTOR_COLUMNS; i++) { | ||
| 4594 | - sqlite3_free(point_data->vectors[i]); | ||
| 4595 | - point_data->vectors[i] = NULL; | ||
| 4596 | - } | ||
| 4597 | -} | ||
| 4598 | - | ||
| 4599 | -typedef enum { | ||
| 4600 | - // If any values are updated, please update the ARCHITECTURE.md docs accordingly! | ||
| 4601 | - | ||
| 4602 | - VEC0_QUERY_PLAN_FULLSCAN = '1', | ||
| 4603 | - VEC0_QUERY_PLAN_POINT = '2', | ||
| 4604 | - VEC0_QUERY_PLAN_KNN = '3', | ||
| 4605 | -} vec0_query_plan; | ||
| 4606 | - | ||
| 4607 | -typedef struct vec0_cursor vec0_cursor; | ||
| 4608 | -struct vec0_cursor { | ||
| 4609 | - sqlite3_vtab_cursor base; | ||
| 4610 | - | ||
| 4611 | - vec0_query_plan query_plan; | ||
| 4612 | - struct vec0_query_fullscan_data *fullscan_data; | ||
| 4613 | - struct vec0_query_knn_data *knn_data; | ||
| 4614 | - struct vec0_query_point_data *point_data; | ||
| 4615 | -}; | ||
| 4616 | - | ||
| 4617 | -void vec0_cursor_clear(vec0_cursor *pCur) { | ||
| 4618 | - if (pCur->fullscan_data) { | ||
| 4619 | - vec0_query_fullscan_data_clear(pCur->fullscan_data); | ||
| 4620 | - sqlite3_free(pCur->fullscan_data); | ||
| 4621 | - pCur->fullscan_data = NULL; | ||
| 4622 | - } | ||
| 4623 | - if (pCur->knn_data) { | ||
| 4624 | - vec0_query_knn_data_clear(pCur->knn_data); | ||
| 4625 | - sqlite3_free(pCur->knn_data); | ||
| 4626 | - pCur->knn_data = NULL; | ||
| 4627 | - } | ||
| 4628 | - if (pCur->point_data) { | ||
| 4629 | - vec0_query_point_data_clear(pCur->point_data); | ||
| 4630 | - sqlite3_free(pCur->point_data); | ||
| 4631 | - pCur->point_data = NULL; | ||
| 4632 | - } | ||
| 4633 | -} | ||
| 4634 | - | ||
| 4635 | -#define VEC_CONSTRUCTOR_ERROR "vec0 constructor error: " | ||
| 4636 | -static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv, | ||
| 4637 | - sqlite3_vtab **ppVtab, char **pzErr, bool isCreate) { | ||
| 4638 | - UNUSED_PARAMETER(pAux); | ||
| 4639 | - vec0_vtab *pNew; | ||
| 4640 | - int rc; | ||
| 4641 | - const char *zSql; | ||
| 4642 | - | ||
| 4643 | - pNew = sqlite3_malloc(sizeof(*pNew)); | ||
| 4644 | - if (pNew == 0) | ||
| 4645 | - return SQLITE_NOMEM; | ||
| 4646 | - memset(pNew, 0, sizeof(*pNew)); | ||
| 4647 | - | ||
| 4648 | - // Declared chunk_size=N for entire table. | ||
| 4649 | - // -1 to use the defualt, otherwise will get re-assigned on `chunk_size=N` | ||
| 4650 | - // option | ||
| 4651 | - int chunk_size = -1; | ||
| 4652 | - int numVectorColumns = 0; | ||
| 4653 | - int numPartitionColumns = 0; | ||
| 4654 | - int numAuxiliaryColumns = 0; | ||
| 4655 | - int numMetadataColumns = 0; | ||
| 4656 | - int user_column_idx = 0; | ||
| 4657 | - | ||
| 4658 | - // track if a "primary key" column is defined | ||
| 4659 | - char *pkColumnName = NULL; | ||
| 4660 | - int pkColumnNameLength; | ||
| 4661 | - int pkColumnType = SQLITE_INTEGER; | ||
| 4662 | - | ||
| 4663 | - for (int i = 3; i < argc; i++) { | ||
| 4664 | - struct VectorColumnDefinition vecColumn; | ||
| 4665 | - struct Vec0PartitionColumnDefinition partitionColumn; | ||
| 4666 | - struct Vec0AuxiliaryColumnDefinition auxColumn; | ||
| 4667 | - struct Vec0MetadataColumnDefinition metadataColumn; | ||
| 4668 | - char *cName = NULL; | ||
| 4669 | - int cNameLength; | ||
| 4670 | - int cType; | ||
| 4671 | - | ||
| 4672 | - // Scenario #1: Constructor argument is a vector column definition, ie `foo float[1024]` | ||
| 4673 | - rc = vec0_parse_vector_column(argv[i], strlen(argv[i]), &vecColumn); | ||
| 4674 | - if (rc == SQLITE_ERROR) { | ||
| 4675 | - *pzErr = sqlite3_mprintf( | ||
| 4676 | - VEC_CONSTRUCTOR_ERROR "could not parse vector column '%s'", argv[i]); | ||
| 4677 | - goto error; | ||
| 4678 | - } | ||
| 4679 | - if (rc == SQLITE_OK) { | ||
| 4680 | - if (numVectorColumns >= VEC0_MAX_VECTOR_COLUMNS) { | ||
| 4681 | - sqlite3_free(vecColumn.name); | ||
| 4682 | - *pzErr = sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR | ||
| 4683 | - "Too many provided vector columns, maximum %d", | ||
| 4684 | - VEC0_MAX_VECTOR_COLUMNS); | ||
| 4685 | - goto error; | ||
| 4686 | - } | ||
| 4687 | - | ||
| 4688 | - if (vecColumn.dimensions > SQLITE_VEC_VEC0_MAX_DIMENSIONS) { | ||
| 4689 | - sqlite3_free(vecColumn.name); | ||
| 4690 | - *pzErr = sqlite3_mprintf( | ||
| 4691 | - VEC_CONSTRUCTOR_ERROR | ||
| 4692 | - "Dimension on vector column too large, provided %lld, maximum %lld", | ||
| 4693 | - (i64)vecColumn.dimensions, SQLITE_VEC_VEC0_MAX_DIMENSIONS); | ||
| 4694 | - goto error; | ||
| 4695 | - } | ||
| 4696 | - pNew->user_column_kinds[user_column_idx] = SQLITE_VEC0_USER_COLUMN_KIND_VECTOR; | ||
| 4697 | - pNew->user_column_idxs[user_column_idx] = numVectorColumns; | ||
| 4698 | - memcpy(&pNew->vector_columns[numVectorColumns], &vecColumn, sizeof(vecColumn)); | ||
| 4699 | - numVectorColumns++; | ||
| 4700 | - user_column_idx++; | ||
| 4701 | - | ||
| 4702 | - continue; | ||
| 4703 | - } | ||
| 4704 | - | ||
| 4705 | - // Scenario #2: Constructor argument is a partition key column definition, ie `user_id text partition key` | ||
| 4706 | - rc = vec0_parse_partition_key_definition(argv[i], strlen(argv[i]), &cName, | ||
| 4707 | - &cNameLength, &cType); | ||
| 4708 | - if (rc == SQLITE_OK) { | ||
| 4709 | - if (numPartitionColumns >= VEC0_MAX_PARTITION_COLUMNS) { | ||
| 4710 | - *pzErr = sqlite3_mprintf( | ||
| 4711 | - VEC_CONSTRUCTOR_ERROR | ||
| 4712 | - "More than %d partition key columns were provided", | ||
| 4713 | - VEC0_MAX_PARTITION_COLUMNS); | ||
| 4714 | - goto error; | ||
| 4715 | - } | ||
| 4716 | - partitionColumn.type = cType; | ||
| 4717 | - partitionColumn.name_length = cNameLength; | ||
| 4718 | - partitionColumn.name = sqlite3_mprintf("%.*s", cNameLength, cName); | ||
| 4719 | - if(!partitionColumn.name) { | ||
| 4720 | - rc = SQLITE_NOMEM; | ||
| 4721 | - goto error; | ||
| 4722 | - } | ||
| 4723 | - | ||
| 4724 | - pNew->user_column_kinds[user_column_idx] = SQLITE_VEC0_USER_COLUMN_KIND_PARTITION; | ||
| 4725 | - pNew->user_column_idxs[user_column_idx] = numPartitionColumns; | ||
| 4726 | - memcpy(&pNew->paritition_columns[numPartitionColumns], &partitionColumn, sizeof(partitionColumn)); | ||
| 4727 | - numPartitionColumns++; | ||
| 4728 | - user_column_idx++; | ||
| 4729 | - continue; | ||
| 4730 | - } | ||
| 4731 | - | ||
| 4732 | - // Scenario #3: Constructor argument is a primary key column definition, ie `article_id text primary key` | ||
| 4733 | - rc = vec0_parse_primary_key_definition(argv[i], strlen(argv[i]), &cName, | ||
| 4734 | - &cNameLength, &cType); | ||
| 4735 | - if (rc == SQLITE_OK) { | ||
| 4736 | - if (pkColumnName) { | ||
| 4737 | - *pzErr = sqlite3_mprintf( | ||
| 4738 | - VEC_CONSTRUCTOR_ERROR | ||
| 4739 | - "More than one primary key definition was provided, vec0 only " | ||
| 4740 | - "suports a single primary key column", | ||
| 4741 | - argv[i]); | ||
| 4742 | - goto error; | ||
| 4743 | - } | ||
| 4744 | - pkColumnName = cName; | ||
| 4745 | - pkColumnNameLength = cNameLength; | ||
| 4746 | - pkColumnType = cType; | ||
| 4747 | - continue; | ||
| 4748 | - } | ||
| 4749 | - | ||
| 4750 | - // Scenario #4: Constructor argument is a auxiliary column definition, ie `+contents text` | ||
| 4751 | - rc = vec0_parse_auxiliary_column_definition(argv[i], strlen(argv[i]), &cName, | ||
| 4752 | - &cNameLength, &cType); | ||
| 4753 | - if(rc == SQLITE_OK) { | ||
| 4754 | - if (numAuxiliaryColumns >= VEC0_MAX_AUXILIARY_COLUMNS) { | ||
| 4755 | - *pzErr = sqlite3_mprintf( | ||
| 4756 | - VEC_CONSTRUCTOR_ERROR | ||
| 4757 | - "More than %d auxiliary columns were provided", | ||
| 4758 | - VEC0_MAX_AUXILIARY_COLUMNS); | ||
| 4759 | - goto error; | ||
| 4760 | - } | ||
| 4761 | - auxColumn.type = cType; | ||
| 4762 | - auxColumn.name_length = cNameLength; | ||
| 4763 | - auxColumn.name = sqlite3_mprintf("%.*s", cNameLength, cName); | ||
| 4764 | - if(!auxColumn.name) { | ||
| 4765 | - rc = SQLITE_NOMEM; | ||
| 4766 | - goto error; | ||
| 4767 | - } | ||
| 4768 | - | ||
| 4769 | - pNew->user_column_kinds[user_column_idx] = SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY; | ||
| 4770 | - pNew->user_column_idxs[user_column_idx] = numAuxiliaryColumns; | ||
| 4771 | - memcpy(&pNew->auxiliary_columns[numAuxiliaryColumns], &auxColumn, sizeof(auxColumn)); | ||
| 4772 | - numAuxiliaryColumns++; | ||
| 4773 | - user_column_idx++; | ||
| 4774 | - continue; | ||
| 4775 | - } | ||
| 4776 | - | ||
| 4777 | - vec0_metadata_column_kind kind; | ||
| 4778 | - rc = vec0_parse_metadata_column_definition(argv[i], strlen(argv[i]), &cName, | ||
| 4779 | - &cNameLength, &kind); | ||
| 4780 | - if(rc == SQLITE_OK) { | ||
| 4781 | - if (numMetadataColumns >= VEC0_MAX_METADATA_COLUMNS) { | ||
| 4782 | - *pzErr = sqlite3_mprintf( | ||
| 4783 | - VEC_CONSTRUCTOR_ERROR | ||
| 4784 | - "More than %d metadata columns were provided", | ||
| 4785 | - VEC0_MAX_METADATA_COLUMNS); | ||
| 4786 | - goto error; | ||
| 4787 | - } | ||
| 4788 | - metadataColumn.kind = kind; | ||
| 4789 | - metadataColumn.name_length = cNameLength; | ||
| 4790 | - metadataColumn.name = sqlite3_mprintf("%.*s", cNameLength, cName); | ||
| 4791 | - if(!metadataColumn.name) { | ||
| 4792 | - rc = SQLITE_NOMEM; | ||
| 4793 | - goto error; | ||
| 4794 | - } | ||
| 4795 | - | ||
| 4796 | - pNew->user_column_kinds[user_column_idx] = SQLITE_VEC0_USER_COLUMN_KIND_METADATA; | ||
| 4797 | - pNew->user_column_idxs[user_column_idx] = numMetadataColumns; | ||
| 4798 | - memcpy(&pNew->metadata_columns[numMetadataColumns], &metadataColumn, sizeof(metadataColumn)); | ||
| 4799 | - numMetadataColumns++; | ||
| 4800 | - user_column_idx++; | ||
| 4801 | - continue; | ||
| 4802 | - } | ||
| 4803 | - | ||
| 4804 | - // Scenario #4: Constructor argument is a table-level option, ie `chunk_size` | ||
| 4805 | - | ||
| 4806 | - char *key; | ||
| 4807 | - char *value; | ||
| 4808 | - int keyLength, valueLength; | ||
| 4809 | - rc = vec0_parse_table_option(argv[i], strlen(argv[i]), &key, &keyLength, | ||
| 4810 | - &value, &valueLength); | ||
| 4811 | - if (rc == SQLITE_ERROR) { | ||
| 4812 | - *pzErr = sqlite3_mprintf( | ||
| 4813 | - VEC_CONSTRUCTOR_ERROR "could not parse table option '%s'", argv[i]); | ||
| 4814 | - goto error; | ||
| 4815 | - } | ||
| 4816 | - if (rc == SQLITE_OK) { | ||
| 4817 | - if (sqlite3_strnicmp(key, "chunk_size", keyLength) == 0) { | ||
| 4818 | - chunk_size = atoi(value); | ||
| 4819 | - if (chunk_size <= 0) { | ||
| 4820 | - // IMP: V01931_18769 | ||
| 4821 | - *pzErr = | ||
| 4822 | - sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR | ||
| 4823 | - "chunk_size must be a non-zero positive integer"); | ||
| 4824 | - goto error; | ||
| 4825 | - } | ||
| 4826 | - if ((chunk_size % 8) != 0) { | ||
| 4827 | - // IMP: V14110_30948 | ||
| 4828 | - *pzErr = sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR | ||
| 4829 | - "chunk_size must be divisible by 8"); | ||
| 4830 | - goto error; | ||
| 4831 | - } | ||
| 4832 | -#define SQLITE_VEC_CHUNK_SIZE_MAX 4096 | ||
| 4833 | - if (chunk_size > SQLITE_VEC_CHUNK_SIZE_MAX) { | ||
| 4834 | - *pzErr = | ||
| 4835 | - sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR "chunk_size too large"); | ||
| 4836 | - goto error; | ||
| 4837 | - } | ||
| 4838 | - } else { | ||
| 4839 | - // IMP: V27642_11712 | ||
| 4840 | - *pzErr = sqlite3_mprintf( | ||
| 4841 | - VEC_CONSTRUCTOR_ERROR "Unknown table option: %.*s", keyLength, key); | ||
| 4842 | - goto error; | ||
| 4843 | - } | ||
| 4844 | - continue; | ||
| 4845 | - } | ||
| 4846 | - | ||
| 4847 | - // Scenario #5: Unknown constructor argument | ||
| 4848 | - *pzErr = | ||
| 4849 | - sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR "Could not parse '%s'", argv[i]); | ||
| 4850 | - goto error; | ||
| 4851 | - } | ||
| 4852 | - | ||
| 4853 | - if (chunk_size < 0) { | ||
| 4854 | - chunk_size = 1024; | ||
| 4855 | - } | ||
| 4856 | - | ||
| 4857 | - if (numVectorColumns <= 0) { | ||
| 4858 | - *pzErr = sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR | ||
| 4859 | - "At least one vector column is required"); | ||
| 4860 | - goto error; | ||
| 4861 | - } | ||
| 4862 | - | ||
| 4863 | - sqlite3_str *createStr = sqlite3_str_new(NULL); | ||
| 4864 | - sqlite3_str_appendall(createStr, "CREATE TABLE x("); | ||
| 4865 | - if (pkColumnName) { | ||
| 4866 | - sqlite3_str_appendf(createStr, "\"%.*w\" primary key, ", pkColumnNameLength, | ||
| 4867 | - pkColumnName); | ||
| 4868 | - } else { | ||
| 4869 | - sqlite3_str_appendall(createStr, "rowid, "); | ||
| 4870 | - } | ||
| 4871 | - for (int i = 0; i < numVectorColumns + numPartitionColumns + numAuxiliaryColumns + numMetadataColumns; i++) { | ||
| 4872 | - switch(pNew->user_column_kinds[i]) { | ||
| 4873 | - case SQLITE_VEC0_USER_COLUMN_KIND_VECTOR: { | ||
| 4874 | - int vector_idx = pNew->user_column_idxs[i]; | ||
| 4875 | - sqlite3_str_appendf(createStr, "\"%.*w\", ", | ||
| 4876 | - pNew->vector_columns[vector_idx].name_length, | ||
| 4877 | - pNew->vector_columns[vector_idx].name); | ||
| 4878 | - break; | ||
| 4879 | - } | ||
| 4880 | - case SQLITE_VEC0_USER_COLUMN_KIND_PARTITION: { | ||
| 4881 | - int partition_idx = pNew->user_column_idxs[i]; | ||
| 4882 | - sqlite3_str_appendf(createStr, "\"%.*w\", ", | ||
| 4883 | - pNew->paritition_columns[partition_idx].name_length, | ||
| 4884 | - pNew->paritition_columns[partition_idx].name); | ||
| 4885 | - break; | ||
| 4886 | - } | ||
| 4887 | - case SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY: { | ||
| 4888 | - int auxiliary_idx = pNew->user_column_idxs[i]; | ||
| 4889 | - sqlite3_str_appendf(createStr, "\"%.*w\", ", | ||
| 4890 | - pNew->auxiliary_columns[auxiliary_idx].name_length, | ||
| 4891 | - pNew->auxiliary_columns[auxiliary_idx].name); | ||
| 4892 | - break; | ||
| 4893 | - } | ||
| 4894 | - case SQLITE_VEC0_USER_COLUMN_KIND_METADATA: { | ||
| 4895 | - int metadata_idx = pNew->user_column_idxs[i]; | ||
| 4896 | - sqlite3_str_appendf(createStr, "\"%.*w\", ", | ||
| 4897 | - pNew->metadata_columns[metadata_idx].name_length, | ||
| 4898 | - pNew->metadata_columns[metadata_idx].name); | ||
| 4899 | - break; | ||
| 4900 | - } | ||
| 4901 | - } | ||
| 4902 | - | ||
| 4903 | - } | ||
| 4904 | - sqlite3_str_appendall(createStr, " distance hidden, k hidden) "); | ||
| 4905 | - if (pkColumnName) { | ||
| 4906 | - sqlite3_str_appendall(createStr, "without rowid "); | ||
| 4907 | - } | ||
| 4908 | - zSql = sqlite3_str_finish(createStr); | ||
| 4909 | - if (!zSql) { | ||
| 4910 | - goto error; | ||
| 4911 | - } | ||
| 4912 | - rc = sqlite3_declare_vtab(db, zSql); | ||
| 4913 | - sqlite3_free((void *)zSql); | ||
| 4914 | - if (rc != SQLITE_OK) { | ||
| 4915 | - *pzErr = sqlite3_mprintf(VEC_CONSTRUCTOR_ERROR | ||
| 4916 | - "could not declare virtual table, '%s'", | ||
| 4917 | - sqlite3_errmsg(db)); | ||
| 4918 | - goto error; | ||
| 4919 | - } | ||
| 4920 | - | ||
| 4921 | - const char *schemaName = argv[1]; | ||
| 4922 | - const char *tableName = argv[2]; | ||
| 4923 | - | ||
| 4924 | - pNew->db = db; | ||
| 4925 | - pNew->pkIsText = pkColumnType == SQLITE_TEXT; | ||
| 4926 | - pNew->schemaName = sqlite3_mprintf("%s", schemaName); | ||
| 4927 | - if (!pNew->schemaName) { | ||
| 4928 | - goto error; | ||
| 4929 | - } | ||
| 4930 | - pNew->tableName = sqlite3_mprintf("%s", tableName); | ||
| 4931 | - if (!pNew->tableName) { | ||
| 4932 | - goto error; | ||
| 4933 | - } | ||
| 4934 | - pNew->shadowRowidsName = sqlite3_mprintf("%s_rowids", tableName); | ||
| 4935 | - if (!pNew->shadowRowidsName) { | ||
| 4936 | - goto error; | ||
| 4937 | - } | ||
| 4938 | - pNew->shadowChunksName = sqlite3_mprintf("%s_chunks", tableName); | ||
| 4939 | - if (!pNew->shadowChunksName) { | ||
| 4940 | - goto error; | ||
| 4941 | - } | ||
| 4942 | - pNew->numVectorColumns = numVectorColumns; | ||
| 4943 | - pNew->numPartitionColumns = numPartitionColumns; | ||
| 4944 | - pNew->numAuxiliaryColumns = numAuxiliaryColumns; | ||
| 4945 | - pNew->numMetadataColumns = numMetadataColumns; | ||
| 4946 | - | ||
| 4947 | - for (int i = 0; i < pNew->numVectorColumns; i++) { | ||
| 4948 | - pNew->shadowVectorChunksNames[i] = | ||
| 4949 | - sqlite3_mprintf("%s_vector_chunks%02d", tableName, i); | ||
| 4950 | - if (!pNew->shadowVectorChunksNames[i]) { | ||
| 4951 | - goto error; | ||
| 4952 | - } | ||
| 4953 | - } | ||
| 4954 | - for (int i = 0; i < pNew->numMetadataColumns; i++) { | ||
| 4955 | - pNew->shadowMetadataChunksNames[i] = | ||
| 4956 | - sqlite3_mprintf("%s_metadatachunks%02d", tableName, i); | ||
| 4957 | - if (!pNew->shadowMetadataChunksNames[i]) { | ||
| 4958 | - goto error; | ||
| 4959 | - } | ||
| 4960 | - } | ||
| 4961 | - pNew->chunk_size = chunk_size; | ||
| 4962 | - | ||
| 4963 | - // if xCreate, then create the necessary shadow tables | ||
| 4964 | - if (isCreate) { | ||
| 4965 | - sqlite3_stmt *stmt; | ||
| 4966 | - int rc; | ||
| 4967 | - | ||
| 4968 | - char * zCreateInfo = sqlite3_mprintf("CREATE TABLE "VEC0_SHADOW_INFO_NAME " (key text primary key, value any)", pNew->schemaName, pNew->tableName); | ||
| 4969 | - if(!zCreateInfo) { | ||
| 4970 | - goto error; | ||
| 4971 | - } | ||
| 4972 | - rc = sqlite3_prepare_v2(db, zCreateInfo, -1, &stmt, NULL); | ||
| 4973 | - | ||
| 4974 | - sqlite3_free((void *) zCreateInfo); | ||
| 4975 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 4976 | - // TODO(IMP) | ||
| 4977 | - sqlite3_finalize(stmt); | ||
| 4978 | - *pzErr = sqlite3_mprintf("Could not create '_info' shadow table: %s", | ||
| 4979 | - sqlite3_errmsg(db)); | ||
| 4980 | - goto error; | ||
| 4981 | - } | ||
| 4982 | - sqlite3_finalize(stmt); | ||
| 4983 | - | ||
| 4984 | - char * zSeedInfo = sqlite3_mprintf( | ||
| 4985 | - "INSERT INTO "VEC0_SHADOW_INFO_NAME "(key, value) VALUES " | ||
| 4986 | - "(?1, ?2), (?3, ?4), (?5, ?6), (?7, ?8) ", | ||
| 4987 | - pNew->schemaName, pNew->tableName | ||
| 4988 | - ); | ||
| 4989 | - if(!zSeedInfo) { | ||
| 4990 | - goto error; | ||
| 4991 | - } | ||
| 4992 | - rc = sqlite3_prepare_v2(db, zSeedInfo, -1, &stmt, NULL); | ||
| 4993 | - sqlite3_free((void *) zSeedInfo); | ||
| 4994 | - if (rc != SQLITE_OK) { | ||
| 4995 | - // TODO(IMP) | ||
| 4996 | - sqlite3_finalize(stmt); | ||
| 4997 | - *pzErr = sqlite3_mprintf("Could not seed '_info' shadow table: %s", | ||
| 4998 | - sqlite3_errmsg(db)); | ||
| 4999 | - goto error; | ||
| 5000 | - } | ||
| 5001 | - sqlite3_bind_text(stmt, 1, "CREATE_VERSION", -1, SQLITE_STATIC); | ||
| 5002 | - sqlite3_bind_text(stmt, 2, SQLITE_VEC_VERSION, -1, SQLITE_STATIC); | ||
| 5003 | - sqlite3_bind_text(stmt, 3, "CREATE_VERSION_MAJOR", -1, SQLITE_STATIC); | ||
| 5004 | - sqlite3_bind_int(stmt, 4, SQLITE_VEC_VERSION_MAJOR); | ||
| 5005 | - sqlite3_bind_text(stmt, 5, "CREATE_VERSION_MINOR", -1, SQLITE_STATIC); | ||
| 5006 | - sqlite3_bind_int(stmt, 6, SQLITE_VEC_VERSION_MINOR); | ||
| 5007 | - sqlite3_bind_text(stmt, 7, "CREATE_VERSION_PATCH", -1, SQLITE_STATIC); | ||
| 5008 | - sqlite3_bind_int(stmt, 8, SQLITE_VEC_VERSION_PATCH); | ||
| 5009 | - | ||
| 5010 | - if(sqlite3_step(stmt) != SQLITE_DONE) { | ||
| 5011 | - // TODO(IMP) | ||
| 5012 | - sqlite3_finalize(stmt); | ||
| 5013 | - *pzErr = sqlite3_mprintf("Could not seed '_info' shadow table: %s", | ||
| 5014 | - sqlite3_errmsg(db)); | ||
| 5015 | - goto error; | ||
| 5016 | - } | ||
| 5017 | - sqlite3_finalize(stmt); | ||
| 5018 | - | ||
| 5019 | - | ||
| 5020 | - | ||
| 5021 | - // create the _chunks shadow table | ||
| 5022 | - char *zCreateShadowChunks = NULL; | ||
| 5023 | - if(pNew->numPartitionColumns) { | ||
| 5024 | - sqlite3_str * s = sqlite3_str_new(NULL); | ||
| 5025 | - sqlite3_str_appendf(s, "CREATE TABLE " VEC0_SHADOW_CHUNKS_NAME "(", pNew->schemaName, pNew->tableName); | ||
| 5026 | - sqlite3_str_appendall(s, "chunk_id INTEGER PRIMARY KEY AUTOINCREMENT," "size INTEGER NOT NULL,"); | ||
| 5027 | - sqlite3_str_appendall(s, "sequence_id integer,"); | ||
| 5028 | - for(int i = 0; i < pNew->numPartitionColumns;i++) { | ||
| 5029 | - sqlite3_str_appendf(s, "partition%02d,", i); | ||
| 5030 | - } | ||
| 5031 | - sqlite3_str_appendall(s, "validity BLOB NOT NULL, rowids BLOB NOT NULL);"); | ||
| 5032 | - zCreateShadowChunks = sqlite3_str_finish(s); | ||
| 5033 | - }else { | ||
| 5034 | - zCreateShadowChunks = sqlite3_mprintf(VEC0_SHADOW_CHUNKS_CREATE, | ||
| 5035 | - pNew->schemaName, pNew->tableName); | ||
| 5036 | - } | ||
| 5037 | - if (!zCreateShadowChunks) { | ||
| 5038 | - goto error; | ||
| 5039 | - } | ||
| 5040 | - rc = sqlite3_prepare_v2(db, zCreateShadowChunks, -1, &stmt, 0); | ||
| 5041 | - sqlite3_free((void *)zCreateShadowChunks); | ||
| 5042 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5043 | - // IMP: V17740_01811 | ||
| 5044 | - sqlite3_finalize(stmt); | ||
| 5045 | - *pzErr = sqlite3_mprintf("Could not create '_chunks' shadow table: %s", | ||
| 5046 | - sqlite3_errmsg(db)); | ||
| 5047 | - goto error; | ||
| 5048 | - } | ||
| 5049 | - sqlite3_finalize(stmt); | ||
| 5050 | - | ||
| 5051 | - // create the _rowids shadow table | ||
| 5052 | - char *zCreateShadowRowids; | ||
| 5053 | - if (pNew->pkIsText) { | ||
| 5054 | - // adds a "text unique not null" constraint to the id column | ||
| 5055 | - zCreateShadowRowids = sqlite3_mprintf(VEC0_SHADOW_ROWIDS_CREATE_PK_TEXT, | ||
| 5056 | - pNew->schemaName, pNew->tableName); | ||
| 5057 | - } else { | ||
| 5058 | - zCreateShadowRowids = sqlite3_mprintf(VEC0_SHADOW_ROWIDS_CREATE_BASIC, | ||
| 5059 | - pNew->schemaName, pNew->tableName); | ||
| 5060 | - } | ||
| 5061 | - if (!zCreateShadowRowids) { | ||
| 5062 | - goto error; | ||
| 5063 | - } | ||
| 5064 | - rc = sqlite3_prepare_v2(db, zCreateShadowRowids, -1, &stmt, 0); | ||
| 5065 | - sqlite3_free((void *)zCreateShadowRowids); | ||
| 5066 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5067 | - // IMP: V11631_28470 | ||
| 5068 | - sqlite3_finalize(stmt); | ||
| 5069 | - *pzErr = sqlite3_mprintf("Could not create '_rowids' shadow table: %s", | ||
| 5070 | - sqlite3_errmsg(db)); | ||
| 5071 | - goto error; | ||
| 5072 | - } | ||
| 5073 | - sqlite3_finalize(stmt); | ||
| 5074 | - | ||
| 5075 | - for (int i = 0; i < pNew->numVectorColumns; i++) { | ||
| 5076 | - char *zSql = sqlite3_mprintf(VEC0_SHADOW_VECTOR_N_CREATE, | ||
| 5077 | - pNew->schemaName, pNew->tableName, i); | ||
| 5078 | - if (!zSql) { | ||
| 5079 | - goto error; | ||
| 5080 | - } | ||
| 5081 | - rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0); | ||
| 5082 | - sqlite3_free((void *)zSql); | ||
| 5083 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5084 | - // IMP: V25919_09989 | ||
| 5085 | - sqlite3_finalize(stmt); | ||
| 5086 | - *pzErr = sqlite3_mprintf( | ||
| 5087 | - "Could not create '_vector_chunks%02d' shadow table: %s", i, | ||
| 5088 | - sqlite3_errmsg(db)); | ||
| 5089 | - goto error; | ||
| 5090 | - } | ||
| 5091 | - sqlite3_finalize(stmt); | ||
| 5092 | - } | ||
| 5093 | - | ||
| 5094 | - for (int i = 0; i < pNew->numMetadataColumns; i++) { | ||
| 5095 | - char *zSql = sqlite3_mprintf("CREATE TABLE " VEC0_SHADOW_METADATA_N_NAME "(rowid PRIMARY KEY, data BLOB NOT NULL);", | ||
| 5096 | - pNew->schemaName, pNew->tableName, i); | ||
| 5097 | - if (!zSql) { | ||
| 5098 | - goto error; | ||
| 5099 | - } | ||
| 5100 | - rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0); | ||
| 5101 | - sqlite3_free((void *)zSql); | ||
| 5102 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5103 | - sqlite3_finalize(stmt); | ||
| 5104 | - *pzErr = sqlite3_mprintf( | ||
| 5105 | - "Could not create '_metata_chunks%02d' shadow table: %s", i, | ||
| 5106 | - sqlite3_errmsg(db)); | ||
| 5107 | - goto error; | ||
| 5108 | - } | ||
| 5109 | - sqlite3_finalize(stmt); | ||
| 5110 | - | ||
| 5111 | - if(pNew->metadata_columns[i].kind == VEC0_METADATA_COLUMN_KIND_TEXT) { | ||
| 5112 | - char *zSql = sqlite3_mprintf("CREATE TABLE " VEC0_SHADOW_METADATA_TEXT_DATA_NAME "(rowid PRIMARY KEY, data TEXT);", | ||
| 5113 | - pNew->schemaName, pNew->tableName, i); | ||
| 5114 | - if (!zSql) { | ||
| 5115 | - goto error; | ||
| 5116 | - } | ||
| 5117 | - rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0); | ||
| 5118 | - sqlite3_free((void *)zSql); | ||
| 5119 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5120 | - sqlite3_finalize(stmt); | ||
| 5121 | - *pzErr = sqlite3_mprintf( | ||
| 5122 | - "Could not create '_metadatatext%02d' shadow table: %s", i, | ||
| 5123 | - sqlite3_errmsg(db)); | ||
| 5124 | - goto error; | ||
| 5125 | - } | ||
| 5126 | - sqlite3_finalize(stmt); | ||
| 5127 | - | ||
| 5128 | - } | ||
| 5129 | - } | ||
| 5130 | - | ||
| 5131 | - if(pNew->numAuxiliaryColumns > 0) { | ||
| 5132 | - sqlite3_stmt * stmt; | ||
| 5133 | - sqlite3_str * s = sqlite3_str_new(NULL); | ||
| 5134 | - sqlite3_str_appendf(s, "CREATE TABLE " VEC0_SHADOW_AUXILIARY_NAME "( rowid integer PRIMARY KEY ", pNew->schemaName, pNew->tableName); | ||
| 5135 | - for(int i = 0; i < pNew->numAuxiliaryColumns; i++) { | ||
| 5136 | - sqlite3_str_appendf(s, ", value%02d", i); | ||
| 5137 | - } | ||
| 5138 | - sqlite3_str_appendall(s, ")"); | ||
| 5139 | - char *zSql = sqlite3_str_finish(s); | ||
| 5140 | - if(!zSql) { | ||
| 5141 | - goto error; | ||
| 5142 | - } | ||
| 5143 | - rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, NULL); | ||
| 5144 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5145 | - sqlite3_finalize(stmt); | ||
| 5146 | - *pzErr = sqlite3_mprintf( | ||
| 5147 | - "Could not create auxiliary shadow table: %s", | ||
| 5148 | - sqlite3_errmsg(db)); | ||
| 5149 | - | ||
| 5150 | - goto error; | ||
| 5151 | - } | ||
| 5152 | - sqlite3_finalize(stmt); | ||
| 5153 | - } | ||
| 5154 | - } | ||
| 5155 | - | ||
| 5156 | - *ppVtab = (sqlite3_vtab *)pNew; | ||
| 5157 | - return SQLITE_OK; | ||
| 5158 | - | ||
| 5159 | -error: | ||
| 5160 | - vec0_free(pNew); | ||
| 5161 | - return SQLITE_ERROR; | ||
| 5162 | -} | ||
| 5163 | - | ||
| 5164 | -static int vec0Create(sqlite3 *db, void *pAux, int argc, | ||
| 5165 | - const char *const *argv, sqlite3_vtab **ppVtab, | ||
| 5166 | - char **pzErr) { | ||
| 5167 | - return vec0_init(db, pAux, argc, argv, ppVtab, pzErr, true); | ||
| 5168 | -} | ||
| 5169 | -static int vec0Connect(sqlite3 *db, void *pAux, int argc, | ||
| 5170 | - const char *const *argv, sqlite3_vtab **ppVtab, | ||
| 5171 | - char **pzErr) { | ||
| 5172 | - return vec0_init(db, pAux, argc, argv, ppVtab, pzErr, false); | ||
| 5173 | -} | ||
| 5174 | - | ||
| 5175 | -static int vec0Disconnect(sqlite3_vtab *pVtab) { | ||
| 5176 | - vec0_vtab *p = (vec0_vtab *)pVtab; | ||
| 5177 | - vec0_free(p); | ||
| 5178 | - sqlite3_free(p); | ||
| 5179 | - return SQLITE_OK; | ||
| 5180 | -} | ||
| 5181 | -static int vec0Destroy(sqlite3_vtab *pVtab) { | ||
| 5182 | - vec0_vtab *p = (vec0_vtab *)pVtab; | ||
| 5183 | - sqlite3_stmt *stmt; | ||
| 5184 | - int rc; | ||
| 5185 | - const char *zSql; | ||
| 5186 | - | ||
| 5187 | - // Free up any sqlite3_stmt, otherwise DROPs on those tables will fail | ||
| 5188 | - vec0_free_resources(p); | ||
| 5189 | - | ||
| 5190 | - // TODO(test) later: can't evidence-of here, bc always gives "SQL logic error" instead of | ||
| 5191 | - // provided error | ||
| 5192 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_CHUNKS_NAME, p->schemaName, | ||
| 5193 | - p->tableName); | ||
| 5194 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | ||
| 5195 | - sqlite3_free((void *)zSql); | ||
| 5196 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5197 | - rc = SQLITE_ERROR; | ||
| 5198 | - vtab_set_error(pVtab, "could not drop chunks shadow table"); | ||
| 5199 | - goto done; | ||
| 5200 | - } | ||
| 5201 | - sqlite3_finalize(stmt); | ||
| 5202 | - | ||
| 5203 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_INFO_NAME, p->schemaName, | ||
| 5204 | - p->tableName); | ||
| 5205 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | ||
| 5206 | - sqlite3_free((void *)zSql); | ||
| 5207 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5208 | - rc = SQLITE_ERROR; | ||
| 5209 | - vtab_set_error(pVtab, "could not drop info shadow table"); | ||
| 5210 | - goto done; | ||
| 5211 | - } | ||
| 5212 | - sqlite3_finalize(stmt); | ||
| 5213 | - | ||
| 5214 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_ROWIDS_NAME, p->schemaName, | ||
| 5215 | - p->tableName); | ||
| 5216 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | ||
| 5217 | - sqlite3_free((void *)zSql); | ||
| 5218 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5219 | - rc = SQLITE_ERROR; | ||
| 5220 | - goto done; | ||
| 5221 | - } | ||
| 5222 | - sqlite3_finalize(stmt); | ||
| 5223 | - | ||
| 5224 | - for (int i = 0; i < p->numVectorColumns; i++) { | ||
| 5225 | - zSql = sqlite3_mprintf("DROP TABLE \"%w\".\"%w\"", p->schemaName, | ||
| 5226 | - p->shadowVectorChunksNames[i]); | ||
| 5227 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | ||
| 5228 | - sqlite3_free((void *)zSql); | ||
| 5229 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5230 | - rc = SQLITE_ERROR; | ||
| 5231 | - goto done; | ||
| 5232 | - } | ||
| 5233 | - sqlite3_finalize(stmt); | ||
| 5234 | - } | ||
| 5235 | - | ||
| 5236 | - if(p->numAuxiliaryColumns > 0) { | ||
| 5237 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_AUXILIARY_NAME, p->schemaName, p->tableName); | ||
| 5238 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | ||
| 5239 | - sqlite3_free((void *)zSql); | ||
| 5240 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5241 | - rc = SQLITE_ERROR; | ||
| 5242 | - goto done; | ||
| 5243 | - } | ||
| 5244 | - sqlite3_finalize(stmt); | ||
| 5245 | - } | ||
| 5246 | - | ||
| 5247 | - | ||
| 5248 | - for (int i = 0; i < p->numMetadataColumns; i++) { | ||
| 5249 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_METADATA_N_NAME, p->schemaName,p->tableName, i); | ||
| 5250 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | ||
| 5251 | - sqlite3_free((void *)zSql); | ||
| 5252 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5253 | - rc = SQLITE_ERROR; | ||
| 5254 | - goto done; | ||
| 5255 | - } | ||
| 5256 | - sqlite3_finalize(stmt); | ||
| 5257 | - | ||
| 5258 | - if(p->metadata_columns[i].kind == VEC0_METADATA_COLUMN_KIND_TEXT) { | ||
| 5259 | - zSql = sqlite3_mprintf("DROP TABLE " VEC0_SHADOW_METADATA_TEXT_DATA_NAME, p->schemaName,p->tableName, i); | ||
| 5260 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0); | ||
| 5261 | - sqlite3_free((void *)zSql); | ||
| 5262 | - if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) { | ||
| 5263 | - rc = SQLITE_ERROR; | ||
| 5264 | - goto done; | ||
| 5265 | - } | ||
| 5266 | - sqlite3_finalize(stmt); | ||
| 5267 | - } | ||
| 5268 | - } | ||
| 5269 | - | ||
| 5270 | - stmt = NULL; | ||
| 5271 | - rc = SQLITE_OK; | ||
| 5272 | - | ||
| 5273 | -done: | ||
| 5274 | - sqlite3_finalize(stmt); | ||
| 5275 | - vec0_free(p); | ||
| 5276 | - // If there was an error | ||
| 5277 | - if (rc == SQLITE_OK) { | ||
| 5278 | - sqlite3_free(p); | ||
| 5279 | - } | ||
| 5280 | - return rc; | ||
| 5281 | -} | ||
| 5282 | - | ||
| 5283 | -static int vec0Open(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor) { | ||
| 5284 | - UNUSED_PARAMETER(p); | ||
| 5285 | - vec0_cursor *pCur; | ||
| 5286 | - pCur = sqlite3_malloc(sizeof(*pCur)); | ||
| 5287 | - if (pCur == 0) | ||
| 5288 | - return SQLITE_NOMEM; | ||
| 5289 | - memset(pCur, 0, sizeof(*pCur)); | ||
| 5290 | - *ppCursor = &pCur->base; | ||
| 5291 | - return SQLITE_OK; | ||
| 5292 | -} | ||
| 5293 | - | ||
| 5294 | -static int vec0Close(sqlite3_vtab_cursor *cur) { | ||
| 5295 | - vec0_cursor *pCur = (vec0_cursor *)cur; | ||
| 5296 | - vec0_cursor_clear(pCur); | ||
| 5297 | - sqlite3_free(pCur); | ||
| 5298 | - return SQLITE_OK; | ||
| 5299 | -} | ||
| 5300 | - | ||
| 5301 | -// All the different type of "values" provided to argv/argc in vec0Filter. | ||
| 5302 | -// These enums denote the use and purpose of all of them. | ||
| 5303 | -typedef enum { | ||
| 5304 | - // If any values are updated, please update the ARCHITECTURE.md docs accordingly! | ||
| 5305 | - | ||
| 5306 | - VEC0_IDXSTR_KIND_KNN_MATCH = '{', | ||
| 5307 | - VEC0_IDXSTR_KIND_KNN_K = '}', | ||
| 5308 | - VEC0_IDXSTR_KIND_KNN_ROWID_IN = '[', | ||
| 5309 | - VEC0_IDXSTR_KIND_KNN_PARTITON_CONSTRAINT = ']', | ||
| 5310 | - VEC0_IDXSTR_KIND_POINT_ID = '!', | ||
| 5311 | - VEC0_IDXSTR_KIND_METADATA_CONSTRAINT = '&', | ||
| 5312 | -} vec0_idxstr_kind; | ||
| 5313 | - | ||
| 5314 | -// The different SQLITE_INDEX_CONSTRAINT values that vec0 partition key columns | ||
| 5315 | -// support, but as characters that fit nicely in idxstr. | ||
| 5316 | -typedef enum { | ||
| 5317 | - // If any values are updated, please update the ARCHITECTURE.md docs accordingly! | ||
| 5318 | - | ||
| 5319 | - VEC0_PARTITION_OPERATOR_EQ = 'a', | ||
| 5320 | - VEC0_PARTITION_OPERATOR_GT = 'b', | ||
| 5321 | - VEC0_PARTITION_OPERATOR_LE = 'c', | ||
| 5322 | - VEC0_PARTITION_OPERATOR_LT = 'd', | ||
| 5323 | - VEC0_PARTITION_OPERATOR_GE = 'e', | ||
| 5324 | - VEC0_PARTITION_OPERATOR_NE = 'f', | ||
| 5325 | -} vec0_partition_operator; | ||
| 5326 | -typedef enum { | ||
| 5327 | - VEC0_METADATA_OPERATOR_EQ = 'a', | ||
| 5328 | - VEC0_METADATA_OPERATOR_GT = 'b', | ||
| 5329 | - VEC0_METADATA_OPERATOR_LE = 'c', | ||
| 5330 | - VEC0_METADATA_OPERATOR_LT = 'd', | ||
| 5331 | - VEC0_METADATA_OPERATOR_GE = 'e', | ||
| 5332 | - VEC0_METADATA_OPERATOR_NE = 'f', | ||
| 5333 | - VEC0_METADATA_OPERATOR_IN = 'g', | ||
| 5334 | -} vec0_metadata_operator; | ||
| 5335 | - | ||
| 5336 | -static int vec0BestIndex(sqlite3_vtab *pVTab, sqlite3_index_info *pIdxInfo) { | ||
| 5337 | - vec0_vtab *p = (vec0_vtab *)pVTab; | ||
| 5338 | - /** | ||
| 5339 | - * Possible query plans are: | ||
| 5340 | - * 1. KNN when: | ||
| 5341 | - * a) An `MATCH` op on vector column | ||
| 5342 | - * b) ORDER BY on distance column | ||
| 5343 | - * c) LIMIT | ||
| 5344 | - * d) rowid in (...) OPTIONAL | ||
| 5345 | - * 2. Point when: | ||
| 5346 | - * a) An `EQ` op on rowid column | ||
| 5347 | - * 3. else: fullscan | ||
| 5348 | - * | ||
| 5349 | - */ | ||
| 5350 | - int iMatchTerm = -1; | ||
| 5351 | - int iMatchVectorTerm = -1; | ||
| 5352 | - int iLimitTerm = -1; | ||
| 5353 | - int iRowidTerm = -1; | ||
| 5354 | - int iKTerm = -1; | ||
| 5355 | - int iRowidInTerm = -1; | ||
| 5356 | - int hasAuxConstraint = 0; | ||
| 5357 | - | ||
| 5358 | -#ifdef SQLITE_VEC_DEBUG | ||
| 5359 | - printf("pIdxInfo->nOrderBy=%d, pIdxInfo->nConstraint=%d\n", pIdxInfo->nOrderBy, pIdxInfo->nConstraint); | ||
| 5360 | -#endif | ||
| 5361 | - | ||
| 5362 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | ||
| 5363 | - u8 vtabIn = 0; | ||
| 5364 | - | ||
| 5365 | -#if COMPILER_SUPPORTS_VTAB_IN | ||
| 5366 | - if (sqlite3_libversion_number() >= 3038000) { | ||
| 5367 | - vtabIn = sqlite3_vtab_in(pIdxInfo, i, -1); | ||
| 5368 | - } | ||
| 5369 | -#endif | ||
| 5370 | - | ||
| 5371 | -#ifdef SQLITE_VEC_DEBUG | ||
| 5372 | - printf("xBestIndex [%d] usable=%d iColumn=%d op=%d vtabin=%d\n", i, | ||
| 5373 | - pIdxInfo->aConstraint[i].usable, pIdxInfo->aConstraint[i].iColumn, | ||
| 5374 | - pIdxInfo->aConstraint[i].op, vtabIn); | ||
| 5375 | -#endif | ||
| 5376 | - if (!pIdxInfo->aConstraint[i].usable) | ||
| 5377 | - continue; | ||
| 5378 | - | ||
| 5379 | - int iColumn = pIdxInfo->aConstraint[i].iColumn; | ||
| 5380 | - int op = pIdxInfo->aConstraint[i].op; | ||
| 5381 | - | ||
| 5382 | - if (op == SQLITE_INDEX_CONSTRAINT_LIMIT) { | ||
| 5383 | - iLimitTerm = i; | ||
| 5384 | - } | ||
| 5385 | - if (op == SQLITE_INDEX_CONSTRAINT_MATCH && | ||
| 5386 | - vec0_column_idx_is_vector(p, iColumn)) { | ||
| 5387 | - if (iMatchTerm > -1) { | ||
| 5388 | - vtab_set_error( | ||
| 5389 | - pVTab, "only 1 MATCH operator is allowed in a single vec0 query"); | ||
| 5390 | - return SQLITE_ERROR; | ||
| 5391 | - } | ||
| 5392 | - iMatchTerm = i; | ||
| 5393 | - iMatchVectorTerm = vec0_column_idx_to_vector_idx(p, iColumn); | ||
| 5394 | - } | ||
| 5395 | - if (op == SQLITE_INDEX_CONSTRAINT_EQ && iColumn == VEC0_COLUMN_ID) { | ||
| 5396 | - if (vtabIn) { | ||
| 5397 | - if (iRowidInTerm != -1) { | ||
| 5398 | - vtab_set_error(pVTab, "only 1 'rowid in (..)' operator is allowed in " | ||
| 5399 | - "a single vec0 query"); | ||
| 5400 | - return SQLITE_ERROR; | ||
| 5401 | - } | ||
| 5402 | - iRowidInTerm = i; | ||
| 5403 | - | ||
| 5404 | - } else { | ||
| 5405 | - iRowidTerm = i; | ||
| 5406 | - } | ||
| 5407 | - } | ||
| 5408 | - if (op == SQLITE_INDEX_CONSTRAINT_EQ && iColumn == vec0_column_k_idx(p)) { | ||
| 5409 | - iKTerm = i; | ||
| 5410 | - } | ||
| 5411 | - if( | ||
| 5412 | - (op != SQLITE_INDEX_CONSTRAINT_LIMIT && op != SQLITE_INDEX_CONSTRAINT_OFFSET) | ||
| 5413 | - && vec0_column_idx_is_auxiliary(p, iColumn)) { | ||
| 5414 | - hasAuxConstraint = 1; | ||
| 5415 | - } | ||
| 5416 | - } | ||
| 5417 | - | ||
| 5418 | - sqlite3_str *idxStr = sqlite3_str_new(NULL); | ||
| 5419 | - int rc; | ||
| 5420 | - | ||
| 5421 | - if (iMatchTerm >= 0) { | ||
| 5422 | - if (iLimitTerm < 0 && iKTerm < 0) { | ||
| 5423 | - vtab_set_error( | ||
| 5424 | - pVTab, | ||
| 5425 | - "A LIMIT or 'k = ?' constraint is required on vec0 knn queries."); | ||
| 5426 | - rc = SQLITE_ERROR; | ||
| 5427 | - goto done; | ||
| 5428 | - } | ||
| 5429 | - if (iLimitTerm >= 0 && iKTerm >= 0) { | ||
| 5430 | - vtab_set_error(pVTab, "Only LIMIT or 'k =?' can be provided, not both"); | ||
| 5431 | - rc = SQLITE_ERROR; | ||
| 5432 | - goto done; | ||
| 5433 | - } | ||
| 5434 | - | ||
| 5435 | - if (pIdxInfo->nOrderBy) { | ||
| 5436 | - if (pIdxInfo->nOrderBy > 1) { | ||
| 5437 | - vtab_set_error(pVTab, "Only a single 'ORDER BY distance' clause is " | ||
| 5438 | - "allowed on vec0 KNN queries"); | ||
| 5439 | - rc = SQLITE_ERROR; | ||
| 5440 | - goto done; | ||
| 5441 | - } | ||
| 5442 | - if (pIdxInfo->aOrderBy[0].iColumn != vec0_column_distance_idx(p)) { | ||
| 5443 | - vtab_set_error(pVTab, | ||
| 5444 | - "Only a single 'ORDER BY distance' clause is allowed on " | ||
| 5445 | - "vec0 KNN queries, not on other columns"); | ||
| 5446 | - rc = SQLITE_ERROR; | ||
| 5447 | - goto done; | ||
| 5448 | - } | ||
| 5449 | - if (pIdxInfo->aOrderBy[0].desc) { | ||
| 5450 | - vtab_set_error( | ||
| 5451 | - pVTab, "Only ascending in ORDER BY distance clause is supported, " | ||
| 5452 | - "DESC is not supported yet."); | ||
| 5453 | - rc = SQLITE_ERROR; | ||
| 5454 | - goto done; | ||
| 5455 | - } | ||
| 5456 | - } | ||
| 5457 | - | ||
| 5458 | - if(hasAuxConstraint) { | ||
| 5459 | - // IMP: V25623_09693 | ||
| 5460 | - vtab_set_error(pVTab, "An illegal WHERE constraint was provided on a vec0 auxiliary column in a KNN query."); | ||
| 5461 | - rc = SQLITE_ERROR; | ||
| 5462 | - goto done; | ||
| 5463 | - } | ||
| 5464 | - | ||
| 5465 | - sqlite3_str_appendchar(idxStr, 1, VEC0_QUERY_PLAN_KNN); | ||
| 5466 | - | ||
| 5467 | - int argvIndex = 1; | ||
| 5468 | - pIdxInfo->aConstraintUsage[iMatchTerm].argvIndex = argvIndex++; | ||
| 5469 | - pIdxInfo->aConstraintUsage[iMatchTerm].omit = 1; | ||
| 5470 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_KNN_MATCH); | ||
| 5471 | - sqlite3_str_appendchar(idxStr, 3, '_'); | ||
| 5472 | - | ||
| 5473 | - if (iLimitTerm >= 0) { | ||
| 5474 | - pIdxInfo->aConstraintUsage[iLimitTerm].argvIndex = argvIndex++; | ||
| 5475 | - pIdxInfo->aConstraintUsage[iLimitTerm].omit = 1; | ||
| 5476 | - } else { | ||
| 5477 | - pIdxInfo->aConstraintUsage[iKTerm].argvIndex = argvIndex++; | ||
| 5478 | - pIdxInfo->aConstraintUsage[iKTerm].omit = 1; | ||
| 5479 | - } | ||
| 5480 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_KNN_K); | ||
| 5481 | - sqlite3_str_appendchar(idxStr, 3, '_'); | ||
| 5482 | - | ||
| 5483 | -#if COMPILER_SUPPORTS_VTAB_IN | ||
| 5484 | - if (iRowidInTerm >= 0) { | ||
| 5485 | - // already validated as >= SQLite 3.38 bc iRowidInTerm is only >= 0 when | ||
| 5486 | - // vtabIn == 1 | ||
| 5487 | - sqlite3_vtab_in(pIdxInfo, iRowidInTerm, 1); | ||
| 5488 | - pIdxInfo->aConstraintUsage[iRowidInTerm].argvIndex = argvIndex++; | ||
| 5489 | - pIdxInfo->aConstraintUsage[iRowidInTerm].omit = 1; | ||
| 5490 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_KNN_ROWID_IN); | ||
| 5491 | - sqlite3_str_appendchar(idxStr, 3, '_'); | ||
| 5492 | - } | ||
| 5493 | -#endif | ||
| 5494 | - | ||
| 5495 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | ||
| 5496 | - if (!pIdxInfo->aConstraint[i].usable) | ||
| 5497 | - continue; | ||
| 5498 | - | ||
| 5499 | - int iColumn = pIdxInfo->aConstraint[i].iColumn; | ||
| 5500 | - int op = pIdxInfo->aConstraint[i].op; | ||
| 5501 | - if(op == SQLITE_INDEX_CONSTRAINT_LIMIT || op == SQLITE_INDEX_CONSTRAINT_OFFSET) { | ||
| 5502 | - continue; | ||
| 5503 | - } | ||
| 5504 | - if(!vec0_column_idx_is_partition(p, iColumn)) { | ||
| 5505 | - continue; | ||
| 5506 | - } | ||
| 5507 | - | ||
| 5508 | - int partition_idx = vec0_column_idx_to_partition_idx(p, iColumn); | ||
| 5509 | - char value = 0; | ||
| 5510 | - | ||
| 5511 | - switch(op) { | ||
| 5512 | - case SQLITE_INDEX_CONSTRAINT_EQ: { | ||
| 5513 | - value = VEC0_PARTITION_OPERATOR_EQ; | ||
| 5514 | - break; | ||
| 5515 | - } | ||
| 5516 | - case SQLITE_INDEX_CONSTRAINT_GT: { | ||
| 5517 | - value = VEC0_PARTITION_OPERATOR_GT; | ||
| 5518 | - break; | ||
| 5519 | - } | ||
| 5520 | - case SQLITE_INDEX_CONSTRAINT_LE: { | ||
| 5521 | - value = VEC0_PARTITION_OPERATOR_LE; | ||
| 5522 | - break; | ||
| 5523 | - } | ||
| 5524 | - case SQLITE_INDEX_CONSTRAINT_LT: { | ||
| 5525 | - value = VEC0_PARTITION_OPERATOR_LT; | ||
| 5526 | - break; | ||
| 5527 | - } | ||
| 5528 | - case SQLITE_INDEX_CONSTRAINT_GE: { | ||
| 5529 | - value = VEC0_PARTITION_OPERATOR_GE; | ||
| 5530 | - break; | ||
| 5531 | - } | ||
| 5532 | - case SQLITE_INDEX_CONSTRAINT_NE: { | ||
| 5533 | - value = VEC0_PARTITION_OPERATOR_NE; | ||
| 5534 | - break; | ||
| 5535 | - } | ||
| 5536 | - } | ||
| 5537 | - | ||
| 5538 | - if(value) { | ||
| 5539 | - pIdxInfo->aConstraintUsage[i].argvIndex = argvIndex++; | ||
| 5540 | - pIdxInfo->aConstraintUsage[i].omit = 1; | ||
| 5541 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_KNN_PARTITON_CONSTRAINT); | ||
| 5542 | - sqlite3_str_appendchar(idxStr, 1, 'A' + partition_idx); | ||
| 5543 | - sqlite3_str_appendchar(idxStr, 1, value); | ||
| 5544 | - sqlite3_str_appendchar(idxStr, 1, '_'); | ||
| 5545 | - } | ||
| 5546 | - | ||
| 5547 | - } | ||
| 5548 | - | ||
| 5549 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | ||
| 5550 | - if (!pIdxInfo->aConstraint[i].usable) | ||
| 5551 | - continue; | ||
| 5552 | - | ||
| 5553 | - int iColumn = pIdxInfo->aConstraint[i].iColumn; | ||
| 5554 | - int op = pIdxInfo->aConstraint[i].op; | ||
| 5555 | - if(op == SQLITE_INDEX_CONSTRAINT_LIMIT || op == SQLITE_INDEX_CONSTRAINT_OFFSET) { | ||
| 5556 | - continue; | ||
| 5557 | - } | ||
| 5558 | - if(!vec0_column_idx_is_metadata(p, iColumn)) { | ||
| 5559 | - continue; | ||
| 5560 | - } | ||
| 5561 | - | ||
| 5562 | - int metadata_idx = vec0_column_idx_to_metadata_idx(p, iColumn); | ||
| 5563 | - char value = 0; | ||
| 5564 | - | ||
| 5565 | - switch(op) { | ||
| 5566 | - case SQLITE_INDEX_CONSTRAINT_EQ: { | ||
| 5567 | - int vtabIn = 0; | ||
| 5568 | - #if COMPILER_SUPPORTS_VTAB_IN | ||
| 5569 | - if (sqlite3_libversion_number() >= 3038000) { | ||
| 5570 | - vtabIn = sqlite3_vtab_in(pIdxInfo, i, -1); | ||
| 5571 | - } | ||
| 5572 | - if(vtabIn) { | ||
| 5573 | - switch(p->metadata_columns[metadata_idx].kind) { | ||
| 5574 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: | ||
| 5575 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | ||
| 5576 | - // IMP: V15248_32086 | ||
| 5577 | - rc = SQLITE_ERROR; | ||
| 5578 | - vtab_set_error(pVTab, "'xxx in (...)' is only available on INTEGER or TEXT metadata columns."); | ||
| 5579 | - goto done; | ||
| 5580 | - break; | ||
| 5581 | - } | ||
| 5582 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: | ||
| 5583 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | ||
| 5584 | - break; | ||
| 5585 | - } | ||
| 5586 | - } | ||
| 5587 | - value = VEC0_METADATA_OPERATOR_IN; | ||
| 5588 | - sqlite3_vtab_in(pIdxInfo, i, 1); | ||
| 5589 | - }else | ||
| 5590 | - #endif | ||
| 5591 | - { | ||
| 5592 | - value = VEC0_PARTITION_OPERATOR_EQ; | ||
| 5593 | - } | ||
| 5594 | - break; | ||
| 5595 | - } | ||
| 5596 | - case SQLITE_INDEX_CONSTRAINT_GT: { | ||
| 5597 | - value = VEC0_METADATA_OPERATOR_GT; | ||
| 5598 | - break; | ||
| 5599 | - } | ||
| 5600 | - case SQLITE_INDEX_CONSTRAINT_LE: { | ||
| 5601 | - value = VEC0_METADATA_OPERATOR_LE; | ||
| 5602 | - break; | ||
| 5603 | - } | ||
| 5604 | - case SQLITE_INDEX_CONSTRAINT_LT: { | ||
| 5605 | - value = VEC0_METADATA_OPERATOR_LT; | ||
| 5606 | - break; | ||
| 5607 | - } | ||
| 5608 | - case SQLITE_INDEX_CONSTRAINT_GE: { | ||
| 5609 | - value = VEC0_METADATA_OPERATOR_GE; | ||
| 5610 | - break; | ||
| 5611 | - } | ||
| 5612 | - case SQLITE_INDEX_CONSTRAINT_NE: { | ||
| 5613 | - value = VEC0_METADATA_OPERATOR_NE; | ||
| 5614 | - break; | ||
| 5615 | - } | ||
| 5616 | - default: { | ||
| 5617 | - // IMP: V16511_00582 | ||
| 5618 | - rc = SQLITE_ERROR; | ||
| 5619 | - vtab_set_error(pVTab, | ||
| 5620 | - "An illegal WHERE constraint was provided on a vec0 metadata column in a KNN query. " | ||
| 5621 | - "Only one of EQUALS, GREATER_THAN, LESS_THAN_OR_EQUAL, LESS_THAN, GREATER_THAN_OR_EQUAL, NOT_EQUALS is allowed." | ||
| 5622 | - ); | ||
| 5623 | - goto done; | ||
| 5624 | - } | ||
| 5625 | - } | ||
| 5626 | - | ||
| 5627 | - if(p->metadata_columns[metadata_idx].kind == VEC0_METADATA_COLUMN_KIND_BOOLEAN) { | ||
| 5628 | - if(!(value == VEC0_METADATA_OPERATOR_EQ || value == VEC0_METADATA_OPERATOR_NE)) { | ||
| 5629 | - // IMP: V10145_26984 | ||
| 5630 | - rc = SQLITE_ERROR; | ||
| 5631 | - vtab_set_error(pVTab, "ONLY EQUALS (=) or NOT_EQUALS (!=) operators are allowed on boolean metadata columns."); | ||
| 5632 | - goto done; | ||
| 5633 | - } | ||
| 5634 | - } | ||
| 5635 | - | ||
| 5636 | - pIdxInfo->aConstraintUsage[i].argvIndex = argvIndex++; | ||
| 5637 | - pIdxInfo->aConstraintUsage[i].omit = 1; | ||
| 5638 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_METADATA_CONSTRAINT); | ||
| 5639 | - sqlite3_str_appendchar(idxStr, 1, 'A' + metadata_idx); | ||
| 5640 | - sqlite3_str_appendchar(idxStr, 1, value); | ||
| 5641 | - sqlite3_str_appendchar(idxStr, 1, '_'); | ||
| 5642 | - | ||
| 5643 | - } | ||
| 5644 | - | ||
| 5645 | - | ||
| 5646 | - | ||
| 5647 | - pIdxInfo->idxNum = iMatchVectorTerm; | ||
| 5648 | - pIdxInfo->estimatedCost = 30.0; | ||
| 5649 | - pIdxInfo->estimatedRows = 10; | ||
| 5650 | - | ||
| 5651 | - } else if (iRowidTerm >= 0) { | ||
| 5652 | - sqlite3_str_appendchar(idxStr, 1, VEC0_QUERY_PLAN_POINT); | ||
| 5653 | - pIdxInfo->aConstraintUsage[iRowidTerm].argvIndex = 1; | ||
| 5654 | - pIdxInfo->aConstraintUsage[iRowidTerm].omit = 1; | ||
| 5655 | - sqlite3_str_appendchar(idxStr, 1, VEC0_IDXSTR_KIND_POINT_ID); | ||
| 5656 | - sqlite3_str_appendchar(idxStr, 3, '_'); | ||
| 5657 | - pIdxInfo->idxNum = pIdxInfo->colUsed; | ||
| 5658 | - pIdxInfo->estimatedCost = 10.0; | ||
| 5659 | - pIdxInfo->estimatedRows = 1; | ||
| 5660 | - } else { | ||
| 5661 | - sqlite3_str_appendchar(idxStr, 1, VEC0_QUERY_PLAN_FULLSCAN); | ||
| 5662 | - pIdxInfo->estimatedCost = 3000000.0; | ||
| 5663 | - pIdxInfo->estimatedRows = 100000; | ||
| 5664 | - } | ||
| 5665 | - pIdxInfo->idxStr = sqlite3_str_finish(idxStr); | ||
| 5666 | - idxStr = NULL; | ||
| 5667 | - if (!pIdxInfo->idxStr) { | ||
| 5668 | - rc = SQLITE_OK; | ||
| 5669 | - goto done; | ||
| 5670 | - } | ||
| 5671 | - pIdxInfo->needToFreeIdxStr = 1; | ||
| 5672 | - | ||
| 5673 | - | ||
| 5674 | - rc = SQLITE_OK; | ||
| 5675 | - | ||
| 5676 | - done: | ||
| 5677 | - if(idxStr) { | ||
| 5678 | - sqlite3_str_finish(idxStr); | ||
| 5679 | - } | ||
| 5680 | - return rc; | ||
| 5681 | -} | ||
| 5682 | - | ||
| 5683 | -// forward delcaration bc vec0Filter uses it | ||
| 5684 | -static int vec0Next(sqlite3_vtab_cursor *cur); | ||
| 5685 | - | ||
| 5686 | -void merge_sorted_lists(f32 *a, i64 *a_rowids, i64 a_length, f32 *b, | ||
| 5687 | - i64 *b_rowids, i32 *b_top_idxs, i64 b_length, f32 *out, | ||
| 5688 | - i64 *out_rowids, i64 out_length, i64 *out_used) { | ||
| 5689 | - // assert((a_length >= out_length) || (b_length >= out_length)); | ||
| 5690 | - i64 ptrA = 0; | ||
| 5691 | - i64 ptrB = 0; | ||
| 5692 | - for (int i = 0; i < out_length; i++) { | ||
| 5693 | - if ((ptrA >= a_length) && (ptrB >= b_length)) { | ||
| 5694 | - *out_used = i; | ||
| 5695 | - return; | ||
| 5696 | - } | ||
| 5697 | - if (ptrA >= a_length) { | ||
| 5698 | - out[i] = b[b_top_idxs[ptrB]]; | ||
| 5699 | - out_rowids[i] = b_rowids[b_top_idxs[ptrB]]; | ||
| 5700 | - ptrB++; | ||
| 5701 | - } else if (ptrB >= b_length) { | ||
| 5702 | - out[i] = a[ptrA]; | ||
| 5703 | - out_rowids[i] = a_rowids[ptrA]; | ||
| 5704 | - ptrA++; | ||
| 5705 | - } else { | ||
| 5706 | - if (a[ptrA] <= b[b_top_idxs[ptrB]]) { | ||
| 5707 | - out[i] = a[ptrA]; | ||
| 5708 | - out_rowids[i] = a_rowids[ptrA]; | ||
| 5709 | - ptrA++; | ||
| 5710 | - } else { | ||
| 5711 | - out[i] = b[b_top_idxs[ptrB]]; | ||
| 5712 | - out_rowids[i] = b_rowids[b_top_idxs[ptrB]]; | ||
| 5713 | - ptrB++; | ||
| 5714 | - } | ||
| 5715 | - } | ||
| 5716 | - } | ||
| 5717 | - | ||
| 5718 | - *out_used = out_length; | ||
| 5719 | -} | ||
| 5720 | - | ||
| 5721 | -u8 *bitmap_new(i32 n) { | ||
| 5722 | - assert(n % 8 == 0); | ||
| 5723 | - u8 *p = sqlite3_malloc(n * sizeof(u8) / CHAR_BIT); | ||
| 5724 | - if (p) { | ||
| 5725 | - memset(p, 0, n * sizeof(u8) / CHAR_BIT); | ||
| 5726 | - } | ||
| 5727 | - return p; | ||
| 5728 | -} | ||
| 5729 | -u8 *bitmap_new_from(i32 n, u8 *from) { | ||
| 5730 | - assert(n % 8 == 0); | ||
| 5731 | - u8 *p = sqlite3_malloc(n * sizeof(u8) / CHAR_BIT); | ||
| 5732 | - if (p) { | ||
| 5733 | - memcpy(p, from, n / CHAR_BIT); | ||
| 5734 | - } | ||
| 5735 | - return p; | ||
| 5736 | -} | ||
| 5737 | - | ||
| 5738 | -void bitmap_copy(u8 *base, u8 *from, i32 n) { | ||
| 5739 | - assert(n % 8 == 0); | ||
| 5740 | - memcpy(base, from, n / CHAR_BIT); | ||
| 5741 | -} | ||
| 5742 | - | ||
| 5743 | -void bitmap_and_inplace(u8 *base, u8 *other, i32 n) { | ||
| 5744 | - assert((n % 8) == 0); | ||
| 5745 | - for (int i = 0; i < n / CHAR_BIT; i++) { | ||
| 5746 | - base[i] = base[i] & other[i]; | ||
| 5747 | - } | ||
| 5748 | -} | ||
| 5749 | - | ||
| 5750 | -void bitmap_set(u8 *bitmap, i32 position, int value) { | ||
| 5751 | - if (value) { | ||
| 5752 | - bitmap[position / CHAR_BIT] |= 1 << (position % CHAR_BIT); | ||
| 5753 | - } else { | ||
| 5754 | - bitmap[position / CHAR_BIT] &= ~(1 << (position % CHAR_BIT)); | ||
| 5755 | - } | ||
| 5756 | -} | ||
| 5757 | - | ||
| 5758 | -int bitmap_get(u8 *bitmap, i32 position) { | ||
| 5759 | - return (((bitmap[position / CHAR_BIT]) >> (position % CHAR_BIT)) & 1); | ||
| 5760 | -} | ||
| 5761 | - | ||
| 5762 | -void bitmap_clear(u8 *bitmap, i32 n) { | ||
| 5763 | - assert((n % 8) == 0); | ||
| 5764 | - memset(bitmap, 0, n / CHAR_BIT); | ||
| 5765 | -} | ||
| 5766 | - | ||
| 5767 | -void bitmap_fill(u8 *bitmap, i32 n) { | ||
| 5768 | - assert((n % 8) == 0); | ||
| 5769 | - memset(bitmap, 0xFF, n / CHAR_BIT); | ||
| 5770 | -} | ||
| 5771 | - | ||
| 5772 | -/** | ||
| 5773 | - * @brief Finds the minimum k items in distances, and writes the indicies to | ||
| 5774 | - * out. | ||
| 5775 | - * | ||
| 5776 | - * @param distances input f32 array of size n, the items to consider. | ||
| 5777 | - * @param n: size of distances array. | ||
| 5778 | - * @param out: Output array of size k, will contain at most k element indicies | ||
| 5779 | - * @param k: Size of output array | ||
| 5780 | - * @return int | ||
| 5781 | - */ | ||
| 5782 | -int min_idx(const f32 *distances, i32 n, u8 *candidates, i32 *out, i32 k, | ||
| 5783 | - u8 *bTaken, i32 *k_used) { | ||
| 5784 | - assert(k > 0); | ||
| 5785 | - assert(k <= n); | ||
| 5786 | - | ||
| 5787 | - bitmap_clear(bTaken, n); | ||
| 5788 | - | ||
| 5789 | - for (int ik = 0; ik < k; ik++) { | ||
| 5790 | - int min_idx = 0; | ||
| 5791 | - while (min_idx < n && | ||
| 5792 | - (bitmap_get(bTaken, min_idx) || !bitmap_get(candidates, min_idx))) { | ||
| 5793 | - min_idx++; | ||
| 5794 | - } | ||
| 5795 | - if (min_idx >= n) { | ||
| 5796 | - *k_used = ik; | ||
| 5797 | - return SQLITE_OK; | ||
| 5798 | - } | ||
| 5799 | - | ||
| 5800 | - for (int i = 0; i < n; i++) { | ||
| 5801 | - if (distances[i] <= distances[min_idx] && !bitmap_get(bTaken, i) && | ||
| 5802 | - (bitmap_get(candidates, i))) { | ||
| 5803 | - min_idx = i; | ||
| 5804 | - } | ||
| 5805 | - } | ||
| 5806 | - | ||
| 5807 | - out[ik] = min_idx; | ||
| 5808 | - bitmap_set(bTaken, min_idx, 1); | ||
| 5809 | - } | ||
| 5810 | - *k_used = k; | ||
| 5811 | - return SQLITE_OK; | ||
| 5812 | -} | ||
| 5813 | - | ||
| 5814 | -int vec0_get_metadata_text_long_value( | ||
| 5815 | - vec0_vtab * p, | ||
| 5816 | - sqlite3_stmt ** stmt, | ||
| 5817 | - int metadata_idx, | ||
| 5818 | - i64 rowid, | ||
| 5819 | - int *n, | ||
| 5820 | - char ** s) { | ||
| 5821 | - int rc; | ||
| 5822 | - if(!(*stmt)) { | ||
| 5823 | - const char * zSql = sqlite3_mprintf("select data from " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " where rowid = ?", p->schemaName, p->tableName, metadata_idx); | ||
| 5824 | - if(!zSql) { | ||
| 5825 | - rc = SQLITE_NOMEM; | ||
| 5826 | - goto done; | ||
| 5827 | - } | ||
| 5828 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, stmt, NULL); | ||
| 5829 | - sqlite3_free( (void *) zSql); | ||
| 5830 | - if(rc != SQLITE_OK) { | ||
| 5831 | - goto done; | ||
| 5832 | - } | ||
| 5833 | - } | ||
| 5834 | - | ||
| 5835 | - sqlite3_reset(*stmt); | ||
| 5836 | - sqlite3_bind_int64(*stmt, 1, rowid); | ||
| 5837 | - rc = sqlite3_step(*stmt); | ||
| 5838 | - if(rc != SQLITE_ROW) { | ||
| 5839 | - rc = SQLITE_ERROR; | ||
| 5840 | - goto done; | ||
| 5841 | - } | ||
| 5842 | - *s = (char *) sqlite3_column_text(*stmt, 0); | ||
| 5843 | - *n = sqlite3_column_bytes(*stmt, 0); | ||
| 5844 | - rc = SQLITE_OK; | ||
| 5845 | - done: | ||
| 5846 | - return rc; | ||
| 5847 | -} | ||
| 5848 | - | ||
| 5849 | -/** | ||
| 5850 | - * @brief Crete at "iterator" (sqlite3_stmt) of chunks with the given constraints | ||
| 5851 | - * | ||
| 5852 | - * Any VEC0_IDXSTR_KIND_KNN_PARTITON_CONSTRAINT values in idxStr/argv will be applied | ||
| 5853 | - * as WHERE constraints in the underlying stmt SQL, and any consumer of the stmt | ||
| 5854 | - * can freely step through the stmt with all constraints satisfied. | ||
| 5855 | - * | ||
| 5856 | - * @param p - vec0_vtab | ||
| 5857 | - * @param idxStr - the xBestIndex/xFilter idxstr containing VEC0_IDXSTR values | ||
| 5858 | - * @param argc - number of argv values from xFilter | ||
| 5859 | - * @param argv - array of sqlite3_value from xFilter | ||
| 5860 | - * @param outStmt - output sqlite3_stmt of chunks with all filters applied | ||
| 5861 | - * @return int SQLITE_OK on success, error code otherwise | ||
| 5862 | - */ | ||
| 5863 | -int vec0_chunks_iter(vec0_vtab * p, const char * idxStr, int argc, sqlite3_value ** argv, sqlite3_stmt** outStmt) { | ||
| 5864 | - // always null terminated, enforced by SQLite | ||
| 5865 | - int idxStrLength = strlen(idxStr); | ||
| 5866 | - // "1" refers to the initial vec0_query_plan char, 4 is the number of chars per "element" | ||
| 5867 | - int numValueEntries = (idxStrLength-1) / 4; | ||
| 5868 | - assert(argc == numValueEntries); | ||
| 5869 | - | ||
| 5870 | - int rc; | ||
| 5871 | - sqlite3_str * s = sqlite3_str_new(NULL); | ||
| 5872 | - sqlite3_str_appendf(s, "select chunk_id, validity, rowids " | ||
| 5873 | - " from " VEC0_SHADOW_CHUNKS_NAME, | ||
| 5874 | - p->schemaName, p->tableName); | ||
| 5875 | - | ||
| 5876 | - int appendedWhere = 0; | ||
| 5877 | - for(int i = 0; i < numValueEntries; i++) { | ||
| 5878 | - int idx = 1 + (i * 4); | ||
| 5879 | - char kind = idxStr[idx + 0]; | ||
| 5880 | - if(kind != VEC0_IDXSTR_KIND_KNN_PARTITON_CONSTRAINT) { | ||
| 5881 | - continue; | ||
| 5882 | - } | ||
| 5883 | - | ||
| 5884 | - int partition_idx = idxStr[idx + 1] - 'A'; | ||
| 5885 | - int operator = idxStr[idx + 2]; | ||
| 5886 | - // idxStr[idx + 3] is just null, a '_' placeholder | ||
| 5887 | - | ||
| 5888 | - if(!appendedWhere) { | ||
| 5889 | - sqlite3_str_appendall(s, " WHERE "); | ||
| 5890 | - appendedWhere = 1; | ||
| 5891 | - }else { | ||
| 5892 | - sqlite3_str_appendall(s, " AND "); | ||
| 5893 | - } | ||
| 5894 | - switch(operator) { | ||
| 5895 | - case VEC0_PARTITION_OPERATOR_EQ: | ||
| 5896 | - sqlite3_str_appendf(s, " partition%02d = ? ", partition_idx); | ||
| 5897 | - break; | ||
| 5898 | - case VEC0_PARTITION_OPERATOR_GT: | ||
| 5899 | - sqlite3_str_appendf(s, " partition%02d > ? ", partition_idx); | ||
| 5900 | - break; | ||
| 5901 | - case VEC0_PARTITION_OPERATOR_LE: | ||
| 5902 | - sqlite3_str_appendf(s, " partition%02d <= ? ", partition_idx); | ||
| 5903 | - break; | ||
| 5904 | - case VEC0_PARTITION_OPERATOR_LT: | ||
| 5905 | - sqlite3_str_appendf(s, " partition%02d < ? ", partition_idx); | ||
| 5906 | - break; | ||
| 5907 | - case VEC0_PARTITION_OPERATOR_GE: | ||
| 5908 | - sqlite3_str_appendf(s, " partition%02d >= ? ", partition_idx); | ||
| 5909 | - break; | ||
| 5910 | - case VEC0_PARTITION_OPERATOR_NE: | ||
| 5911 | - sqlite3_str_appendf(s, " partition%02d != ? ", partition_idx); | ||
| 5912 | - break; | ||
| 5913 | - default: { | ||
| 5914 | - char * zSql = sqlite3_str_finish(s); | ||
| 5915 | - sqlite3_free(zSql); | ||
| 5916 | - return SQLITE_ERROR; | ||
| 5917 | - } | ||
| 5918 | - | ||
| 5919 | - } | ||
| 5920 | - | ||
| 5921 | - } | ||
| 5922 | - | ||
| 5923 | - char *zSql = sqlite3_str_finish(s); | ||
| 5924 | - if (!zSql) { | ||
| 5925 | - return SQLITE_NOMEM; | ||
| 5926 | - } | ||
| 5927 | - | ||
| 5928 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, outStmt, NULL); | ||
| 5929 | - sqlite3_free(zSql); | ||
| 5930 | - if(rc != SQLITE_OK) { | ||
| 5931 | - return rc; | ||
| 5932 | - } | ||
| 5933 | - | ||
| 5934 | - int n = 1; | ||
| 5935 | - for(int i = 0; i < numValueEntries; i++) { | ||
| 5936 | - int idx = 1 + (i * 4); | ||
| 5937 | - char kind = idxStr[idx + 0]; | ||
| 5938 | - if(kind != VEC0_IDXSTR_KIND_KNN_PARTITON_CONSTRAINT) { | ||
| 5939 | - continue; | ||
| 5940 | - } | ||
| 5941 | - sqlite3_bind_value(*outStmt, n++, argv[i]); | ||
| 5942 | - } | ||
| 5943 | - | ||
| 5944 | - return rc; | ||
| 5945 | -} | ||
| 5946 | - | ||
| 5947 | -// a single `xxx in (...)` constraint on a metadata column. TEXT or INTEGER only for now. | ||
| 5948 | -struct Vec0MetadataIn{ | ||
| 5949 | - // index of argv[i]` the constraint is on | ||
| 5950 | - int argv_idx; | ||
| 5951 | - // metadata column index of the constraint, derived from idxStr + argv_idx | ||
| 5952 | - int metadata_idx; | ||
| 5953 | - // array of the copied `(...)` values from sqlite3_vtab_in_first()/sqlite3_vtab_in_next() | ||
| 5954 | - struct Array array; | ||
| 5955 | -}; | ||
| 5956 | - | ||
| 5957 | -// Array elements for `xxx in (...)` values for a text column. basically just a string | ||
| 5958 | -struct Vec0MetadataInTextEntry { | ||
| 5959 | - int n; | ||
| 5960 | - char * zString; | ||
| 5961 | -}; | ||
| 5962 | - | ||
| 5963 | - | ||
| 5964 | -int vec0_metadata_filter_text(vec0_vtab * p, sqlite3_value * value, const void * buffer, int size, vec0_metadata_operator op, u8* b, int metadata_idx, int chunk_rowid, struct Array * aMetadataIn, int argv_idx) { | ||
| 5965 | - int rc; | ||
| 5966 | - sqlite3_stmt * stmt = NULL; | ||
| 5967 | - i64 * rowids = NULL; | ||
| 5968 | - sqlite3_blob * rowidsBlob; | ||
| 5969 | - const char * sTarget = (const char *) sqlite3_value_text(value); | ||
| 5970 | - int nTarget = sqlite3_value_bytes(value); | ||
| 5971 | - | ||
| 5972 | - | ||
| 5973 | - // TODO(perf): only text metadata news the rowids BLOB. Make it so that | ||
| 5974 | - // rowids BLOB is re-used when multiple fitlers on text columns, | ||
| 5975 | - // ex "name BETWEEN 'a' and 'b'"" | ||
| 5976 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowChunksName, "rowids", chunk_rowid, 0, &rowidsBlob); | ||
| 5977 | - if(rc != SQLITE_OK) { | ||
| 5978 | - return rc; | ||
| 5979 | - } | ||
| 5980 | - assert(sqlite3_blob_bytes(rowidsBlob) % sizeof(i64) == 0); | ||
| 5981 | - assert((sqlite3_blob_bytes(rowidsBlob) / sizeof(i64)) == size); | ||
| 5982 | - | ||
| 5983 | - rowids = sqlite3_malloc(sqlite3_blob_bytes(rowidsBlob)); | ||
| 5984 | - if(!rowids) { | ||
| 5985 | - sqlite3_blob_close(rowidsBlob); | ||
| 5986 | - return SQLITE_NOMEM; | ||
| 5987 | - } | ||
| 5988 | - | ||
| 5989 | - rc = sqlite3_blob_read(rowidsBlob, rowids, sqlite3_blob_bytes(rowidsBlob), 0); | ||
| 5990 | - if(rc != SQLITE_OK) { | ||
| 5991 | - sqlite3_blob_close(rowidsBlob); | ||
| 5992 | - return rc; | ||
| 5993 | - } | ||
| 5994 | - sqlite3_blob_close(rowidsBlob); | ||
| 5995 | - | ||
| 5996 | - switch(op) { | ||
| 5997 | - int nPrefix; | ||
| 5998 | - char * sPrefix; | ||
| 5999 | - char *sFull; | ||
| 6000 | - int nFull; | ||
| 6001 | - u8 * view; | ||
| 6002 | - case VEC0_METADATA_OPERATOR_EQ: { | ||
| 6003 | - for(int i = 0; i < size; i++) { | ||
| 6004 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | ||
| 6005 | - nPrefix = ((int*) view)[0]; | ||
| 6006 | - sPrefix = (char *) &view[4]; | ||
| 6007 | - | ||
| 6008 | - // for EQ the text lengths must match | ||
| 6009 | - if(nPrefix != nTarget) { | ||
| 6010 | - bitmap_set(b, i, 0); | ||
| 6011 | - continue; | ||
| 6012 | - } | ||
| 6013 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH)); | ||
| 6014 | - | ||
| 6015 | - // for short strings, use the prefix comparison direclty | ||
| 6016 | - if(nPrefix <= VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | ||
| 6017 | - bitmap_set(b, i, cmpPrefix == 0); | ||
| 6018 | - continue; | ||
| 6019 | - } | ||
| 6020 | - // for EQ on longs strings, the prefix must match | ||
| 6021 | - if(cmpPrefix) { | ||
| 6022 | - bitmap_set(b, i, 0); | ||
| 6023 | - continue; | ||
| 6024 | - } | ||
| 6025 | - // consult the full string | ||
| 6026 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | ||
| 6027 | - if(rc != SQLITE_OK) { | ||
| 6028 | - goto done; | ||
| 6029 | - } | ||
| 6030 | - if(nPrefix != nFull) { | ||
| 6031 | - rc = SQLITE_ERROR; | ||
| 6032 | - goto done; | ||
| 6033 | - } | ||
| 6034 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) == 0); | ||
| 6035 | - } | ||
| 6036 | - break; | ||
| 6037 | - } | ||
| 6038 | - case VEC0_METADATA_OPERATOR_NE: { | ||
| 6039 | - for(int i = 0; i < size; i++) { | ||
| 6040 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | ||
| 6041 | - nPrefix = ((int*) view)[0]; | ||
| 6042 | - sPrefix = (char *) &view[4]; | ||
| 6043 | - | ||
| 6044 | - // for NE if text lengths dont match, it never will | ||
| 6045 | - if(nPrefix != nTarget) { | ||
| 6046 | - bitmap_set(b, i, 1); | ||
| 6047 | - continue; | ||
| 6048 | - } | ||
| 6049 | - | ||
| 6050 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH)); | ||
| 6051 | - | ||
| 6052 | - // for short strings, use the prefix comparison direclty | ||
| 6053 | - if(nPrefix <= VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | ||
| 6054 | - bitmap_set(b, i, cmpPrefix != 0); | ||
| 6055 | - continue; | ||
| 6056 | - } | ||
| 6057 | - // for NE on longs strings, if prefixes dont match, then long string wont | ||
| 6058 | - if(cmpPrefix) { | ||
| 6059 | - bitmap_set(b, i, 1); | ||
| 6060 | - continue; | ||
| 6061 | - } | ||
| 6062 | - // consult the full string | ||
| 6063 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | ||
| 6064 | - if(rc != SQLITE_OK) { | ||
| 6065 | - goto done; | ||
| 6066 | - } | ||
| 6067 | - if(nPrefix != nFull) { | ||
| 6068 | - rc = SQLITE_ERROR; | ||
| 6069 | - goto done; | ||
| 6070 | - } | ||
| 6071 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) != 0); | ||
| 6072 | - } | ||
| 6073 | - break; | ||
| 6074 | - } | ||
| 6075 | - case VEC0_METADATA_OPERATOR_GT: { | ||
| 6076 | - for(int i = 0; i < size; i++) { | ||
| 6077 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | ||
| 6078 | - nPrefix = ((int*) view)[0]; | ||
| 6079 | - sPrefix = (char *) &view[4]; | ||
| 6080 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH), nTarget)); | ||
| 6081 | - | ||
| 6082 | - if(nPrefix < VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | ||
| 6083 | - // if prefix match, check which is longer | ||
| 6084 | - if(cmpPrefix == 0) { | ||
| 6085 | - bitmap_set(b, i, nPrefix > nTarget); | ||
| 6086 | - } | ||
| 6087 | - else { | ||
| 6088 | - bitmap_set(b, i, cmpPrefix > 0); | ||
| 6089 | - } | ||
| 6090 | - continue; | ||
| 6091 | - } | ||
| 6092 | - // TODO(perf): may not need to compare full text in some cases | ||
| 6093 | - | ||
| 6094 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | ||
| 6095 | - if(rc != SQLITE_OK) { | ||
| 6096 | - goto done; | ||
| 6097 | - } | ||
| 6098 | - if(nPrefix != nFull) { | ||
| 6099 | - rc = SQLITE_ERROR; | ||
| 6100 | - goto done; | ||
| 6101 | - } | ||
| 6102 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) > 0); | ||
| 6103 | - } | ||
| 6104 | - break; | ||
| 6105 | - } | ||
| 6106 | - case VEC0_METADATA_OPERATOR_GE: { | ||
| 6107 | - for(int i = 0; i < size; i++) { | ||
| 6108 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | ||
| 6109 | - nPrefix = ((int*) view)[0]; | ||
| 6110 | - sPrefix = (char *) &view[4]; | ||
| 6111 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH), nTarget)); | ||
| 6112 | - | ||
| 6113 | - if(nPrefix < VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | ||
| 6114 | - // if prefix match, check which is longer | ||
| 6115 | - if(cmpPrefix == 0) { | ||
| 6116 | - bitmap_set(b, i, nPrefix >= nTarget); | ||
| 6117 | - } | ||
| 6118 | - else { | ||
| 6119 | - bitmap_set(b, i, cmpPrefix >= 0); | ||
| 6120 | - } | ||
| 6121 | - continue; | ||
| 6122 | - } | ||
| 6123 | - // TODO(perf): may not need to compare full text in some cases | ||
| 6124 | - | ||
| 6125 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | ||
| 6126 | - if(rc != SQLITE_OK) { | ||
| 6127 | - goto done; | ||
| 6128 | - } | ||
| 6129 | - if(nPrefix != nFull) { | ||
| 6130 | - rc = SQLITE_ERROR; | ||
| 6131 | - goto done; | ||
| 6132 | - } | ||
| 6133 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) >= 0); | ||
| 6134 | - } | ||
| 6135 | - break; | ||
| 6136 | - } | ||
| 6137 | - case VEC0_METADATA_OPERATOR_LE: { | ||
| 6138 | - for(int i = 0; i < size; i++) { | ||
| 6139 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | ||
| 6140 | - nPrefix = ((int*) view)[0]; | ||
| 6141 | - sPrefix = (char *) &view[4]; | ||
| 6142 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH), nTarget)); | ||
| 6143 | - | ||
| 6144 | - if(nPrefix < VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | ||
| 6145 | - // if prefix match, check which is longer | ||
| 6146 | - if(cmpPrefix == 0) { | ||
| 6147 | - bitmap_set(b, i, nPrefix <= nTarget); | ||
| 6148 | - } | ||
| 6149 | - else { | ||
| 6150 | - bitmap_set(b, i, cmpPrefix <= 0); | ||
| 6151 | - } | ||
| 6152 | - continue; | ||
| 6153 | - } | ||
| 6154 | - // TODO(perf): may not need to compare full text in some cases | ||
| 6155 | - | ||
| 6156 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | ||
| 6157 | - if(rc != SQLITE_OK) { | ||
| 6158 | - goto done; | ||
| 6159 | - } | ||
| 6160 | - if(nPrefix != nFull) { | ||
| 6161 | - rc = SQLITE_ERROR; | ||
| 6162 | - goto done; | ||
| 6163 | - } | ||
| 6164 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) <= 0); | ||
| 6165 | - } | ||
| 6166 | - break; | ||
| 6167 | - } | ||
| 6168 | - case VEC0_METADATA_OPERATOR_LT: { | ||
| 6169 | - for(int i = 0; i < size; i++) { | ||
| 6170 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | ||
| 6171 | - nPrefix = ((int*) view)[0]; | ||
| 6172 | - sPrefix = (char *) &view[4]; | ||
| 6173 | - int cmpPrefix = strncmp(sPrefix, sTarget, min(min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH), nTarget)); | ||
| 6174 | - | ||
| 6175 | - if(nPrefix < VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | ||
| 6176 | - // if prefix match, check which is longer | ||
| 6177 | - if(cmpPrefix == 0) { | ||
| 6178 | - bitmap_set(b, i, nPrefix < nTarget); | ||
| 6179 | - } | ||
| 6180 | - else { | ||
| 6181 | - bitmap_set(b, i, cmpPrefix < 0); | ||
| 6182 | - } | ||
| 6183 | - continue; | ||
| 6184 | - } | ||
| 6185 | - // TODO(perf): may not need to compare full text in some cases | ||
| 6186 | - | ||
| 6187 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | ||
| 6188 | - if(rc != SQLITE_OK) { | ||
| 6189 | - goto done; | ||
| 6190 | - } | ||
| 6191 | - if(nPrefix != nFull) { | ||
| 6192 | - rc = SQLITE_ERROR; | ||
| 6193 | - goto done; | ||
| 6194 | - } | ||
| 6195 | - bitmap_set(b, i, strncmp(sFull, sTarget, nFull) < 0); | ||
| 6196 | - } | ||
| 6197 | - break; | ||
| 6198 | - } | ||
| 6199 | - | ||
| 6200 | - case VEC0_METADATA_OPERATOR_IN: { | ||
| 6201 | - size_t metadataInIdx = -1; | ||
| 6202 | - for(size_t i = 0; i < aMetadataIn->length; i++) { | ||
| 6203 | - struct Vec0MetadataIn * metadataIn = &(((struct Vec0MetadataIn *) aMetadataIn->z)[i]); | ||
| 6204 | - if(metadataIn->argv_idx == argv_idx) { | ||
| 6205 | - metadataInIdx = i; | ||
| 6206 | - break; | ||
| 6207 | - } | ||
| 6208 | - } | ||
| 6209 | - if(metadataInIdx < 0) { | ||
| 6210 | - rc = SQLITE_ERROR; | ||
| 6211 | - goto done; | ||
| 6212 | - } | ||
| 6213 | - | ||
| 6214 | - struct Vec0MetadataIn * metadataIn = &((struct Vec0MetadataIn *) aMetadataIn->z)[metadataInIdx]; | ||
| 6215 | - struct Array * aTarget = &(metadataIn->array); | ||
| 6216 | - | ||
| 6217 | - | ||
| 6218 | - int nPrefix; | ||
| 6219 | - char * sPrefix; | ||
| 6220 | - char *sFull; | ||
| 6221 | - int nFull; | ||
| 6222 | - u8 * view; | ||
| 6223 | - for(int i = 0; i < size; i++) { | ||
| 6224 | - view = &((u8*) buffer)[i * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | ||
| 6225 | - nPrefix = ((int*) view)[0]; | ||
| 6226 | - sPrefix = (char *) &view[4]; | ||
| 6227 | - for(size_t target_idx = 0; target_idx < aTarget->length; target_idx++) { | ||
| 6228 | - struct Vec0MetadataInTextEntry * entry = &(((struct Vec0MetadataInTextEntry*)aTarget->z)[target_idx]); | ||
| 6229 | - if(entry->n != nPrefix) { | ||
| 6230 | - continue; | ||
| 6231 | - } | ||
| 6232 | - int cmpPrefix = strncmp(sPrefix, entry->zString, min(nPrefix, VEC0_METADATA_TEXT_VIEW_DATA_LENGTH)); | ||
| 6233 | - if(nPrefix <= VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | ||
| 6234 | - if(cmpPrefix == 0) { | ||
| 6235 | - bitmap_set(b, i, 1); | ||
| 6236 | - break; | ||
| 6237 | - } | ||
| 6238 | - continue; | ||
| 6239 | - } | ||
| 6240 | - if(cmpPrefix) { | ||
| 6241 | - continue; | ||
| 6242 | - } | ||
| 6243 | - | ||
| 6244 | - rc = vec0_get_metadata_text_long_value(p, &stmt, metadata_idx, rowids[i], &nFull, &sFull); | ||
| 6245 | - if(rc != SQLITE_OK) { | ||
| 6246 | - goto done; | ||
| 6247 | - } | ||
| 6248 | - if(nPrefix != nFull) { | ||
| 6249 | - rc = SQLITE_ERROR; | ||
| 6250 | - goto done; | ||
| 6251 | - } | ||
| 6252 | - if(strncmp(sFull, entry->zString, nFull) == 0) { | ||
| 6253 | - bitmap_set(b, i, 1); | ||
| 6254 | - break; | ||
| 6255 | - } | ||
| 6256 | - } | ||
| 6257 | - } | ||
| 6258 | - break; | ||
| 6259 | - } | ||
| 6260 | - | ||
| 6261 | - } | ||
| 6262 | - rc = SQLITE_OK; | ||
| 6263 | - | ||
| 6264 | - done: | ||
| 6265 | - sqlite3_finalize(stmt); | ||
| 6266 | - sqlite3_free(rowids); | ||
| 6267 | - return rc; | ||
| 6268 | - | ||
| 6269 | -} | ||
| 6270 | - | ||
| 6271 | -/** | ||
| 6272 | - * @brief Fill in bitmap of chunk values, whether or not the values match a metadata constraint | ||
| 6273 | - * | ||
| 6274 | - * @param p vec0_vtab | ||
| 6275 | - * @param metadata_idx index of the metatadata column to perfrom constraints on | ||
| 6276 | - * @param value sqlite3_value of the constraints value | ||
| 6277 | - * @param blob sqlite3_blob that is already opened on the metdata column's shadow chunk table | ||
| 6278 | - * @param chunk_rowid rowid of the chunk to calculate on | ||
| 6279 | - * @param b pre-allocated and zero'd out bitmap to write results to | ||
| 6280 | - * @param size size of the chunk | ||
| 6281 | - * @return int SQLITE_OK on success, error code otherwise | ||
| 6282 | - */ | ||
| 6283 | -int vec0_set_metadata_filter_bitmap( | ||
| 6284 | - vec0_vtab *p, | ||
| 6285 | - int metadata_idx, | ||
| 6286 | - vec0_metadata_operator op, | ||
| 6287 | - sqlite3_value * value, | ||
| 6288 | - sqlite3_blob * blob, | ||
| 6289 | - i64 chunk_rowid, | ||
| 6290 | - u8* b, | ||
| 6291 | - int size, | ||
| 6292 | - struct Array * aMetadataIn, int argv_idx) { | ||
| 6293 | - // TODO: shouldn't this skip in-valid entries from the chunk's validity bitmap? | ||
| 6294 | - | ||
| 6295 | - int rc; | ||
| 6296 | - rc = sqlite3_blob_reopen(blob, chunk_rowid); | ||
| 6297 | - if(rc != SQLITE_OK) { | ||
| 6298 | - return rc; | ||
| 6299 | - } | ||
| 6300 | - | ||
| 6301 | - vec0_metadata_column_kind kind = p->metadata_columns[metadata_idx].kind; | ||
| 6302 | - int szMatch = 0; | ||
| 6303 | - int blobSize = sqlite3_blob_bytes(blob); | ||
| 6304 | - switch(kind) { | ||
| 6305 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | ||
| 6306 | - szMatch = blobSize == size / CHAR_BIT; | ||
| 6307 | - break; | ||
| 6308 | - } | ||
| 6309 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | ||
| 6310 | - szMatch = blobSize == size * sizeof(i64); | ||
| 6311 | - break; | ||
| 6312 | - } | ||
| 6313 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | ||
| 6314 | - szMatch = blobSize == size * sizeof(double); | ||
| 6315 | - break; | ||
| 6316 | - } | ||
| 6317 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | ||
| 6318 | - szMatch = blobSize == size * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH; | ||
| 6319 | - break; | ||
| 6320 | - } | ||
| 6321 | - } | ||
| 6322 | - if(!szMatch) { | ||
| 6323 | - return SQLITE_ERROR; | ||
| 6324 | - } | ||
| 6325 | - void * buffer = sqlite3_malloc(blobSize); | ||
| 6326 | - if(!buffer) { | ||
| 6327 | - return SQLITE_NOMEM; | ||
| 6328 | - } | ||
| 6329 | - rc = sqlite3_blob_read(blob, buffer, blobSize, 0); | ||
| 6330 | - if(rc != SQLITE_OK) { | ||
| 6331 | - goto done; | ||
| 6332 | - } | ||
| 6333 | - switch(kind) { | ||
| 6334 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | ||
| 6335 | - int target = sqlite3_value_int(value); | ||
| 6336 | - if( (target && op == VEC0_METADATA_OPERATOR_EQ) || (!target && op == VEC0_METADATA_OPERATOR_NE)) { | ||
| 6337 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, bitmap_get((u8*) buffer, i)); } | ||
| 6338 | - } | ||
| 6339 | - else { | ||
| 6340 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, !bitmap_get((u8*) buffer, i)); } | ||
| 6341 | - } | ||
| 6342 | - break; | ||
| 6343 | - } | ||
| 6344 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | ||
| 6345 | - i64 * array = (i64*) buffer; | ||
| 6346 | - i64 target = sqlite3_value_int64(value); | ||
| 6347 | - switch(op) { | ||
| 6348 | - case VEC0_METADATA_OPERATOR_EQ: { | ||
| 6349 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] == target); } | ||
| 6350 | - break; | ||
| 6351 | - } | ||
| 6352 | - case VEC0_METADATA_OPERATOR_GT: { | ||
| 6353 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] > target); } | ||
| 6354 | - break; | ||
| 6355 | - } | ||
| 6356 | - case VEC0_METADATA_OPERATOR_LE: { | ||
| 6357 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] <= target); } | ||
| 6358 | - break; | ||
| 6359 | - } | ||
| 6360 | - case VEC0_METADATA_OPERATOR_LT: { | ||
| 6361 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] < target); } | ||
| 6362 | - break; | ||
| 6363 | - } | ||
| 6364 | - case VEC0_METADATA_OPERATOR_GE: { | ||
| 6365 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] >= target); } | ||
| 6366 | - break; | ||
| 6367 | - } | ||
| 6368 | - case VEC0_METADATA_OPERATOR_NE: { | ||
| 6369 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] != target); } | ||
| 6370 | - break; | ||
| 6371 | - } | ||
| 6372 | - case VEC0_METADATA_OPERATOR_IN: { | ||
| 6373 | - int metadataInIdx = -1; | ||
| 6374 | - for(size_t i = 0; i < aMetadataIn->length; i++) { | ||
| 6375 | - struct Vec0MetadataIn * metadataIn = &((struct Vec0MetadataIn *) aMetadataIn->z)[i]; | ||
| 6376 | - if(metadataIn->argv_idx == argv_idx) { | ||
| 6377 | - metadataInIdx = i; | ||
| 6378 | - break; | ||
| 6379 | - } | ||
| 6380 | - } | ||
| 6381 | - if(metadataInIdx < 0) { | ||
| 6382 | - rc = SQLITE_ERROR; | ||
| 6383 | - goto done; | ||
| 6384 | - } | ||
| 6385 | - struct Vec0MetadataIn * metadataIn = &((struct Vec0MetadataIn *) aMetadataIn->z)[metadataInIdx]; | ||
| 6386 | - struct Array * aTarget = &(metadataIn->array); | ||
| 6387 | - | ||
| 6388 | - for(int i = 0; i < size; i++) { | ||
| 6389 | - for(size_t target_idx = 0; target_idx < aTarget->length; target_idx++) { | ||
| 6390 | - if( ((i64*)aTarget->z)[target_idx] == array[i]) { | ||
| 6391 | - bitmap_set(b, i, 1); | ||
| 6392 | - break; | ||
| 6393 | - } | ||
| 6394 | - } | ||
| 6395 | - } | ||
| 6396 | - break; | ||
| 6397 | - } | ||
| 6398 | - } | ||
| 6399 | - break; | ||
| 6400 | - } | ||
| 6401 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | ||
| 6402 | - double * array = (double*) buffer; | ||
| 6403 | - double target = sqlite3_value_double(value); | ||
| 6404 | - switch(op) { | ||
| 6405 | - case VEC0_METADATA_OPERATOR_EQ: { | ||
| 6406 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] == target); } | ||
| 6407 | - break; | ||
| 6408 | - } | ||
| 6409 | - case VEC0_METADATA_OPERATOR_GT: { | ||
| 6410 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] > target); } | ||
| 6411 | - break; | ||
| 6412 | - } | ||
| 6413 | - case VEC0_METADATA_OPERATOR_LE: { | ||
| 6414 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] <= target); } | ||
| 6415 | - break; | ||
| 6416 | - } | ||
| 6417 | - case VEC0_METADATA_OPERATOR_LT: { | ||
| 6418 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] < target); } | ||
| 6419 | - break; | ||
| 6420 | - } | ||
| 6421 | - case VEC0_METADATA_OPERATOR_GE: { | ||
| 6422 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] >= target); } | ||
| 6423 | - break; | ||
| 6424 | - } | ||
| 6425 | - case VEC0_METADATA_OPERATOR_NE: { | ||
| 6426 | - for(int i = 0; i < size; i++) { bitmap_set(b, i, array[i] != target); } | ||
| 6427 | - break; | ||
| 6428 | - } | ||
| 6429 | - case VEC0_METADATA_OPERATOR_IN: { | ||
| 6430 | - // should never be reached | ||
| 6431 | - break; | ||
| 6432 | - } | ||
| 6433 | - } | ||
| 6434 | - break; | ||
| 6435 | - } | ||
| 6436 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | ||
| 6437 | - rc = vec0_metadata_filter_text(p, value, buffer, size, op, b, metadata_idx, chunk_rowid, aMetadataIn, argv_idx); | ||
| 6438 | - if(rc != SQLITE_OK) { | ||
| 6439 | - goto done; | ||
| 6440 | - } | ||
| 6441 | - break; | ||
| 6442 | - } | ||
| 6443 | - } | ||
| 6444 | - done: | ||
| 6445 | - sqlite3_free(buffer); | ||
| 6446 | - return rc; | ||
| 6447 | -} | ||
| 6448 | - | ||
| 6449 | -int vec0Filter_knn_chunks_iter(vec0_vtab *p, sqlite3_stmt *stmtChunks, | ||
| 6450 | - struct VectorColumnDefinition *vector_column, | ||
| 6451 | - int vectorColumnIdx, struct Array *arrayRowidsIn, | ||
| 6452 | - struct Array * aMetadataIn, | ||
| 6453 | - const char * idxStr, int argc, sqlite3_value ** argv, | ||
| 6454 | - void *queryVector, i64 k, i64 **out_topk_rowids, | ||
| 6455 | - f32 **out_topk_distances, i64 *out_used) { | ||
| 6456 | - // for each chunk, get top min(k, chunk_size) rowid + distances to query vec. | ||
| 6457 | - // then reconcile all topk_chunks for a true top k. | ||
| 6458 | - // output only rowids + distances for now | ||
| 6459 | - | ||
| 6460 | - int rc = SQLITE_OK; | ||
| 6461 | - sqlite3_blob *blobVectors = NULL; | ||
| 6462 | - | ||
| 6463 | - void *baseVectors = NULL; // memory: chunk_size * dimensions * element_size | ||
| 6464 | - | ||
| 6465 | - // OWNED BY CALLER ON SUCCESS | ||
| 6466 | - i64 *topk_rowids = NULL; // memory: k * 4 | ||
| 6467 | - // OWNED BY CALLER ON SUCCESS | ||
| 6468 | - f32 *topk_distances = NULL; // memory: k * 4 | ||
| 6469 | - | ||
| 6470 | - i64 *tmp_topk_rowids = NULL; // memory: k * 4 | ||
| 6471 | - f32 *tmp_topk_distances = NULL; // memory: k * 4 | ||
| 6472 | - f32 *chunk_distances = NULL; // memory: chunk_size * 4 | ||
| 6473 | - u8 *b = NULL; // memory: chunk_size / 8 | ||
| 6474 | - u8 *bTaken = NULL; // memory: chunk_size / 8 | ||
| 6475 | - i32 *chunk_topk_idxs = NULL; // memory: k * 4 | ||
| 6476 | - u8 *bmRowids = NULL; // memory: chunk_size / 8 | ||
| 6477 | - u8 *bmMetadata = NULL; // memory: chunk_size / 8 | ||
| 6478 | - // // total: a lot??? | ||
| 6479 | - | ||
| 6480 | - // 6 * (k * 4) + (k * 2) + (chunk_size / 8) + (chunk_size * dimensions * 4) | ||
| 6481 | - | ||
| 6482 | - topk_rowids = sqlite3_malloc(k * sizeof(i64)); | ||
| 6483 | - if (!topk_rowids) { | ||
| 6484 | - rc = SQLITE_NOMEM; | ||
| 6485 | - goto cleanup; | ||
| 6486 | - } | ||
| 6487 | - memset(topk_rowids, 0, k * sizeof(i64)); | ||
| 6488 | - | ||
| 6489 | - topk_distances = sqlite3_malloc(k * sizeof(f32)); | ||
| 6490 | - if (!topk_distances) { | ||
| 6491 | - rc = SQLITE_NOMEM; | ||
| 6492 | - goto cleanup; | ||
| 6493 | - } | ||
| 6494 | - memset(topk_distances, 0, k * sizeof(f32)); | ||
| 6495 | - | ||
| 6496 | - tmp_topk_rowids = sqlite3_malloc(k * sizeof(i64)); | ||
| 6497 | - if (!tmp_topk_rowids) { | ||
| 6498 | - rc = SQLITE_NOMEM; | ||
| 6499 | - goto cleanup; | ||
| 6500 | - } | ||
| 6501 | - memset(tmp_topk_rowids, 0, k * sizeof(i64)); | ||
| 6502 | - | ||
| 6503 | - tmp_topk_distances = sqlite3_malloc(k * sizeof(f32)); | ||
| 6504 | - if (!tmp_topk_distances) { | ||
| 6505 | - rc = SQLITE_NOMEM; | ||
| 6506 | - goto cleanup; | ||
| 6507 | - } | ||
| 6508 | - memset(tmp_topk_distances, 0, k * sizeof(f32)); | ||
| 6509 | - | ||
| 6510 | - i64 k_used = 0; | ||
| 6511 | - i64 baseVectorsSize = p->chunk_size * vector_column_byte_size(*vector_column); | ||
| 6512 | - baseVectors = sqlite3_malloc(baseVectorsSize); | ||
| 6513 | - if (!baseVectors) { | ||
| 6514 | - rc = SQLITE_NOMEM; | ||
| 6515 | - goto cleanup; | ||
| 6516 | - } | ||
| 6517 | - | ||
| 6518 | - chunk_distances = sqlite3_malloc(p->chunk_size * sizeof(f32)); | ||
| 6519 | - if (!chunk_distances) { | ||
| 6520 | - rc = SQLITE_NOMEM; | ||
| 6521 | - goto cleanup; | ||
| 6522 | - } | ||
| 6523 | - | ||
| 6524 | - b = bitmap_new(p->chunk_size); | ||
| 6525 | - if (!b) { | ||
| 6526 | - rc = SQLITE_NOMEM; | ||
| 6527 | - goto cleanup; | ||
| 6528 | - } | ||
| 6529 | - | ||
| 6530 | - bTaken = bitmap_new(p->chunk_size); | ||
| 6531 | - if (!bTaken) { | ||
| 6532 | - rc = SQLITE_NOMEM; | ||
| 6533 | - goto cleanup; | ||
| 6534 | - } | ||
| 6535 | - | ||
| 6536 | - chunk_topk_idxs = sqlite3_malloc(k * sizeof(i32)); | ||
| 6537 | - if (!chunk_topk_idxs) { | ||
| 6538 | - rc = SQLITE_NOMEM; | ||
| 6539 | - goto cleanup; | ||
| 6540 | - } | ||
| 6541 | - | ||
| 6542 | - bmRowids = arrayRowidsIn ? bitmap_new(p->chunk_size) : NULL; | ||
| 6543 | - if (arrayRowidsIn && !bmRowids) { | ||
| 6544 | - rc = SQLITE_NOMEM; | ||
| 6545 | - goto cleanup; | ||
| 6546 | - } | ||
| 6547 | - | ||
| 6548 | - sqlite3_blob * metadataBlobs[VEC0_MAX_METADATA_COLUMNS]; | ||
| 6549 | - memset(metadataBlobs, 0, sizeof(sqlite3_blob*) * VEC0_MAX_METADATA_COLUMNS); | ||
| 6550 | - | ||
| 6551 | - bmMetadata = bitmap_new(p->chunk_size); | ||
| 6552 | - if(!bmMetadata) { | ||
| 6553 | - rc = SQLITE_NOMEM; | ||
| 6554 | - goto cleanup; | ||
| 6555 | - } | ||
| 6556 | - | ||
| 6557 | - int idxStrLength = strlen(idxStr); | ||
| 6558 | - int numValueEntries = (idxStrLength-1) / 4; | ||
| 6559 | - assert(numValueEntries == argc); | ||
| 6560 | - int hasMetadataFilters = 0; | ||
| 6561 | - for(int i = 0; i < argc; i++) { | ||
| 6562 | - int idx = 1 + (i * 4); | ||
| 6563 | - char kind = idxStr[idx + 0]; | ||
| 6564 | - if(kind == VEC0_IDXSTR_KIND_METADATA_CONSTRAINT) { | ||
| 6565 | - hasMetadataFilters = 1; | ||
| 6566 | - break; | ||
| 6567 | - } | ||
| 6568 | - } | ||
| 6569 | - | ||
| 6570 | - while (true) { | ||
| 6571 | - rc = sqlite3_step(stmtChunks); | ||
| 6572 | - if (rc == SQLITE_DONE) { | ||
| 6573 | - break; | ||
| 6574 | - } | ||
| 6575 | - if (rc != SQLITE_ROW) { | ||
| 6576 | - vtab_set_error(&p->base, "chunks iter error"); | ||
| 6577 | - rc = SQLITE_ERROR; | ||
| 6578 | - goto cleanup; | ||
| 6579 | - } | ||
| 6580 | - memset(chunk_distances, 0, p->chunk_size * sizeof(f32)); | ||
| 6581 | - memset(chunk_topk_idxs, 0, k * sizeof(i32)); | ||
| 6582 | - bitmap_clear(b, p->chunk_size); | ||
| 6583 | - | ||
| 6584 | - i64 chunk_id = sqlite3_column_int64(stmtChunks, 0); | ||
| 6585 | - unsigned char *chunkValidity = | ||
| 6586 | - (unsigned char *)sqlite3_column_blob(stmtChunks, 1); | ||
| 6587 | - i64 validitySize = sqlite3_column_bytes(stmtChunks, 1); | ||
| 6588 | - if (validitySize != p->chunk_size / CHAR_BIT) { | ||
| 6589 | - // IMP: V05271_22109 | ||
| 6590 | - vtab_set_error( | ||
| 6591 | - &p->base, | ||
| 6592 | - "chunk validity size doesn't match - expected %lld, found %lld", | ||
| 6593 | - p->chunk_size / CHAR_BIT, validitySize); | ||
| 6594 | - rc = SQLITE_ERROR; | ||
| 6595 | - goto cleanup; | ||
| 6596 | - } | ||
| 6597 | - | ||
| 6598 | - i64 *chunkRowids = (i64 *)sqlite3_column_blob(stmtChunks, 2); | ||
| 6599 | - i64 rowidsSize = sqlite3_column_bytes(stmtChunks, 2); | ||
| 6600 | - if (rowidsSize != p->chunk_size * sizeof(i64)) { | ||
| 6601 | - // IMP: V02796_19635 | ||
| 6602 | - vtab_set_error(&p->base, "rowids size doesn't match"); | ||
| 6603 | - vtab_set_error( | ||
| 6604 | - &p->base, | ||
| 6605 | - "chunk rowids size doesn't match - expected %lld, found %lld", | ||
| 6606 | - p->chunk_size * sizeof(i64), rowidsSize); | ||
| 6607 | - rc = SQLITE_ERROR; | ||
| 6608 | - goto cleanup; | ||
| 6609 | - } | ||
| 6610 | - | ||
| 6611 | - // open the vector chunk blob for the current chunk | ||
| 6612 | - rc = sqlite3_blob_open(p->db, p->schemaName, | ||
| 6613 | - p->shadowVectorChunksNames[vectorColumnIdx], | ||
| 6614 | - "vectors", chunk_id, 0, &blobVectors); | ||
| 6615 | - if (rc != SQLITE_OK) { | ||
| 6616 | - vtab_set_error(&p->base, "could not open vectors blob for chunk %lld", | ||
| 6617 | - chunk_id); | ||
| 6618 | - rc = SQLITE_ERROR; | ||
| 6619 | - goto cleanup; | ||
| 6620 | - } | ||
| 6621 | - | ||
| 6622 | - i64 currentBaseVectorsSize = sqlite3_blob_bytes(blobVectors); | ||
| 6623 | - i64 expectedBaseVectorsSize = | ||
| 6624 | - p->chunk_size * vector_column_byte_size(*vector_column); | ||
| 6625 | - if (currentBaseVectorsSize != expectedBaseVectorsSize) { | ||
| 6626 | - // IMP: V16465_00535 | ||
| 6627 | - vtab_set_error( | ||
| 6628 | - &p->base, | ||
| 6629 | - "vectors blob size doesn't match - expected %lld, found %lld", | ||
| 6630 | - expectedBaseVectorsSize, currentBaseVectorsSize); | ||
| 6631 | - rc = SQLITE_ERROR; | ||
| 6632 | - goto cleanup; | ||
| 6633 | - } | ||
| 6634 | - rc = sqlite3_blob_read(blobVectors, baseVectors, currentBaseVectorsSize, 0); | ||
| 6635 | - | ||
| 6636 | - if (rc != SQLITE_OK) { | ||
| 6637 | - vtab_set_error(&p->base, "vectors blob read error for %lld", chunk_id); | ||
| 6638 | - rc = SQLITE_ERROR; | ||
| 6639 | - goto cleanup; | ||
| 6640 | - } | ||
| 6641 | - | ||
| 6642 | - bitmap_copy(b, chunkValidity, p->chunk_size); | ||
| 6643 | - if (arrayRowidsIn) { | ||
| 6644 | - bitmap_clear(bmRowids, p->chunk_size); | ||
| 6645 | - | ||
| 6646 | - for (int i = 0; i < p->chunk_size; i++) { | ||
| 6647 | - if (!bitmap_get(chunkValidity, i)) { | ||
| 6648 | - continue; | ||
| 6649 | - } | ||
| 6650 | - i64 rowid = chunkRowids[i]; | ||
| 6651 | - void *in = bsearch(&rowid, arrayRowidsIn->z, arrayRowidsIn->length, | ||
| 6652 | - sizeof(i64), _cmp); | ||
| 6653 | - bitmap_set(bmRowids, i, in ? 1 : 0); | ||
| 6654 | - } | ||
| 6655 | - bitmap_and_inplace(b, bmRowids, p->chunk_size); | ||
| 6656 | - } | ||
| 6657 | - | ||
| 6658 | - if(hasMetadataFilters) { | ||
| 6659 | - for(int i = 0; i < argc; i++) { | ||
| 6660 | - int idx = 1 + (i * 4); | ||
| 6661 | - char kind = idxStr[idx + 0]; | ||
| 6662 | - if(kind != VEC0_IDXSTR_KIND_METADATA_CONSTRAINT) { | ||
| 6663 | - continue; | ||
| 6664 | - } | ||
| 6665 | - int metadata_idx = idxStr[idx + 1] - 'A'; | ||
| 6666 | - int operator = idxStr[idx + 2]; | ||
| 6667 | - | ||
| 6668 | - if(!metadataBlobs[metadata_idx]) { | ||
| 6669 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowMetadataChunksNames[metadata_idx], "data", chunk_id, 0, &metadataBlobs[metadata_idx]); | ||
| 6670 | - vtab_set_error(&p->base, "Could not open metadata blob"); | ||
| 6671 | - if(rc != SQLITE_OK) { | ||
| 6672 | - goto cleanup; | ||
| 6673 | - } | ||
| 6674 | - } | ||
| 6675 | - | ||
| 6676 | - bitmap_clear(bmMetadata, p->chunk_size); | ||
| 6677 | - rc = vec0_set_metadata_filter_bitmap(p, metadata_idx, operator, argv[i], metadataBlobs[metadata_idx], chunk_id, bmMetadata, p->chunk_size, aMetadataIn, i); | ||
| 6678 | - if(rc != SQLITE_OK) { | ||
| 6679 | - vtab_set_error(&p->base, "Could not filter metadata fields"); | ||
| 6680 | - if(rc != SQLITE_OK) { | ||
| 6681 | - goto cleanup; | ||
| 6682 | - } | ||
| 6683 | - } | ||
| 6684 | - bitmap_and_inplace(b, bmMetadata, p->chunk_size); | ||
| 6685 | - } | ||
| 6686 | - } | ||
| 6687 | - | ||
| 6688 | - | ||
| 6689 | - for (int i = 0; i < p->chunk_size; i++) { | ||
| 6690 | - if (!bitmap_get(b, i)) { | ||
| 6691 | - continue; | ||
| 6692 | - }; | ||
| 6693 | - | ||
| 6694 | - f32 result; | ||
| 6695 | - switch (vector_column->element_type) { | ||
| 6696 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: { | ||
| 6697 | - const f32 *base_i = | ||
| 6698 | - ((f32 *)baseVectors) + (i * vector_column->dimensions); | ||
| 6699 | - switch (vector_column->distance_metric) { | ||
| 6700 | - case VEC0_DISTANCE_METRIC_L2: { | ||
| 6701 | - result = distance_l2_sqr_float(base_i, (f32 *)queryVector, | ||
| 6702 | - &vector_column->dimensions); | ||
| 6703 | - break; | ||
| 6704 | - } | ||
| 6705 | - case VEC0_DISTANCE_METRIC_L1: { | ||
| 6706 | - result = distance_l1_f32(base_i, (f32 *)queryVector, | ||
| 6707 | - &vector_column->dimensions); | ||
| 6708 | - break; | ||
| 6709 | - } | ||
| 6710 | - case VEC0_DISTANCE_METRIC_COSINE: { | ||
| 6711 | - result = distance_cosine_float(base_i, (f32 *)queryVector, | ||
| 6712 | - &vector_column->dimensions); | ||
| 6713 | - break; | ||
| 6714 | - } | ||
| 6715 | - } | ||
| 6716 | - break; | ||
| 6717 | - } | ||
| 6718 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: { | ||
| 6719 | - const i8 *base_i = | ||
| 6720 | - ((i8 *)baseVectors) + (i * vector_column->dimensions); | ||
| 6721 | - switch (vector_column->distance_metric) { | ||
| 6722 | - case VEC0_DISTANCE_METRIC_L2: { | ||
| 6723 | - result = distance_l2_sqr_int8(base_i, (i8 *)queryVector, | ||
| 6724 | - &vector_column->dimensions); | ||
| 6725 | - break; | ||
| 6726 | - } | ||
| 6727 | - case VEC0_DISTANCE_METRIC_L1: { | ||
| 6728 | - result = distance_l1_int8(base_i, (i8 *)queryVector, | ||
| 6729 | - &vector_column->dimensions); | ||
| 6730 | - break; | ||
| 6731 | - } | ||
| 6732 | - case VEC0_DISTANCE_METRIC_COSINE: { | ||
| 6733 | - result = distance_cosine_int8(base_i, (i8 *)queryVector, | ||
| 6734 | - &vector_column->dimensions); | ||
| 6735 | - break; | ||
| 6736 | - } | ||
| 6737 | - } | ||
| 6738 | - | ||
| 6739 | - break; | ||
| 6740 | - } | ||
| 6741 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: { | ||
| 6742 | - const u8 *base_i = | ||
| 6743 | - ((u8 *)baseVectors) + (i * (vector_column->dimensions / CHAR_BIT)); | ||
| 6744 | - result = distance_hamming(base_i, (u8 *)queryVector, | ||
| 6745 | - &vector_column->dimensions); | ||
| 6746 | - break; | ||
| 6747 | - } | ||
| 6748 | - } | ||
| 6749 | - | ||
| 6750 | - chunk_distances[i] = result; | ||
| 6751 | - } | ||
| 6752 | - | ||
| 6753 | - int used1; | ||
| 6754 | - min_idx(chunk_distances, p->chunk_size, b, chunk_topk_idxs, | ||
| 6755 | - min(k, p->chunk_size), bTaken, &used1); | ||
| 6756 | - | ||
| 6757 | - i64 used; | ||
| 6758 | - merge_sorted_lists(topk_distances, topk_rowids, k_used, chunk_distances, | ||
| 6759 | - chunkRowids, chunk_topk_idxs, | ||
| 6760 | - min(min(k, p->chunk_size), used1), tmp_topk_distances, | ||
| 6761 | - tmp_topk_rowids, k, &used); | ||
| 6762 | - | ||
| 6763 | - for (int i = 0; i < used; i++) { | ||
| 6764 | - topk_rowids[i] = tmp_topk_rowids[i]; | ||
| 6765 | - topk_distances[i] = tmp_topk_distances[i]; | ||
| 6766 | - } | ||
| 6767 | - k_used = used; | ||
| 6768 | - // blobVectors is always opened with read-only permissions, so this never | ||
| 6769 | - // fails. | ||
| 6770 | - sqlite3_blob_close(blobVectors); | ||
| 6771 | - blobVectors = NULL; | ||
| 6772 | - } | ||
| 6773 | - | ||
| 6774 | - *out_topk_rowids = topk_rowids; | ||
| 6775 | - *out_topk_distances = topk_distances; | ||
| 6776 | - *out_used = k_used; | ||
| 6777 | - rc = SQLITE_OK; | ||
| 6778 | - | ||
| 6779 | -cleanup: | ||
| 6780 | - if (rc != SQLITE_OK) { | ||
| 6781 | - sqlite3_free(topk_rowids); | ||
| 6782 | - sqlite3_free(topk_distances); | ||
| 6783 | - } | ||
| 6784 | - sqlite3_free(chunk_topk_idxs); | ||
| 6785 | - sqlite3_free(tmp_topk_rowids); | ||
| 6786 | - sqlite3_free(tmp_topk_distances); | ||
| 6787 | - sqlite3_free(b); | ||
| 6788 | - sqlite3_free(bTaken); | ||
| 6789 | - sqlite3_free(bmRowids); | ||
| 6790 | - sqlite3_free(baseVectors); | ||
| 6791 | - sqlite3_free(chunk_distances); | ||
| 6792 | - sqlite3_free(bmMetadata); | ||
| 6793 | - for(int i = 0; i < VEC0_MAX_METADATA_COLUMNS; i++) { | ||
| 6794 | - sqlite3_blob_close(metadataBlobs[i]); | ||
| 6795 | - } | ||
| 6796 | - // blobVectors is always opened with read-only permissions, so this never | ||
| 6797 | - // fails. | ||
| 6798 | - sqlite3_blob_close(blobVectors); | ||
| 6799 | - return rc; | ||
| 6800 | -} | ||
| 6801 | - | ||
| 6802 | -int vec0Filter_knn(vec0_cursor *pCur, vec0_vtab *p, int idxNum, | ||
| 6803 | - const char *idxStr, int argc, sqlite3_value **argv) { | ||
| 6804 | - assert(argc == (strlen(idxStr)-1) / 4); | ||
| 6805 | - int rc; | ||
| 6806 | - struct vec0_query_knn_data *knn_data; | ||
| 6807 | - | ||
| 6808 | - int vectorColumnIdx = idxNum; | ||
| 6809 | - struct VectorColumnDefinition *vector_column = | ||
| 6810 | - &p->vector_columns[vectorColumnIdx]; | ||
| 6811 | - | ||
| 6812 | - struct Array *arrayRowidsIn = NULL; | ||
| 6813 | - sqlite3_stmt *stmtChunks = NULL; | ||
| 6814 | - void *queryVector; | ||
| 6815 | - size_t dimensions; | ||
| 6816 | - enum VectorElementType elementType; | ||
| 6817 | - vector_cleanup queryVectorCleanup = vector_cleanup_noop; | ||
| 6818 | - char *pzError; | ||
| 6819 | - knn_data = sqlite3_malloc(sizeof(*knn_data)); | ||
| 6820 | - if (!knn_data) { | ||
| 6821 | - return SQLITE_NOMEM; | ||
| 6822 | - } | ||
| 6823 | - memset(knn_data, 0, sizeof(*knn_data)); | ||
| 6824 | - // array of `struct Vec0MetadataIn`, IF there are any `xxx in (...)` metadata constraints | ||
| 6825 | - struct Array * aMetadataIn = NULL; | ||
| 6826 | - | ||
| 6827 | - int query_idx =-1; | ||
| 6828 | - int k_idx = -1; | ||
| 6829 | - int rowid_in_idx = -1; | ||
| 6830 | - for(int i = 0; i < argc; i++) { | ||
| 6831 | - if(idxStr[1 + (i*4)] == VEC0_IDXSTR_KIND_KNN_MATCH) { | ||
| 6832 | - query_idx = i; | ||
| 6833 | - } | ||
| 6834 | - if(idxStr[1 + (i*4)] == VEC0_IDXSTR_KIND_KNN_K) { | ||
| 6835 | - k_idx = i; | ||
| 6836 | - } | ||
| 6837 | - if(idxStr[1 + (i*4)] == VEC0_IDXSTR_KIND_KNN_ROWID_IN) { | ||
| 6838 | - rowid_in_idx = i; | ||
| 6839 | - } | ||
| 6840 | - } | ||
| 6841 | - assert(query_idx >= 0); | ||
| 6842 | - assert(k_idx >= 0); | ||
| 6843 | - | ||
| 6844 | - // make sure the query vector matches the vector column (type dimensions etc.) | ||
| 6845 | - rc = vector_from_value(argv[query_idx], &queryVector, &dimensions, &elementType, | ||
| 6846 | - &queryVectorCleanup, &pzError); | ||
| 6847 | - | ||
| 6848 | - if (rc != SQLITE_OK) { | ||
| 6849 | - vtab_set_error(&p->base, | ||
| 6850 | - "Query vector on the \"%.*s\" column is invalid: %z", | ||
| 6851 | - vector_column->name_length, vector_column->name, pzError); | ||
| 6852 | - rc = SQLITE_ERROR; | ||
| 6853 | - goto cleanup; | ||
| 6854 | - } | ||
| 6855 | - if (elementType != vector_column->element_type) { | ||
| 6856 | - vtab_set_error( | ||
| 6857 | - &p->base, | ||
| 6858 | - "Query vector for the \"%.*s\" column is expected to be of type " | ||
| 6859 | - "%s, but a %s vector was provided.", | ||
| 6860 | - vector_column->name_length, vector_column->name, | ||
| 6861 | - vector_subtype_name(vector_column->element_type), | ||
| 6862 | - vector_subtype_name(elementType)); | ||
| 6863 | - rc = SQLITE_ERROR; | ||
| 6864 | - goto cleanup; | ||
| 6865 | - } | ||
| 6866 | - if (dimensions != vector_column->dimensions) { | ||
| 6867 | - vtab_set_error( | ||
| 6868 | - &p->base, | ||
| 6869 | - "Dimension mismatch for query vector for the \"%.*s\" column. " | ||
| 6870 | - "Expected %d dimensions but received %d.", | ||
| 6871 | - vector_column->name_length, vector_column->name, | ||
| 6872 | - vector_column->dimensions, dimensions); | ||
| 6873 | - rc = SQLITE_ERROR; | ||
| 6874 | - goto cleanup; | ||
| 6875 | - } | ||
| 6876 | - | ||
| 6877 | - i64 k = sqlite3_value_int64(argv[k_idx]); | ||
| 6878 | - if (k < 0) { | ||
| 6879 | - vtab_set_error( | ||
| 6880 | - &p->base, "k value in knn queries must be greater than or equal to 0."); | ||
| 6881 | - rc = SQLITE_ERROR; | ||
| 6882 | - goto cleanup; | ||
| 6883 | - } | ||
| 6884 | -#define SQLITE_VEC_VEC0_K_MAX 4096 | ||
| 6885 | - if (k > SQLITE_VEC_VEC0_K_MAX) { | ||
| 6886 | - vtab_set_error( | ||
| 6887 | - &p->base, | ||
| 6888 | - "k value in knn query too large, provided %lld and the limit is %lld", | ||
| 6889 | - k, SQLITE_VEC_VEC0_K_MAX); | ||
| 6890 | - rc = SQLITE_ERROR; | ||
| 6891 | - goto cleanup; | ||
| 6892 | - } | ||
| 6893 | - | ||
| 6894 | - if (k == 0) { | ||
| 6895 | - knn_data->k = 0; | ||
| 6896 | - pCur->knn_data = knn_data; | ||
| 6897 | - pCur->query_plan = VEC0_QUERY_PLAN_KNN; | ||
| 6898 | - rc = SQLITE_OK; | ||
| 6899 | - goto cleanup; | ||
| 6900 | - } | ||
| 6901 | - | ||
| 6902 | -// handle when a `rowid in (...)` operation was provided | ||
| 6903 | -// Array of all the rowids that appear in any `rowid in (...)` constraint. | ||
| 6904 | -// NULL if none were provided, which means a "full" scan. | ||
| 6905 | -#if COMPILER_SUPPORTS_VTAB_IN | ||
| 6906 | - if (rowid_in_idx >= 0) { | ||
| 6907 | - sqlite3_value *item; | ||
| 6908 | - int rc; | ||
| 6909 | - arrayRowidsIn = sqlite3_malloc(sizeof(*arrayRowidsIn)); | ||
| 6910 | - if (!arrayRowidsIn) { | ||
| 6911 | - rc = SQLITE_NOMEM; | ||
| 6912 | - goto cleanup; | ||
| 6913 | - } | ||
| 6914 | - memset(arrayRowidsIn, 0, sizeof(*arrayRowidsIn)); | ||
| 6915 | - | ||
| 6916 | - rc = array_init(arrayRowidsIn, sizeof(i64), 32); | ||
| 6917 | - if (rc != SQLITE_OK) { | ||
| 6918 | - goto cleanup; | ||
| 6919 | - } | ||
| 6920 | - for (rc = sqlite3_vtab_in_first(argv[rowid_in_idx], &item); rc == SQLITE_OK && item; | ||
| 6921 | - rc = sqlite3_vtab_in_next(argv[rowid_in_idx], &item)) { | ||
| 6922 | - i64 rowid; | ||
| 6923 | - if (p->pkIsText) { | ||
| 6924 | - rc = vec0_rowid_from_id(p, item, &rowid); | ||
| 6925 | - if (rc != SQLITE_OK) { | ||
| 6926 | - goto cleanup; | ||
| 6927 | - } | ||
| 6928 | - } else { | ||
| 6929 | - rowid = sqlite3_value_int64(item); | ||
| 6930 | - } | ||
| 6931 | - rc = array_append(arrayRowidsIn, &rowid); | ||
| 6932 | - if (rc != SQLITE_OK) { | ||
| 6933 | - goto cleanup; | ||
| 6934 | - } | ||
| 6935 | - } | ||
| 6936 | - if (rc != SQLITE_DONE) { | ||
| 6937 | - vtab_set_error(&p->base, "error processing rowid in (...) array"); | ||
| 6938 | - goto cleanup; | ||
| 6939 | - } | ||
| 6940 | - qsort(arrayRowidsIn->z, arrayRowidsIn->length, arrayRowidsIn->element_size, | ||
| 6941 | - _cmp); | ||
| 6942 | - } | ||
| 6943 | -#endif | ||
| 6944 | - | ||
| 6945 | - #if COMPILER_SUPPORTS_VTAB_IN | ||
| 6946 | - for(int i = 0; i < argc; i++) { | ||
| 6947 | - if(!(idxStr[1 + (i*4)] == VEC0_IDXSTR_KIND_METADATA_CONSTRAINT && idxStr[1 + (i*4) + 2] == VEC0_METADATA_OPERATOR_IN)) { | ||
| 6948 | - continue; | ||
| 6949 | - } | ||
| 6950 | - int metadata_idx = idxStr[1 + (i*4) + 1] - 'A'; | ||
| 6951 | - if(!aMetadataIn) { | ||
| 6952 | - aMetadataIn = sqlite3_malloc(sizeof(*aMetadataIn)); | ||
| 6953 | - if(!aMetadataIn) { | ||
| 6954 | - rc = SQLITE_NOMEM; | ||
| 6955 | - goto cleanup; | ||
| 6956 | - } | ||
| 6957 | - memset(aMetadataIn, 0, sizeof(*aMetadataIn)); | ||
| 6958 | - rc = array_init(aMetadataIn, sizeof(struct Vec0MetadataIn), 8); | ||
| 6959 | - if(rc != SQLITE_OK) { | ||
| 6960 | - goto cleanup; | ||
| 6961 | - } | ||
| 6962 | - } | ||
| 6963 | - | ||
| 6964 | - struct Vec0MetadataIn item; | ||
| 6965 | - memset(&item, 0, sizeof(item)); | ||
| 6966 | - item.metadata_idx=metadata_idx; | ||
| 6967 | - item.argv_idx = i; | ||
| 6968 | - | ||
| 6969 | - switch(p->metadata_columns[metadata_idx].kind) { | ||
| 6970 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | ||
| 6971 | - rc = array_init(&item.array, sizeof(i64), 16); | ||
| 6972 | - if(rc != SQLITE_OK) { | ||
| 6973 | - goto cleanup; | ||
| 6974 | - } | ||
| 6975 | - sqlite3_value *entry; | ||
| 6976 | - for (rc = sqlite3_vtab_in_first(argv[i], &entry); rc == SQLITE_OK && entry; rc = sqlite3_vtab_in_next(argv[i], &entry)) { | ||
| 6977 | - i64 v = sqlite3_value_int64(entry); | ||
| 6978 | - rc = array_append(&item.array, &v); | ||
| 6979 | - if (rc != SQLITE_OK) { | ||
| 6980 | - goto cleanup; | ||
| 6981 | - } | ||
| 6982 | - } | ||
| 6983 | - | ||
| 6984 | - if (rc != SQLITE_DONE) { | ||
| 6985 | - vtab_set_error(&p->base, "Error fetching next value in `x in (...)` integer expression"); | ||
| 6986 | - goto cleanup; | ||
| 6987 | - } | ||
| 6988 | - | ||
| 6989 | - break; | ||
| 6990 | - } | ||
| 6991 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | ||
| 6992 | - rc = array_init(&item.array, sizeof(struct Vec0MetadataInTextEntry), 16); | ||
| 6993 | - if(rc != SQLITE_OK) { | ||
| 6994 | - goto cleanup; | ||
| 6995 | - } | ||
| 6996 | - sqlite3_value *entry; | ||
| 6997 | - for (rc = sqlite3_vtab_in_first(argv[i], &entry); rc == SQLITE_OK && entry; rc = sqlite3_vtab_in_next(argv[i], &entry)) { | ||
| 6998 | - const char * s = (const char *) sqlite3_value_text(entry); | ||
| 6999 | - int n = sqlite3_value_bytes(entry); | ||
| 7000 | - | ||
| 7001 | - struct Vec0MetadataInTextEntry entry; | ||
| 7002 | - entry.zString = sqlite3_mprintf("%.*s", n, s); | ||
| 7003 | - if(!entry.zString) { | ||
| 7004 | - rc = SQLITE_NOMEM; | ||
| 7005 | - goto cleanup; | ||
| 7006 | - } | ||
| 7007 | - entry.n = n; | ||
| 7008 | - rc = array_append(&item.array, &entry); | ||
| 7009 | - if (rc != SQLITE_OK) { | ||
| 7010 | - goto cleanup; | ||
| 7011 | - } | ||
| 7012 | - } | ||
| 7013 | - | ||
| 7014 | - if (rc != SQLITE_DONE) { | ||
| 7015 | - vtab_set_error(&p->base, "Error fetching next value in `x in (...)` text expression"); | ||
| 7016 | - goto cleanup; | ||
| 7017 | - } | ||
| 7018 | - | ||
| 7019 | - break; | ||
| 7020 | - } | ||
| 7021 | - default: { | ||
| 7022 | - vtab_set_error(&p->base, "Internal sqlite-vec error"); | ||
| 7023 | - goto cleanup; | ||
| 7024 | - } | ||
| 7025 | - } | ||
| 7026 | - | ||
| 7027 | - rc = array_append(aMetadataIn, &item); | ||
| 7028 | - if(rc != SQLITE_OK) { | ||
| 7029 | - goto cleanup; | ||
| 7030 | - } | ||
| 7031 | - } | ||
| 7032 | - #endif | ||
| 7033 | - | ||
| 7034 | - rc = vec0_chunks_iter(p, idxStr, argc, argv, &stmtChunks); | ||
| 7035 | - if (rc != SQLITE_OK) { | ||
| 7036 | - // IMP: V06942_23781 | ||
| 7037 | - vtab_set_error(&p->base, "Error preparing stmtChunk: %s", | ||
| 7038 | - sqlite3_errmsg(p->db)); | ||
| 7039 | - goto cleanup; | ||
| 7040 | - } | ||
| 7041 | - | ||
| 7042 | - i64 *topk_rowids = NULL; | ||
| 7043 | - f32 *topk_distances = NULL; | ||
| 7044 | - i64 k_used = 0; | ||
| 7045 | - rc = vec0Filter_knn_chunks_iter(p, stmtChunks, vector_column, vectorColumnIdx, | ||
| 7046 | - arrayRowidsIn, aMetadataIn, idxStr, argc, argv, queryVector, k, &topk_rowids, | ||
| 7047 | - &topk_distances, &k_used); | ||
| 7048 | - if (rc != SQLITE_OK) { | ||
| 7049 | - goto cleanup; | ||
| 7050 | - } | ||
| 7051 | - | ||
| 7052 | - knn_data->current_idx = 0; | ||
| 7053 | - knn_data->k = k; | ||
| 7054 | - knn_data->rowids = topk_rowids; | ||
| 7055 | - knn_data->distances = topk_distances; | ||
| 7056 | - knn_data->k_used = k_used; | ||
| 7057 | - | ||
| 7058 | - pCur->knn_data = knn_data; | ||
| 7059 | - pCur->query_plan = VEC0_QUERY_PLAN_KNN; | ||
| 7060 | - rc = SQLITE_OK; | ||
| 7061 | - | ||
| 7062 | -cleanup: | ||
| 7063 | - sqlite3_finalize(stmtChunks); | ||
| 7064 | - array_cleanup(arrayRowidsIn); | ||
| 7065 | - sqlite3_free(arrayRowidsIn); | ||
| 7066 | - queryVectorCleanup(queryVector); | ||
| 7067 | - if(aMetadataIn) { | ||
| 7068 | - for(size_t i = 0; i < aMetadataIn->length; i++) { | ||
| 7069 | - struct Vec0MetadataIn* item = &((struct Vec0MetadataIn *) aMetadataIn->z)[i]; | ||
| 7070 | - for(size_t j = 0; j < item->array.length; j++) { | ||
| 7071 | - if(p->metadata_columns[item->metadata_idx].kind == VEC0_METADATA_COLUMN_KIND_TEXT) { | ||
| 7072 | - struct Vec0MetadataInTextEntry entry = ((struct Vec0MetadataInTextEntry*)item->array.z)[j]; | ||
| 7073 | - sqlite3_free(entry.zString); | ||
| 7074 | - } | ||
| 7075 | - } | ||
| 7076 | - array_cleanup(&item->array); | ||
| 7077 | - } | ||
| 7078 | - array_cleanup(aMetadataIn); | ||
| 7079 | - } | ||
| 7080 | - | ||
| 7081 | - sqlite3_free(aMetadataIn); | ||
| 7082 | - | ||
| 7083 | - return rc; | ||
| 7084 | -} | ||
| 7085 | - | ||
| 7086 | -int vec0Filter_fullscan(vec0_vtab *p, vec0_cursor *pCur) { | ||
| 7087 | - int rc; | ||
| 7088 | - char *zSql; | ||
| 7089 | - struct vec0_query_fullscan_data *fullscan_data; | ||
| 7090 | - | ||
| 7091 | - fullscan_data = sqlite3_malloc(sizeof(*fullscan_data)); | ||
| 7092 | - if (!fullscan_data) { | ||
| 7093 | - return SQLITE_NOMEM; | ||
| 7094 | - } | ||
| 7095 | - memset(fullscan_data, 0, sizeof(*fullscan_data)); | ||
| 7096 | - | ||
| 7097 | - zSql = sqlite3_mprintf(" SELECT rowid " | ||
| 7098 | - " FROM " VEC0_SHADOW_ROWIDS_NAME | ||
| 7099 | - " ORDER by chunk_id, chunk_offset ", | ||
| 7100 | - p->schemaName, p->tableName); | ||
| 7101 | - if (!zSql) { | ||
| 7102 | - rc = SQLITE_NOMEM; | ||
| 7103 | - goto error; | ||
| 7104 | - } | ||
| 7105 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &fullscan_data->rowids_stmt, NULL); | ||
| 7106 | - sqlite3_free(zSql); | ||
| 7107 | - if (rc != SQLITE_OK) { | ||
| 7108 | - // IMP: V09901_26739 | ||
| 7109 | - vtab_set_error(&p->base, "Error preparing rowid scan: %s", | ||
| 7110 | - sqlite3_errmsg(p->db)); | ||
| 7111 | - goto error; | ||
| 7112 | - } | ||
| 7113 | - | ||
| 7114 | - rc = sqlite3_step(fullscan_data->rowids_stmt); | ||
| 7115 | - | ||
| 7116 | - // DONE when there's no rowids, ROW when there are, both "success" | ||
| 7117 | - if (!(rc == SQLITE_ROW || rc == SQLITE_DONE)) { | ||
| 7118 | - goto error; | ||
| 7119 | - } | ||
| 7120 | - | ||
| 7121 | - fullscan_data->done = rc == SQLITE_DONE; | ||
| 7122 | - pCur->query_plan = VEC0_QUERY_PLAN_FULLSCAN; | ||
| 7123 | - pCur->fullscan_data = fullscan_data; | ||
| 7124 | - return SQLITE_OK; | ||
| 7125 | - | ||
| 7126 | -error: | ||
| 7127 | - vec0_query_fullscan_data_clear(fullscan_data); | ||
| 7128 | - sqlite3_free(fullscan_data); | ||
| 7129 | - return rc; | ||
| 7130 | -} | ||
| 7131 | - | ||
| 7132 | -int vec0Filter_point(vec0_cursor *pCur, vec0_vtab *p, int argc, | ||
| 7133 | - sqlite3_value **argv) { | ||
| 7134 | - int rc; | ||
| 7135 | - assert(argc == 1); | ||
| 7136 | - i64 rowid; | ||
| 7137 | - struct vec0_query_point_data *point_data = NULL; | ||
| 7138 | - | ||
| 7139 | - point_data = sqlite3_malloc(sizeof(*point_data)); | ||
| 7140 | - if (!point_data) { | ||
| 7141 | - rc = SQLITE_NOMEM; | ||
| 7142 | - goto error; | ||
| 7143 | - } | ||
| 7144 | - memset(point_data, 0, sizeof(*point_data)); | ||
| 7145 | - | ||
| 7146 | - if (p->pkIsText) { | ||
| 7147 | - rc = vec0_rowid_from_id(p, argv[0], &rowid); | ||
| 7148 | - if (rc == SQLITE_EMPTY) { | ||
| 7149 | - goto eof; | ||
| 7150 | - } | ||
| 7151 | - if (rc != SQLITE_OK) { | ||
| 7152 | - goto error; | ||
| 7153 | - } | ||
| 7154 | - } else { | ||
| 7155 | - rowid = sqlite3_value_int64(argv[0]); | ||
| 7156 | - } | ||
| 7157 | - | ||
| 7158 | - for (int i = 0; i < p->numVectorColumns; i++) { | ||
| 7159 | - rc = vec0_get_vector_data(p, rowid, i, &point_data->vectors[i], NULL); | ||
| 7160 | - if (rc == SQLITE_EMPTY) { | ||
| 7161 | - goto eof; | ||
| 7162 | - } | ||
| 7163 | - if (rc != SQLITE_OK) { | ||
| 7164 | - goto error; | ||
| 7165 | - } | ||
| 7166 | - } | ||
| 7167 | - | ||
| 7168 | - point_data->rowid = rowid; | ||
| 7169 | - point_data->done = 0; | ||
| 7170 | - pCur->point_data = point_data; | ||
| 7171 | - pCur->query_plan = VEC0_QUERY_PLAN_POINT; | ||
| 7172 | - return SQLITE_OK; | ||
| 7173 | - | ||
| 7174 | -eof: | ||
| 7175 | - point_data->rowid = rowid; | ||
| 7176 | - point_data->done = 1; | ||
| 7177 | - pCur->point_data = point_data; | ||
| 7178 | - pCur->query_plan = VEC0_QUERY_PLAN_POINT; | ||
| 7179 | - return SQLITE_OK; | ||
| 7180 | - | ||
| 7181 | -error: | ||
| 7182 | - vec0_query_point_data_clear(point_data); | ||
| 7183 | - sqlite3_free(point_data); | ||
| 7184 | - return rc; | ||
| 7185 | -} | ||
| 7186 | - | ||
| 7187 | -static int vec0Filter(sqlite3_vtab_cursor *pVtabCursor, int idxNum, | ||
| 7188 | - const char *idxStr, int argc, sqlite3_value **argv) { | ||
| 7189 | - vec0_vtab *p = (vec0_vtab *)pVtabCursor->pVtab; | ||
| 7190 | - vec0_cursor *pCur = (vec0_cursor *)pVtabCursor; | ||
| 7191 | - vec0_cursor_clear(pCur); | ||
| 7192 | - | ||
| 7193 | - int idxStrLength = strlen(idxStr); | ||
| 7194 | - if(idxStrLength <= 0) { | ||
| 7195 | - return SQLITE_ERROR; | ||
| 7196 | - } | ||
| 7197 | - if((idxStrLength-1) % 4 != 0) { | ||
| 7198 | - return SQLITE_ERROR; | ||
| 7199 | - } | ||
| 7200 | - int numValueEntries = (idxStrLength-1) / 4; | ||
| 7201 | - if(numValueEntries != argc) { | ||
| 7202 | - return SQLITE_ERROR; | ||
| 7203 | - } | ||
| 7204 | - | ||
| 7205 | - char query_plan = idxStr[0]; | ||
| 7206 | - switch(query_plan) { | ||
| 7207 | - case VEC0_QUERY_PLAN_FULLSCAN: | ||
| 7208 | - return vec0Filter_fullscan(p, pCur); | ||
| 7209 | - case VEC0_QUERY_PLAN_KNN: | ||
| 7210 | - return vec0Filter_knn(pCur, p, idxNum, idxStr, argc, argv); | ||
| 7211 | - case VEC0_QUERY_PLAN_POINT: | ||
| 7212 | - return vec0Filter_point(pCur, p, argc, argv); | ||
| 7213 | - default: | ||
| 7214 | - vtab_set_error(pVtabCursor->pVtab, "unknown idxStr '%s'", idxStr); | ||
| 7215 | - return SQLITE_ERROR; | ||
| 7216 | - } | ||
| 7217 | -} | ||
| 7218 | - | ||
| 7219 | -static int vec0Rowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid) { | ||
| 7220 | - vec0_cursor *pCur = (vec0_cursor *)cur; | ||
| 7221 | - switch (pCur->query_plan) { | ||
| 7222 | - case VEC0_QUERY_PLAN_FULLSCAN: { | ||
| 7223 | - *pRowid = sqlite3_column_int64(pCur->fullscan_data->rowids_stmt, 0); | ||
| 7224 | - return SQLITE_OK; | ||
| 7225 | - } | ||
| 7226 | - case VEC0_QUERY_PLAN_POINT: { | ||
| 7227 | - *pRowid = pCur->point_data->rowid; | ||
| 7228 | - return SQLITE_OK; | ||
| 7229 | - } | ||
| 7230 | - case VEC0_QUERY_PLAN_KNN: { | ||
| 7231 | - vtab_set_error(cur->pVtab, | ||
| 7232 | - "Internal sqlite-vec error: expected point query plan in " | ||
| 7233 | - "vec0Rowid, found %d", | ||
| 7234 | - pCur->query_plan); | ||
| 7235 | - return SQLITE_ERROR; | ||
| 7236 | - } | ||
| 7237 | - } | ||
| 7238 | - return SQLITE_ERROR; | ||
| 7239 | -} | ||
| 7240 | - | ||
| 7241 | -static int vec0Next(sqlite3_vtab_cursor *cur) { | ||
| 7242 | - vec0_cursor *pCur = (vec0_cursor *)cur; | ||
| 7243 | - switch (pCur->query_plan) { | ||
| 7244 | - case VEC0_QUERY_PLAN_FULLSCAN: { | ||
| 7245 | - if (!pCur->fullscan_data) { | ||
| 7246 | - return SQLITE_ERROR; | ||
| 7247 | - } | ||
| 7248 | - int rc = sqlite3_step(pCur->fullscan_data->rowids_stmt); | ||
| 7249 | - if (rc == SQLITE_DONE) { | ||
| 7250 | - pCur->fullscan_data->done = 1; | ||
| 7251 | - return SQLITE_OK; | ||
| 7252 | - } | ||
| 7253 | - if (rc == SQLITE_ROW) { | ||
| 7254 | - return SQLITE_OK; | ||
| 7255 | - } | ||
| 7256 | - return SQLITE_ERROR; | ||
| 7257 | - } | ||
| 7258 | - case VEC0_QUERY_PLAN_KNN: { | ||
| 7259 | - if (!pCur->knn_data) { | ||
| 7260 | - return SQLITE_ERROR; | ||
| 7261 | - } | ||
| 7262 | - | ||
| 7263 | - pCur->knn_data->current_idx++; | ||
| 7264 | - return SQLITE_OK; | ||
| 7265 | - } | ||
| 7266 | - case VEC0_QUERY_PLAN_POINT: { | ||
| 7267 | - if (!pCur->point_data) { | ||
| 7268 | - return SQLITE_ERROR; | ||
| 7269 | - } | ||
| 7270 | - pCur->point_data->done = 1; | ||
| 7271 | - return SQLITE_OK; | ||
| 7272 | - } | ||
| 7273 | - } | ||
| 7274 | - return SQLITE_ERROR; | ||
| 7275 | -} | ||
| 7276 | - | ||
| 7277 | -static int vec0Eof(sqlite3_vtab_cursor *cur) { | ||
| 7278 | - vec0_cursor *pCur = (vec0_cursor *)cur; | ||
| 7279 | - switch (pCur->query_plan) { | ||
| 7280 | - case VEC0_QUERY_PLAN_FULLSCAN: { | ||
| 7281 | - if (!pCur->fullscan_data) { | ||
| 7282 | - return 1; | ||
| 7283 | - } | ||
| 7284 | - return pCur->fullscan_data->done; | ||
| 7285 | - } | ||
| 7286 | - case VEC0_QUERY_PLAN_KNN: { | ||
| 7287 | - if (!pCur->knn_data) { | ||
| 7288 | - return 1; | ||
| 7289 | - } | ||
| 7290 | - // return (pCur->knn_data->current_idx >= pCur->knn_data->k) || | ||
| 7291 | - // (pCur->knn_data->distances[pCur->knn_data->current_idx] == FLT_MAX); | ||
| 7292 | - return (pCur->knn_data->current_idx >= pCur->knn_data->k_used); | ||
| 7293 | - } | ||
| 7294 | - case VEC0_QUERY_PLAN_POINT: { | ||
| 7295 | - if (!pCur->point_data) { | ||
| 7296 | - return 1; | ||
| 7297 | - } | ||
| 7298 | - return pCur->point_data->done; | ||
| 7299 | - } | ||
| 7300 | - } | ||
| 7301 | - return 1; | ||
| 7302 | -} | ||
| 7303 | - | ||
| 7304 | -static int vec0Column_fullscan(vec0_vtab *pVtab, vec0_cursor *pCur, | ||
| 7305 | - sqlite3_context *context, int i) { | ||
| 7306 | - if (!pCur->fullscan_data) { | ||
| 7307 | - sqlite3_result_error( | ||
| 7308 | - context, "Internal sqlite-vec error: fullscan_data is NULL.", -1); | ||
| 7309 | - return SQLITE_ERROR; | ||
| 7310 | - } | ||
| 7311 | - i64 rowid = sqlite3_column_int64(pCur->fullscan_data->rowids_stmt, 0); | ||
| 7312 | - if (i == VEC0_COLUMN_ID) { | ||
| 7313 | - return vec0_result_id(pVtab, context, rowid); | ||
| 7314 | - } | ||
| 7315 | - else if (vec0_column_idx_is_vector(pVtab, i)) { | ||
| 7316 | - void *v; | ||
| 7317 | - int sz; | ||
| 7318 | - int vector_idx = vec0_column_idx_to_vector_idx(pVtab, i); | ||
| 7319 | - int rc = vec0_get_vector_data(pVtab, rowid, vector_idx, &v, &sz); | ||
| 7320 | - if (rc != SQLITE_OK) { | ||
| 7321 | - return rc; | ||
| 7322 | - } | ||
| 7323 | - sqlite3_result_blob(context, v, sz, sqlite3_free); | ||
| 7324 | - sqlite3_result_subtype(context, | ||
| 7325 | - pVtab->vector_columns[vector_idx].element_type); | ||
| 7326 | - | ||
| 7327 | - } | ||
| 7328 | - else if (i == vec0_column_distance_idx(pVtab)) { | ||
| 7329 | - sqlite3_result_null(context); | ||
| 7330 | - } | ||
| 7331 | - else if(vec0_column_idx_is_partition(pVtab, i)) { | ||
| 7332 | - int partition_idx = vec0_column_idx_to_partition_idx(pVtab, i); | ||
| 7333 | - sqlite3_value * v; | ||
| 7334 | - int rc = vec0_get_partition_value_for_rowid(pVtab, rowid, partition_idx, &v); | ||
| 7335 | - if(rc == SQLITE_OK) { | ||
| 7336 | - sqlite3_result_value(context, v); | ||
| 7337 | - sqlite3_value_free(v); | ||
| 7338 | - }else { | ||
| 7339 | - sqlite3_result_error_code(context, rc); | ||
| 7340 | - } | ||
| 7341 | - } | ||
| 7342 | - else if(vec0_column_idx_is_auxiliary(pVtab, i)) { | ||
| 7343 | - int auxiliary_idx = vec0_column_idx_to_auxiliary_idx(pVtab, i); | ||
| 7344 | - sqlite3_value * v; | ||
| 7345 | - int rc = vec0_get_auxiliary_value_for_rowid(pVtab, rowid, auxiliary_idx, &v); | ||
| 7346 | - if(rc == SQLITE_OK) { | ||
| 7347 | - sqlite3_result_value(context, v); | ||
| 7348 | - sqlite3_value_free(v); | ||
| 7349 | - }else { | ||
| 7350 | - sqlite3_result_error_code(context, rc); | ||
| 7351 | - } | ||
| 7352 | - } | ||
| 7353 | - | ||
| 7354 | - else if(vec0_column_idx_is_metadata(pVtab, i)) { | ||
| 7355 | - if(sqlite3_vtab_nochange(context)) { | ||
| 7356 | - return SQLITE_OK; | ||
| 7357 | - } | ||
| 7358 | - int metadata_idx = vec0_column_idx_to_metadata_idx(pVtab, i); | ||
| 7359 | - int rc = vec0_result_metadata_value_for_rowid(pVtab, rowid, metadata_idx, context); | ||
| 7360 | - if(rc != SQLITE_OK) { | ||
| 7361 | - // IMP: V15466_32305 | ||
| 7362 | - const char * zErr = sqlite3_mprintf( | ||
| 7363 | - "Could not extract metadata value for column %.*s at rowid %lld", | ||
| 7364 | - pVtab->metadata_columns[metadata_idx].name_length, | ||
| 7365 | - pVtab->metadata_columns[metadata_idx].name, rowid | ||
| 7366 | - ); | ||
| 7367 | - if(zErr) { | ||
| 7368 | - sqlite3_result_error(context, zErr, -1); | ||
| 7369 | - sqlite3_free((void *) zErr); | ||
| 7370 | - }else { | ||
| 7371 | - sqlite3_result_error_nomem(context); | ||
| 7372 | - } | ||
| 7373 | - } | ||
| 7374 | - } | ||
| 7375 | - | ||
| 7376 | - return SQLITE_OK; | ||
| 7377 | -} | ||
| 7378 | - | ||
| 7379 | -static int vec0Column_point(vec0_vtab *pVtab, vec0_cursor *pCur, | ||
| 7380 | - sqlite3_context *context, int i) { | ||
| 7381 | - if (!pCur->point_data) { | ||
| 7382 | - sqlite3_result_error(context, | ||
| 7383 | - "Internal sqlite-vec error: point_data is NULL.", -1); | ||
| 7384 | - return SQLITE_ERROR; | ||
| 7385 | - } | ||
| 7386 | - if (i == VEC0_COLUMN_ID) { | ||
| 7387 | - return vec0_result_id(pVtab, context, pCur->point_data->rowid); | ||
| 7388 | - } | ||
| 7389 | - else if (i == vec0_column_distance_idx(pVtab)) { | ||
| 7390 | - sqlite3_result_null(context); | ||
| 7391 | - return SQLITE_OK; | ||
| 7392 | - } | ||
| 7393 | - else if (vec0_column_idx_is_vector(pVtab, i)) { | ||
| 7394 | - if (sqlite3_vtab_nochange(context)) { | ||
| 7395 | - sqlite3_result_null(context); | ||
| 7396 | - return SQLITE_OK; | ||
| 7397 | - } | ||
| 7398 | - int vector_idx = vec0_column_idx_to_vector_idx(pVtab, i); | ||
| 7399 | - sqlite3_result_blob( | ||
| 7400 | - context, pCur->point_data->vectors[vector_idx], | ||
| 7401 | - vector_column_byte_size(pVtab->vector_columns[vector_idx]), | ||
| 7402 | - SQLITE_TRANSIENT); | ||
| 7403 | - sqlite3_result_subtype(context, | ||
| 7404 | - pVtab->vector_columns[vector_idx].element_type); | ||
| 7405 | - return SQLITE_OK; | ||
| 7406 | - } | ||
| 7407 | - else if(vec0_column_idx_is_partition(pVtab, i)) { | ||
| 7408 | - if(sqlite3_vtab_nochange(context)) { | ||
| 7409 | - return SQLITE_OK; | ||
| 7410 | - } | ||
| 7411 | - int partition_idx = vec0_column_idx_to_partition_idx(pVtab, i); | ||
| 7412 | - i64 rowid = pCur->point_data->rowid; | ||
| 7413 | - sqlite3_value * v; | ||
| 7414 | - int rc = vec0_get_partition_value_for_rowid(pVtab, rowid, partition_idx, &v); | ||
| 7415 | - if(rc == SQLITE_OK) { | ||
| 7416 | - sqlite3_result_value(context, v); | ||
| 7417 | - sqlite3_value_free(v); | ||
| 7418 | - }else { | ||
| 7419 | - sqlite3_result_error_code(context, rc); | ||
| 7420 | - } | ||
| 7421 | - } | ||
| 7422 | - else if(vec0_column_idx_is_auxiliary(pVtab, i)) { | ||
| 7423 | - if(sqlite3_vtab_nochange(context)) { | ||
| 7424 | - return SQLITE_OK; | ||
| 7425 | - } | ||
| 7426 | - i64 rowid = pCur->point_data->rowid; | ||
| 7427 | - int auxiliary_idx = vec0_column_idx_to_auxiliary_idx(pVtab, i); | ||
| 7428 | - sqlite3_value * v; | ||
| 7429 | - int rc = vec0_get_auxiliary_value_for_rowid(pVtab, rowid, auxiliary_idx, &v); | ||
| 7430 | - if(rc == SQLITE_OK) { | ||
| 7431 | - sqlite3_result_value(context, v); | ||
| 7432 | - sqlite3_value_free(v); | ||
| 7433 | - }else { | ||
| 7434 | - sqlite3_result_error_code(context, rc); | ||
| 7435 | - } | ||
| 7436 | - } | ||
| 7437 | - | ||
| 7438 | - else if(vec0_column_idx_is_metadata(pVtab, i)) { | ||
| 7439 | - if(sqlite3_vtab_nochange(context)) { | ||
| 7440 | - return SQLITE_OK; | ||
| 7441 | - } | ||
| 7442 | - i64 rowid = pCur->point_data->rowid; | ||
| 7443 | - int metadata_idx = vec0_column_idx_to_metadata_idx(pVtab, i); | ||
| 7444 | - int rc = vec0_result_metadata_value_for_rowid(pVtab, rowid, metadata_idx, context); | ||
| 7445 | - if(rc != SQLITE_OK) { | ||
| 7446 | - const char * zErr = sqlite3_mprintf( | ||
| 7447 | - "Could not extract metadata value for column %.*s at rowid %lld", | ||
| 7448 | - pVtab->metadata_columns[metadata_idx].name_length, | ||
| 7449 | - pVtab->metadata_columns[metadata_idx].name, rowid | ||
| 7450 | - ); | ||
| 7451 | - if(zErr) { | ||
| 7452 | - sqlite3_result_error(context, zErr, -1); | ||
| 7453 | - sqlite3_free((void *) zErr); | ||
| 7454 | - }else { | ||
| 7455 | - sqlite3_result_error_nomem(context); | ||
| 7456 | - } | ||
| 7457 | - } | ||
| 7458 | - } | ||
| 7459 | - | ||
| 7460 | - return SQLITE_OK; | ||
| 7461 | -} | ||
| 7462 | - | ||
| 7463 | -static int vec0Column_knn(vec0_vtab *pVtab, vec0_cursor *pCur, | ||
| 7464 | - sqlite3_context *context, int i) { | ||
| 7465 | - if (!pCur->knn_data) { | ||
| 7466 | - sqlite3_result_error(context, | ||
| 7467 | - "Internal sqlite-vec error: knn_data is NULL.", -1); | ||
| 7468 | - return SQLITE_ERROR; | ||
| 7469 | - } | ||
| 7470 | - if (i == VEC0_COLUMN_ID) { | ||
| 7471 | - i64 rowid = pCur->knn_data->rowids[pCur->knn_data->current_idx]; | ||
| 7472 | - return vec0_result_id(pVtab, context, rowid); | ||
| 7473 | - } | ||
| 7474 | - else if (i == vec0_column_distance_idx(pVtab)) { | ||
| 7475 | - sqlite3_result_double( | ||
| 7476 | - context, pCur->knn_data->distances[pCur->knn_data->current_idx]); | ||
| 7477 | - return SQLITE_OK; | ||
| 7478 | - } | ||
| 7479 | - else if (vec0_column_idx_is_vector(pVtab, i)) { | ||
| 7480 | - void *out; | ||
| 7481 | - int sz; | ||
| 7482 | - int vector_idx = vec0_column_idx_to_vector_idx(pVtab, i); | ||
| 7483 | - int rc = vec0_get_vector_data( | ||
| 7484 | - pVtab, pCur->knn_data->rowids[pCur->knn_data->current_idx], vector_idx, | ||
| 7485 | - &out, &sz); | ||
| 7486 | - if (rc != SQLITE_OK) { | ||
| 7487 | - return rc; | ||
| 7488 | - } | ||
| 7489 | - sqlite3_result_blob(context, out, sz, sqlite3_free); | ||
| 7490 | - sqlite3_result_subtype(context, | ||
| 7491 | - pVtab->vector_columns[vector_idx].element_type); | ||
| 7492 | - return SQLITE_OK; | ||
| 7493 | - } | ||
| 7494 | - else if(vec0_column_idx_is_partition(pVtab, i)) { | ||
| 7495 | - int partition_idx = vec0_column_idx_to_partition_idx(pVtab, i); | ||
| 7496 | - i64 rowid = pCur->knn_data->rowids[pCur->knn_data->current_idx]; | ||
| 7497 | - sqlite3_value * v; | ||
| 7498 | - int rc = vec0_get_partition_value_for_rowid(pVtab, rowid, partition_idx, &v); | ||
| 7499 | - if(rc == SQLITE_OK) { | ||
| 7500 | - sqlite3_result_value(context, v); | ||
| 7501 | - sqlite3_value_free(v); | ||
| 7502 | - }else { | ||
| 7503 | - sqlite3_result_error_code(context, rc); | ||
| 7504 | - } | ||
| 7505 | - } | ||
| 7506 | - else if(vec0_column_idx_is_auxiliary(pVtab, i)) { | ||
| 7507 | - int auxiliary_idx = vec0_column_idx_to_auxiliary_idx(pVtab, i); | ||
| 7508 | - i64 rowid = pCur->knn_data->rowids[pCur->knn_data->current_idx]; | ||
| 7509 | - sqlite3_value * v; | ||
| 7510 | - int rc = vec0_get_auxiliary_value_for_rowid(pVtab, rowid, auxiliary_idx, &v); | ||
| 7511 | - if(rc == SQLITE_OK) { | ||
| 7512 | - sqlite3_result_value(context, v); | ||
| 7513 | - sqlite3_value_free(v); | ||
| 7514 | - }else { | ||
| 7515 | - sqlite3_result_error_code(context, rc); | ||
| 7516 | - } | ||
| 7517 | - } | ||
| 7518 | - | ||
| 7519 | - else if(vec0_column_idx_is_metadata(pVtab, i)) { | ||
| 7520 | - int metadata_idx = vec0_column_idx_to_metadata_idx(pVtab, i); | ||
| 7521 | - i64 rowid = pCur->knn_data->rowids[pCur->knn_data->current_idx]; | ||
| 7522 | - int rc = vec0_result_metadata_value_for_rowid(pVtab, rowid, metadata_idx, context); | ||
| 7523 | - if(rc != SQLITE_OK) { | ||
| 7524 | - const char * zErr = sqlite3_mprintf( | ||
| 7525 | - "Could not extract metadata value for column %.*s at rowid %lld", | ||
| 7526 | - pVtab->metadata_columns[metadata_idx].name_length, | ||
| 7527 | - pVtab->metadata_columns[metadata_idx].name, rowid | ||
| 7528 | - ); | ||
| 7529 | - if(zErr) { | ||
| 7530 | - sqlite3_result_error(context, zErr, -1); | ||
| 7531 | - sqlite3_free((void *) zErr); | ||
| 7532 | - }else { | ||
| 7533 | - sqlite3_result_error_nomem(context); | ||
| 7534 | - } | ||
| 7535 | - } | ||
| 7536 | - } | ||
| 7537 | - | ||
| 7538 | - return SQLITE_OK; | ||
| 7539 | -} | ||
| 7540 | - | ||
| 7541 | -static int vec0Column(sqlite3_vtab_cursor *cur, sqlite3_context *context, | ||
| 7542 | - int i) { | ||
| 7543 | - vec0_cursor *pCur = (vec0_cursor *)cur; | ||
| 7544 | - vec0_vtab *pVtab = (vec0_vtab *)cur->pVtab; | ||
| 7545 | - switch (pCur->query_plan) { | ||
| 7546 | - case VEC0_QUERY_PLAN_FULLSCAN: { | ||
| 7547 | - return vec0Column_fullscan(pVtab, pCur, context, i); | ||
| 7548 | - } | ||
| 7549 | - case VEC0_QUERY_PLAN_KNN: { | ||
| 7550 | - return vec0Column_knn(pVtab, pCur, context, i); | ||
| 7551 | - } | ||
| 7552 | - case VEC0_QUERY_PLAN_POINT: { | ||
| 7553 | - return vec0Column_point(pVtab, pCur, context, i); | ||
| 7554 | - } | ||
| 7555 | - } | ||
| 7556 | - return SQLITE_OK; | ||
| 7557 | -} | ||
| 7558 | - | ||
| 7559 | -/** | ||
| 7560 | - * @brief Handles the "insert rowid" step of a row insert operation of a vec0 | ||
| 7561 | - * table. | ||
| 7562 | - * | ||
| 7563 | - * This function will insert a new row into the _rowids vec0 shadow table. | ||
| 7564 | - * | ||
| 7565 | - * @param p: virtual table | ||
| 7566 | - * @param idValue: Value containing the inserted rowid/id value. | ||
| 7567 | - * @param rowid: Output rowid, will point to the "real" i64 rowid | ||
| 7568 | - * value that was inserted | ||
| 7569 | - * @return int SQLITE_OK on success, error code on failure | ||
| 7570 | - */ | ||
| 7571 | -int vec0Update_InsertRowidStep(vec0_vtab *p, sqlite3_value *idValue, | ||
| 7572 | - i64 *rowid) { | ||
| 7573 | - | ||
| 7574 | - /** | ||
| 7575 | - * An insert into a vec0 table can happen a few different ways: | ||
| 7576 | - * 1) With default INTEGER primary key: With a supplied i64 rowid | ||
| 7577 | - * 2) With default INTEGER primary key: WITHOUT a supplied rowid | ||
| 7578 | - * 3) With TEXT primary key: supplied text rowid | ||
| 7579 | - */ | ||
| 7580 | - | ||
| 7581 | - int rc; | ||
| 7582 | - | ||
| 7583 | - // Option 3: vtab has a user-defined TEXT primary key, so ensure a text value | ||
| 7584 | - // is provided. | ||
| 7585 | - if (p->pkIsText) { | ||
| 7586 | - if (sqlite3_value_type(idValue) != SQLITE_TEXT) { | ||
| 7587 | - // IMP: V04200_21039 | ||
| 7588 | - vtab_set_error(&p->base, | ||
| 7589 | - "The %s virtual table was declared with a TEXT primary " | ||
| 7590 | - "key, but a non-TEXT value was provided in an INSERT.", | ||
| 7591 | - p->tableName); | ||
| 7592 | - return SQLITE_ERROR; | ||
| 7593 | - } | ||
| 7594 | - | ||
| 7595 | - return vec0_rowids_insert_id(p, idValue, rowid); | ||
| 7596 | - } | ||
| 7597 | - | ||
| 7598 | - // Option 1: User supplied a i64 rowid | ||
| 7599 | - if (sqlite3_value_type(idValue) == SQLITE_INTEGER) { | ||
| 7600 | - i64 suppliedRowid = sqlite3_value_int64(idValue); | ||
| 7601 | - rc = vec0_rowids_insert_rowid(p, suppliedRowid); | ||
| 7602 | - if (rc == SQLITE_OK) { | ||
| 7603 | - *rowid = suppliedRowid; | ||
| 7604 | - } | ||
| 7605 | - return rc; | ||
| 7606 | - } | ||
| 7607 | - | ||
| 7608 | - // Option 2: User did not suppled a rowid | ||
| 7609 | - | ||
| 7610 | - if (sqlite3_value_type(idValue) != SQLITE_NULL) { | ||
| 7611 | - // IMP: V30855_14925 | ||
| 7612 | - vtab_set_error(&p->base, | ||
| 7613 | - "Only integers are allows for primary key values on %s", | ||
| 7614 | - p->tableName); | ||
| 7615 | - return SQLITE_ERROR; | ||
| 7616 | - } | ||
| 7617 | - // NULL to get next auto-incremented value | ||
| 7618 | - return vec0_rowids_insert_id(p, NULL, rowid); | ||
| 7619 | -} | ||
| 7620 | - | ||
| 7621 | -/** | ||
| 7622 | - * @brief Determines the "next available" chunk position for a newly inserted | ||
| 7623 | - * vec0 row. | ||
| 7624 | - * | ||
| 7625 | - * This operation may insert a new "blank" chunk the _chunks table, if there is | ||
| 7626 | - * no more space in previous chunks. | ||
| 7627 | - * | ||
| 7628 | - * @param p: virtual table | ||
| 7629 | - * @param partitionKeyValues: array of partition key column values, to constrain | ||
| 7630 | - * against any partition key columns. | ||
| 7631 | - * @param chunk_rowid: Output rowid of the chunk in the _chunks virtual table | ||
| 7632 | - * that has the avialabiity. | ||
| 7633 | - * @param chunk_offset: Output the index of the available space insert the | ||
| 7634 | - * chunk, based on the index of the first available validity bit. | ||
| 7635 | - * @param pBlobValidity: Output blob of the validity column of the available | ||
| 7636 | - * chunk. Will be opened with read/write permissions. | ||
| 7637 | - * @param pValidity: Output buffer of the original chunk's validity column. | ||
| 7638 | - * Needs to be cleaned up with sqlite3_free(). | ||
| 7639 | - * @return int SQLITE_OK on success, error code on failure | ||
| 7640 | - */ | ||
| 7641 | -int vec0Update_InsertNextAvailableStep( | ||
| 7642 | - vec0_vtab *p, | ||
| 7643 | - sqlite3_value ** partitionKeyValues, | ||
| 7644 | - i64 *chunk_rowid, i64 *chunk_offset, | ||
| 7645 | - sqlite3_blob **blobChunksValidity, | ||
| 7646 | - const unsigned char **bufferChunksValidity) { | ||
| 7647 | - | ||
| 7648 | - int rc; | ||
| 7649 | - i64 validitySize; | ||
| 7650 | - *chunk_offset = -1; | ||
| 7651 | - | ||
| 7652 | - rc = vec0_get_latest_chunk_rowid(p, chunk_rowid, partitionKeyValues); | ||
| 7653 | - if(rc == SQLITE_EMPTY) { | ||
| 7654 | - goto done; | ||
| 7655 | - } | ||
| 7656 | - if (rc != SQLITE_OK) { | ||
| 7657 | - goto cleanup; | ||
| 7658 | - } | ||
| 7659 | - | ||
| 7660 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowChunksName, "validity", | ||
| 7661 | - *chunk_rowid, 1, blobChunksValidity); | ||
| 7662 | - if (rc != SQLITE_OK) { | ||
| 7663 | - // IMP: V22053_06123 | ||
| 7664 | - vtab_set_error(&p->base, | ||
| 7665 | - VEC_INTERAL_ERROR | ||
| 7666 | - "could not open validity blob on %s.%s.%lld", | ||
| 7667 | - p->schemaName, p->shadowChunksName, *chunk_rowid); | ||
| 7668 | - goto cleanup; | ||
| 7669 | - } | ||
| 7670 | - | ||
| 7671 | - validitySize = sqlite3_blob_bytes(*blobChunksValidity); | ||
| 7672 | - if (validitySize != p->chunk_size / CHAR_BIT) { | ||
| 7673 | - // IMP: V29362_13432 | ||
| 7674 | - vtab_set_error(&p->base, | ||
| 7675 | - VEC_INTERAL_ERROR | ||
| 7676 | - "validity blob size mismatch on " | ||
| 7677 | - "%s.%s.%lld, expected %lld but received %lld.", | ||
| 7678 | - p->schemaName, p->shadowChunksName, *chunk_rowid, | ||
| 7679 | - (i64)(p->chunk_size / CHAR_BIT), validitySize); | ||
| 7680 | - rc = SQLITE_ERROR; | ||
| 7681 | - goto cleanup; | ||
| 7682 | - } | ||
| 7683 | - | ||
| 7684 | - *bufferChunksValidity = sqlite3_malloc(validitySize); | ||
| 7685 | - if (!(*bufferChunksValidity)) { | ||
| 7686 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | ||
| 7687 | - "Could not allocate memory for validity bitmap"); | ||
| 7688 | - rc = SQLITE_NOMEM; | ||
| 7689 | - goto cleanup; | ||
| 7690 | - } | ||
| 7691 | - | ||
| 7692 | - rc = sqlite3_blob_read(*blobChunksValidity, (void *)*bufferChunksValidity, | ||
| 7693 | - validitySize, 0); | ||
| 7694 | - | ||
| 7695 | - if (rc != SQLITE_OK) { | ||
| 7696 | - vtab_set_error(&p->base, | ||
| 7697 | - VEC_INTERAL_ERROR | ||
| 7698 | - "Could not read validity bitmap for %s.%s.%lld", | ||
| 7699 | - p->schemaName, p->shadowChunksName, *chunk_rowid); | ||
| 7700 | - goto cleanup; | ||
| 7701 | - } | ||
| 7702 | - | ||
| 7703 | - // find the next available offset, ie first `0` in the bitmap. | ||
| 7704 | - for (int i = 0; i < validitySize; i++) { | ||
| 7705 | - if ((*bufferChunksValidity)[i] == 0b11111111) | ||
| 7706 | - continue; | ||
| 7707 | - for (int j = 0; j < CHAR_BIT; j++) { | ||
| 7708 | - if (((((*bufferChunksValidity)[i] >> j) & 1) == 0)) { | ||
| 7709 | - *chunk_offset = (i * CHAR_BIT) + j; | ||
| 7710 | - goto done; | ||
| 7711 | - } | ||
| 7712 | - } | ||
| 7713 | - } | ||
| 7714 | - | ||
| 7715 | -done: | ||
| 7716 | - // latest chunk was full, so need to create a new one | ||
| 7717 | - if (*chunk_offset == -1) { | ||
| 7718 | - rc = vec0_new_chunk(p, partitionKeyValues, chunk_rowid); | ||
| 7719 | - if (rc != SQLITE_OK) { | ||
| 7720 | - // IMP: V08441_25279 | ||
| 7721 | - vtab_set_error(&p->base, | ||
| 7722 | - VEC_INTERAL_ERROR "Could not insert a new vector chunk"); | ||
| 7723 | - rc = SQLITE_ERROR; // otherwise raises a DatabaseError and not operational | ||
| 7724 | - // error? | ||
| 7725 | - goto cleanup; | ||
| 7726 | - } | ||
| 7727 | - *chunk_offset = 0; | ||
| 7728 | - | ||
| 7729 | - // blobChunksValidity and pValidity are stale, pointing to the previous | ||
| 7730 | - // (full) chunk. to re-assign them | ||
| 7731 | - rc = sqlite3_blob_close(*blobChunksValidity); | ||
| 7732 | - sqlite3_free((void *)*bufferChunksValidity); | ||
| 7733 | - *blobChunksValidity = NULL; | ||
| 7734 | - *bufferChunksValidity = NULL; | ||
| 7735 | - if (rc != SQLITE_OK) { | ||
| 7736 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR | ||
| 7737 | - "unknown error, blobChunksValidity could not be closed, " | ||
| 7738 | - "please file an issue."); | ||
| 7739 | - rc = SQLITE_ERROR; | ||
| 7740 | - goto cleanup; | ||
| 7741 | - } | ||
| 7742 | - | ||
| 7743 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowChunksName, | ||
| 7744 | - "validity", *chunk_rowid, 1, blobChunksValidity); | ||
| 7745 | - if (rc != SQLITE_OK) { | ||
| 7746 | - vtab_set_error( | ||
| 7747 | - &p->base, | ||
| 7748 | - VEC_INTERAL_ERROR | ||
| 7749 | - "Could not open validity blob for newly created chunk %s.%s.%lld", | ||
| 7750 | - p->schemaName, p->shadowChunksName, *chunk_rowid); | ||
| 7751 | - goto cleanup; | ||
| 7752 | - } | ||
| 7753 | - validitySize = sqlite3_blob_bytes(*blobChunksValidity); | ||
| 7754 | - if (validitySize != p->chunk_size / CHAR_BIT) { | ||
| 7755 | - vtab_set_error(&p->base, | ||
| 7756 | - VEC_INTERAL_ERROR | ||
| 7757 | - "validity blob size mismatch for newly created chunk " | ||
| 7758 | - "%s.%s.%lld. Exepcted %lld, got %lld", | ||
| 7759 | - p->schemaName, p->shadowChunksName, *chunk_rowid, | ||
| 7760 | - p->chunk_size / CHAR_BIT, validitySize); | ||
| 7761 | - goto cleanup; | ||
| 7762 | - } | ||
| 7763 | - *bufferChunksValidity = sqlite3_malloc(validitySize); | ||
| 7764 | - rc = sqlite3_blob_read(*blobChunksValidity, (void *)*bufferChunksValidity, | ||
| 7765 | - validitySize, 0); | ||
| 7766 | - if (rc != SQLITE_OK) { | ||
| 7767 | - vtab_set_error(&p->base, | ||
| 7768 | - VEC_INTERAL_ERROR | ||
| 7769 | - "could not read validity blob newly created chunk " | ||
| 7770 | - "%s.%s.%lld", | ||
| 7771 | - p->schemaName, p->shadowChunksName, *chunk_rowid); | ||
| 7772 | - goto cleanup; | ||
| 7773 | - } | ||
| 7774 | - } | ||
| 7775 | - | ||
| 7776 | - rc = SQLITE_OK; | ||
| 7777 | - | ||
| 7778 | -cleanup: | ||
| 7779 | - return rc; | ||
| 7780 | -} | ||
| 7781 | - | ||
| 7782 | -/** | ||
| 7783 | - * @brief Write the vector data into the provided vector blob at the given | ||
| 7784 | - * offset | ||
| 7785 | - * | ||
| 7786 | - * @param blobVectors SQLite BLOB to write to | ||
| 7787 | - * @param chunk_offset the "offset" (ie validity bitmap position) to write the | ||
| 7788 | - * vector to | ||
| 7789 | - * @param bVector pointer to the vector containing data | ||
| 7790 | - * @param dimensions how many dimensions the vector has | ||
| 7791 | - * @param element_type the vector type | ||
| 7792 | - * @return result of sqlite3_blob_write, SQLITE_OK on success, otherwise failure | ||
| 7793 | - */ | ||
| 7794 | -static int | ||
| 7795 | -vec0_write_vector_to_vector_blob(sqlite3_blob *blobVectors, i64 chunk_offset, | ||
| 7796 | - const void *bVector, size_t dimensions, | ||
| 7797 | - enum VectorElementType element_type) { | ||
| 7798 | - int n; | ||
| 7799 | - int offset; | ||
| 7800 | - | ||
| 7801 | - switch (element_type) { | ||
| 7802 | - case SQLITE_VEC_ELEMENT_TYPE_FLOAT32: | ||
| 7803 | - n = dimensions * sizeof(f32); | ||
| 7804 | - offset = chunk_offset * dimensions * sizeof(f32); | ||
| 7805 | - break; | ||
| 7806 | - case SQLITE_VEC_ELEMENT_TYPE_INT8: | ||
| 7807 | - n = dimensions * sizeof(i8); | ||
| 7808 | - offset = chunk_offset * dimensions * sizeof(i8); | ||
| 7809 | - break; | ||
| 7810 | - case SQLITE_VEC_ELEMENT_TYPE_BIT: | ||
| 7811 | - n = dimensions / CHAR_BIT; | ||
| 7812 | - offset = chunk_offset * dimensions / CHAR_BIT; | ||
| 7813 | - break; | ||
| 7814 | - } | ||
| 7815 | - | ||
| 7816 | - return sqlite3_blob_write(blobVectors, bVector, n, offset); | ||
| 7817 | -} | ||
| 7818 | - | ||
| 7819 | -/** | ||
| 7820 | - * @brief | ||
| 7821 | - * | ||
| 7822 | - * @param p vec0 virtual table | ||
| 7823 | - * @param chunk_rowid: which chunk to write to | ||
| 7824 | - * @param chunk_offset: the offset inside the chunk to write the vector to. | ||
| 7825 | - * @param rowid: the rowid of the inserting row | ||
| 7826 | - * @param vectorDatas: array of the vector data to insert | ||
| 7827 | - * @param blobValidity: writeable validity blob of the row's assigned chunk. | ||
| 7828 | - * @param validity: snapshot buffer of the valdity column from the row's | ||
| 7829 | - * assigned chunk. | ||
| 7830 | - * @return int SQLITE_OK on success, error code on failure | ||
| 7831 | - */ | ||
| 7832 | -int vec0Update_InsertWriteFinalStep(vec0_vtab *p, i64 chunk_rowid, | ||
| 7833 | - i64 chunk_offset, i64 rowid, | ||
| 7834 | - void *vectorDatas[], | ||
| 7835 | - sqlite3_blob *blobChunksValidity, | ||
| 7836 | - const unsigned char *bufferChunksValidity) { | ||
| 7837 | - int rc, brc; | ||
| 7838 | - sqlite3_blob *blobChunksRowids = NULL; | ||
| 7839 | - | ||
| 7840 | - // mark the validity bit for this row in the chunk's validity bitmap | ||
| 7841 | - // Get the byte offset of the bitmap | ||
| 7842 | - char unsigned bx = bufferChunksValidity[chunk_offset / CHAR_BIT]; | ||
| 7843 | - // set the bit at the chunk_offset position inside that byte | ||
| 7844 | - bx = bx | (1 << (chunk_offset % CHAR_BIT)); | ||
| 7845 | - // write that 1 byte | ||
| 7846 | - rc = sqlite3_blob_write(blobChunksValidity, &bx, 1, chunk_offset / CHAR_BIT); | ||
| 7847 | - if (rc != SQLITE_OK) { | ||
| 7848 | - vtab_set_error(&p->base, VEC_INTERAL_ERROR "could not mark validity bit "); | ||
| 7849 | - return rc; | ||
| 7850 | - } | ||
| 7851 | - | ||
| 7852 | - // Go insert the vector data into the vector chunk shadow tables | ||
| 7853 | - for (int i = 0; i < p->numVectorColumns; i++) { | ||
| 7854 | - sqlite3_blob *blobVectors; | ||
| 7855 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowVectorChunksNames[i], | ||
| 7856 | - "vectors", chunk_rowid, 1, &blobVectors); | ||
| 7857 | - if (rc != SQLITE_OK) { | ||
| 7858 | - vtab_set_error(&p->base, "Error opening vector blob at %s.%s.%lld", | ||
| 7859 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_rowid); | ||
| 7860 | - goto cleanup; | ||
| 7861 | - } | ||
| 7862 | - | ||
| 7863 | - i64 expected = | ||
| 7864 | - p->chunk_size * vector_column_byte_size(p->vector_columns[i]); | ||
| 7865 | - i64 actual = sqlite3_blob_bytes(blobVectors); | ||
| 7866 | - | ||
| 7867 | - if (actual != expected) { | ||
| 7868 | - // IMP: V16386_00456 | ||
| 7869 | - vtab_set_error( | ||
| 7870 | - &p->base, | ||
| 7871 | - VEC_INTERAL_ERROR | ||
| 7872 | - "vector blob size mismatch on %s.%s.%lld. Expected %lld, actual %lld", | ||
| 7873 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_rowid, expected, | ||
| 7874 | - actual); | ||
| 7875 | - rc = SQLITE_ERROR; | ||
| 7876 | - // already error, can ignore result code | ||
| 7877 | - sqlite3_blob_close(blobVectors); | ||
| 7878 | - goto cleanup; | ||
| 7879 | - }; | ||
| 7880 | - | ||
| 7881 | - rc = vec0_write_vector_to_vector_blob( | ||
| 7882 | - blobVectors, chunk_offset, vectorDatas[i], | ||
| 7883 | - p->vector_columns[i].dimensions, p->vector_columns[i].element_type); | ||
| 7884 | - if (rc != SQLITE_OK) { | ||
| 7885 | - vtab_set_error(&p->base, | ||
| 7886 | - VEC_INTERAL_ERROR | ||
| 7887 | - "could not write vector blob on %s.%s.%lld", | ||
| 7888 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_rowid); | ||
| 7889 | - rc = SQLITE_ERROR; | ||
| 7890 | - // already error, can ignore result code | ||
| 7891 | - sqlite3_blob_close(blobVectors); | ||
| 7892 | - goto cleanup; | ||
| 7893 | - } | ||
| 7894 | - rc = sqlite3_blob_close(blobVectors); | ||
| 7895 | - if (rc != SQLITE_OK) { | ||
| 7896 | - vtab_set_error(&p->base, | ||
| 7897 | - VEC_INTERAL_ERROR | ||
| 7898 | - "could not close vector blob on %s.%s.%lld", | ||
| 7899 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_rowid); | ||
| 7900 | - rc = SQLITE_ERROR; | ||
| 7901 | - goto cleanup; | ||
| 7902 | - } | ||
| 7903 | - } | ||
| 7904 | - | ||
| 7905 | - // write the new rowid to the rowids column of the _chunks table | ||
| 7906 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowChunksName, "rowids", | ||
| 7907 | - chunk_rowid, 1, &blobChunksRowids); | ||
| 7908 | - if (rc != SQLITE_OK) { | ||
| 7909 | - // IMP: V09221_26060 | ||
| 7910 | - vtab_set_error(&p->base, | ||
| 7911 | - VEC_INTERAL_ERROR "could not open rowids blob on %s.%s.%lld", | ||
| 7912 | - p->schemaName, p->shadowChunksName, chunk_rowid); | ||
| 7913 | - goto cleanup; | ||
| 7914 | - } | ||
| 7915 | - i64 expected = p->chunk_size * sizeof(i64); | ||
| 7916 | - i64 actual = sqlite3_blob_bytes(blobChunksRowids); | ||
| 7917 | - if (expected != actual) { | ||
| 7918 | - // IMP: V12779_29618 | ||
| 7919 | - vtab_set_error( | ||
| 7920 | - &p->base, | ||
| 7921 | - VEC_INTERAL_ERROR | ||
| 7922 | - "rowids blob size mismatch on %s.%s.%lld. Expected %lld, actual %lld", | ||
| 7923 | - p->schemaName, p->shadowChunksName, chunk_rowid, expected, actual); | ||
| 7924 | - rc = SQLITE_ERROR; | ||
| 7925 | - goto cleanup; | ||
| 7926 | - } | ||
| 7927 | - rc = sqlite3_blob_write(blobChunksRowids, &rowid, sizeof(i64), | ||
| 7928 | - chunk_offset * sizeof(i64)); | ||
| 7929 | - if (rc != SQLITE_OK) { | ||
| 7930 | - vtab_set_error( | ||
| 7931 | - &p->base, VEC_INTERAL_ERROR "could not write rowids blob on %s.%s.%lld", | ||
| 7932 | - p->schemaName, p->shadowChunksName, chunk_rowid); | ||
| 7933 | - rc = SQLITE_ERROR; | ||
| 7934 | - goto cleanup; | ||
| 7935 | - } | ||
| 7936 | - | ||
| 7937 | - // Now with all the vectors inserted, go back and update the _rowids table | ||
| 7938 | - // with the new chunk_rowid/chunk_offset values | ||
| 7939 | - rc = vec0_rowids_update_position(p, rowid, chunk_rowid, chunk_offset); | ||
| 7940 | - | ||
| 7941 | -cleanup: | ||
| 7942 | - brc = sqlite3_blob_close(blobChunksRowids); | ||
| 7943 | - if ((rc == SQLITE_OK) && (brc != SQLITE_OK)) { | ||
| 7944 | - vtab_set_error( | ||
| 7945 | - &p->base, VEC_INTERAL_ERROR "could not close rowids blob on %s.%s.%lld", | ||
| 7946 | - p->schemaName, p->shadowChunksName, chunk_rowid); | ||
| 7947 | - return brc; | ||
| 7948 | - } | ||
| 7949 | - return rc; | ||
| 7950 | -} | ||
| 7951 | - | ||
| 7952 | -int vec0_write_metadata_value(vec0_vtab *p, int metadata_column_idx, i64 rowid, i64 chunk_id, i64 chunk_offset, sqlite3_value * v, int isupdate) { | ||
| 7953 | - int rc; | ||
| 7954 | - struct Vec0MetadataColumnDefinition * metadata_column = &p->metadata_columns[metadata_column_idx]; | ||
| 7955 | - vec0_metadata_column_kind kind = metadata_column->kind; | ||
| 7956 | - | ||
| 7957 | - // verify input value matches column type | ||
| 7958 | - switch(kind) { | ||
| 7959 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | ||
| 7960 | - if(sqlite3_value_type(v) != SQLITE_INTEGER || ((sqlite3_value_int(v) != 0) && (sqlite3_value_int(v) != 1))) { | ||
| 7961 | - rc = SQLITE_ERROR; | ||
| 7962 | - vtab_set_error(&p->base, "Expected 0 or 1 for BOOLEAN metadata column %.*s", metadata_column->name_length, metadata_column->name); | ||
| 7963 | - goto done; | ||
| 7964 | - } | ||
| 7965 | - break; | ||
| 7966 | - } | ||
| 7967 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | ||
| 7968 | - if(sqlite3_value_type(v) != SQLITE_INTEGER) { | ||
| 7969 | - rc = SQLITE_ERROR; | ||
| 7970 | - vtab_set_error(&p->base, "Expected integer for INTEGER metadata column %.*s, received %s", metadata_column->name_length, metadata_column->name, type_name(sqlite3_value_type(v))); | ||
| 7971 | - goto done; | ||
| 7972 | - } | ||
| 7973 | - break; | ||
| 7974 | - } | ||
| 7975 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | ||
| 7976 | - if(sqlite3_value_type(v) != SQLITE_FLOAT) { | ||
| 7977 | - rc = SQLITE_ERROR; | ||
| 7978 | - vtab_set_error(&p->base, "Expected float for FLOAT metadata column %.*s, received %s", metadata_column->name_length, metadata_column->name, type_name(sqlite3_value_type(v))); | ||
| 7979 | - goto done; | ||
| 7980 | - } | ||
| 7981 | - break; | ||
| 7982 | - } | ||
| 7983 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | ||
| 7984 | - if(sqlite3_value_type(v) != SQLITE_TEXT) { | ||
| 7985 | - rc = SQLITE_ERROR; | ||
| 7986 | - vtab_set_error(&p->base, "Expected text for TEXT metadata column %.*s, received %s", metadata_column->name_length, metadata_column->name, type_name(sqlite3_value_type(v))); | ||
| 7987 | - goto done; | ||
| 7988 | - } | ||
| 7989 | - break; | ||
| 7990 | - } | ||
| 7991 | - } | ||
| 7992 | - | ||
| 7993 | - sqlite3_blob * blobValue = NULL; | ||
| 7994 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowMetadataChunksNames[metadata_column_idx], "data", chunk_id, 1, &blobValue); | ||
| 7995 | - if(rc != SQLITE_OK) { | ||
| 7996 | - goto done; | ||
| 7997 | - } | ||
| 7998 | - | ||
| 7999 | - switch(kind) { | ||
| 8000 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | ||
| 8001 | - u8 block; | ||
| 8002 | - int value = sqlite3_value_int(v); | ||
| 8003 | - rc = sqlite3_blob_read(blobValue, &block, sizeof(u8), (int) (chunk_offset / CHAR_BIT)); | ||
| 8004 | - if(rc != SQLITE_OK) { | ||
| 8005 | - goto done; | ||
| 8006 | - } | ||
| 8007 | - | ||
| 8008 | - if (value) { | ||
| 8009 | - block |= 1 << (chunk_offset % CHAR_BIT); | ||
| 8010 | - } else { | ||
| 8011 | - block &= ~(1 << (chunk_offset % CHAR_BIT)); | ||
| 8012 | - } | ||
| 8013 | - | ||
| 8014 | - rc = sqlite3_blob_write(blobValue, &block, sizeof(u8), chunk_offset / CHAR_BIT); | ||
| 8015 | - break; | ||
| 8016 | - } | ||
| 8017 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | ||
| 8018 | - i64 value = sqlite3_value_int64(v); | ||
| 8019 | - rc = sqlite3_blob_write(blobValue, &value, sizeof(value), chunk_offset * sizeof(i64)); | ||
| 8020 | - break; | ||
| 8021 | - } | ||
| 8022 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | ||
| 8023 | - double value = sqlite3_value_double(v); | ||
| 8024 | - rc = sqlite3_blob_write(blobValue, &value, sizeof(value), chunk_offset * sizeof(double)); | ||
| 8025 | - break; | ||
| 8026 | - } | ||
| 8027 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | ||
| 8028 | - int prev_n; | ||
| 8029 | - rc = sqlite3_blob_read(blobValue, &prev_n, sizeof(int), chunk_offset * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | ||
| 8030 | - if(rc != SQLITE_OK) { | ||
| 8031 | - goto done; | ||
| 8032 | - } | ||
| 8033 | - | ||
| 8034 | - const char * s = (const char *) sqlite3_value_text(v); | ||
| 8035 | - int n = sqlite3_value_bytes(v); | ||
| 8036 | - u8 view[VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | ||
| 8037 | - memset(view, 0, VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | ||
| 8038 | - memcpy(view, &n, sizeof(int)); | ||
| 8039 | - memcpy(view+4, s, min(n, VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH-4)); | ||
| 8040 | - | ||
| 8041 | - rc = sqlite3_blob_write(blobValue, &view, VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH, chunk_offset * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | ||
| 8042 | - if(n > VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | ||
| 8043 | - const char * zSql; | ||
| 8044 | - | ||
| 8045 | - if(isupdate && (prev_n > VEC0_METADATA_TEXT_VIEW_DATA_LENGTH)) { | ||
| 8046 | - zSql = sqlite3_mprintf("UPDATE " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " SET data = ?2 WHERE rowid = ?1", p->schemaName, p->tableName, metadata_column_idx); | ||
| 8047 | - }else { | ||
| 8048 | - zSql = sqlite3_mprintf("INSERT INTO " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " (rowid, data) VALUES (?1, ?2)", p->schemaName, p->tableName, metadata_column_idx); | ||
| 8049 | - } | ||
| 8050 | - if(!zSql) { | ||
| 8051 | - rc = SQLITE_NOMEM; | ||
| 8052 | - goto done; | ||
| 8053 | - } | ||
| 8054 | - sqlite3_stmt * stmt; | ||
| 8055 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 8056 | - if(rc != SQLITE_OK) { | ||
| 8057 | - goto done; | ||
| 8058 | - } | ||
| 8059 | - sqlite3_bind_int64(stmt, 1, rowid); | ||
| 8060 | - sqlite3_bind_text(stmt, 2, s, n, SQLITE_STATIC); | ||
| 8061 | - rc = sqlite3_step(stmt); | ||
| 8062 | - sqlite3_finalize(stmt); | ||
| 8063 | - | ||
| 8064 | - if(rc != SQLITE_DONE) { | ||
| 8065 | - rc = SQLITE_ERROR; | ||
| 8066 | - goto done; | ||
| 8067 | - } | ||
| 8068 | - } | ||
| 8069 | - else if(prev_n > VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | ||
| 8070 | - const char * zSql = sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " WHERE rowid = ?", p->schemaName, p->tableName, metadata_column_idx); | ||
| 8071 | - if(!zSql) { | ||
| 8072 | - rc = SQLITE_NOMEM; | ||
| 8073 | - goto done; | ||
| 8074 | - } | ||
| 8075 | - sqlite3_stmt * stmt; | ||
| 8076 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 8077 | - if(rc != SQLITE_OK) { | ||
| 8078 | - goto done; | ||
| 8079 | - } | ||
| 8080 | - sqlite3_bind_int64(stmt, 1, rowid); | ||
| 8081 | - rc = sqlite3_step(stmt); | ||
| 8082 | - sqlite3_finalize(stmt); | ||
| 8083 | - | ||
| 8084 | - if(rc != SQLITE_DONE) { | ||
| 8085 | - rc = SQLITE_ERROR; | ||
| 8086 | - goto done; | ||
| 8087 | - } | ||
| 8088 | - } | ||
| 8089 | - break; | ||
| 8090 | - } | ||
| 8091 | - } | ||
| 8092 | - | ||
| 8093 | - if(rc != SQLITE_OK) { | ||
| 8094 | - | ||
| 8095 | - } | ||
| 8096 | - rc = sqlite3_blob_close(blobValue); | ||
| 8097 | - if(rc != SQLITE_OK) { | ||
| 8098 | - goto done; | ||
| 8099 | - } | ||
| 8100 | - | ||
| 8101 | - done: | ||
| 8102 | - return rc; | ||
| 8103 | -} | ||
| 8104 | - | ||
| 8105 | - | ||
| 8106 | -/** | ||
| 8107 | - * @brief Handles INSERT INTO operations on a vec0 table. | ||
| 8108 | - * | ||
| 8109 | - * @return int SQLITE_OK on success, otherwise error code on failure | ||
| 8110 | - */ | ||
| 8111 | -int vec0Update_Insert(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv, | ||
| 8112 | - sqlite_int64 *pRowid) { | ||
| 8113 | - UNUSED_PARAMETER(argc); | ||
| 8114 | - vec0_vtab *p = (vec0_vtab *)pVTab; | ||
| 8115 | - int rc; | ||
| 8116 | - // Rowid for the inserted row, deterimined by the inserted ID + _rowids shadow | ||
| 8117 | - // table | ||
| 8118 | - i64 rowid; | ||
| 8119 | - | ||
| 8120 | - // Array to hold the vector data of the inserted row. Individual elements will | ||
| 8121 | - // have a lifetime bound to the argv[..] values. | ||
| 8122 | - void *vectorDatas[VEC0_MAX_VECTOR_COLUMNS]; | ||
| 8123 | - // Array to hold cleanup functions for vectorDatas[] | ||
| 8124 | - vector_cleanup cleanups[VEC0_MAX_VECTOR_COLUMNS]; | ||
| 8125 | - | ||
| 8126 | - sqlite3_value * partitionKeyValues[VEC0_MAX_PARTITION_COLUMNS]; | ||
| 8127 | - | ||
| 8128 | - // Rowid of the chunk in the _chunks shadow table that the row will be a part | ||
| 8129 | - // of. | ||
| 8130 | - i64 chunk_rowid; | ||
| 8131 | - // offset within the chunk where the rowid belongs | ||
| 8132 | - i64 chunk_offset; | ||
| 8133 | - | ||
| 8134 | - // a write-able blob of the validity column for the given chunk. Used to mark | ||
| 8135 | - // validity bit | ||
| 8136 | - sqlite3_blob *blobChunksValidity = NULL; | ||
| 8137 | - // buffer for the valididty column for the given chunk. Maybe not needed here? | ||
| 8138 | - const unsigned char *bufferChunksValidity = NULL; | ||
| 8139 | - int numReadVectors = 0; | ||
| 8140 | - | ||
| 8141 | - // Read all provided partition key values into partitionKeyValues | ||
| 8142 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | ||
| 8143 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_PARTITION) { | ||
| 8144 | - continue; | ||
| 8145 | - } | ||
| 8146 | - int partition_key_idx = p->user_column_idxs[i]; | ||
| 8147 | - partitionKeyValues[partition_key_idx] = argv[2+VEC0_COLUMN_USERN_START + i]; | ||
| 8148 | - | ||
| 8149 | - int new_value_type = sqlite3_value_type(partitionKeyValues[partition_key_idx]); | ||
| 8150 | - if((new_value_type != SQLITE_NULL) && (new_value_type != p->paritition_columns[partition_key_idx].type)) { | ||
| 8151 | - // IMP: V11454_28292 | ||
| 8152 | - vtab_set_error( | ||
| 8153 | - pVTab, | ||
| 8154 | - "Parition key type mismatch: The partition key column %.*s has type %s, but %s was provided.", | ||
| 8155 | - p->paritition_columns[partition_key_idx].name_length, | ||
| 8156 | - p->paritition_columns[partition_key_idx].name, | ||
| 8157 | - type_name(p->paritition_columns[partition_key_idx].type), | ||
| 8158 | - type_name(new_value_type) | ||
| 8159 | - ); | ||
| 8160 | - rc = SQLITE_ERROR; | ||
| 8161 | - goto cleanup; | ||
| 8162 | - } | ||
| 8163 | - } | ||
| 8164 | - | ||
| 8165 | - // read all the inserted vectors into vectorDatas, validate their lengths. | ||
| 8166 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | ||
| 8167 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_VECTOR) { | ||
| 8168 | - continue; | ||
| 8169 | - } | ||
| 8170 | - int vector_column_idx = p->user_column_idxs[i]; | ||
| 8171 | - sqlite3_value *valueVector = argv[2 + VEC0_COLUMN_USERN_START + i]; | ||
| 8172 | - size_t dimensions; | ||
| 8173 | - | ||
| 8174 | - char *pzError; | ||
| 8175 | - enum VectorElementType elementType; | ||
| 8176 | - rc = vector_from_value(valueVector, &vectorDatas[vector_column_idx], &dimensions, | ||
| 8177 | - &elementType, &cleanups[vector_column_idx], &pzError); | ||
| 8178 | - if (rc != SQLITE_OK) { | ||
| 8179 | - // IMP: V06519_23358 | ||
| 8180 | - vtab_set_error( | ||
| 8181 | - pVTab, "Inserted vector for the \"%.*s\" column is invalid: %z", | ||
| 8182 | - p->vector_columns[vector_column_idx].name_length, p->vector_columns[vector_column_idx].name, pzError); | ||
| 8183 | - rc = SQLITE_ERROR; | ||
| 8184 | - goto cleanup; | ||
| 8185 | - } | ||
| 8186 | - | ||
| 8187 | - numReadVectors++; | ||
| 8188 | - if (elementType != p->vector_columns[vector_column_idx].element_type) { | ||
| 8189 | - // IMP: V08221_25059 | ||
| 8190 | - vtab_set_error( | ||
| 8191 | - pVTab, | ||
| 8192 | - "Inserted vector for the \"%.*s\" column is expected to be of type " | ||
| 8193 | - "%s, but a %s vector was provided.", | ||
| 8194 | - p->vector_columns[i].name_length, p->vector_columns[i].name, | ||
| 8195 | - vector_subtype_name(p->vector_columns[i].element_type), | ||
| 8196 | - vector_subtype_name(elementType)); | ||
| 8197 | - rc = SQLITE_ERROR; | ||
| 8198 | - goto cleanup; | ||
| 8199 | - } | ||
| 8200 | - | ||
| 8201 | - if (dimensions != p->vector_columns[vector_column_idx].dimensions) { | ||
| 8202 | - // IMP: V01145_17984 | ||
| 8203 | - vtab_set_error( | ||
| 8204 | - pVTab, | ||
| 8205 | - "Dimension mismatch for inserted vector for the \"%.*s\" column. " | ||
| 8206 | - "Expected %d dimensions but received %d.", | ||
| 8207 | - p->vector_columns[vector_column_idx].name_length, p->vector_columns[vector_column_idx].name, | ||
| 8208 | - p->vector_columns[vector_column_idx].dimensions, dimensions); | ||
| 8209 | - rc = SQLITE_ERROR; | ||
| 8210 | - goto cleanup; | ||
| 8211 | - } | ||
| 8212 | - } | ||
| 8213 | - | ||
| 8214 | - // Cannot insert a value in the hidden "distance" column | ||
| 8215 | - if (sqlite3_value_type(argv[2 + vec0_column_distance_idx(p)]) != | ||
| 8216 | - SQLITE_NULL) { | ||
| 8217 | - // IMP: V24228_08298 | ||
| 8218 | - vtab_set_error(pVTab, | ||
| 8219 | - "A value was provided for the hidden \"distance\" column."); | ||
| 8220 | - rc = SQLITE_ERROR; | ||
| 8221 | - goto cleanup; | ||
| 8222 | - } | ||
| 8223 | - // Cannot insert a value in the hidden "k" column | ||
| 8224 | - if (sqlite3_value_type(argv[2 + vec0_column_k_idx(p)]) != SQLITE_NULL) { | ||
| 8225 | - // IMP: V11875_28713 | ||
| 8226 | - vtab_set_error(pVTab, "A value was provided for the hidden \"k\" column."); | ||
| 8227 | - rc = SQLITE_ERROR; | ||
| 8228 | - goto cleanup; | ||
| 8229 | - } | ||
| 8230 | - | ||
| 8231 | - // Step #1: Insert/get a rowid for this row, from the _rowids table. | ||
| 8232 | - rc = vec0Update_InsertRowidStep(p, argv[2 + VEC0_COLUMN_ID], &rowid); | ||
| 8233 | - if (rc != SQLITE_OK) { | ||
| 8234 | - goto cleanup; | ||
| 8235 | - } | ||
| 8236 | - | ||
| 8237 | - // Step #2: Find the next "available" position in the _chunks table for this | ||
| 8238 | - // row. | ||
| 8239 | - rc = vec0Update_InsertNextAvailableStep(p, partitionKeyValues, | ||
| 8240 | - &chunk_rowid, &chunk_offset, | ||
| 8241 | - &blobChunksValidity, | ||
| 8242 | - &bufferChunksValidity); | ||
| 8243 | - if (rc != SQLITE_OK) { | ||
| 8244 | - goto cleanup; | ||
| 8245 | - } | ||
| 8246 | - | ||
| 8247 | - // Step #3: With the next available chunk position, write out all the vectors | ||
| 8248 | - // to their specified location. | ||
| 8249 | - rc = vec0Update_InsertWriteFinalStep(p, chunk_rowid, chunk_offset, rowid, | ||
| 8250 | - vectorDatas, blobChunksValidity, | ||
| 8251 | - bufferChunksValidity); | ||
| 8252 | - if (rc != SQLITE_OK) { | ||
| 8253 | - goto cleanup; | ||
| 8254 | - } | ||
| 8255 | - | ||
| 8256 | - if(p->numAuxiliaryColumns > 0) { | ||
| 8257 | - sqlite3_stmt *stmt; | ||
| 8258 | - sqlite3_str * s = sqlite3_str_new(NULL); | ||
| 8259 | - sqlite3_str_appendf(s, "INSERT INTO " VEC0_SHADOW_AUXILIARY_NAME "(rowid ", p->schemaName, p->tableName); | ||
| 8260 | - for(int i = 0; i < p->numAuxiliaryColumns; i++) { | ||
| 8261 | - sqlite3_str_appendf(s, ", value%02d", i); | ||
| 8262 | - } | ||
| 8263 | - sqlite3_str_appendall(s, ") VALUES (? "); | ||
| 8264 | - for(int i = 0; i < p->numAuxiliaryColumns; i++) { | ||
| 8265 | - sqlite3_str_appendall(s, ", ?"); | ||
| 8266 | - } | ||
| 8267 | - sqlite3_str_appendall(s, ")"); | ||
| 8268 | - char * zSql = sqlite3_str_finish(s); | ||
| 8269 | - // TODO double check error handling ehre | ||
| 8270 | - if(!zSql) { | ||
| 8271 | - rc = SQLITE_NOMEM; | ||
| 8272 | - goto cleanup; | ||
| 8273 | - } | ||
| 8274 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 8275 | - if(rc != SQLITE_OK) { | ||
| 8276 | - goto cleanup; | ||
| 8277 | - } | ||
| 8278 | - sqlite3_bind_int64(stmt, 1, rowid); | ||
| 8279 | - | ||
| 8280 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | ||
| 8281 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY) { | ||
| 8282 | - continue; | ||
| 8283 | - } | ||
| 8284 | - int auxiliary_key_idx = p->user_column_idxs[i]; | ||
| 8285 | - sqlite3_value * v = argv[2+VEC0_COLUMN_USERN_START + i]; | ||
| 8286 | - int v_type = sqlite3_value_type(v); | ||
| 8287 | - if(v_type != SQLITE_NULL && (v_type != p->auxiliary_columns[auxiliary_key_idx].type)) { | ||
| 8288 | - sqlite3_finalize(stmt); | ||
| 8289 | - rc = SQLITE_CONSTRAINT; | ||
| 8290 | - vtab_set_error( | ||
| 8291 | - pVTab, | ||
| 8292 | - "Auxiliary column type mismatch: The auxiliary column %.*s has type %s, but %s was provided.", | ||
| 8293 | - p->auxiliary_columns[auxiliary_key_idx].name_length, | ||
| 8294 | - p->auxiliary_columns[auxiliary_key_idx].name, | ||
| 8295 | - type_name(p->auxiliary_columns[auxiliary_key_idx].type), | ||
| 8296 | - type_name(v_type) | ||
| 8297 | - ); | ||
| 8298 | - goto cleanup; | ||
| 8299 | - } | ||
| 8300 | - // first 1 is for 1-based indexing on sqlite3_bind_*, second 1 is to account for initial rowid parameter | ||
| 8301 | - sqlite3_bind_value(stmt, 1 + 1 + auxiliary_key_idx, v); | ||
| 8302 | - } | ||
| 8303 | - | ||
| 8304 | - rc = sqlite3_step(stmt); | ||
| 8305 | - if(rc != SQLITE_DONE) { | ||
| 8306 | - sqlite3_finalize(stmt); | ||
| 8307 | - rc = SQLITE_ERROR; | ||
| 8308 | - goto cleanup; | ||
| 8309 | - } | ||
| 8310 | - sqlite3_finalize(stmt); | ||
| 8311 | - } | ||
| 8312 | - | ||
| 8313 | - | ||
| 8314 | - for(int i = 0; i < vec0_num_defined_user_columns(p); i++) { | ||
| 8315 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_METADATA) { | ||
| 8316 | - continue; | ||
| 8317 | - } | ||
| 8318 | - int metadata_idx = p->user_column_idxs[i]; | ||
| 8319 | - sqlite3_value *v = argv[2 + VEC0_COLUMN_USERN_START + i]; | ||
| 8320 | - rc = vec0_write_metadata_value(p, metadata_idx, rowid, chunk_rowid, chunk_offset, v, 0); | ||
| 8321 | - if(rc != SQLITE_OK) { | ||
| 8322 | - goto cleanup; | ||
| 8323 | - } | ||
| 8324 | - } | ||
| 8325 | - | ||
| 8326 | - *pRowid = rowid; | ||
| 8327 | - rc = SQLITE_OK; | ||
| 8328 | - | ||
| 8329 | -cleanup: | ||
| 8330 | - for (int i = 0; i < numReadVectors; i++) { | ||
| 8331 | - cleanups[i](vectorDatas[i]); | ||
| 8332 | - } | ||
| 8333 | - sqlite3_free((void *)bufferChunksValidity); | ||
| 8334 | - int brc = sqlite3_blob_close(blobChunksValidity); | ||
| 8335 | - if ((rc == SQLITE_OK) && (brc != SQLITE_OK)) { | ||
| 8336 | - vtab_set_error(&p->base, | ||
| 8337 | - VEC_INTERAL_ERROR "unknown error, blobChunksValidity could " | ||
| 8338 | - "not be closed, please file an issue"); | ||
| 8339 | - return brc; | ||
| 8340 | - } | ||
| 8341 | - return rc; | ||
| 8342 | -} | ||
| 8343 | - | ||
| 8344 | -int vec0Update_Delete_ClearValidity(vec0_vtab *p, i64 chunk_id, | ||
| 8345 | - u64 chunk_offset) { | ||
| 8346 | - int rc, brc; | ||
| 8347 | - sqlite3_blob *blobChunksValidity = NULL; | ||
| 8348 | - char unsigned bx; | ||
| 8349 | - int validityOffset = chunk_offset / CHAR_BIT; | ||
| 8350 | - | ||
| 8351 | - // 2. ensure chunks.validity bit is 1, then set to 0 | ||
| 8352 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowChunksName, "validity", | ||
| 8353 | - chunk_id, 1, &blobChunksValidity); | ||
| 8354 | - if (rc != SQLITE_OK) { | ||
| 8355 | - // IMP: V26002_10073 | ||
| 8356 | - vtab_set_error(&p->base, "could not open validity blob for %s.%s.%lld", | ||
| 8357 | - p->schemaName, p->shadowChunksName, chunk_id); | ||
| 8358 | - return SQLITE_ERROR; | ||
| 8359 | - } | ||
| 8360 | - // will skip the sqlite3_blob_bytes(blobChunksValidity) check for now, | ||
| 8361 | - // the read below would catch it | ||
| 8362 | - | ||
| 8363 | - rc = sqlite3_blob_read(blobChunksValidity, &bx, sizeof(bx), validityOffset); | ||
| 8364 | - if (rc != SQLITE_OK) { | ||
| 8365 | - // IMP: V21193_05263 | ||
| 8366 | - vtab_set_error( | ||
| 8367 | - &p->base, "could not read validity blob for %s.%s.%lld at %d", | ||
| 8368 | - p->schemaName, p->shadowChunksName, chunk_id, validityOffset); | ||
| 8369 | - goto cleanup; | ||
| 8370 | - } | ||
| 8371 | - if (!(bx >> (chunk_offset % CHAR_BIT))) { | ||
| 8372 | - // IMP: V21193_05263 | ||
| 8373 | - rc = SQLITE_ERROR; | ||
| 8374 | - vtab_set_error( | ||
| 8375 | - &p->base, | ||
| 8376 | - "vec0 deletion error: validity bit is not set for %s.%s.%lld at %d", | ||
| 8377 | - p->schemaName, p->shadowChunksName, chunk_id, validityOffset); | ||
| 8378 | - goto cleanup; | ||
| 8379 | - } | ||
| 8380 | - char unsigned mask = ~(1 << (chunk_offset % CHAR_BIT)); | ||
| 8381 | - char result = bx & mask; | ||
| 8382 | - rc = sqlite3_blob_write(blobChunksValidity, &result, sizeof(bx), | ||
| 8383 | - validityOffset); | ||
| 8384 | - if (rc != SQLITE_OK) { | ||
| 8385 | - vtab_set_error( | ||
| 8386 | - &p->base, "could not write to validity blob for %s.%s.%lld at %d", | ||
| 8387 | - p->schemaName, p->shadowChunksName, chunk_id, validityOffset); | ||
| 8388 | - goto cleanup; | ||
| 8389 | - } | ||
| 8390 | - | ||
| 8391 | -cleanup: | ||
| 8392 | - | ||
| 8393 | - brc = sqlite3_blob_close(blobChunksValidity); | ||
| 8394 | - if (rc != SQLITE_OK) | ||
| 8395 | - return rc; | ||
| 8396 | - if (brc != SQLITE_OK) { | ||
| 8397 | - vtab_set_error(&p->base, | ||
| 8398 | - "vec0 deletion error: Error commiting validity blob " | ||
| 8399 | - "transaction on %s.%s.%lld at %d", | ||
| 8400 | - p->schemaName, p->shadowChunksName, chunk_id, | ||
| 8401 | - validityOffset); | ||
| 8402 | - return brc; | ||
| 8403 | - } | ||
| 8404 | - return SQLITE_OK; | ||
| 8405 | -} | ||
| 8406 | - | ||
| 8407 | -int vec0Update_Delete_DeleteRowids(vec0_vtab *p, i64 rowid) { | ||
| 8408 | - int rc; | ||
| 8409 | - sqlite3_stmt *stmt = NULL; | ||
| 8410 | - | ||
| 8411 | - char *zSql = | ||
| 8412 | - sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_ROWIDS_NAME " WHERE rowid = ?", | ||
| 8413 | - p->schemaName, p->tableName); | ||
| 8414 | - if (!zSql) { | ||
| 8415 | - return SQLITE_NOMEM; | ||
| 8416 | - } | ||
| 8417 | - | ||
| 8418 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 8419 | - sqlite3_free(zSql); | ||
| 8420 | - if (rc != SQLITE_OK) { | ||
| 8421 | - goto cleanup; | ||
| 8422 | - } | ||
| 8423 | - sqlite3_bind_int64(stmt, 1, rowid); | ||
| 8424 | - rc = sqlite3_step(stmt); | ||
| 8425 | - if (rc != SQLITE_DONE) { | ||
| 8426 | - goto cleanup; | ||
| 8427 | - } | ||
| 8428 | - rc = SQLITE_OK; | ||
| 8429 | - | ||
| 8430 | -cleanup: | ||
| 8431 | - sqlite3_finalize(stmt); | ||
| 8432 | - return rc; | ||
| 8433 | -} | ||
| 8434 | - | ||
| 8435 | -int vec0Update_Delete_DeleteAux(vec0_vtab *p, i64 rowid) { | ||
| 8436 | - int rc; | ||
| 8437 | - sqlite3_stmt *stmt = NULL; | ||
| 8438 | - | ||
| 8439 | - char *zSql = | ||
| 8440 | - sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_AUXILIARY_NAME " WHERE rowid = ?", | ||
| 8441 | - p->schemaName, p->tableName); | ||
| 8442 | - if (!zSql) { | ||
| 8443 | - return SQLITE_NOMEM; | ||
| 8444 | - } | ||
| 8445 | - | ||
| 8446 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 8447 | - sqlite3_free(zSql); | ||
| 8448 | - if (rc != SQLITE_OK) { | ||
| 8449 | - goto cleanup; | ||
| 8450 | - } | ||
| 8451 | - sqlite3_bind_int64(stmt, 1, rowid); | ||
| 8452 | - rc = sqlite3_step(stmt); | ||
| 8453 | - if (rc != SQLITE_DONE) { | ||
| 8454 | - goto cleanup; | ||
| 8455 | - } | ||
| 8456 | - rc = SQLITE_OK; | ||
| 8457 | - | ||
| 8458 | -cleanup: | ||
| 8459 | - sqlite3_finalize(stmt); | ||
| 8460 | - return rc; | ||
| 8461 | -} | ||
| 8462 | - | ||
| 8463 | -int vec0Update_Delete_ClearMetadata(vec0_vtab *p, int metadata_idx, i64 rowid, i64 chunk_id, | ||
| 8464 | - u64 chunk_offset) { | ||
| 8465 | - int rc; | ||
| 8466 | - sqlite3_blob * blobValue; | ||
| 8467 | - vec0_metadata_column_kind kind = p->metadata_columns[metadata_idx].kind; | ||
| 8468 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowMetadataChunksNames[metadata_idx], "data", chunk_id, 1, &blobValue); | ||
| 8469 | - if(rc != SQLITE_OK) { | ||
| 8470 | - return rc; | ||
| 8471 | - } | ||
| 8472 | - | ||
| 8473 | - switch(kind) { | ||
| 8474 | - case VEC0_METADATA_COLUMN_KIND_BOOLEAN: { | ||
| 8475 | - u8 block; | ||
| 8476 | - rc = sqlite3_blob_read(blobValue, &block, sizeof(u8), (int) (chunk_offset / CHAR_BIT)); | ||
| 8477 | - if(rc != SQLITE_OK) { | ||
| 8478 | - goto done; | ||
| 8479 | - } | ||
| 8480 | - | ||
| 8481 | - block &= ~(1 << (chunk_offset % CHAR_BIT)); | ||
| 8482 | - rc = sqlite3_blob_write(blobValue, &block, sizeof(u8), chunk_offset / CHAR_BIT); | ||
| 8483 | - break; | ||
| 8484 | - } | ||
| 8485 | - case VEC0_METADATA_COLUMN_KIND_INTEGER: { | ||
| 8486 | - i64 v = 0; | ||
| 8487 | - rc = sqlite3_blob_write(blobValue, &v, sizeof(v), chunk_offset * sizeof(i64)); | ||
| 8488 | - break; | ||
| 8489 | - } | ||
| 8490 | - case VEC0_METADATA_COLUMN_KIND_FLOAT: { | ||
| 8491 | - double v = 0; | ||
| 8492 | - rc = sqlite3_blob_write(blobValue, &v, sizeof(v), chunk_offset * sizeof(double)); | ||
| 8493 | - break; | ||
| 8494 | - } | ||
| 8495 | - case VEC0_METADATA_COLUMN_KIND_TEXT: { | ||
| 8496 | - int n; | ||
| 8497 | - rc = sqlite3_blob_read(blobValue, &n, sizeof(int), chunk_offset * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | ||
| 8498 | - if(rc != SQLITE_OK) { | ||
| 8499 | - goto done; | ||
| 8500 | - } | ||
| 8501 | - | ||
| 8502 | - u8 view[VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH]; | ||
| 8503 | - memset(view, 0, VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | ||
| 8504 | - rc = sqlite3_blob_write(blobValue, &view, sizeof(view), chunk_offset * VEC0_METADATA_TEXT_VIEW_BUFFER_LENGTH); | ||
| 8505 | - if(rc != SQLITE_OK) { | ||
| 8506 | - goto done; | ||
| 8507 | - } | ||
| 8508 | - | ||
| 8509 | - if(n > VEC0_METADATA_TEXT_VIEW_DATA_LENGTH) { | ||
| 8510 | - const char * zSql = sqlite3_mprintf("DELETE FROM " VEC0_SHADOW_METADATA_TEXT_DATA_NAME " WHERE rowid = ?", p->schemaName, p->tableName, metadata_idx); | ||
| 8511 | - if(!zSql) { | ||
| 8512 | - rc = SQLITE_NOMEM; | ||
| 8513 | - goto done; | ||
| 8514 | - } | ||
| 8515 | - sqlite3_stmt * stmt; | ||
| 8516 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 8517 | - if(rc != SQLITE_OK) { | ||
| 8518 | - goto done; | ||
| 8519 | - } | ||
| 8520 | - sqlite3_bind_int64(stmt, 1, rowid); | ||
| 8521 | - rc = sqlite3_step(stmt); | ||
| 8522 | - if(rc != SQLITE_DONE) { | ||
| 8523 | - rc = SQLITE_ERROR; | ||
| 8524 | - goto done; | ||
| 8525 | - } | ||
| 8526 | - sqlite3_finalize(stmt); | ||
| 8527 | - } | ||
| 8528 | - break; | ||
| 8529 | - } | ||
| 8530 | - } | ||
| 8531 | - int rc2; | ||
| 8532 | - done: | ||
| 8533 | - rc2 = sqlite3_blob_close(blobValue); | ||
| 8534 | - if(rc == SQLITE_OK) { | ||
| 8535 | - return rc2; | ||
| 8536 | - } | ||
| 8537 | - return rc; | ||
| 8538 | -} | ||
| 8539 | - | ||
| 8540 | -int vec0Update_Delete(sqlite3_vtab *pVTab, sqlite3_value *idValue) { | ||
| 8541 | - vec0_vtab *p = (vec0_vtab *)pVTab; | ||
| 8542 | - int rc; | ||
| 8543 | - i64 rowid; | ||
| 8544 | - i64 chunk_id; | ||
| 8545 | - i64 chunk_offset; | ||
| 8546 | - | ||
| 8547 | - if (p->pkIsText) { | ||
| 8548 | - rc = vec0_rowid_from_id(p, idValue, &rowid); | ||
| 8549 | - if (rc != SQLITE_OK) { | ||
| 8550 | - return rc; | ||
| 8551 | - } | ||
| 8552 | - } else { | ||
| 8553 | - rowid = sqlite3_value_int64(idValue); | ||
| 8554 | - } | ||
| 8555 | - | ||
| 8556 | - // 1. Find chunk position for given rowid | ||
| 8557 | - // 2. Ensure that validity bit for position is 1, then set to 0 | ||
| 8558 | - // 3. Zero out rowid in chunks.rowid | ||
| 8559 | - // 4. Zero out vector data in all vector column chunks | ||
| 8560 | - // 5. Delete value in _rowids table | ||
| 8561 | - | ||
| 8562 | - // 1. get chunk_id and chunk_offset from _rowids | ||
| 8563 | - rc = vec0_get_chunk_position(p, rowid, NULL, &chunk_id, &chunk_offset); | ||
| 8564 | - if (rc != SQLITE_OK) { | ||
| 8565 | - return rc; | ||
| 8566 | - } | ||
| 8567 | - | ||
| 8568 | - rc = vec0Update_Delete_ClearValidity(p, chunk_id, chunk_offset); | ||
| 8569 | - if (rc != SQLITE_OK) { | ||
| 8570 | - return rc; | ||
| 8571 | - } | ||
| 8572 | - | ||
| 8573 | - // 3. zero out rowid in chunks.rowids | ||
| 8574 | - // https://github.com/asg017/sqlite-vec/issues/54 | ||
| 8575 | - | ||
| 8576 | - // 4. zero out any data in vector chunks tables | ||
| 8577 | - // https://github.com/asg017/sqlite-vec/issues/54 | ||
| 8578 | - | ||
| 8579 | - // 5. delete from _rowids table | ||
| 8580 | - rc = vec0Update_Delete_DeleteRowids(p, rowid); | ||
| 8581 | - if (rc != SQLITE_OK) { | ||
| 8582 | - return rc; | ||
| 8583 | - } | ||
| 8584 | - | ||
| 8585 | - // 6. delete any auxiliary rows | ||
| 8586 | - if(p->numAuxiliaryColumns > 0) { | ||
| 8587 | - rc = vec0Update_Delete_DeleteAux(p, rowid); | ||
| 8588 | - if (rc != SQLITE_OK) { | ||
| 8589 | - return rc; | ||
| 8590 | - } | ||
| 8591 | - } | ||
| 8592 | - | ||
| 8593 | - // 6. delete metadata | ||
| 8594 | - for(int i = 0; i < p->numMetadataColumns; i++) { | ||
| 8595 | - rc = vec0Update_Delete_ClearMetadata(p, i, rowid, chunk_id, chunk_offset); | ||
| 8596 | - } | ||
| 8597 | - | ||
| 8598 | - return SQLITE_OK; | ||
| 8599 | -} | ||
| 8600 | - | ||
| 8601 | -int vec0Update_UpdateAuxColumn(vec0_vtab *p, int auxiliary_column_idx, sqlite3_value * value, i64 rowid) { | ||
| 8602 | - int rc; | ||
| 8603 | - sqlite3_stmt *stmt; | ||
| 8604 | - const char * zSql = sqlite3_mprintf("UPDATE " VEC0_SHADOW_AUXILIARY_NAME " SET value%02d = ? WHERE rowid = ?", p->schemaName, p->tableName, auxiliary_column_idx); | ||
| 8605 | - if(!zSql) { | ||
| 8606 | - return SQLITE_NOMEM; | ||
| 8607 | - } | ||
| 8608 | - rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, NULL); | ||
| 8609 | - if(rc != SQLITE_OK) { | ||
| 8610 | - return rc; | ||
| 8611 | - } | ||
| 8612 | - sqlite3_bind_value(stmt, 1, value); | ||
| 8613 | - sqlite3_bind_int64(stmt, 2, rowid); | ||
| 8614 | - rc = sqlite3_step(stmt); | ||
| 8615 | - if(rc != SQLITE_DONE) { | ||
| 8616 | - sqlite3_finalize(stmt); | ||
| 8617 | - return SQLITE_ERROR; | ||
| 8618 | - } | ||
| 8619 | - sqlite3_finalize(stmt); | ||
| 8620 | - return SQLITE_OK; | ||
| 8621 | -} | ||
| 8622 | - | ||
| 8623 | -int vec0Update_UpdateVectorColumn(vec0_vtab *p, i64 chunk_id, i64 chunk_offset, | ||
| 8624 | - int i, sqlite3_value *valueVector) { | ||
| 8625 | - int rc; | ||
| 8626 | - | ||
| 8627 | - sqlite3_blob *blobVectors = NULL; | ||
| 8628 | - | ||
| 8629 | - char *pzError; | ||
| 8630 | - size_t dimensions; | ||
| 8631 | - enum VectorElementType elementType; | ||
| 8632 | - void *vector; | ||
| 8633 | - vector_cleanup cleanup = vector_cleanup_noop; | ||
| 8634 | - // https://github.com/asg017/sqlite-vec/issues/53 | ||
| 8635 | - rc = vector_from_value(valueVector, &vector, &dimensions, &elementType, | ||
| 8636 | - &cleanup, &pzError); | ||
| 8637 | - if (rc != SQLITE_OK) { | ||
| 8638 | - // IMP: V15203_32042 | ||
| 8639 | - vtab_set_error( | ||
| 8640 | - &p->base, "Updated vector for the \"%.*s\" column is invalid: %z", | ||
| 8641 | - p->vector_columns[i].name_length, p->vector_columns[i].name, pzError); | ||
| 8642 | - rc = SQLITE_ERROR; | ||
| 8643 | - goto cleanup; | ||
| 8644 | - } | ||
| 8645 | - if (elementType != p->vector_columns[i].element_type) { | ||
| 8646 | - // IMP: V03643_20481 | ||
| 8647 | - vtab_set_error( | ||
| 8648 | - &p->base, | ||
| 8649 | - "Updated vector for the \"%.*s\" column is expected to be of type " | ||
| 8650 | - "%s, but a %s vector was provided.", | ||
| 8651 | - p->vector_columns[i].name_length, p->vector_columns[i].name, | ||
| 8652 | - vector_subtype_name(p->vector_columns[i].element_type), | ||
| 8653 | - vector_subtype_name(elementType)); | ||
| 8654 | - rc = SQLITE_ERROR; | ||
| 8655 | - goto cleanup; | ||
| 8656 | - } | ||
| 8657 | - if (dimensions != p->vector_columns[i].dimensions) { | ||
| 8658 | - // IMP: V25739_09810 | ||
| 8659 | - vtab_set_error( | ||
| 8660 | - &p->base, | ||
| 8661 | - "Dimension mismatch for new updated vector for the \"%.*s\" column. " | ||
| 8662 | - "Expected %d dimensions but received %d.", | ||
| 8663 | - p->vector_columns[i].name_length, p->vector_columns[i].name, | ||
| 8664 | - p->vector_columns[i].dimensions, dimensions); | ||
| 8665 | - rc = SQLITE_ERROR; | ||
| 8666 | - goto cleanup; | ||
| 8667 | - } | ||
| 8668 | - | ||
| 8669 | - rc = sqlite3_blob_open(p->db, p->schemaName, p->shadowVectorChunksNames[i], | ||
| 8670 | - "vectors", chunk_id, 1, &blobVectors); | ||
| 8671 | - if (rc != SQLITE_OK) { | ||
| 8672 | - vtab_set_error(&p->base, "Could not open vectors blob for %s.%s.%lld", | ||
| 8673 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_id); | ||
| 8674 | - goto cleanup; | ||
| 8675 | - } | ||
| 8676 | - rc = vec0_write_vector_to_vector_blob(blobVectors, chunk_offset, vector, | ||
| 8677 | - p->vector_columns[i].dimensions, | ||
| 8678 | - p->vector_columns[i].element_type); | ||
| 8679 | - if (rc != SQLITE_OK) { | ||
| 8680 | - vtab_set_error(&p->base, "Could not write to vectors blob for %s.%s.%lld", | ||
| 8681 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_id); | ||
| 8682 | - goto cleanup; | ||
| 8683 | - } | ||
| 8684 | - | ||
| 8685 | -cleanup: | ||
| 8686 | - cleanup(vector); | ||
| 8687 | - int brc = sqlite3_blob_close(blobVectors); | ||
| 8688 | - if (rc != SQLITE_OK) { | ||
| 8689 | - return rc; | ||
| 8690 | - } | ||
| 8691 | - if (brc != SQLITE_OK) { | ||
| 8692 | - vtab_set_error( | ||
| 8693 | - &p->base, | ||
| 8694 | - "Could not commit blob transaction for vectors blob for %s.%s.%lld", | ||
| 8695 | - p->schemaName, p->shadowVectorChunksNames[i], chunk_id); | ||
| 8696 | - return brc; | ||
| 8697 | - } | ||
| 8698 | - return SQLITE_OK; | ||
| 8699 | -} | ||
| 8700 | - | ||
| 8701 | -int vec0Update_Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv) { | ||
| 8702 | - UNUSED_PARAMETER(argc); | ||
| 8703 | - vec0_vtab *p = (vec0_vtab *)pVTab; | ||
| 8704 | - int rc; | ||
| 8705 | - i64 chunk_id; | ||
| 8706 | - i64 chunk_offset; | ||
| 8707 | - | ||
| 8708 | - i64 rowid; | ||
| 8709 | - if (p->pkIsText) { | ||
| 8710 | - const char *a = (const char *)sqlite3_value_text(argv[0]); | ||
| 8711 | - const char *b = (const char *)sqlite3_value_text(argv[1]); | ||
| 8712 | - // IMP: V08886_25725 | ||
| 8713 | - if ((sqlite3_value_bytes(argv[0]) != sqlite3_value_bytes(argv[1])) || | ||
| 8714 | - strncmp(a, b, sqlite3_value_bytes(argv[0])) != 0) { | ||
| 8715 | - vtab_set_error(pVTab, | ||
| 8716 | - "UPDATEs on vec0 primary key values are not allowed."); | ||
| 8717 | - return SQLITE_ERROR; | ||
| 8718 | - } | ||
| 8719 | - rc = vec0_rowid_from_id(p, argv[0], &rowid); | ||
| 8720 | - if (rc != SQLITE_OK) { | ||
| 8721 | - return rc; | ||
| 8722 | - } | ||
| 8723 | - } else { | ||
| 8724 | - rowid = sqlite3_value_int64(argv[0]); | ||
| 8725 | - } | ||
| 8726 | - | ||
| 8727 | - // 1) get chunk_id and chunk_offset from _rowids | ||
| 8728 | - rc = vec0_get_chunk_position(p, rowid, NULL, &chunk_id, &chunk_offset); | ||
| 8729 | - if (rc != SQLITE_OK) { | ||
| 8730 | - return rc; | ||
| 8731 | - } | ||
| 8732 | - | ||
| 8733 | - // 2) update any partition key values | ||
| 8734 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | ||
| 8735 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_PARTITION) { | ||
| 8736 | - continue; | ||
| 8737 | - } | ||
| 8738 | - sqlite3_value * value = argv[2+VEC0_COLUMN_USERN_START + i]; | ||
| 8739 | - if(sqlite3_value_nochange(value)) { | ||
| 8740 | - continue; | ||
| 8741 | - } | ||
| 8742 | - vtab_set_error(pVTab, "UPDATE on partition key columns are not supported yet. "); | ||
| 8743 | - return SQLITE_ERROR; | ||
| 8744 | - } | ||
| 8745 | - | ||
| 8746 | - // 3) handle auxiliary column updates | ||
| 8747 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | ||
| 8748 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_AUXILIARY) { | ||
| 8749 | - continue; | ||
| 8750 | - } | ||
| 8751 | - int auxiliary_column_idx = p->user_column_idxs[i]; | ||
| 8752 | - sqlite3_value * value = argv[2+VEC0_COLUMN_USERN_START + i]; | ||
| 8753 | - if(sqlite3_value_nochange(value)) { | ||
| 8754 | - continue; | ||
| 8755 | - } | ||
| 8756 | - rc = vec0Update_UpdateAuxColumn(p, auxiliary_column_idx, value, rowid); | ||
| 8757 | - if(rc != SQLITE_OK) { | ||
| 8758 | - return SQLITE_ERROR; | ||
| 8759 | - } | ||
| 8760 | - } | ||
| 8761 | - | ||
| 8762 | - // 4) handle metadata column updates | ||
| 8763 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | ||
| 8764 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_METADATA) { | ||
| 8765 | - continue; | ||
| 8766 | - } | ||
| 8767 | - int metadata_column_idx = p->user_column_idxs[i]; | ||
| 8768 | - sqlite3_value * value = argv[2+VEC0_COLUMN_USERN_START + i]; | ||
| 8769 | - if(sqlite3_value_nochange(value)) { | ||
| 8770 | - continue; | ||
| 8771 | - } | ||
| 8772 | - rc = vec0_write_metadata_value(p, metadata_column_idx, rowid, chunk_id, chunk_offset, value, 1); | ||
| 8773 | - if(rc != SQLITE_OK) { | ||
| 8774 | - return rc; | ||
| 8775 | - } | ||
| 8776 | - } | ||
| 8777 | - | ||
| 8778 | - // 5) iterate over all new vectors, update the vectors | ||
| 8779 | - for (int i = 0; i < vec0_num_defined_user_columns(p); i++) { | ||
| 8780 | - if(p->user_column_kinds[i] != SQLITE_VEC0_USER_COLUMN_KIND_VECTOR) { | ||
| 8781 | - continue; | ||
| 8782 | - } | ||
| 8783 | - int vector_idx = p->user_column_idxs[i]; | ||
| 8784 | - sqlite3_value *valueVector = argv[2 + VEC0_COLUMN_USERN_START + i]; | ||
| 8785 | - // in vec0Column, we check sqlite3_vtab_nochange() on vector columns. | ||
| 8786 | - // If the vector column isn't being changed, we return NULL; | ||
| 8787 | - // That's not great, that means vector columns can never be NULLABLE | ||
| 8788 | - // (bc we cant distinguish if an updated vector is truly NULL or nochange). | ||
| 8789 | - // Also it means that if someone tries to run `UPDATE v SET X = NULL`, | ||
| 8790 | - // we can't effectively detect and raise an error. | ||
| 8791 | - // A better solution would be to use a custom result_type for "empty", | ||
| 8792 | - // but subtypes don't appear to survive xColumn -> xUpdate, it's always 0. | ||
| 8793 | - // So for now, we'll just use NULL and warn people to not SET X = NULL | ||
| 8794 | - // in the docs. | ||
| 8795 | - if (sqlite3_value_type(valueVector) == SQLITE_NULL) { | ||
| 8796 | - continue; | ||
| 8797 | - } | ||
| 8798 | - | ||
| 8799 | - rc = vec0Update_UpdateVectorColumn(p, chunk_id, chunk_offset, vector_idx, | ||
| 8800 | - valueVector); | ||
| 8801 | - if (rc != SQLITE_OK) { | ||
| 8802 | - return SQLITE_ERROR; | ||
| 8803 | - } | ||
| 8804 | - } | ||
| 8805 | - | ||
| 8806 | - return SQLITE_OK; | ||
| 8807 | -} | ||
| 8808 | - | ||
| 8809 | -static int vec0Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv, | ||
| 8810 | - sqlite_int64 *pRowid) { | ||
| 8811 | - // DELETE operation | ||
| 8812 | - if (argc == 1 && sqlite3_value_type(argv[0]) != SQLITE_NULL) { | ||
| 8813 | - return vec0Update_Delete(pVTab, argv[0]); | ||
| 8814 | - } | ||
| 8815 | - // INSERT operation | ||
| 8816 | - else if (argc > 1 && sqlite3_value_type(argv[0]) == SQLITE_NULL) { | ||
| 8817 | - return vec0Update_Insert(pVTab, argc, argv, pRowid); | ||
| 8818 | - } | ||
| 8819 | - // UPDATE operation | ||
| 8820 | - else if (argc > 1 && sqlite3_value_type(argv[0]) != SQLITE_NULL) { | ||
| 8821 | - return vec0Update_Update(pVTab, argc, argv); | ||
| 8822 | - } else { | ||
| 8823 | - vtab_set_error(pVTab, "Unrecognized xUpdate operation provided for vec0."); | ||
| 8824 | - return SQLITE_ERROR; | ||
| 8825 | - } | ||
| 8826 | -} | ||
| 8827 | - | ||
| 8828 | -static int vec0ShadowName(const char *zName) { | ||
| 8829 | - static const char *azName[] = { | ||
| 8830 | - "rowids", "chunks", "auxiliary", "info", | ||
| 8831 | - | ||
| 8832 | - // Up to VEC0_MAX_METADATA_COLUMNS | ||
| 8833 | - // TODO be smarter about this man | ||
| 8834 | - "metadatachunks00", | ||
| 8835 | - "metadatachunks01", | ||
| 8836 | - "metadatachunks02", | ||
| 8837 | - "metadatachunks03", | ||
| 8838 | - "metadatachunks04", | ||
| 8839 | - "metadatachunks05", | ||
| 8840 | - "metadatachunks06", | ||
| 8841 | - "metadatachunks07", | ||
| 8842 | - "metadatachunks08", | ||
| 8843 | - "metadatachunks09", | ||
| 8844 | - "metadatachunks10", | ||
| 8845 | - "metadatachunks11", | ||
| 8846 | - "metadatachunks12", | ||
| 8847 | - "metadatachunks13", | ||
| 8848 | - "metadatachunks14", | ||
| 8849 | - "metadatachunks15", | ||
| 8850 | - | ||
| 8851 | - // Up to | ||
| 8852 | - "metadatatext00", | ||
| 8853 | - "metadatatext01", | ||
| 8854 | - "metadatatext02", | ||
| 8855 | - "metadatatext03", | ||
| 8856 | - "metadatatext04", | ||
| 8857 | - "metadatatext05", | ||
| 8858 | - "metadatatext06", | ||
| 8859 | - "metadatatext07", | ||
| 8860 | - "metadatatext08", | ||
| 8861 | - "metadatatext09", | ||
| 8862 | - "metadatatext10", | ||
| 8863 | - "metadatatext11", | ||
| 8864 | - "metadatatext12", | ||
| 8865 | - "metadatatext13", | ||
| 8866 | - "metadatatext14", | ||
| 8867 | - "metadatatext15", | ||
| 8868 | - }; | ||
| 8869 | - | ||
| 8870 | - for (size_t i = 0; i < sizeof(azName) / sizeof(azName[0]); i++) { | ||
| 8871 | - if (sqlite3_stricmp(zName, azName[i]) == 0) | ||
| 8872 | - return 1; | ||
| 8873 | - } | ||
| 8874 | - //for(size_t i = 0; i < )"vector_chunks", "metadatachunks" | ||
| 8875 | - return 0; | ||
| 8876 | -} | ||
| 8877 | - | ||
| 8878 | -static int vec0Begin(sqlite3_vtab *pVTab) { | ||
| 8879 | - UNUSED_PARAMETER(pVTab); | ||
| 8880 | - return SQLITE_OK; | ||
| 8881 | -} | ||
| 8882 | -static int vec0Sync(sqlite3_vtab *pVTab) { | ||
| 8883 | - UNUSED_PARAMETER(pVTab); | ||
| 8884 | - vec0_vtab *p = (vec0_vtab *)pVTab; | ||
| 8885 | - if (p->stmtLatestChunk) { | ||
| 8886 | - sqlite3_finalize(p->stmtLatestChunk); | ||
| 8887 | - p->stmtLatestChunk = NULL; | ||
| 8888 | - } | ||
| 8889 | - if (p->stmtRowidsInsertRowid) { | ||
| 8890 | - sqlite3_finalize(p->stmtRowidsInsertRowid); | ||
| 8891 | - p->stmtRowidsInsertRowid = NULL; | ||
| 8892 | - } | ||
| 8893 | - if (p->stmtRowidsInsertId) { | ||
| 8894 | - sqlite3_finalize(p->stmtRowidsInsertId); | ||
| 8895 | - p->stmtRowidsInsertId = NULL; | ||
| 8896 | - } | ||
| 8897 | - if (p->stmtRowidsUpdatePosition) { | ||
| 8898 | - sqlite3_finalize(p->stmtRowidsUpdatePosition); | ||
| 8899 | - p->stmtRowidsUpdatePosition = NULL; | ||
| 8900 | - } | ||
| 8901 | - if (p->stmtRowidsGetChunkPosition) { | ||
| 8902 | - sqlite3_finalize(p->stmtRowidsGetChunkPosition); | ||
| 8903 | - p->stmtRowidsGetChunkPosition = NULL; | ||
| 8904 | - } | ||
| 8905 | - return SQLITE_OK; | ||
| 8906 | -} | ||
| 8907 | -static int vec0Commit(sqlite3_vtab *pVTab) { | ||
| 8908 | - UNUSED_PARAMETER(pVTab); | ||
| 8909 | - return SQLITE_OK; | ||
| 8910 | -} | ||
| 8911 | -static int vec0Rollback(sqlite3_vtab *pVTab) { | ||
| 8912 | - UNUSED_PARAMETER(pVTab); | ||
| 8913 | - return SQLITE_OK; | ||
| 8914 | -} | ||
| 8915 | - | ||
| 8916 | -static sqlite3_module vec0Module = { | ||
| 8917 | - /* iVersion */ 3, | ||
| 8918 | - /* xCreate */ vec0Create, | ||
| 8919 | - /* xConnect */ vec0Connect, | ||
| 8920 | - /* xBestIndex */ vec0BestIndex, | ||
| 8921 | - /* xDisconnect */ vec0Disconnect, | ||
| 8922 | - /* xDestroy */ vec0Destroy, | ||
| 8923 | - /* xOpen */ vec0Open, | ||
| 8924 | - /* xClose */ vec0Close, | ||
| 8925 | - /* xFilter */ vec0Filter, | ||
| 8926 | - /* xNext */ vec0Next, | ||
| 8927 | - /* xEof */ vec0Eof, | ||
| 8928 | - /* xColumn */ vec0Column, | ||
| 8929 | - /* xRowid */ vec0Rowid, | ||
| 8930 | - /* xUpdate */ vec0Update, | ||
| 8931 | - /* xBegin */ vec0Begin, | ||
| 8932 | - /* xSync */ vec0Sync, | ||
| 8933 | - /* xCommit */ vec0Commit, | ||
| 8934 | - /* xRollback */ vec0Rollback, | ||
| 8935 | - /* xFindFunction */ 0, | ||
| 8936 | - /* xRename */ 0, // https://github.com/asg017/sqlite-vec/issues/43 | ||
| 8937 | - /* xSavepoint */ 0, | ||
| 8938 | - /* xRelease */ 0, | ||
| 8939 | - /* xRollbackTo */ 0, | ||
| 8940 | - /* xShadowName */ vec0ShadowName, | ||
| 8941 | -#if SQLITE_VERSION_NUMBER >= 3044000 | ||
| 8942 | - /* xIntegrity */ 0, // https://github.com/asg017/sqlite-vec/issues/44 | ||
| 8943 | -#endif | ||
| 8944 | -}; | ||
| 8945 | -#pragma endregion | ||
| 8946 | - | ||
| 8947 | -static char *POINTER_NAME_STATIC_BLOB_DEF = "vec0-static_blob_def"; | ||
| 8948 | -struct static_blob_definition { | ||
| 8949 | - void *p; | ||
| 8950 | - size_t dimensions; | ||
| 8951 | - size_t nvectors; | ||
| 8952 | - enum VectorElementType element_type; | ||
| 8953 | -}; | ||
| 8954 | -static void vec_static_blob_from_raw(sqlite3_context *context, int argc, | ||
| 8955 | - sqlite3_value **argv) { | ||
| 8956 | - | ||
| 8957 | - assert(argc == 4); | ||
| 8958 | - struct static_blob_definition *p; | ||
| 8959 | - p = sqlite3_malloc(sizeof(*p)); | ||
| 8960 | - if (!p) { | ||
| 8961 | - sqlite3_result_error_nomem(context); | ||
| 8962 | - return; | ||
| 8963 | - } | ||
| 8964 | - memset(p, 0, sizeof(*p)); | ||
| 8965 | - p->p = (void *)sqlite3_value_int64(argv[0]); | ||
| 8966 | - p->element_type = SQLITE_VEC_ELEMENT_TYPE_FLOAT32; | ||
| 8967 | - p->dimensions = sqlite3_value_int64(argv[2]); | ||
| 8968 | - p->nvectors = sqlite3_value_int64(argv[3]); | ||
| 8969 | - sqlite3_result_pointer(context, p, POINTER_NAME_STATIC_BLOB_DEF, | ||
| 8970 | - sqlite3_free); | ||
| 8971 | -} | ||
| 8972 | -#pragma region vec_static_blobs() table function | ||
| 8973 | - | ||
| 8974 | -#define MAX_STATIC_BLOBS 16 | ||
| 8975 | - | ||
| 8976 | -typedef struct static_blob static_blob; | ||
| 8977 | -struct static_blob { | ||
| 8978 | - char *name; | ||
| 8979 | - void *p; | ||
| 8980 | - size_t dimensions; | ||
| 8981 | - size_t nvectors; | ||
| 8982 | - enum VectorElementType element_type; | ||
| 8983 | -}; | ||
| 8984 | - | ||
| 8985 | -typedef struct vec_static_blob_data vec_static_blob_data; | ||
| 8986 | -struct vec_static_blob_data { | ||
| 8987 | - static_blob static_blobs[MAX_STATIC_BLOBS]; | ||
| 8988 | -}; | ||
| 8989 | - | ||
| 8990 | -typedef struct vec_static_blobs_vtab vec_static_blobs_vtab; | ||
| 8991 | -struct vec_static_blobs_vtab { | ||
| 8992 | - sqlite3_vtab base; | ||
| 8993 | - vec_static_blob_data *data; | ||
| 8994 | -}; | ||
| 8995 | - | ||
| 8996 | -typedef struct vec_static_blobs_cursor vec_static_blobs_cursor; | ||
| 8997 | -struct vec_static_blobs_cursor { | ||
| 8998 | - sqlite3_vtab_cursor base; | ||
| 8999 | - sqlite3_int64 iRowid; | ||
| 9000 | -}; | ||
| 9001 | - | ||
| 9002 | -static int vec_static_blobsConnect(sqlite3 *db, void *pAux, int argc, | ||
| 9003 | - const char *const *argv, | ||
| 9004 | - sqlite3_vtab **ppVtab, char **pzErr) { | ||
| 9005 | - UNUSED_PARAMETER(argc); | ||
| 9006 | - UNUSED_PARAMETER(argv); | ||
| 9007 | - UNUSED_PARAMETER(pzErr); | ||
| 9008 | - | ||
| 9009 | - vec_static_blobs_vtab *pNew; | ||
| 9010 | -#define VEC_STATIC_BLOBS_NAME 0 | ||
| 9011 | -#define VEC_STATIC_BLOBS_DATA 1 | ||
| 9012 | -#define VEC_STATIC_BLOBS_DIMENSIONS 2 | ||
| 9013 | -#define VEC_STATIC_BLOBS_COUNT 3 | ||
| 9014 | - int rc = sqlite3_declare_vtab( | ||
| 9015 | - db, "CREATE TABLE x(name, data, dimensions hidden, count hidden)"); | ||
| 9016 | - if (rc == SQLITE_OK) { | ||
| 9017 | - pNew = sqlite3_malloc(sizeof(*pNew)); | ||
| 9018 | - *ppVtab = (sqlite3_vtab *)pNew; | ||
| 9019 | - if (pNew == 0) | ||
| 9020 | - return SQLITE_NOMEM; | ||
| 9021 | - memset(pNew, 0, sizeof(*pNew)); | ||
| 9022 | - pNew->data = pAux; | ||
| 9023 | - } | ||
| 9024 | - return rc; | ||
| 9025 | -} | ||
| 9026 | - | ||
| 9027 | -static int vec_static_blobsDisconnect(sqlite3_vtab *pVtab) { | ||
| 9028 | - vec_static_blobs_vtab *p = (vec_static_blobs_vtab *)pVtab; | ||
| 9029 | - sqlite3_free(p); | ||
| 9030 | - return SQLITE_OK; | ||
| 9031 | -} | ||
| 9032 | - | ||
| 9033 | -static int vec_static_blobsUpdate(sqlite3_vtab *pVTab, int argc, | ||
| 9034 | - sqlite3_value **argv, sqlite_int64 *pRowid) { | ||
| 9035 | - UNUSED_PARAMETER(pRowid); | ||
| 9036 | - vec_static_blobs_vtab *p = (vec_static_blobs_vtab *)pVTab; | ||
| 9037 | - // DELETE operation | ||
| 9038 | - if (argc == 1 && sqlite3_value_type(argv[0]) != SQLITE_NULL) { | ||
| 9039 | - return SQLITE_ERROR; | ||
| 9040 | - } | ||
| 9041 | - // INSERT operation | ||
| 9042 | - else if (argc > 1 && sqlite3_value_type(argv[0]) == SQLITE_NULL) { | ||
| 9043 | - const char *key = | ||
| 9044 | - (const char *)sqlite3_value_text(argv[2 + VEC_STATIC_BLOBS_NAME]); | ||
| 9045 | - int idx = -1; | ||
| 9046 | - for (int i = 0; i < MAX_STATIC_BLOBS; i++) { | ||
| 9047 | - if (!p->data->static_blobs[i].name) { | ||
| 9048 | - p->data->static_blobs[i].name = sqlite3_mprintf("%s", key); | ||
| 9049 | - idx = i; | ||
| 9050 | - break; | ||
| 9051 | - } | ||
| 9052 | - } | ||
| 9053 | - if (idx < 0) | ||
| 9054 | - abort(); | ||
| 9055 | - struct static_blob_definition *def = sqlite3_value_pointer( | ||
| 9056 | - argv[2 + VEC_STATIC_BLOBS_DATA], POINTER_NAME_STATIC_BLOB_DEF); | ||
| 9057 | - p->data->static_blobs[idx].p = def->p; | ||
| 9058 | - p->data->static_blobs[idx].dimensions = def->dimensions; | ||
| 9059 | - p->data->static_blobs[idx].nvectors = def->nvectors; | ||
| 9060 | - p->data->static_blobs[idx].element_type = def->element_type; | ||
| 9061 | - | ||
| 9062 | - return SQLITE_OK; | ||
| 9063 | - } | ||
| 9064 | - // UPDATE operation | ||
| 9065 | - else if (argc > 1 && sqlite3_value_type(argv[0]) != SQLITE_NULL) { | ||
| 9066 | - return SQLITE_ERROR; | ||
| 9067 | - } | ||
| 9068 | - return SQLITE_ERROR; | ||
| 9069 | -} | ||
| 9070 | - | ||
| 9071 | -static int vec_static_blobsOpen(sqlite3_vtab *p, | ||
| 9072 | - sqlite3_vtab_cursor **ppCursor) { | ||
| 9073 | - UNUSED_PARAMETER(p); | ||
| 9074 | - vec_static_blobs_cursor *pCur; | ||
| 9075 | - pCur = sqlite3_malloc(sizeof(*pCur)); | ||
| 9076 | - if (pCur == 0) | ||
| 9077 | - return SQLITE_NOMEM; | ||
| 9078 | - memset(pCur, 0, sizeof(*pCur)); | ||
| 9079 | - *ppCursor = &pCur->base; | ||
| 9080 | - return SQLITE_OK; | ||
| 9081 | -} | ||
| 9082 | - | ||
| 9083 | -static int vec_static_blobsClose(sqlite3_vtab_cursor *cur) { | ||
| 9084 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)cur; | ||
| 9085 | - sqlite3_free(pCur); | ||
| 9086 | - return SQLITE_OK; | ||
| 9087 | -} | ||
| 9088 | - | ||
| 9089 | -static int vec_static_blobsBestIndex(sqlite3_vtab *pVTab, | ||
| 9090 | - sqlite3_index_info *pIdxInfo) { | ||
| 9091 | - UNUSED_PARAMETER(pVTab); | ||
| 9092 | - pIdxInfo->idxNum = 1; | ||
| 9093 | - pIdxInfo->estimatedCost = (double)10; | ||
| 9094 | - pIdxInfo->estimatedRows = 10; | ||
| 9095 | - return SQLITE_OK; | ||
| 9096 | -} | ||
| 9097 | - | ||
| 9098 | -static int vec_static_blobsNext(sqlite3_vtab_cursor *cur); | ||
| 9099 | -static int vec_static_blobsFilter(sqlite3_vtab_cursor *pVtabCursor, int idxNum, | ||
| 9100 | - const char *idxStr, int argc, | ||
| 9101 | - sqlite3_value **argv) { | ||
| 9102 | - UNUSED_PARAMETER(idxNum); | ||
| 9103 | - UNUSED_PARAMETER(idxStr); | ||
| 9104 | - UNUSED_PARAMETER(argc); | ||
| 9105 | - UNUSED_PARAMETER(argv); | ||
| 9106 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)pVtabCursor; | ||
| 9107 | - pCur->iRowid = -1; | ||
| 9108 | - vec_static_blobsNext(pVtabCursor); | ||
| 9109 | - return SQLITE_OK; | ||
| 9110 | -} | ||
| 9111 | - | ||
| 9112 | -static int vec_static_blobsRowid(sqlite3_vtab_cursor *cur, | ||
| 9113 | - sqlite_int64 *pRowid) { | ||
| 9114 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)cur; | ||
| 9115 | - *pRowid = pCur->iRowid; | ||
| 9116 | - return SQLITE_OK; | ||
| 9117 | -} | ||
| 9118 | - | ||
| 9119 | -static int vec_static_blobsNext(sqlite3_vtab_cursor *cur) { | ||
| 9120 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)cur; | ||
| 9121 | - vec_static_blobs_vtab *p = (vec_static_blobs_vtab *)pCur->base.pVtab; | ||
| 9122 | - pCur->iRowid++; | ||
| 9123 | - while (pCur->iRowid < MAX_STATIC_BLOBS) { | ||
| 9124 | - if (p->data->static_blobs[pCur->iRowid].name) { | ||
| 9125 | - return SQLITE_OK; | ||
| 9126 | - } | ||
| 9127 | - pCur->iRowid++; | ||
| 9128 | - } | ||
| 9129 | - return SQLITE_OK; | ||
| 9130 | -} | ||
| 9131 | - | ||
| 9132 | -static int vec_static_blobsEof(sqlite3_vtab_cursor *cur) { | ||
| 9133 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)cur; | ||
| 9134 | - return pCur->iRowid >= MAX_STATIC_BLOBS; | ||
| 9135 | -} | ||
| 9136 | - | ||
| 9137 | -static int vec_static_blobsColumn(sqlite3_vtab_cursor *cur, | ||
| 9138 | - sqlite3_context *context, int i) { | ||
| 9139 | - vec_static_blobs_cursor *pCur = (vec_static_blobs_cursor *)cur; | ||
| 9140 | - vec_static_blobs_vtab *p = (vec_static_blobs_vtab *)cur->pVtab; | ||
| 9141 | - switch (i) { | ||
| 9142 | - case VEC_STATIC_BLOBS_NAME: | ||
| 9143 | - sqlite3_result_text(context, p->data->static_blobs[pCur->iRowid].name, -1, | ||
| 9144 | - SQLITE_TRANSIENT); | ||
| 9145 | - break; | ||
| 9146 | - case VEC_STATIC_BLOBS_DATA: | ||
| 9147 | - sqlite3_result_null(context); | ||
| 9148 | - break; | ||
| 9149 | - case VEC_STATIC_BLOBS_DIMENSIONS: | ||
| 9150 | - sqlite3_result_int64(context, | ||
| 9151 | - p->data->static_blobs[pCur->iRowid].dimensions); | ||
| 9152 | - break; | ||
| 9153 | - case VEC_STATIC_BLOBS_COUNT: | ||
| 9154 | - sqlite3_result_int64(context, p->data->static_blobs[pCur->iRowid].nvectors); | ||
| 9155 | - break; | ||
| 9156 | - } | ||
| 9157 | - return SQLITE_OK; | ||
| 9158 | -} | ||
| 9159 | - | ||
| 9160 | -static sqlite3_module vec_static_blobsModule = { | ||
| 9161 | - /* iVersion */ 3, | ||
| 9162 | - /* xCreate */ 0, | ||
| 9163 | - /* xConnect */ vec_static_blobsConnect, | ||
| 9164 | - /* xBestIndex */ vec_static_blobsBestIndex, | ||
| 9165 | - /* xDisconnect */ vec_static_blobsDisconnect, | ||
| 9166 | - /* xDestroy */ 0, | ||
| 9167 | - /* xOpen */ vec_static_blobsOpen, | ||
| 9168 | - /* xClose */ vec_static_blobsClose, | ||
| 9169 | - /* xFilter */ vec_static_blobsFilter, | ||
| 9170 | - /* xNext */ vec_static_blobsNext, | ||
| 9171 | - /* xEof */ vec_static_blobsEof, | ||
| 9172 | - /* xColumn */ vec_static_blobsColumn, | ||
| 9173 | - /* xRowid */ vec_static_blobsRowid, | ||
| 9174 | - /* xUpdate */ vec_static_blobsUpdate, | ||
| 9175 | - /* xBegin */ 0, | ||
| 9176 | - /* xSync */ 0, | ||
| 9177 | - /* xCommit */ 0, | ||
| 9178 | - /* xRollback */ 0, | ||
| 9179 | - /* xFindMethod */ 0, | ||
| 9180 | - /* xRename */ 0, | ||
| 9181 | - /* xSavepoint */ 0, | ||
| 9182 | - /* xRelease */ 0, | ||
| 9183 | - /* xRollbackTo */ 0, | ||
| 9184 | - /* xShadowName */ 0, | ||
| 9185 | -#if SQLITE_VERSION_NUMBER >= 3044000 | ||
| 9186 | - /* xIntegrity */ 0 | ||
| 9187 | -#endif | ||
| 9188 | -}; | ||
| 9189 | -#pragma endregion | ||
| 9190 | - | ||
| 9191 | -#pragma region vec_static_blob_entries() table function | ||
| 9192 | - | ||
| 9193 | -typedef struct vec_static_blob_entries_vtab vec_static_blob_entries_vtab; | ||
| 9194 | -struct vec_static_blob_entries_vtab { | ||
| 9195 | - sqlite3_vtab base; | ||
| 9196 | - static_blob *blob; | ||
| 9197 | -}; | ||
| 9198 | -typedef enum { | ||
| 9199 | - VEC_SBE__QUERYPLAN_FULLSCAN = 1, | ||
| 9200 | - VEC_SBE__QUERYPLAN_KNN = 2 | ||
| 9201 | -} vec_sbe_query_plan; | ||
| 9202 | - | ||
| 9203 | -struct sbe_query_knn_data { | ||
| 9204 | - i64 k; | ||
| 9205 | - i64 k_used; | ||
| 9206 | - // Array of rowids of size k. Must be freed with sqlite3_free(). | ||
| 9207 | - i32 *rowids; | ||
| 9208 | - // Array of distances of size k. Must be freed with sqlite3_free(). | ||
| 9209 | - f32 *distances; | ||
| 9210 | - i64 current_idx; | ||
| 9211 | -}; | ||
| 9212 | -void sbe_query_knn_data_clear(struct sbe_query_knn_data *knn_data) { | ||
| 9213 | - if (!knn_data) | ||
| 9214 | - return; | ||
| 9215 | - | ||
| 9216 | - if (knn_data->rowids) { | ||
| 9217 | - sqlite3_free(knn_data->rowids); | ||
| 9218 | - knn_data->rowids = NULL; | ||
| 9219 | - } | ||
| 9220 | - if (knn_data->distances) { | ||
| 9221 | - sqlite3_free(knn_data->distances); | ||
| 9222 | - knn_data->distances = NULL; | ||
| 9223 | - } | ||
| 9224 | -} | ||
| 9225 | - | ||
| 9226 | -typedef struct vec_static_blob_entries_cursor vec_static_blob_entries_cursor; | ||
| 9227 | -struct vec_static_blob_entries_cursor { | ||
| 9228 | - sqlite3_vtab_cursor base; | ||
| 9229 | - sqlite3_int64 iRowid; | ||
| 9230 | - vec_sbe_query_plan query_plan; | ||
| 9231 | - struct sbe_query_knn_data *knn_data; | ||
| 9232 | -}; | ||
| 9233 | - | ||
| 9234 | -static int vec_static_blob_entriesConnect(sqlite3 *db, void *pAux, int argc, | ||
| 9235 | - const char *const *argv, | ||
| 9236 | - sqlite3_vtab **ppVtab, char **pzErr) { | ||
| 9237 | - UNUSED_PARAMETER(argc); | ||
| 9238 | - UNUSED_PARAMETER(argv); | ||
| 9239 | - UNUSED_PARAMETER(pzErr); | ||
| 9240 | - vec_static_blob_data *blob_data = pAux; | ||
| 9241 | - int idx = -1; | ||
| 9242 | - for (int i = 0; i < MAX_STATIC_BLOBS; i++) { | ||
| 9243 | - if (!blob_data->static_blobs[i].name) | ||
| 9244 | - continue; | ||
| 9245 | - if (strncmp(blob_data->static_blobs[i].name, argv[3], | ||
| 9246 | - strlen(blob_data->static_blobs[i].name)) == 0) { | ||
| 9247 | - idx = i; | ||
| 9248 | - break; | ||
| 9249 | - } | ||
| 9250 | - } | ||
| 9251 | - if (idx < 0) | ||
| 9252 | - abort(); | ||
| 9253 | - vec_static_blob_entries_vtab *pNew; | ||
| 9254 | -#define VEC_STATIC_BLOB_ENTRIES_VECTOR 0 | ||
| 9255 | -#define VEC_STATIC_BLOB_ENTRIES_DISTANCE 1 | ||
| 9256 | -#define VEC_STATIC_BLOB_ENTRIES_K 2 | ||
| 9257 | - int rc = sqlite3_declare_vtab( | ||
| 9258 | - db, "CREATE TABLE x(vector, distance hidden, k hidden)"); | ||
| 9259 | - if (rc == SQLITE_OK) { | ||
| 9260 | - pNew = sqlite3_malloc(sizeof(*pNew)); | ||
| 9261 | - *ppVtab = (sqlite3_vtab *)pNew; | ||
| 9262 | - if (pNew == 0) | ||
| 9263 | - return SQLITE_NOMEM; | ||
| 9264 | - memset(pNew, 0, sizeof(*pNew)); | ||
| 9265 | - pNew->blob = &blob_data->static_blobs[idx]; | ||
| 9266 | - } | ||
| 9267 | - return rc; | ||
| 9268 | -} | ||
| 9269 | - | ||
| 9270 | -static int vec_static_blob_entriesCreate(sqlite3 *db, void *pAux, int argc, | ||
| 9271 | - const char *const *argv, | ||
| 9272 | - sqlite3_vtab **ppVtab, char **pzErr) { | ||
| 9273 | - return vec_static_blob_entriesConnect(db, pAux, argc, argv, ppVtab, pzErr); | ||
| 9274 | -} | ||
| 9275 | - | ||
| 9276 | -static int vec_static_blob_entriesDisconnect(sqlite3_vtab *pVtab) { | ||
| 9277 | - vec_static_blob_entries_vtab *p = (vec_static_blob_entries_vtab *)pVtab; | ||
| 9278 | - sqlite3_free(p); | ||
| 9279 | - return SQLITE_OK; | ||
| 9280 | -} | ||
| 9281 | - | ||
| 9282 | -static int vec_static_blob_entriesOpen(sqlite3_vtab *p, | ||
| 9283 | - sqlite3_vtab_cursor **ppCursor) { | ||
| 9284 | - UNUSED_PARAMETER(p); | ||
| 9285 | - vec_static_blob_entries_cursor *pCur; | ||
| 9286 | - pCur = sqlite3_malloc(sizeof(*pCur)); | ||
| 9287 | - if (pCur == 0) | ||
| 9288 | - return SQLITE_NOMEM; | ||
| 9289 | - memset(pCur, 0, sizeof(*pCur)); | ||
| 9290 | - *ppCursor = &pCur->base; | ||
| 9291 | - return SQLITE_OK; | ||
| 9292 | -} | ||
| 9293 | - | ||
| 9294 | -static int vec_static_blob_entriesClose(sqlite3_vtab_cursor *cur) { | ||
| 9295 | - vec_static_blob_entries_cursor *pCur = (vec_static_blob_entries_cursor *)cur; | ||
| 9296 | - sqlite3_free(pCur->knn_data); | ||
| 9297 | - sqlite3_free(pCur); | ||
| 9298 | - return SQLITE_OK; | ||
| 9299 | -} | ||
| 9300 | - | ||
| 9301 | -static int vec_static_blob_entriesBestIndex(sqlite3_vtab *pVTab, | ||
| 9302 | - sqlite3_index_info *pIdxInfo) { | ||
| 9303 | - vec_static_blob_entries_vtab *p = (vec_static_blob_entries_vtab *)pVTab; | ||
| 9304 | - int iMatchTerm = -1; | ||
| 9305 | - int iLimitTerm = -1; | ||
| 9306 | - // int iRowidTerm = -1; // https://github.com/asg017/sqlite-vec/issues/47 | ||
| 9307 | - int iKTerm = -1; | ||
| 9308 | - | ||
| 9309 | - for (int i = 0; i < pIdxInfo->nConstraint; i++) { | ||
| 9310 | - if (!pIdxInfo->aConstraint[i].usable) | ||
| 9311 | - continue; | ||
| 9312 | - | ||
| 9313 | - int iColumn = pIdxInfo->aConstraint[i].iColumn; | ||
| 9314 | - int op = pIdxInfo->aConstraint[i].op; | ||
| 9315 | - if (op == SQLITE_INDEX_CONSTRAINT_MATCH && | ||
| 9316 | - iColumn == VEC_STATIC_BLOB_ENTRIES_VECTOR) { | ||
| 9317 | - if (iMatchTerm > -1) { | ||
| 9318 | - // https://github.com/asg017/sqlite-vec/issues/51 | ||
| 9319 | - return SQLITE_ERROR; | ||
| 9320 | - } | ||
| 9321 | - iMatchTerm = i; | ||
| 9322 | - } | ||
| 9323 | - if (op == SQLITE_INDEX_CONSTRAINT_LIMIT) { | ||
| 9324 | - iLimitTerm = i; | ||
| 9325 | - } | ||
| 9326 | - if (op == SQLITE_INDEX_CONSTRAINT_EQ && | ||
| 9327 | - iColumn == VEC_STATIC_BLOB_ENTRIES_K) { | ||
| 9328 | - iKTerm = i; | ||
| 9329 | - } | ||
| 9330 | - } | ||
| 9331 | - if (iMatchTerm >= 0) { | ||
| 9332 | - if (iLimitTerm < 0 && iKTerm < 0) { | ||
| 9333 | - // https://github.com/asg017/sqlite-vec/issues/51 | ||
| 9334 | - return SQLITE_ERROR; | ||
| 9335 | - } | ||
| 9336 | - if (iLimitTerm >= 0 && iKTerm >= 0) { | ||
| 9337 | - return SQLITE_ERROR; // limit or k, not both | ||
| 9338 | - } | ||
| 9339 | - if (pIdxInfo->nOrderBy < 1) { | ||
| 9340 | - vtab_set_error(pVTab, "ORDER BY distance required"); | ||
| 9341 | - return SQLITE_CONSTRAINT; | ||
| 9342 | - } | ||
| 9343 | - if (pIdxInfo->nOrderBy > 1) { | ||
| 9344 | - // https://github.com/asg017/sqlite-vec/issues/51 | ||
| 9345 | - vtab_set_error(pVTab, "more than 1 ORDER BY clause provided"); | ||
| 9346 | - return SQLITE_CONSTRAINT; | ||
| 9347 | - } | ||
| 9348 | - if (pIdxInfo->aOrderBy[0].iColumn != VEC_STATIC_BLOB_ENTRIES_DISTANCE) { | ||
| 9349 | - vtab_set_error(pVTab, "ORDER BY must be on the distance column"); | ||
| 9350 | - return SQLITE_CONSTRAINT; | ||
| 9351 | - } | ||
| 9352 | - if (pIdxInfo->aOrderBy[0].desc) { | ||
| 9353 | - vtab_set_error(pVTab, | ||
| 9354 | - "Only ascending in ORDER BY distance clause is supported, " | ||
| 9355 | - "DESC is not supported yet."); | ||
| 9356 | - return SQLITE_CONSTRAINT; | ||
| 9357 | - } | ||
| 9358 | - | ||
| 9359 | - pIdxInfo->idxNum = VEC_SBE__QUERYPLAN_KNN; | ||
| 9360 | - pIdxInfo->estimatedCost = (double)10; | ||
| 9361 | - pIdxInfo->estimatedRows = 10; | ||
| 9362 | - | ||
| 9363 | - pIdxInfo->orderByConsumed = 1; | ||
| 9364 | - pIdxInfo->aConstraintUsage[iMatchTerm].argvIndex = 1; | ||
| 9365 | - pIdxInfo->aConstraintUsage[iMatchTerm].omit = 1; | ||
| 9366 | - if (iLimitTerm >= 0) { | ||
| 9367 | - pIdxInfo->aConstraintUsage[iLimitTerm].argvIndex = 2; | ||
| 9368 | - pIdxInfo->aConstraintUsage[iLimitTerm].omit = 1; | ||
| 9369 | - } else { | ||
| 9370 | - pIdxInfo->aConstraintUsage[iKTerm].argvIndex = 2; | ||
| 9371 | - pIdxInfo->aConstraintUsage[iKTerm].omit = 1; | ||
| 9372 | - } | ||
| 9373 | - | ||
| 9374 | - } else { | ||
| 9375 | - pIdxInfo->idxNum = VEC_SBE__QUERYPLAN_FULLSCAN; | ||
| 9376 | - pIdxInfo->estimatedCost = (double)p->blob->nvectors; | ||
| 9377 | - pIdxInfo->estimatedRows = p->blob->nvectors; | ||
| 9378 | - } | ||
| 9379 | - return SQLITE_OK; | ||
| 9380 | -} | ||
| 9381 | - | ||
| 9382 | -static int vec_static_blob_entriesFilter(sqlite3_vtab_cursor *pVtabCursor, | ||
| 9383 | - int idxNum, const char *idxStr, | ||
| 9384 | - int argc, sqlite3_value **argv) { | ||
| 9385 | - UNUSED_PARAMETER(idxStr); | ||
| 9386 | - assert(argc >= 0 && argc <= 3); | ||
| 9387 | - vec_static_blob_entries_cursor *pCur = | ||
| 9388 | - (vec_static_blob_entries_cursor *)pVtabCursor; | ||
| 9389 | - vec_static_blob_entries_vtab *p = | ||
| 9390 | - (vec_static_blob_entries_vtab *)pCur->base.pVtab; | ||
| 9391 | - | ||
| 9392 | - if (idxNum == VEC_SBE__QUERYPLAN_KNN) { | ||
| 9393 | - assert(argc == 2); | ||
| 9394 | - pCur->query_plan = VEC_SBE__QUERYPLAN_KNN; | ||
| 9395 | - struct sbe_query_knn_data *knn_data; | ||
| 9396 | - knn_data = sqlite3_malloc(sizeof(*knn_data)); | ||
| 9397 | - if (!knn_data) { | ||
| 9398 | - return SQLITE_NOMEM; | ||
| 9399 | - } | ||
| 9400 | - memset(knn_data, 0, sizeof(*knn_data)); | ||
| 9401 | - | ||
| 9402 | - void *queryVector; | ||
| 9403 | - size_t dimensions; | ||
| 9404 | - enum VectorElementType elementType; | ||
| 9405 | - vector_cleanup cleanup; | ||
| 9406 | - char *err; | ||
| 9407 | - int rc = vector_from_value(argv[0], &queryVector, &dimensions, &elementType, | ||
| 9408 | - &cleanup, &err); | ||
| 9409 | - if (rc != SQLITE_OK) { | ||
| 9410 | - return SQLITE_ERROR; | ||
| 9411 | - } | ||
| 9412 | - if (elementType != p->blob->element_type) { | ||
| 9413 | - return SQLITE_ERROR; | ||
| 9414 | - } | ||
| 9415 | - if (dimensions != p->blob->dimensions) { | ||
| 9416 | - return SQLITE_ERROR; | ||
| 9417 | - } | ||
| 9418 | - | ||
| 9419 | - i64 k = min(sqlite3_value_int64(argv[1]), (i64)p->blob->nvectors); | ||
| 9420 | - if (k < 0) { | ||
| 9421 | - // HANDLE https://github.com/asg017/sqlite-vec/issues/55 | ||
| 9422 | - return SQLITE_ERROR; | ||
| 9423 | - } | ||
| 9424 | - if (k == 0) { | ||
| 9425 | - knn_data->k = 0; | ||
| 9426 | - pCur->knn_data = knn_data; | ||
| 9427 | - return SQLITE_OK; | ||
| 9428 | - } | ||
| 9429 | - | ||
| 9430 | - size_t bsize = (p->blob->nvectors + 7) & ~7; | ||
| 9431 | - | ||
| 9432 | - i32 *topk_rowids = sqlite3_malloc(k * sizeof(i32)); | ||
| 9433 | - if (!topk_rowids) { | ||
| 9434 | - // HANDLE https://github.com/asg017/sqlite-vec/issues/55 | ||
| 9435 | - return SQLITE_ERROR; | ||
| 9436 | - } | ||
| 9437 | - f32 *distances = sqlite3_malloc(bsize * sizeof(f32)); | ||
| 9438 | - if (!distances) { | ||
| 9439 | - // HANDLE https://github.com/asg017/sqlite-vec/issues/55 | ||
| 9440 | - return SQLITE_ERROR; | ||
| 9441 | - } | ||
| 9442 | - | ||
| 9443 | - for (size_t i = 0; i < p->blob->nvectors; i++) { | ||
| 9444 | - // https://github.com/asg017/sqlite-vec/issues/52 | ||
| 9445 | - float *v = ((float *)p->blob->p) + (i * p->blob->dimensions); | ||
| 9446 | - distances[i] = | ||
| 9447 | - distance_l2_sqr_float(v, (float *)queryVector, &p->blob->dimensions); | ||
| 9448 | - } | ||
| 9449 | - u8 *candidates = bitmap_new(bsize); | ||
| 9450 | - assert(candidates); | ||
| 9451 | - | ||
| 9452 | - u8 *taken = bitmap_new(bsize); | ||
| 9453 | - assert(taken); | ||
| 9454 | - | ||
| 9455 | - bitmap_fill(candidates, bsize); | ||
| 9456 | - for (size_t i = bsize; i >= p->blob->nvectors; i--) { | ||
| 9457 | - bitmap_set(candidates, i, 0); | ||
| 9458 | - } | ||
| 9459 | - i32 k_used = 0; | ||
| 9460 | - min_idx(distances, bsize, candidates, topk_rowids, k, taken, &k_used); | ||
| 9461 | - knn_data->current_idx = 0; | ||
| 9462 | - knn_data->distances = distances; | ||
| 9463 | - knn_data->k = k; | ||
| 9464 | - knn_data->rowids = topk_rowids; | ||
| 9465 | - | ||
| 9466 | - pCur->knn_data = knn_data; | ||
| 9467 | - } else { | ||
| 9468 | - pCur->query_plan = VEC_SBE__QUERYPLAN_FULLSCAN; | ||
| 9469 | - pCur->iRowid = 0; | ||
| 9470 | - } | ||
| 9471 | - | ||
| 9472 | - return SQLITE_OK; | ||
| 9473 | -} | ||
| 9474 | - | ||
| 9475 | -static int vec_static_blob_entriesRowid(sqlite3_vtab_cursor *cur, | ||
| 9476 | - sqlite_int64 *pRowid) { | ||
| 9477 | - vec_static_blob_entries_cursor *pCur = (vec_static_blob_entries_cursor *)cur; | ||
| 9478 | - switch (pCur->query_plan) { | ||
| 9479 | - case VEC_SBE__QUERYPLAN_FULLSCAN: { | ||
| 9480 | - *pRowid = pCur->iRowid; | ||
| 9481 | - return SQLITE_OK; | ||
| 9482 | - } | ||
| 9483 | - case VEC_SBE__QUERYPLAN_KNN: { | ||
| 9484 | - i32 rowid = ((i32 *)pCur->knn_data->rowids)[pCur->knn_data->current_idx]; | ||
| 9485 | - *pRowid = (sqlite3_int64)rowid; | ||
| 9486 | - return SQLITE_OK; | ||
| 9487 | - } | ||
| 9488 | - } | ||
| 9489 | - return SQLITE_ERROR; | ||
| 9490 | -} | ||
| 9491 | - | ||
| 9492 | -static int vec_static_blob_entriesNext(sqlite3_vtab_cursor *cur) { | ||
| 9493 | - vec_static_blob_entries_cursor *pCur = (vec_static_blob_entries_cursor *)cur; | ||
| 9494 | - switch (pCur->query_plan) { | ||
| 9495 | - case VEC_SBE__QUERYPLAN_FULLSCAN: { | ||
| 9496 | - pCur->iRowid++; | ||
| 9497 | - return SQLITE_OK; | ||
| 9498 | - } | ||
| 9499 | - case VEC_SBE__QUERYPLAN_KNN: { | ||
| 9500 | - pCur->knn_data->current_idx++; | ||
| 9501 | - return SQLITE_OK; | ||
| 9502 | - } | ||
| 9503 | - } | ||
| 9504 | - return SQLITE_ERROR; | ||
| 9505 | -} | ||
| 9506 | - | ||
| 9507 | -static int vec_static_blob_entriesEof(sqlite3_vtab_cursor *cur) { | ||
| 9508 | - vec_static_blob_entries_cursor *pCur = (vec_static_blob_entries_cursor *)cur; | ||
| 9509 | - vec_static_blob_entries_vtab *p = | ||
| 9510 | - (vec_static_blob_entries_vtab *)pCur->base.pVtab; | ||
| 9511 | - switch (pCur->query_plan) { | ||
| 9512 | - case VEC_SBE__QUERYPLAN_FULLSCAN: { | ||
| 9513 | - return (size_t)pCur->iRowid >= p->blob->nvectors; | ||
| 9514 | - } | ||
| 9515 | - case VEC_SBE__QUERYPLAN_KNN: { | ||
| 9516 | - return pCur->knn_data->current_idx >= pCur->knn_data->k; | ||
| 9517 | - } | ||
| 9518 | - } | ||
| 9519 | - return SQLITE_ERROR; | ||
| 9520 | -} | ||
| 9521 | - | ||
| 9522 | -static int vec_static_blob_entriesColumn(sqlite3_vtab_cursor *cur, | ||
| 9523 | - sqlite3_context *context, int i) { | ||
| 9524 | - vec_static_blob_entries_cursor *pCur = (vec_static_blob_entries_cursor *)cur; | ||
| 9525 | - vec_static_blob_entries_vtab *p = (vec_static_blob_entries_vtab *)cur->pVtab; | ||
| 9526 | - | ||
| 9527 | - switch (pCur->query_plan) { | ||
| 9528 | - case VEC_SBE__QUERYPLAN_FULLSCAN: { | ||
| 9529 | - switch (i) { | ||
| 9530 | - case VEC_STATIC_BLOB_ENTRIES_VECTOR: | ||
| 9531 | - | ||
| 9532 | - sqlite3_result_blob( | ||
| 9533 | - context, | ||
| 9534 | - ((unsigned char *)p->blob->p) + | ||
| 9535 | - (pCur->iRowid * p->blob->dimensions * sizeof(float)), | ||
| 9536 | - p->blob->dimensions * sizeof(float), SQLITE_TRANSIENT); | ||
| 9537 | - sqlite3_result_subtype(context, p->blob->element_type); | ||
| 9538 | - break; | ||
| 9539 | - } | ||
| 9540 | - return SQLITE_OK; | ||
| 9541 | - } | ||
| 9542 | - case VEC_SBE__QUERYPLAN_KNN: { | ||
| 9543 | - switch (i) { | ||
| 9544 | - case VEC_STATIC_BLOB_ENTRIES_VECTOR: { | ||
| 9545 | - i32 rowid = ((i32 *)pCur->knn_data->rowids)[pCur->knn_data->current_idx]; | ||
| 9546 | - sqlite3_result_blob(context, | ||
| 9547 | - ((unsigned char *)p->blob->p) + | ||
| 9548 | - (rowid * p->blob->dimensions * sizeof(float)), | ||
| 9549 | - p->blob->dimensions * sizeof(float), | ||
| 9550 | - SQLITE_TRANSIENT); | ||
| 9551 | - sqlite3_result_subtype(context, p->blob->element_type); | ||
| 9552 | - break; | ||
| 9553 | - } | ||
| 9554 | - } | ||
| 9555 | - return SQLITE_OK; | ||
| 9556 | - } | ||
| 9557 | - } | ||
| 9558 | - return SQLITE_ERROR; | ||
| 9559 | -} | ||
| 9560 | - | ||
| 9561 | -static sqlite3_module vec_static_blob_entriesModule = { | ||
| 9562 | - /* iVersion */ 3, | ||
| 9563 | - /* xCreate */ | ||
| 9564 | - vec_static_blob_entriesCreate, // handle rm? | ||
| 9565 | - // https://github.com/asg017/sqlite-vec/issues/55 | ||
| 9566 | - /* xConnect */ vec_static_blob_entriesConnect, | ||
| 9567 | - /* xBestIndex */ vec_static_blob_entriesBestIndex, | ||
| 9568 | - /* xDisconnect */ vec_static_blob_entriesDisconnect, | ||
| 9569 | - /* xDestroy */ vec_static_blob_entriesDisconnect, | ||
| 9570 | - /* xOpen */ vec_static_blob_entriesOpen, | ||
| 9571 | - /* xClose */ vec_static_blob_entriesClose, | ||
| 9572 | - /* xFilter */ vec_static_blob_entriesFilter, | ||
| 9573 | - /* xNext */ vec_static_blob_entriesNext, | ||
| 9574 | - /* xEof */ vec_static_blob_entriesEof, | ||
| 9575 | - /* xColumn */ vec_static_blob_entriesColumn, | ||
| 9576 | - /* xRowid */ vec_static_blob_entriesRowid, | ||
| 9577 | - /* xUpdate */ 0, | ||
| 9578 | - /* xBegin */ 0, | ||
| 9579 | - /* xSync */ 0, | ||
| 9580 | - /* xCommit */ 0, | ||
| 9581 | - /* xRollback */ 0, | ||
| 9582 | - /* xFindMethod */ 0, | ||
| 9583 | - /* xRename */ 0, | ||
| 9584 | - /* xSavepoint */ 0, | ||
| 9585 | - /* xRelease */ 0, | ||
| 9586 | - /* xRollbackTo */ 0, | ||
| 9587 | - /* xShadowName */ 0, | ||
| 9588 | -#if SQLITE_VERSION_NUMBER >= 3044000 | ||
| 9589 | - /* xIntegrity */ 0 | ||
| 9590 | -#endif | ||
| 9591 | -}; | ||
| 9592 | -#pragma endregion | ||
| 9593 | - | ||
| 9594 | -#ifdef SQLITE_VEC_ENABLE_AVX | ||
| 9595 | -#define SQLITE_VEC_DEBUG_BUILD_AVX "avx" | ||
| 9596 | -#else | ||
| 9597 | -#define SQLITE_VEC_DEBUG_BUILD_AVX "" | ||
| 9598 | -#endif | ||
| 9599 | -#ifdef SQLITE_VEC_ENABLE_NEON | ||
| 9600 | -#define SQLITE_VEC_DEBUG_BUILD_NEON "neon" | ||
| 9601 | -#else | ||
| 9602 | -#define SQLITE_VEC_DEBUG_BUILD_NEON "" | ||
| 9603 | -#endif | ||
| 9604 | - | ||
| 9605 | -#define SQLITE_VEC_DEBUG_BUILD \ | ||
| 9606 | - SQLITE_VEC_DEBUG_BUILD_AVX " " SQLITE_VEC_DEBUG_BUILD_NEON | ||
| 9607 | - | ||
| 9608 | -#define SQLITE_VEC_DEBUG_STRING \ | ||
| 9609 | - "Version: " SQLITE_VEC_VERSION "\n" \ | ||
| 9610 | - "Date: " SQLITE_VEC_DATE "\n" \ | ||
| 9611 | - "Commit: " SQLITE_VEC_SOURCE "\n" \ | ||
| 9612 | - "Build flags: " SQLITE_VEC_DEBUG_BUILD | ||
| 9613 | - | ||
| 9614 | -SQLITE_VEC_API int sqlite3_vec_init(sqlite3 *db, char **pzErrMsg, | ||
| 9615 | - const sqlite3_api_routines *pApi) { | ||
| 9616 | -#ifndef SQLITE_CORE | ||
| 9617 | - SQLITE_EXTENSION_INIT2(pApi); | ||
| 9618 | -#endif | ||
| 9619 | - int rc = SQLITE_OK; | ||
| 9620 | - | ||
| 9621 | -#define DEFAULT_FLAGS (SQLITE_UTF8 | SQLITE_INNOCUOUS | SQLITE_DETERMINISTIC) | ||
| 9622 | - | ||
| 9623 | - rc = sqlite3_create_function_v2(db, "vec_version", 0, DEFAULT_FLAGS, | ||
| 9624 | - SQLITE_VEC_VERSION, _static_text_func, NULL, | ||
| 9625 | - NULL, NULL); | ||
| 9626 | - if (rc != SQLITE_OK) { | ||
| 9627 | - return rc; | ||
| 9628 | - } | ||
| 9629 | - rc = sqlite3_create_function_v2(db, "vec_debug", 0, DEFAULT_FLAGS, | ||
| 9630 | - SQLITE_VEC_DEBUG_STRING, _static_text_func, | ||
| 9631 | - NULL, NULL, NULL); | ||
| 9632 | - if (rc != SQLITE_OK) { | ||
| 9633 | - return rc; | ||
| 9634 | - } | ||
| 9635 | - static struct { | ||
| 9636 | - const char *zFName; | ||
| 9637 | - void (*xFunc)(sqlite3_context *, int, sqlite3_value **); | ||
| 9638 | - int nArg; | ||
| 9639 | - int flags; | ||
| 9640 | - } aFunc[] = { | ||
| 9641 | - // clang-format off | ||
| 9642 | - //{"vec_version", _static_text_func, 0, DEFAULT_FLAGS, (void *) SQLITE_VEC_VERSION }, | ||
| 9643 | - //{"vec_debug", _static_text_func, 0, DEFAULT_FLAGS, (void *) SQLITE_VEC_DEBUG_STRING }, | ||
| 9644 | - {"vec_distance_l2", vec_distance_l2, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE, }, | ||
| 9645 | - {"vec_distance_l1", vec_distance_l1, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE, }, | ||
| 9646 | - {"vec_distance_hamming",vec_distance_hamming, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE, }, | ||
| 9647 | - {"vec_distance_cosine", vec_distance_cosine, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE, }, | ||
| 9648 | - {"vec_length", vec_length, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE, }, | ||
| 9649 | - {"vec_type", vec_type, 1, DEFAULT_FLAGS, }, | ||
| 9650 | - {"vec_to_json", vec_to_json, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | ||
| 9651 | - {"vec_add", vec_add, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | ||
| 9652 | - {"vec_sub", vec_sub, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | ||
| 9653 | - {"vec_slice", vec_slice, 3, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | ||
| 9654 | - {"vec_normalize", vec_normalize, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | ||
| 9655 | - {"vec_f32", vec_f32, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | ||
| 9656 | - {"vec_bit", vec_bit, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | ||
| 9657 | - {"vec_int8", vec_int8, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | ||
| 9658 | - {"vec_quantize_int8", vec_quantize_int8, 2, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | ||
| 9659 | - {"vec_quantize_binary", vec_quantize_binary, 1, DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, }, | ||
| 9660 | - // clang-format on | ||
| 9661 | - }; | ||
| 9662 | - | ||
| 9663 | - static struct { | ||
| 9664 | - char *name; | ||
| 9665 | - const sqlite3_module *module; | ||
| 9666 | - void *p; | ||
| 9667 | - void (*xDestroy)(void *); | ||
| 9668 | - } aMod[] = { | ||
| 9669 | - // clang-format off | ||
| 9670 | - {"vec0", &vec0Module, NULL, NULL}, | ||
| 9671 | - {"vec_each", &vec_eachModule, NULL, NULL}, | ||
| 9672 | - // clang-format on | ||
| 9673 | - }; | ||
| 9674 | - | ||
| 9675 | - for (unsigned long i = 0; i < countof(aFunc) && rc == SQLITE_OK; i++) { | ||
| 9676 | - rc = sqlite3_create_function_v2(db, aFunc[i].zFName, aFunc[i].nArg, | ||
| 9677 | - aFunc[i].flags, NULL, aFunc[i].xFunc, NULL, | ||
| 9678 | - NULL, NULL); | ||
| 9679 | - if (rc != SQLITE_OK) { | ||
| 9680 | - *pzErrMsg = sqlite3_mprintf("Error creating function %s: %s", | ||
| 9681 | - aFunc[i].zFName, sqlite3_errmsg(db)); | ||
| 9682 | - return rc; | ||
| 9683 | - } | ||
| 9684 | - } | ||
| 9685 | - | ||
| 9686 | - for (unsigned long i = 0; i < countof(aMod) && rc == SQLITE_OK; i++) { | ||
| 9687 | - rc = sqlite3_create_module_v2(db, aMod[i].name, aMod[i].module, NULL, NULL); | ||
| 9688 | - if (rc != SQLITE_OK) { | ||
| 9689 | - *pzErrMsg = sqlite3_mprintf("Error creating module %s: %s", aMod[i].name, | ||
| 9690 | - sqlite3_errmsg(db)); | ||
| 9691 | - return rc; | ||
| 9692 | - } | ||
| 9693 | - } | ||
| 9694 | - | ||
| 9695 | - return SQLITE_OK; | ||
| 9696 | -} | ||
| 9697 | - | ||
| 9698 | -#ifndef SQLITE_VEC_OMIT_FS | ||
| 9699 | -SQLITE_VEC_API int sqlite3_vec_numpy_init(sqlite3 *db, char **pzErrMsg, | ||
| 9700 | - const sqlite3_api_routines *pApi) { | ||
| 9701 | - UNUSED_PARAMETER(pzErrMsg); | ||
| 9702 | -#ifndef SQLITE_CORE | ||
| 9703 | - SQLITE_EXTENSION_INIT2(pApi); | ||
| 9704 | -#endif | ||
| 9705 | - int rc = SQLITE_OK; | ||
| 9706 | - rc = sqlite3_create_function_v2(db, "vec_npy_file", 1, SQLITE_RESULT_SUBTYPE, | ||
| 9707 | - NULL, vec_npy_file, NULL, NULL, NULL); | ||
| 9708 | - if(rc != SQLITE_OK) { | ||
| 9709 | - return rc; | ||
| 9710 | - } | ||
| 9711 | - rc = sqlite3_create_module_v2(db, "vec_npy_each", &vec_npy_eachModule, NULL, NULL); | ||
| 9712 | - return rc; | ||
| 9713 | -} | ||
| 9714 | -#endif | ||
| 9715 | - | ||
| 9716 | -SQLITE_VEC_API int | ||
| 9717 | -sqlite3_vec_static_blobs_init(sqlite3 *db, char **pzErrMsg, | ||
| 9718 | - const sqlite3_api_routines *pApi) { | ||
| 9719 | - UNUSED_PARAMETER(pzErrMsg); | ||
| 9720 | -#ifndef SQLITE_CORE | ||
| 9721 | - SQLITE_EXTENSION_INIT2(pApi); | ||
| 9722 | -#endif | ||
| 9723 | - | ||
| 9724 | - int rc = SQLITE_OK; | ||
| 9725 | - vec_static_blob_data *static_blob_data; | ||
| 9726 | - static_blob_data = sqlite3_malloc(sizeof(*static_blob_data)); | ||
| 9727 | - if (!static_blob_data) { | ||
| 9728 | - return SQLITE_NOMEM; | ||
| 9729 | - } | ||
| 9730 | - memset(static_blob_data, 0, sizeof(*static_blob_data)); | ||
| 9731 | - | ||
| 9732 | - rc = sqlite3_create_function_v2( | ||
| 9733 | - db, "vec_static_blob_from_raw", 4, | ||
| 9734 | - DEFAULT_FLAGS | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, NULL, | ||
| 9735 | - vec_static_blob_from_raw, NULL, NULL, NULL); | ||
| 9736 | - if (rc != SQLITE_OK) | ||
| 9737 | - return rc; | ||
| 9738 | - | ||
| 9739 | - rc = sqlite3_create_module_v2(db, "vec_static_blobs", &vec_static_blobsModule, | ||
| 9740 | - static_blob_data, sqlite3_free); | ||
| 9741 | - if (rc != SQLITE_OK) | ||
| 9742 | - return rc; | ||
| 9743 | - rc = sqlite3_create_module_v2(db, "vec_static_blob_entries", | ||
| 9744 | - &vec_static_blob_entriesModule, | ||
| 9745 | - static_blob_data, NULL); | ||
| 9746 | - if (rc != SQLITE_OK) | ||
| 9747 | - return rc; | ||
| 9748 | - return rc; | ||
| 9749 | -} | ||
deleted
spike/nim/vendor/sqlite-vec.h +0 -41 | deleted file mode 100644 | ||
| @@ -1,41 +0,0 @@ | ||
| 1 | -#ifndef SQLITE_VEC_H | |
| 2 | -#define SQLITE_VEC_H | |
| 3 | - | |
| 4 | -#ifndef SQLITE_CORE | |
| 5 | -#include "sqlite3ext.h" | |
| 6 | -#else | |
| 7 | -#include "sqlite3.h" | |
| 8 | -#endif | |
| 9 | - | |
| 10 | -#ifdef SQLITE_VEC_STATIC | |
| 11 | - #define SQLITE_VEC_API | |
| 12 | -#else | |
| 13 | - #ifdef _WIN32 | |
| 14 | - #define SQLITE_VEC_API __declspec(dllexport) | |
| 15 | - #else | |
| 16 | - #define SQLITE_VEC_API | |
| 17 | - #endif | |
| 18 | -#endif | |
| 19 | - | |
| 20 | -#define SQLITE_VEC_VERSION "v0.1.6" | |
| 21 | -// TODO rm | |
| 22 | -#define SQLITE_VEC_DATE "2024-11-20T16:38:29Z+0000" | |
| 23 | -#define SQLITE_VEC_SOURCE "639fca5739fe056fdc98f3d539c4cd79328d7dc7" | |
| 24 | - | |
| 25 | - | |
| 26 | -#define SQLITE_VEC_VERSION_MAJOR 0 | |
| 27 | -#define SQLITE_VEC_VERSION_MINOR 1 | |
| 28 | -#define SQLITE_VEC_VERSION_PATCH 6 | |
| 29 | - | |
| 30 | -#ifdef __cplusplus | |
| 31 | -extern "C" { | |
| 32 | -#endif | |
| 33 | - | |
| 34 | -SQLITE_VEC_API int sqlite3_vec_init(sqlite3 *db, char **pzErrMsg, | |
| 35 | - const sqlite3_api_routines *pApi); | |
| 36 | - | |
| 37 | -#ifdef __cplusplus | |
| 38 | -} /* end of the 'extern "C"' block */ | |
| 39 | -#endif | |
| 40 | - | |
| 41 | -#endif /* ifndef SQLITE_VEC_H */ | |
| deleted file mode 100644 | |||
| @@ -1,41 +0,0 @@ | |||
| 1 | -#ifndef SQLITE_VEC_H | ||
| 2 | -#define SQLITE_VEC_H | ||
| 3 | - | ||
| 4 | -#ifndef SQLITE_CORE | ||
| 5 | -#include "sqlite3ext.h" | ||
| 6 | -#else | ||
| 7 | -#include "sqlite3.h" | ||
| 8 | -#endif | ||
| 9 | - | ||
| 10 | -#ifdef SQLITE_VEC_STATIC | ||
| 11 | - #define SQLITE_VEC_API | ||
| 12 | -#else | ||
| 13 | - #ifdef _WIN32 | ||
| 14 | - #define SQLITE_VEC_API __declspec(dllexport) | ||
| 15 | - #else | ||
| 16 | - #define SQLITE_VEC_API | ||
| 17 | - #endif | ||
| 18 | -#endif | ||
| 19 | - | ||
| 20 | -#define SQLITE_VEC_VERSION "v0.1.6" | ||
| 21 | -// TODO rm | ||
| 22 | -#define SQLITE_VEC_DATE "2024-11-20T16:38:29Z+0000" | ||
| 23 | -#define SQLITE_VEC_SOURCE "639fca5739fe056fdc98f3d539c4cd79328d7dc7" | ||
| 24 | - | ||
| 25 | - | ||
| 26 | -#define SQLITE_VEC_VERSION_MAJOR 0 | ||
| 27 | -#define SQLITE_VEC_VERSION_MINOR 1 | ||
| 28 | -#define SQLITE_VEC_VERSION_PATCH 6 | ||
| 29 | - | ||
| 30 | -#ifdef __cplusplus | ||
| 31 | -extern "C" { | ||
| 32 | -#endif | ||
| 33 | - | ||
| 34 | -SQLITE_VEC_API int sqlite3_vec_init(sqlite3 *db, char **pzErrMsg, | ||
| 35 | - const sqlite3_api_routines *pApi); | ||
| 36 | - | ||
| 37 | -#ifdef __cplusplus | ||
| 38 | -} /* end of the 'extern "C"' block */ | ||
| 39 | -#endif | ||
| 40 | - | ||
| 41 | -#endif /* ifndef SQLITE_VEC_H */ | ||
deleted
spike/nim/verify/go.mod +0 -3 | deleted file mode 100644 | ||
| @@ -1,3 +0,0 @@ | ||
| 1 | -module glean.spike/verify | |
| 2 | - | |
| 3 | -go 1.26.2 | |
| deleted file mode 100644 | |||
| @@ -1,3 +0,0 @@ | |||
| 1 | -module glean.spike/verify | ||
| 2 | - | ||
| 3 | -go 1.26.2 | ||
deleted
spike/nim/verify/verify.go +0 -178 | deleted file mode 100644 | ||
| @@ -1,178 +0,0 @@ | ||
| 1 | -// Independent check on the Nim DPoP spike. | |
| 2 | -// | |
| 3 | -// Self-verification in the same library would only prove internal | |
| 4 | -// consistency. This re-implements the verifier side from Go's standard | |
| 5 | -// library -- the same stack the current server runs on -- so agreement here | |
| 6 | -// means the Nim output is genuinely interoperable, not merely self-coherent. | |
| 7 | -// | |
| 8 | -// ./spike/nim/dpop_probe | go run ./verify | |
| 9 | -package main | |
| 10 | - | |
| 11 | -import ( | |
| 12 | - "crypto/ecdsa" | |
| 13 | - "crypto/elliptic" | |
| 14 | - "crypto/sha256" | |
| 15 | - "encoding/base64" | |
| 16 | - "encoding/json" | |
| 17 | - "fmt" | |
| 18 | - "io" | |
| 19 | - "math/big" | |
| 20 | - "os" | |
| 21 | - "strings" | |
| 22 | -) | |
| 23 | - | |
| 24 | -type jwk struct { | |
| 25 | - Crv string `json:"crv"` | |
| 26 | - Kty string `json:"kty"` | |
| 27 | - X string `json:"x"` | |
| 28 | - Y string `json:"y"` | |
| 29 | -} | |
| 30 | - | |
| 31 | -type spikeOutput struct { | |
| 32 | - JWT string `json:"jwt"` | |
| 33 | - JWK jwk `json:"jwk"` | |
| 34 | - Thumbprint string `json:"thumbprint"` | |
| 35 | -} | |
| 36 | - | |
| 37 | -var failures int | |
| 38 | - | |
| 39 | -func check(name string, ok bool, detail string) { | |
| 40 | - label := "PASS" | |
| 41 | - if !ok { | |
| 42 | - label = "FAIL" | |
| 43 | - failures++ | |
| 44 | - } | |
| 45 | - if detail != "" { | |
| 46 | - fmt.Printf(" %s %s -- %s\n", label, name, detail) | |
| 47 | - return | |
| 48 | - } | |
| 49 | - fmt.Printf(" %s %s\n", label, name) | |
| 50 | -} | |
| 51 | - | |
| 52 | -func main() { | |
| 53 | - raw, err := io.ReadAll(os.Stdin) | |
| 54 | - if err != nil { | |
| 55 | - fmt.Fprintln(os.Stderr, "read stdin:", err) | |
| 56 | - os.Exit(2) | |
| 57 | - } | |
| 58 | - var out spikeOutput | |
| 59 | - if err := json.Unmarshal(raw, &out); err != nil { | |
| 60 | - fmt.Fprintln(os.Stderr, "parse spike output:", err) | |
| 61 | - os.Exit(2) | |
| 62 | - } | |
| 63 | - | |
| 64 | - fmt.Println("Go cross-verification of the Nim DPoP proof") | |
| 65 | - | |
| 66 | - parts := strings.Split(out.JWT, ".") | |
| 67 | - check("JWT has three parts", len(parts) == 3, fmt.Sprintf("%d", len(parts))) | |
| 68 | - if len(parts) != 3 { | |
| 69 | - os.Exit(1) | |
| 70 | - } | |
| 71 | - | |
| 72 | - // Header: ATProto requires typ=dpop+jwt and the embedded public key. | |
| 73 | - headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0]) | |
| 74 | - check("header is unpadded base64url", err == nil, errText(err)) | |
| 75 | - var header struct { | |
| 76 | - Typ string `json:"typ"` | |
| 77 | - Alg string `json:"alg"` | |
| 78 | - JWK jwk `json:"jwk"` | |
| 79 | - } | |
| 80 | - if err == nil { | |
| 81 | - err = json.Unmarshal(headerBytes, &header) | |
| 82 | - } | |
| 83 | - check("header parses", err == nil, errText(err)) | |
| 84 | - check("typ is dpop+jwt", header.Typ == "dpop+jwt", header.Typ) | |
| 85 | - check("alg is ES256", header.Alg == "ES256", header.Alg) | |
| 86 | - check("header jwk matches the reported key", | |
| 87 | - header.JWK == out.JWK, "") | |
| 88 | - | |
| 89 | - payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1]) | |
| 90 | - check("payload is unpadded base64url", err == nil, errText(err)) | |
| 91 | - var payload map[string]any | |
| 92 | - if err == nil { | |
| 93 | - err = json.Unmarshal(payloadBytes, &payload) | |
| 94 | - } | |
| 95 | - check("payload parses", err == nil, errText(err)) | |
| 96 | - for _, claim := range []string{"jti", "htm", "htu", "iat"} { | |
| 97 | - _, ok := payload[claim] | |
| 98 | - check("payload has "+claim, ok, "") | |
| 99 | - } | |
| 100 | - | |
| 101 | - // Signature: JOSE requires raw R||S, not DER. A DER signature here would | |
| 102 | - // be the single easiest mistake to make and would fail every PDS. | |
| 103 | - sig, err := base64.RawURLEncoding.DecodeString(parts[2]) | |
| 104 | - check("signature decodes", err == nil, errText(err)) | |
| 105 | - check("signature is 64 raw bytes (R||S, not DER)", len(sig) == 64, | |
| 106 | - fmt.Sprintf("%d bytes", len(sig))) | |
| 107 | - if len(sig) != 64 { | |
| 108 | - os.Exit(1) | |
| 109 | - } | |
| 110 | - check("signature is not DER-wrapped", sig[0] != 0x30, fmt.Sprintf("0x%02x", sig[0])) | |
| 111 | - | |
| 112 | - pub, err := publicKey(out.JWK) | |
| 113 | - check("JWK converts to a P-256 public key", err == nil, errText(err)) | |
| 114 | - if err != nil { | |
| 115 | - os.Exit(1) | |
| 116 | - } | |
| 117 | - | |
| 118 | - signingInput := parts[0] + "." + parts[1] | |
| 119 | - digest := sha256.Sum256([]byte(signingInput)) | |
| 120 | - r := new(big.Int).SetBytes(sig[:32]) | |
| 121 | - s := new(big.Int).SetBytes(sig[32:]) | |
| 122 | - check("ECDSA signature verifies", ecdsa.Verify(pub, digest[:], r, s), "") | |
| 123 | - | |
| 124 | - // Negative control: a verifier that accepts anything proves nothing. | |
| 125 | - tampered := []byte(signingInput) | |
| 126 | - tampered[len(tampered)-1] ^= 0x01 | |
| 127 | - badDigest := sha256.Sum256(tampered) | |
| 128 | - check("a tampered payload is rejected", | |
| 129 | - !ecdsa.Verify(pub, badDigest[:], r, s), "") | |
| 130 | - | |
| 131 | - // RFC 7638 thumbprint, recomputed independently. | |
| 132 | - canonical := fmt.Sprintf(`{"crv":"%s","kty":"%s","x":"%s","y":"%s"}`, | |
| 133 | - out.JWK.Crv, out.JWK.Kty, out.JWK.X, out.JWK.Y) | |
| 134 | - sum := sha256.Sum256([]byte(canonical)) | |
| 135 | - want := base64.RawURLEncoding.EncodeToString(sum[:]) | |
| 136 | - check("RFC 7638 thumbprint matches", want == out.Thumbprint, out.Thumbprint) | |
| 137 | - | |
| 138 | - fmt.Println() | |
| 139 | - if failures == 0 { | |
| 140 | - fmt.Println("RESULT: the Nim proof is valid and interoperable.") | |
| 141 | - return | |
| 142 | - } | |
| 143 | - fmt.Printf("RESULT: %d check(s) failed.\n", failures) | |
| 144 | - os.Exit(1) | |
| 145 | -} | |
| 146 | - | |
| 147 | -func publicKey(k jwk) (*ecdsa.PublicKey, error) { | |
| 148 | - if k.Kty != "EC" || k.Crv != "P-256" { | |
| 149 | - return nil, fmt.Errorf("unexpected key type %s/%s", k.Kty, k.Crv) | |
| 150 | - } | |
| 151 | - x, err := base64.RawURLEncoding.DecodeString(k.X) | |
| 152 | - if err != nil { | |
| 153 | - return nil, fmt.Errorf("x: %w", err) | |
| 154 | - } | |
| 155 | - y, err := base64.RawURLEncoding.DecodeString(k.Y) | |
| 156 | - if err != nil { | |
| 157 | - return nil, fmt.Errorf("y: %w", err) | |
| 158 | - } | |
| 159 | - if len(x) != 32 || len(y) != 32 { | |
| 160 | - return nil, fmt.Errorf("coordinates must be 32 bytes, got %d/%d", len(x), len(y)) | |
| 161 | - } | |
| 162 | - pub := &ecdsa.PublicKey{ | |
| 163 | - Curve: elliptic.P256(), | |
| 164 | - X: new(big.Int).SetBytes(x), | |
| 165 | - Y: new(big.Int).SetBytes(y), | |
| 166 | - } | |
| 167 | - if !pub.Curve.IsOnCurve(pub.X, pub.Y) { | |
| 168 | - return nil, fmt.Errorf("point is not on P-256") | |
| 169 | - } | |
| 170 | - return pub, nil | |
| 171 | -} | |
| 172 | - | |
| 173 | -func errText(err error) string { | |
| 174 | - if err == nil { | |
| 175 | - return "" | |
| 176 | - } | |
| 177 | - return err.Error() | |
| 178 | -} | |
| deleted file mode 100644 | |||
| @@ -1,178 +0,0 @@ | |||
| 1 | -// Independent check on the Nim DPoP spike. | ||
| 2 | -// | ||
| 3 | -// Self-verification in the same library would only prove internal | ||
| 4 | -// consistency. This re-implements the verifier side from Go's standard | ||
| 5 | -// library -- the same stack the current server runs on -- so agreement here | ||
| 6 | -// means the Nim output is genuinely interoperable, not merely self-coherent. | ||
| 7 | -// | ||
| 8 | -// ./spike/nim/dpop_probe | go run ./verify | ||
| 9 | -package main | ||
| 10 | - | ||
| 11 | -import ( | ||
| 12 | - "crypto/ecdsa" | ||
| 13 | - "crypto/elliptic" | ||
| 14 | - "crypto/sha256" | ||
| 15 | - "encoding/base64" | ||
| 16 | - "encoding/json" | ||
| 17 | - "fmt" | ||
| 18 | - "io" | ||
| 19 | - "math/big" | ||
| 20 | - "os" | ||
| 21 | - "strings" | ||
| 22 | -) | ||
| 23 | - | ||
| 24 | -type jwk struct { | ||
| 25 | - Crv string `json:"crv"` | ||
| 26 | - Kty string `json:"kty"` | ||
| 27 | - X string `json:"x"` | ||
| 28 | - Y string `json:"y"` | ||
| 29 | -} | ||
| 30 | - | ||
| 31 | -type spikeOutput struct { | ||
| 32 | - JWT string `json:"jwt"` | ||
| 33 | - JWK jwk `json:"jwk"` | ||
| 34 | - Thumbprint string `json:"thumbprint"` | ||
| 35 | -} | ||
| 36 | - | ||
| 37 | -var failures int | ||
| 38 | - | ||
| 39 | -func check(name string, ok bool, detail string) { | ||
| 40 | - label := "PASS" | ||
| 41 | - if !ok { | ||
| 42 | - label = "FAIL" | ||
| 43 | - failures++ | ||
| 44 | - } | ||
| 45 | - if detail != "" { | ||
| 46 | - fmt.Printf(" %s %s -- %s\n", label, name, detail) | ||
| 47 | - return | ||
| 48 | - } | ||
| 49 | - fmt.Printf(" %s %s\n", label, name) | ||
| 50 | -} | ||
| 51 | - | ||
| 52 | -func main() { | ||
| 53 | - raw, err := io.ReadAll(os.Stdin) | ||
| 54 | - if err != nil { | ||
| 55 | - fmt.Fprintln(os.Stderr, "read stdin:", err) | ||
| 56 | - os.Exit(2) | ||
| 57 | - } | ||
| 58 | - var out spikeOutput | ||
| 59 | - if err := json.Unmarshal(raw, &out); err != nil { | ||
| 60 | - fmt.Fprintln(os.Stderr, "parse spike output:", err) | ||
| 61 | - os.Exit(2) | ||
| 62 | - } | ||
| 63 | - | ||
| 64 | - fmt.Println("Go cross-verification of the Nim DPoP proof") | ||
| 65 | - | ||
| 66 | - parts := strings.Split(out.JWT, ".") | ||
| 67 | - check("JWT has three parts", len(parts) == 3, fmt.Sprintf("%d", len(parts))) | ||
| 68 | - if len(parts) != 3 { | ||
| 69 | - os.Exit(1) | ||
| 70 | - } | ||
| 71 | - | ||
| 72 | - // Header: ATProto requires typ=dpop+jwt and the embedded public key. | ||
| 73 | - headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0]) | ||
| 74 | - check("header is unpadded base64url", err == nil, errText(err)) | ||
| 75 | - var header struct { | ||
| 76 | - Typ string `json:"typ"` | ||
| 77 | - Alg string `json:"alg"` | ||
| 78 | - JWK jwk `json:"jwk"` | ||
| 79 | - } | ||
| 80 | - if err == nil { | ||
| 81 | - err = json.Unmarshal(headerBytes, &header) | ||
| 82 | - } | ||
| 83 | - check("header parses", err == nil, errText(err)) | ||
| 84 | - check("typ is dpop+jwt", header.Typ == "dpop+jwt", header.Typ) | ||
| 85 | - check("alg is ES256", header.Alg == "ES256", header.Alg) | ||
| 86 | - check("header jwk matches the reported key", | ||
| 87 | - header.JWK == out.JWK, "") | ||
| 88 | - | ||
| 89 | - payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1]) | ||
| 90 | - check("payload is unpadded base64url", err == nil, errText(err)) | ||
| 91 | - var payload map[string]any | ||
| 92 | - if err == nil { | ||
| 93 | - err = json.Unmarshal(payloadBytes, &payload) | ||
| 94 | - } | ||
| 95 | - check("payload parses", err == nil, errText(err)) | ||
| 96 | - for _, claim := range []string{"jti", "htm", "htu", "iat"} { | ||
| 97 | - _, ok := payload[claim] | ||
| 98 | - check("payload has "+claim, ok, "") | ||
| 99 | - } | ||
| 100 | - | ||
| 101 | - // Signature: JOSE requires raw R||S, not DER. A DER signature here would | ||
| 102 | - // be the single easiest mistake to make and would fail every PDS. | ||
| 103 | - sig, err := base64.RawURLEncoding.DecodeString(parts[2]) | ||
| 104 | - check("signature decodes", err == nil, errText(err)) | ||
| 105 | - check("signature is 64 raw bytes (R||S, not DER)", len(sig) == 64, | ||
| 106 | - fmt.Sprintf("%d bytes", len(sig))) | ||
| 107 | - if len(sig) != 64 { | ||
| 108 | - os.Exit(1) | ||
| 109 | - } | ||
| 110 | - check("signature is not DER-wrapped", sig[0] != 0x30, fmt.Sprintf("0x%02x", sig[0])) | ||
| 111 | - | ||
| 112 | - pub, err := publicKey(out.JWK) | ||
| 113 | - check("JWK converts to a P-256 public key", err == nil, errText(err)) | ||
| 114 | - if err != nil { | ||
| 115 | - os.Exit(1) | ||
| 116 | - } | ||
| 117 | - | ||
| 118 | - signingInput := parts[0] + "." + parts[1] | ||
| 119 | - digest := sha256.Sum256([]byte(signingInput)) | ||
| 120 | - r := new(big.Int).SetBytes(sig[:32]) | ||
| 121 | - s := new(big.Int).SetBytes(sig[32:]) | ||
| 122 | - check("ECDSA signature verifies", ecdsa.Verify(pub, digest[:], r, s), "") | ||
| 123 | - | ||
| 124 | - // Negative control: a verifier that accepts anything proves nothing. | ||
| 125 | - tampered := []byte(signingInput) | ||
| 126 | - tampered[len(tampered)-1] ^= 0x01 | ||
| 127 | - badDigest := sha256.Sum256(tampered) | ||
| 128 | - check("a tampered payload is rejected", | ||
| 129 | - !ecdsa.Verify(pub, badDigest[:], r, s), "") | ||
| 130 | - | ||
| 131 | - // RFC 7638 thumbprint, recomputed independently. | ||
| 132 | - canonical := fmt.Sprintf(`{"crv":"%s","kty":"%s","x":"%s","y":"%s"}`, | ||
| 133 | - out.JWK.Crv, out.JWK.Kty, out.JWK.X, out.JWK.Y) | ||
| 134 | - sum := sha256.Sum256([]byte(canonical)) | ||
| 135 | - want := base64.RawURLEncoding.EncodeToString(sum[:]) | ||
| 136 | - check("RFC 7638 thumbprint matches", want == out.Thumbprint, out.Thumbprint) | ||
| 137 | - | ||
| 138 | - fmt.Println() | ||
| 139 | - if failures == 0 { | ||
| 140 | - fmt.Println("RESULT: the Nim proof is valid and interoperable.") | ||
| 141 | - return | ||
| 142 | - } | ||
| 143 | - fmt.Printf("RESULT: %d check(s) failed.\n", failures) | ||
| 144 | - os.Exit(1) | ||
| 145 | -} | ||
| 146 | - | ||
| 147 | -func publicKey(k jwk) (*ecdsa.PublicKey, error) { | ||
| 148 | - if k.Kty != "EC" || k.Crv != "P-256" { | ||
| 149 | - return nil, fmt.Errorf("unexpected key type %s/%s", k.Kty, k.Crv) | ||
| 150 | - } | ||
| 151 | - x, err := base64.RawURLEncoding.DecodeString(k.X) | ||
| 152 | - if err != nil { | ||
| 153 | - return nil, fmt.Errorf("x: %w", err) | ||
| 154 | - } | ||
| 155 | - y, err := base64.RawURLEncoding.DecodeString(k.Y) | ||
| 156 | - if err != nil { | ||
| 157 | - return nil, fmt.Errorf("y: %w", err) | ||
| 158 | - } | ||
| 159 | - if len(x) != 32 || len(y) != 32 { | ||
| 160 | - return nil, fmt.Errorf("coordinates must be 32 bytes, got %d/%d", len(x), len(y)) | ||
| 161 | - } | ||
| 162 | - pub := &ecdsa.PublicKey{ | ||
| 163 | - Curve: elliptic.P256(), | ||
| 164 | - X: new(big.Int).SetBytes(x), | ||
| 165 | - Y: new(big.Int).SetBytes(y), | ||
| 166 | - } | ||
| 167 | - if !pub.Curve.IsOnCurve(pub.X, pub.Y) { | ||
| 168 | - return nil, fmt.Errorf("point is not on P-256") | ||
| 169 | - } | ||
| 170 | - return pub, nil | ||
| 171 | -} | ||
| 172 | - | ||
| 173 | -func errText(err error) string { | ||
| 174 | - if err == nil { | ||
| 175 | - return "" | ||
| 176 | - } | ||
| 177 | - return err.Error() | ||
| 178 | -} | ||
deleted
spike/nim/xrpc_probe.nim +0 -123 | deleted file mode 100644 | ||
| @@ -1,123 +0,0 @@ | ||
| 1 | -## Exercises the XRPC layer as far as it goes without a consented session. | |
| 2 | -## | |
| 3 | -## nim c -r -d:ssl xrpc_probe.nim | |
| 4 | -## | |
| 5 | -## A valid access token needs a human in a browser, so the authenticated path | |
| 6 | -## is probed with a deliberately invalid one. That still proves the parts most | |
| 7 | -## likely to be wrong: that a real PDS accepts the *shape* of our request and | |
| 8 | -## rejects it for the right reason, and that the 401/WWW-Authenticate nonce | |
| 9 | -## handshake behaves the way the retry logic assumes. | |
| 10 | - | |
| 11 | -import std/[httpclient, json, strformat, strutils] | |
| 12 | -import bearssl/rand | |
| 13 | -import atproto/[dpop, identity, xrpc] | |
| 14 | - | |
| 15 | -var failures = 0 | |
| 16 | - | |
| 17 | -proc report(name: string, ok: bool, detail = "") = | |
| 18 | - if not ok: inc failures | |
| 19 | - let label = if ok: "PASS" else: "FAIL" | |
| 20 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | |
| 21 | - else: echo &" {label} {name}" | |
| 22 | - | |
| 23 | -when isMainModule: | |
| 24 | - echo "XRPC signing probe" | |
| 25 | - echo "" | |
| 26 | - | |
| 27 | - let rngRef = HmacDrbgContext.new() | |
| 28 | - doAssert rngRef != nil | |
| 29 | - var rng = rngRef[] | |
| 30 | - | |
| 31 | - # --- claim construction ------------------------------------------------- | |
| 32 | - block: | |
| 33 | - report("htu strips the query string", | |
| 34 | - dpopUrl("https://pds.example/xrpc/a.b.c?limit=5#frag") == | |
| 35 | - "https://pds.example/xrpc/a.b.c", | |
| 36 | - dpopUrl("https://pds.example/xrpc/a.b.c?limit=5#frag")) | |
| 37 | - | |
| 38 | - block: | |
| 39 | - # Pinned against an independently computed value: | |
| 40 | - # printf 'test-access-token' | openssl dgst -sha256 -binary \ | |
| 41 | - # | basenc --base64url | tr -d '=' | |
| 42 | - # A self-consistent ath would pass any internal check and still be | |
| 43 | - # rejected by every PDS. | |
| 44 | - const | |
| 45 | - token = "test-access-token" | |
| 46 | - want = "WXSA1LYsphIZPxnnP-TMOtF_C_nPwWp8v0tQZBMcSAU" | |
| 47 | - let ath = accessTokenHash(token) | |
| 48 | - report("ath matches an independently computed SHA-256", ath == want, ath) | |
| 49 | - report("ath is unpadded base64url", ath.len == 43 and '=' notin ath) | |
| 50 | - | |
| 51 | - block: | |
| 52 | - let key = generateKey(rng) | |
| 53 | - let proof = dpopProof(key, rng, "GET", "https://pds.example/xrpc/a.b.c", | |
| 54 | - accessToken = "tok", issuer = "https://auth.example") | |
| 55 | - let payload = parseJson(b64uDecode(proof.split('.')[1])) | |
| 56 | - report("resource proof carries ath", payload.hasKey("ath")) | |
| 57 | - report("resource proof carries iss", payload{"iss"}.getStr == "https://auth.example") | |
| 58 | - report("resource proof carries exp", payload.hasKey("exp")) | |
| 59 | - | |
| 60 | - let authProof = dpopProof(key, rng, "POST", "https://auth.example/oauth/par") | |
| 61 | - let authPayload = parseJson(b64uDecode(authProof.split('.')[1])) | |
| 62 | - # An auth-server proof must NOT claim to be bound to a token. | |
| 63 | - report("auth-server proof omits ath", not authPayload.hasKey("ath")) | |
| 64 | - | |
| 65 | - # --- unauthenticated XRPC ----------------------------------------------- | |
| 66 | - # Separates "can we speak XRPC" from "is our signing right". | |
| 67 | - block: | |
| 68 | - try: | |
| 69 | - let j = publicQuery("public.api.bsky.app", "app.bsky.actor.getProfile", | |
| 70 | - {"actor": "bsky.app"}) | |
| 71 | - report("unauthenticated XRPC query works", j{"did"}.getStr.isDid, | |
| 72 | - j{"handle"}.getStr) | |
| 73 | - except CatchableError as e: | |
| 74 | - report("unauthenticated XRPC query works", false, e.msg) | |
| 75 | - | |
| 76 | - # --- the authenticated path, against a real PDS ------------------------- | |
| 77 | - var ident: Identity | |
| 78 | - try: | |
| 79 | - ident = lookup("bsky.app") | |
| 80 | - except CatchableError as e: | |
| 81 | - report("resolve a PDS to talk to", false, e.msg) | |
| 82 | - quit 1 | |
| 83 | - report("resolved a PDS", true, ident.pdsEndpoint) | |
| 84 | - | |
| 85 | - block: | |
| 86 | - # A well-formed DPoP request carrying a token the PDS has never issued. | |
| 87 | - # The interesting question is *which* error comes back: a shape problem | |
| 88 | - # would read as invalid_dpop_proof, whereas invalid_token means the PDS | |
| 89 | - # parsed and accepted everything except the credential itself. | |
| 90 | - var sess = Session( | |
| 91 | - did: ident.did, | |
| 92 | - pdsUrl: ident.pdsEndpoint, | |
| 93 | - authServerUrl: "https://bsky.social", | |
| 94 | - accessToken: "not-a-real-token", | |
| 95 | - dpopKey: generateKey(rng), | |
| 96 | - clientId: "http://localhost", | |
| 97 | - ) | |
| 98 | - # getSession, not listRecords: repo reads are public, so a PDS serves them | |
| 99 | - # happily while ignoring the Authorization header entirely -- which looks | |
| 100 | - # like a pass and tests nothing. This endpoint exists to answer "who am | |
| 101 | - # I", so it has to evaluate the credential. | |
| 102 | - let url = &"{sess.pdsUrl}/xrpc/com.atproto.server.getSession" | |
| 103 | - let res = request(sess, rng, HttpGet, url) | |
| 104 | - | |
| 105 | - report("PDS rejects an unknown token with 401", res.code.int == 401, | |
| 106 | - &"HTTP {res.code.int}") | |
| 107 | - | |
| 108 | - let wwwAuth = res.headers.getOrDefault("WWW-Authenticate").string | |
| 109 | - report("401 carries WWW-Authenticate", wwwAuth.len > 0, wwwAuth) | |
| 110 | - report("challenge is DPoP, not Bearer", | |
| 111 | - wwwAuth.startsWith("DPoP"), wwwAuth.split(' ')[0]) | |
| 112 | - report("the proof itself was accepted (error is about the token, "& | |
| 113 | - "not the proof)", | |
| 114 | - wwwAuth.len > 0 and "invalid_dpop_proof" notin wwwAuth, wwwAuth) | |
| 115 | - report("PDS issued a host nonce for the retry", sess.hostNonce.len > 0, | |
| 116 | - sess.hostNonce[0 ..< min(12, sess.hostNonce.len)] & "...") | |
| 117 | - | |
| 118 | - echo "" | |
| 119 | - if failures == 0: | |
| 120 | - echo "RESULT: XRPC signing is correct as far as an unconsented session can show." | |
| 121 | - else: | |
| 122 | - echo &"RESULT: {failures} check(s) failed." | |
| 123 | - quit 1 | |
| deleted file mode 100644 | |||
| @@ -1,123 +0,0 @@ | |||
| 1 | -## Exercises the XRPC layer as far as it goes without a consented session. | ||
| 2 | -## | ||
| 3 | -## nim c -r -d:ssl xrpc_probe.nim | ||
| 4 | -## | ||
| 5 | -## A valid access token needs a human in a browser, so the authenticated path | ||
| 6 | -## is probed with a deliberately invalid one. That still proves the parts most | ||
| 7 | -## likely to be wrong: that a real PDS accepts the *shape* of our request and | ||
| 8 | -## rejects it for the right reason, and that the 401/WWW-Authenticate nonce | ||
| 9 | -## handshake behaves the way the retry logic assumes. | ||
| 10 | - | ||
| 11 | -import std/[httpclient, json, strformat, strutils] | ||
| 12 | -import bearssl/rand | ||
| 13 | -import atproto/[dpop, identity, xrpc] | ||
| 14 | - | ||
| 15 | -var failures = 0 | ||
| 16 | - | ||
| 17 | -proc report(name: string, ok: bool, detail = "") = | ||
| 18 | - if not ok: inc failures | ||
| 19 | - let label = if ok: "PASS" else: "FAIL" | ||
| 20 | - if detail.len > 0: echo &" {label} {name} -- {detail}" | ||
| 21 | - else: echo &" {label} {name}" | ||
| 22 | - | ||
| 23 | -when isMainModule: | ||
| 24 | - echo "XRPC signing probe" | ||
| 25 | - echo "" | ||
| 26 | - | ||
| 27 | - let rngRef = HmacDrbgContext.new() | ||
| 28 | - doAssert rngRef != nil | ||
| 29 | - var rng = rngRef[] | ||
| 30 | - | ||
| 31 | - # --- claim construction ------------------------------------------------- | ||
| 32 | - block: | ||
| 33 | - report("htu strips the query string", | ||
| 34 | - dpopUrl("https://pds.example/xrpc/a.b.c?limit=5#frag") == | ||
| 35 | - "https://pds.example/xrpc/a.b.c", | ||
| 36 | - dpopUrl("https://pds.example/xrpc/a.b.c?limit=5#frag")) | ||
| 37 | - | ||
| 38 | - block: | ||
| 39 | - # Pinned against an independently computed value: | ||
| 40 | - # printf 'test-access-token' | openssl dgst -sha256 -binary \ | ||
| 41 | - # | basenc --base64url | tr -d '=' | ||
| 42 | - # A self-consistent ath would pass any internal check and still be | ||
| 43 | - # rejected by every PDS. | ||
| 44 | - const | ||
| 45 | - token = "test-access-token" | ||
| 46 | - want = "WXSA1LYsphIZPxnnP-TMOtF_C_nPwWp8v0tQZBMcSAU" | ||
| 47 | - let ath = accessTokenHash(token) | ||
| 48 | - report("ath matches an independently computed SHA-256", ath == want, ath) | ||
| 49 | - report("ath is unpadded base64url", ath.len == 43 and '=' notin ath) | ||
| 50 | - | ||
| 51 | - block: | ||
| 52 | - let key = generateKey(rng) | ||
| 53 | - let proof = dpopProof(key, rng, "GET", "https://pds.example/xrpc/a.b.c", | ||
| 54 | - accessToken = "tok", issuer = "https://auth.example") | ||
| 55 | - let payload = parseJson(b64uDecode(proof.split('.')[1])) | ||
| 56 | - report("resource proof carries ath", payload.hasKey("ath")) | ||
| 57 | - report("resource proof carries iss", payload{"iss"}.getStr == "https://auth.example") | ||
| 58 | - report("resource proof carries exp", payload.hasKey("exp")) | ||
| 59 | - | ||
| 60 | - let authProof = dpopProof(key, rng, "POST", "https://auth.example/oauth/par") | ||
| 61 | - let authPayload = parseJson(b64uDecode(authProof.split('.')[1])) | ||
| 62 | - # An auth-server proof must NOT claim to be bound to a token. | ||
| 63 | - report("auth-server proof omits ath", not authPayload.hasKey("ath")) | ||
| 64 | - | ||
| 65 | - # --- unauthenticated XRPC ----------------------------------------------- | ||
| 66 | - # Separates "can we speak XRPC" from "is our signing right". | ||
| 67 | - block: | ||
| 68 | - try: | ||
| 69 | - let j = publicQuery("public.api.bsky.app", "app.bsky.actor.getProfile", | ||
| 70 | - {"actor": "bsky.app"}) | ||
| 71 | - report("unauthenticated XRPC query works", j{"did"}.getStr.isDid, | ||
| 72 | - j{"handle"}.getStr) | ||
| 73 | - except CatchableError as e: | ||
| 74 | - report("unauthenticated XRPC query works", false, e.msg) | ||
| 75 | - | ||
| 76 | - # --- the authenticated path, against a real PDS ------------------------- | ||
| 77 | - var ident: Identity | ||
| 78 | - try: | ||
| 79 | - ident = lookup("bsky.app") | ||
| 80 | - except CatchableError as e: | ||
| 81 | - report("resolve a PDS to talk to", false, e.msg) | ||
| 82 | - quit 1 | ||
| 83 | - report("resolved a PDS", true, ident.pdsEndpoint) | ||
| 84 | - | ||
| 85 | - block: | ||
| 86 | - # A well-formed DPoP request carrying a token the PDS has never issued. | ||
| 87 | - # The interesting question is *which* error comes back: a shape problem | ||
| 88 | - # would read as invalid_dpop_proof, whereas invalid_token means the PDS | ||
| 89 | - # parsed and accepted everything except the credential itself. | ||
| 90 | - var sess = Session( | ||
| 91 | - did: ident.did, | ||
| 92 | - pdsUrl: ident.pdsEndpoint, | ||
| 93 | - authServerUrl: "https://bsky.social", | ||
| 94 | - accessToken: "not-a-real-token", | ||
| 95 | - dpopKey: generateKey(rng), | ||
| 96 | - clientId: "http://localhost", | ||
| 97 | - ) | ||
| 98 | - # getSession, not listRecords: repo reads are public, so a PDS serves them | ||
| 99 | - # happily while ignoring the Authorization header entirely -- which looks | ||
| 100 | - # like a pass and tests nothing. This endpoint exists to answer "who am | ||
| 101 | - # I", so it has to evaluate the credential. | ||
| 102 | - let url = &"{sess.pdsUrl}/xrpc/com.atproto.server.getSession" | ||
| 103 | - let res = request(sess, rng, HttpGet, url) | ||
| 104 | - | ||
| 105 | - report("PDS rejects an unknown token with 401", res.code.int == 401, | ||
| 106 | - &"HTTP {res.code.int}") | ||
| 107 | - | ||
| 108 | - let wwwAuth = res.headers.getOrDefault("WWW-Authenticate").string | ||
| 109 | - report("401 carries WWW-Authenticate", wwwAuth.len > 0, wwwAuth) | ||
| 110 | - report("challenge is DPoP, not Bearer", | ||
| 111 | - wwwAuth.startsWith("DPoP"), wwwAuth.split(' ')[0]) | ||
| 112 | - report("the proof itself was accepted (error is about the token, "& | ||
| 113 | - "not the proof)", | ||
| 114 | - wwwAuth.len > 0 and "invalid_dpop_proof" notin wwwAuth, wwwAuth) | ||
| 115 | - report("PDS issued a host nonce for the retry", sess.hostNonce.len > 0, | ||
| 116 | - sess.hostNonce[0 ..< min(12, sess.hostNonce.len)] & "...") | ||
| 117 | - | ||
| 118 | - echo "" | ||
| 119 | - if failures == 0: | ||
| 120 | - echo "RESULT: XRPC signing is correct as far as an unconsented session can show." | ||
| 121 | - else: | ||
| 122 | - echo &"RESULT: {failures} check(s) failed." | ||
| 123 | - quit 1 | ||