Skip to content

Commit

Permalink
packages/modeldb: add modeldb-sqlite-wasm composite key support
Browse files Browse the repository at this point in the history
  • Loading branch information
Joel Gustafson committed Jan 22, 2025
1 parent cba7e30 commit 3780dea
Show file tree
Hide file tree
Showing 12 changed files with 659 additions and 534 deletions.
38 changes: 24 additions & 14 deletions packages/modeldb-durable-objects/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ import {
PropertyValue,
} from "@canvas-js/modeldb"

import { decodePrimitiveValue, decodeReferenceValue, encodePrimitiveValue, encodeReferenceValue } from "./encoding.js"
import {
SqlitePrimitiveValue,
decodePrimitiveValue,
decodeReferenceValue,
encodePrimitiveValue,
encodeReferenceValue,
} from "./encoding.js"

import { Method, Query } from "./utils.js"

Expand Down Expand Up @@ -68,8 +74,8 @@ export class ModelAPI {
string,
{
columns: string[]
encode: (value: PropertyValue) => SqlStorageValue[]
decode: (record: Record<string, SqlStorageValue>) => PropertyValue
encode: (value: PropertyValue) => SqlitePrimitiveValue[]
decode: (record: Record<string, SqlitePrimitiveValue>) => PropertyValue
}
> = {}

Expand Down Expand Up @@ -275,8 +281,8 @@ export class ModelAPI {
}
}

