Skip to content

Commit

Permalink
wip: relation deletes
Browse files Browse the repository at this point in the history
  • Loading branch information
raykyri committed Dec 7, 2024
1 parent f119f67 commit ff2b082
Show file tree
Hide file tree
Showing 14 changed files with 598 additions and 385 deletions.
2 changes: 1 addition & 1 deletion packages/modeldb-durable-objects/src/ModelDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class ModelDB extends AbstractModelDB {
this.db = db

for (const model of Object.values(this.models)) {
this.#models[model.name] = new ModelAPI(this.db, model)
this.#models[model.name] = new ModelAPI(this.db, model, Object.values(this.models))
}
}

Expand Down
39 changes: 37 additions & 2 deletions packages/modeldb-durable-objects/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,15 @@ export class ModelAPI {
#count: Query<{ count: number }>

readonly #relations: Record<string, RelationAPI> = {}
readonly #backlinks: Record<string, Record<string, RelationAPI>> = {}

readonly #primaryKeyName: string
columnNames: `"${string}"`[]

public constructor(
readonly db: SqlStorage,
readonly model: Model,
readonly models: Model[],
) {
// in the cloudflare runtime, `this` cannot be used when assigning default values to private properties
this.#table = model.name
Expand Down Expand Up @@ -116,6 +119,20 @@ export class ModelAPI {
signalInvalidType(property)
}
}
for (const backlink of models) {
if (backlink.name === model.name) continue
for (const property of backlink.properties.values()) {
if (property.kind === "relation" && property.target === model.name) {
this.#backlinks[backlink.name] ||= {}
this.#backlinks[backlink.name][property.name] = new RelationAPI(db, {
source: backlink.name,
property: property.name,
target: property.target,
indexed: false,
})
}
}
}