private encodeProperties(properties: Property[], value: ModelValue): SqlStorageValue[] {
const result: SqlStorageValue[] = []
private encodeProperties(properties: Property[], value: ModelValue): SqlitePrimitiveValue[] {
const result: SqlitePrimitiveValue[] = []
for (const property of properties) {
if (property.kind === "primitive") {
const { name, type, nullable } = property
Expand Down Expand Up @@ -320,7 +326,7 @@ export class ModelAPI {

public count(where?: WhereCondition): number {
const sql: string[] = []
const params: SqlStorageValue[] = []
const params: SqlitePrimitiveValue[] = []

// SELECT
sql.push(`SELECT COUNT(*) AS count FROM "${this.#table}"`)
Expand Down Expand Up @@ -357,7 +363,11 @@ export class ModelAPI {
}
}

private parseRecord(row: Record<string, SqlStorageValue>, properties: string[], relations: Relation[]): ModelValue {
private parseRecord(
row: Record<string, SqlitePrimitiveValue>,
properties: string[],
relations: Relation[],
): ModelValue {
const record: ModelValue = {}
for (const name of properties) {
record[name] = this.codecs[name].decode(row)
Expand All @@ -381,10 +391,10 @@ export class ModelAPI {

private parseQuery(
query: QueryParams,
): [sql: string, properties: string[], relations: Relation[], params: SqlStorageValue[]] {
): [sql: string, properties: string[], relations: Relation[], params: SqlitePrimitiveValue[]] {
// See https://www.sqlite.org/lang_select.html for railroad diagram
const sql: string[] = []
const params: SqlStorageValue[] = []
const params: SqlitePrimitiveValue[] = []

// SELECT
const select = query.select ?? mapValues(this.#properties, () => true)
Expand Down Expand Up @@ -471,8 +481,8 @@ export class ModelAPI {
return [columns.join(", "), properties, relations]
}

private getWhereExpression(where: WhereCondition = {}): [where: string | null, params: SqlStorageValue[]] {
const params: SqlStorageValue[] = []
private getWhereExpression(where: WhereCondition = {}): [where: string | null, params: SqlitePrimitiveValue[]] {
const params: SqlitePrimitiveValue[] = []

const filters: string[] = []
for (const [name, expression] of Object.entries(where)) {
Expand Down Expand Up @@ -671,18 +681,18 @@ export class RelationAPI {
this.#select = new Query(this.db, `SELECT ${targetColumns} FROM "${this.table}" WHERE ${selectBySource}`)
}

public get(sourceKey: SqlStorageValue[]): SqlStorageValue[][] {
public get(sourceKey: SqlitePrimitiveValue[]): SqlitePrimitiveValue[][] {
const targets = this.#select.all(sourceKey)
return targets.map((record) => this.sourceColumnNames.map((name) => record[name]))
}

public add(sourceKey: SqlStorageValue[], targetKeys: SqlStorageValue[][]) {
public add(sourceKey: SqlitePrimitiveValue[], targetKeys: SqlitePrimitiveValue[][]) {
for (const targetKey of targetKeys) {
this.#insert.run([...sourceKey, ...targetKey])
}
}

public delete(sourceKey: SqlStorageValue[]) {
public delete(sourceKey: SqlitePrimitiveValue[]) {
this.#delete.run(sourceKey)
}

Expand Down
10 changes: 6 additions & 4 deletions packages/modeldb-durable-objects/src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { PrimaryKeyValue, PrimitiveProperty, PrimitiveType, PrimitiveValue, Prop

import { assert, signalInvalidType } from "@canvas-js/utils"

export type SqlitePrimitiveValue = SqlStorageValue

export function toArrayBuffer(data: Uint8Array): ArrayBuffer {
if (data.byteOffset === 0 && data.byteLength === data.buffer.byteLength) {
return data.buffer
Expand All @@ -25,7 +27,7 @@ export function encodePrimitiveValue(
type: PrimitiveType,
nullable: boolean,
value: PropertyValue,
): SqlStorageValue {
): SqlitePrimitiveValue {
if (value === null) {
if (nullable) {
return null
Expand Down Expand Up @@ -80,7 +82,7 @@ export function encodeReferenceValue(
target: PrimitiveProperty[],
nullable: boolean,
value: PropertyValue,
): SqlStorageValue[] {
): SqlitePrimitiveValue[] {
if (value === null) {
if (nullable) {
return Array.from<null>({ length: target.length }).fill(null)
Expand All @@ -101,7 +103,7 @@ export function decodePrimitiveValue(
propertyName: string,
type: PrimitiveType,
nullable: boolean,
value: SqlStorageValue,
value: SqlitePrimitiveValue,
): PrimitiveValue {
if (value === null) {
if (nullable) {
Expand Down Expand Up @@ -161,7 +163,7 @@ export function decodeReferenceValue(
propertyName: string,
nullable: boolean,
target: PrimitiveProperty[],
values: SqlStorageValue[],
values: SqlitePrimitiveValue[],
): PrimaryKeyValue | PrimaryKeyValue[] | null {
if (values.every((value) => value === null)) {
if (nullable) {
Expand Down
4 changes: 2 additions & 2 deletions packages/modeldb-sqlite-expo/src/ModelDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { assert, signalInvalidType } from "@canvas-js/utils"
import {
AbstractModelDB,
ModelDBBackend,
parseConfig,
Effect,
ModelValue,
ModelSchema,
QueryParams,
WhereCondition,
Config,
} from "@canvas-js/modeldb"

import { ModelAPI } from "./api.js"
Expand All @@ -28,7 +28,7 @@ export class ModelDB extends AbstractModelDB {
#transaction: (effects: Effect[]) => void

constructor({ path, models, clear }: { clear?: boolean } & ModelDBOptions) {
super(parseConfig(models))
super(Config.parse(models))

this.db = SQLite.openDatabaseSync(path ?? ":memory:")

Expand Down
36 changes: 22 additions & 14 deletions packages/modeldb-sqlite-expo/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SQLiteDatabase, SQLiteBindValue } from "expo-sqlite"
import { SQLiteDatabase } from "expo-sqlite"

import { assert, signalInvalidType, mapValues } from "@canvas-js/utils"

Expand All @@ -20,9 +20,13 @@ import {
PropertyValue,
} from "@canvas-js/modeldb"

import { zip } from "@canvas-js/utils"

import { decodePrimitiveValue, decodeReferenceValue, encodePrimitiveValue, encodeReferenceValue } from "./encoding.js"
import {
SqlitePrimitiveValue,
decodePrimitiveValue,
decodeReferenceValue,
encodePrimitiveValue,
encodeReferenceValue,
} from "./encoding.js"

import { Method, Query } from "./utils.js"

Expand Down Expand Up @@ -70,8 +74,8 @@ export class ModelAPI {
string,
{
columns: string[]
encode: (value: PropertyValue) => SQLiteBindValue[]
decode: (record: Record<string, SQLiteBindValue>) => PropertyValue
encode: (value: PropertyValue) => SqlitePrimitiveValue[]
decode: (record: Record<string, SqlitePrimitiveValue>) => PropertyValue
}
> = {}

Expand Down Expand Up @@ -276,8 +280,8 @@ export class ModelAPI {
}
}

private encodeProperties(properties: Property[], value: ModelValue): SQLiteBindValue[] {
const result: SQLiteBindValue[] = []
private encodeProperties(properties: Property[], value: ModelValue): SqlitePrimitiveValue[] {
const result: SqlitePrimitiveValue[] = []
for (const property of properties) {
if (property.kind === "primitive") {
const { name, type, nullable } = property
Expand Down Expand Up @@ -358,7 +362,11 @@ export class ModelAPI {
}
}

private parseRecord(row: Record<string, SQLiteBindValue>, properties: string[], relations: Relation[]): ModelValue {
private parseRecord(
row: Record<string, SqlitePrimitiveValue>,
properties: string[],
relations: Relation[],
): ModelValue {
const record: ModelValue = {}
for (const name of properties) {
record[name] = this.codecs[name].decode(row)
Expand All @@ -382,10 +390,10 @@ export class ModelAPI {

private parseQuery(
query: QueryParams,
): [sql: string, properties: string[], relations: Relation[], params: SQLiteBindValue[]] {
): [sql: string, properties: string[], relations: Relation[], params: SqlitePrimitiveValue[]] {
// See https://www.sqlite.org/lang_select.html for railroad diagram
const sql: string[] = []
const params: SQLiteBindValue[] = []
const params: SqlitePrimitiveValue[] = []

// SELECT
const select = query.select ?? mapValues(this.#properties, () => true)
Expand Down Expand Up @@ -672,18 +680,18 @@ export class RelationAPI {
this.#select = new Query(this.db, `SELECT ${targetColumns} FROM "${this.table}" WHERE ${selectBySource}`)
}

public get(sourceKey: SQLiteBindValue[]): SQLiteBindValue[][] {
public get(sourceKey: SqlitePrimitiveValue[]): SqlitePrimitiveValue[][] {
const targets = this.#select.all(sourceKey)
return targets.map((record) => this.sourceColumnNames.map((name) => record[name]))
}

public add(sourceKey: SQLiteBindValue[], targetKeys: SQLiteBindValue[][]) {
public add(sourceKey: SqlitePrimitiveValue[], targetKeys: SqlitePrimitiveValue[][]) {
for (const targetKey of targetKeys) {
this.#insert.run([...sourceKey, ...targetKey])
}
}

public delete(sourceKey: SQLiteBindValue[]) {
public delete(sourceKey: SqlitePrimitiveValue[]) {
this.#delete.run(sourceKey)
}

Expand Down
10 changes: 6 additions & 4 deletions packages/modeldb-sqlite-expo/src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import type {

import { assert, signalInvalidType } from "@canvas-js/utils"

export type SqlitePrimitiveValue = SQLiteBindValue

export function encodePrimitiveValue(
propertyName: string,
type: PrimitiveType,
nullable: boolean,
value: PropertyValue,
): SQLiteBindValue {
): SqlitePrimitiveValue {
if (value === null) {
if (nullable) {
return null
Expand Down Expand Up @@ -72,7 +74,7 @@ export function encodeReferenceValue(
target: PrimitiveProperty[],
nullable: boolean,
value: PropertyValue,
): SQLiteBindValue[] {
): SqlitePrimitiveValue[] {
if (value === null) {
if (nullable) {
return Array.from<null>({ length: target.length }).fill(null)
Expand All @@ -93,7 +95,7 @@ export function decodePrimitiveValue(
propertyName: string,
type: PrimitiveType,
nullable: boolean,
value: SQLiteBindValue,
value: SqlitePrimitiveValue,
): PrimitiveValue {
if (value === null) {
if (nullable) {
Expand Down Expand Up @@ -153,7 +155,7 @@ export function decodeReferenceValue(
propertyName: string,
nullable: boolean,
target: PrimitiveProperty[],
values: SQLiteBindValue[],
values: SqlitePrimitiveValue[],
): PrimaryKeyValue | PrimaryKeyValue[] | null {
if (values.every((value) => value === null)) {
if (nullable) {
Expand Down
5 changes: 4 additions & 1 deletion packages/modeldb-sqlite-expo/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { SQLiteBindValue, SQLiteDatabase, SQLiteStatement } from "expo-sqlite"

export class Query<P extends SQLiteBindValue[] = SQLiteBindValue[], R = Record<string, SQLiteBindValue>> {
export class Query<
P extends SQLiteBindValue[] = SQLiteBindValue[],
R extends Record<string, SQLiteBindValue> = Record<string, SQLiteBindValue>,
> {
private readonly statement: SQLiteStatement

constructor(db: SQLiteDatabase, private readonly sql: string) {
Expand Down
25 changes: 23 additions & 2 deletions packages/modeldb-sqlite-wasm/src/InnerModelDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,29 @@ export class InnerModelDB {

public iterate(modelName: string, query: QueryParams = {}): AsyncIterable<ModelValue> {
const api = this.#models[modelName]
assert(api !== undefined, `model ${modelName} not found`)
return Comlink.proxy(api.iterate(query))
if (api === undefined) {
throw new Error(`model ${modelName} not found`)
}

return Comlink.proxy({
[Symbol.asyncIterator]() {
const iter = api.iterate(query)

return {
async next() {
return iter.next()
},

async return(value) {
return iter.return?.(value) ?? { done: true, value: undefined }
},

async throw(err) {
return iter.throw?.(err) ?? { done: true, value: undefined }
},
}
},
})
}

public count(modelName: string, where?: WhereCondition): number {
Expand Down
Loading

0 comments on commit 3780dea

Please sign in to comment.