assert(primaryKey !== null, "expected primaryKey !== null")
assert(primaryKeyIndex !== null, "expected primaryKeyIndex !== null")
Expand Down Expand Up @@ -233,6 +250,12 @@ export class ModelAPI {
for (const relation of Object.values(this.#relations)) {
relation.delete(key)
}

for (const relations of Object.values(this.#backlinks)) {
for (const relation of Object.values(relations)) {
relation.deleteByTarget(key)
}
}
}

public clear() {
Expand All @@ -241,11 +264,16 @@ export class ModelAPI {
this.#clear.run([])

for (const record of existingRecords) {
const key = record[this.#primaryKeyName] // TODO: this was primaryKeyParam elsewhere, was that right?
const key = record[this.#primaryKeyName]
if (!key || typeof key !== "string") continue
for (const relation of Object.values(this.#relations)) {
if (!key || typeof key !== "string") continue
relation.delete(key)
}
for (const relations of Object.values(this.#backlinks)) {
for (const relation of Object.values(relations)) {
relation.deleteByTarget(key)
}
}
}
}

Expand Down Expand Up @@ -600,6 +628,7 @@ export class RelationAPI {
readonly #select: Query<{ _target: string }>
readonly #insert: Method<{ _source: string; _target: string }>
readonly #delete: Method<{ _source: string }>
readonly #deleteByTarget: Method<{ _target: string }>

public constructor(
readonly db: SqlStorage,
Expand All @@ -622,6 +651,8 @@ export class RelationAPI {

this.#delete = new Method<{ _source: string }>(this.db, `DELETE FROM "${this.table}" WHERE _source = :_source`)

this.#deleteByTarget = new Method<{ _target: string }>(this.db, `DELETE FROM "${this.table}" WHERE _target = :_target`)

// Prepare queries
this.#select = new Query<{ _target: string }>(
this.db,
Expand All @@ -645,4 +676,8 @@ export class RelationAPI {
public delete(source: string) {
this.#delete.run([source])
}

public deleteByTarget(target: string) {
this.#deleteByTarget.run([target])
}
}
2 changes: 1 addition & 1 deletion packages/modeldb-idb/src/ModelDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class ModelDB extends AbstractModelDB {
super(config)

for (const model of config.models) {
this.#models[model.name] = new ModelAPI(model)
this.#models[model.name] = new ModelAPI(model, config.models)
}

db.addEventListener("error", (event) => this.log("db: error", event))
Expand Down
39 changes: 34 additions & 5 deletions packages/modeldb-idb/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,19 @@ export class ModelAPI {

private readonly log = logger(`canvas:modeldb:[${this.model.name}]`)

constructor(readonly model: Model) {}
constructor(
readonly model: Model,
readonly models: Model[],
) {}

private getStore<Mode extends IDBTransactionMode>(txn: IDBPTransaction<any, any, Mode>) {
return txn.objectStore(this.storeName)
}

private getOtherStore<Mode extends IDBTransactionMode>(txn: IDBPTransaction<any, any, Mode>, storeName: string) {
return txn.objectStore(storeName)
}

public async get<Mode extends IDBTransactionMode>(
txn: IDBPTransaction<any, any, Mode>,
key: string,
Expand Down Expand Up @@ -72,6 +79,28 @@ export class ModelAPI {

async delete(txn: IDBPTransaction<any, any, "readwrite">, key: string): Promise<void> {
await this.getStore(txn).delete(key)

for (const model of this.models) {
if (model.name === this.model.name) continue
for (const property of model.properties) {
if (property.kind === "relation" && property.target === this.model.name) {
// Use index to find all records where the relation includes our key

// TODO: We can't do this because relations would just be indexed on the entire set of related keys
// TODO: We need to create a relation table in IDB too

const otherStore = this.getOtherStore(txn, model.name)
const index = otherStore.index(getIndexName([property.name]))
const range = IDBKeyRange.only(key)

// Delete all matching records
for await (const cursor of index.iterate(range)) {
console.log('record')
await otherStore.delete(cursor.primaryKey)
}
}
}
}
}

async clear(txn: IDBPTransaction<any, any, "readwrite">) {
Expand Down Expand Up @@ -306,8 +335,8 @@ export class ModelAPI {
expression.neq === undefined
? null
: expression.neq === null
? IDBKeyRange.lowerBound(encodePropertyValue(property, null), true)
: IDBKeyRange.upperBound(encodePropertyValue(property, expression.neq), true)
? IDBKeyRange.lowerBound(encodePropertyValue(property, null), true)
: IDBKeyRange.upperBound(encodePropertyValue(property, expression.neq), true)

return await storeIndex.count(keyRange)
} else if (isRangeExpression(expression)) {
Expand Down Expand Up @@ -354,8 +383,8 @@ export class ModelAPI {
expression.neq === undefined
? null
: expression.neq === null
? IDBKeyRange.lowerBound(encodePropertyValue(property, null), true)
: IDBKeyRange.upperBound(encodePropertyValue(property, expression.neq), true)
? IDBKeyRange.lowerBound(encodePropertyValue(property, null), true)
: IDBKeyRange.upperBound(encodePropertyValue(property, expression.neq), true)

for (
let cursor = await storeIndex.openCursor(keyRange, direction);
Expand Down
2 changes: 1 addition & 1 deletion packages/modeldb-pg/src/ModelDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class ModelDB extends AbstractModelDB {

const modelDBAPIs: Record<string, ModelAPI> = {}
for (const model of Object.values(modelDBConfig.models)) {
modelDBAPIs[model.name] = await ModelAPI.initialize(client, model, clear)
modelDBAPIs[model.name] = await ModelAPI.initialize(client, model, Object.values(modelDBConfig.models), clear)
}

return new ModelDB(client, modelDBConfig, modelDBAPIs)
Expand Down
42 changes: 39 additions & 3 deletions packages/modeldb-pg/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,30 +68,35 @@ export class ModelAPI {
readonly #columns: string[]
readonly #columnNames: `"${string}"`[]
readonly #relations: Record<string, RelationAPI> = {}
readonly #backlinks: Record<string, Record<string, RelationAPI>> = {}
readonly #primaryKeyName: string

constructor(
readonly client: pg.Client,
readonly model: Model,
readonly models: Model[],
columns: string[],
columnNames: `"${string}"`[],
relations: Record<string, RelationAPI> = {},
backlinks: Record<string, Record<string, RelationAPI>> = {},
primaryKeyName: string,
) {
this.#columns = columns
this.#columnNames = columnNames // quoted column names for non-relation properties
this.#relations = relations
this.#backlinks = backlinks
this.#primaryKeyName = primaryKeyName
}

public static async initialize(client: pg.Client, model: Model, clear: boolean = false) {
public static async initialize(client: pg.Client, model: Model, models: Model[], clear: boolean = false) {
let primaryKeyIndex: number | null = null
let primaryKey: PrimaryKeyProperty | null = null
let primaryKeyName: string | null

const columns: string[] = []
const columnNames: `"${string}"`[] = []
const relations: Record<string, RelationAPI> = {}
const backlinks: Record<string, Record<string, RelationAPI>> = {}

for (const [i, property] of model.properties.entries()) {
if (property.kind === "primary" || property.kind === "primitive" || property.kind === "reference") {
Expand All @@ -117,11 +122,25 @@ export class ModelAPI {
signalInvalidType(property)
}
}
for (const backlink of models) {
if (backlink.name === model.name) continue
for (const property of backlink.properties.values()) {
if (property.kind === "relation" && property.target === model.name) {
backlinks[backlink.name] ||= {}
backlinks[backlink.name][property.name] = await RelationAPI.initialize(client, {
source: backlink.name,
property: property.name,
target: property.target,
indexed: false,
}, clear)
}
}
}

assert(primaryKey !== null, "expected primaryKey !== null")
assert(primaryKeyIndex !== null, "expected primaryKeyIndex !== null")

const api = new ModelAPI(client, model, columns, columnNames, relations, primaryKey.name)
const api = new ModelAPI(client, model, models, columns, columnNames, relations, backlinks, primaryKey.name)

const queries = []

Expand Down Expand Up @@ -254,6 +273,12 @@ export class ModelAPI {
for (const relation of Object.values(this.#relations)) {
await relation.delete(key)
}

for (const relations of Object.values(this.#backlinks)) {
for (const relation of Object.values(relations)) {
relation.deleteByTarget(key)
}
}
}

public async count(where?: WhereCondition): Promise<number> {
Expand All @@ -276,9 +301,16 @@ export class ModelAPI {
const results = await this.client.query(`DELETE FROM "${this.#table}" RETURNING "${this.#primaryKeyName}"`)

for (const row of results.rows) {

const key = row[this.#primaryKeyName].id
if (!key || typeof key !== "string") continue
for (const relation of Object.values(this.#relations)) {
await relation.delete(key)
relation.delete(key)
}
for (const relations of Object.values(this.#backlinks)) {
for (const relation of Object.values(relations)) {
relation.deleteByTarget(key)
}
}
}
}
Expand Down Expand Up @@ -727,4 +759,8 @@ export class RelationAPI {
public async delete(source: string) {
await this.client.query(`DELETE FROM "${this.table}" WHERE _source = $1`, [source])
}

public async deleteByTarget(target: string) {
await this.client.query(`DELETE FROM "${this.table}" WHERE _target = $1`, [target])
}
}
2 changes: 1 addition & 1 deletion packages/modeldb-sqlite-expo/src/ModelDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class ModelDB extends AbstractModelDB {
}

for (const model of Object.values(this.models)) {
this.#models[model.name] = new ModelAPI(this.db, model)
this.#models[model.name] = new ModelAPI(this.db, model, Object.values(this.models))
}

this.#transaction = (effects: Effect[]) =>
Expand Down
Loading

0 comments on commit ff2b082

Please sign in to comment.