diff --git a/packages/core/src/selection.ts b/packages/core/src/selection.ts index aab3421f..655f3e54 100644 --- a/packages/core/src/selection.ts +++ b/packages/core/src/selection.ts @@ -1,4 +1,4 @@ -import { defineProperty, Dict, filterKeys, valueMap } from 'cosmokit' +import { defineProperty, Dict, filterKeys, mapValues, valueMap } from 'cosmokit' import { Driver } from './driver.ts' import { Eval, executeEval } from './eval.ts' import { Model } from './model.ts' @@ -235,6 +235,30 @@ export class Selection extends Executable { return new Selection(this.driver, this) } + join( + name: K, + selection: Selection, + callback: (self: Row, other: Row) => Eval.Expr = () => Eval.and(), + type: 'inner' | 'left' | 'right' = 'inner', + ): Selection { + const fields = Object.fromEntries(Object.entries(this.model.fields) + .filter(([, field]) => !field!.deprecated) + .map(([key]) => [key, (row) => key.split('.').reduce((r, k) => r[k], row[this.ref])])) + if (type === 'left') { + return this.driver.database + .join({ [this.ref]: this as Selection, [name]: selection }, (t: any) => callback(t[this.ref], t[name]), { [this.ref]: false, [name]: true }) + .project({ ...fields, [name]: name }) as any + } else if (type === 'right') { + return this.driver.database + .join({ [name]: selection, [this.ref]: this as Selection }, (t: any) => callback(t[this.ref], t[name]), { [name]: true, [this.ref]: false }) + .project({ ...fields, [name]: name }) as any + } else { + return this.driver.database + .join({ [this.ref]: this as Selection, [name]: selection }, (t: any) => callback(t[this.ref], t[name])) + .project({ ...fields, [name]: name }) as any + } + } + _action(type: Executable.Action, ...args: any[]) { return new Executable(this.driver, { ...this, type, args }) } @@ -278,6 +302,14 @@ export class Selection extends Executable { }) }) } + + format(result = {}) { + result['ref'] = this.ref + if (typeof this.table === 'string') result['table'] = this.table + else if (this.table instanceof Selection) result['table'] = this.table.format() + else result['table'] = mapValues(this.table, (v: any) => v.format()) + return result + } } export function executeSort(data: any[], modifier: Modifier, name: string) { diff --git a/packages/tests/src/selection.ts b/packages/tests/src/selection.ts index 4e7bc4a8..1ab5d3fc 100644 --- a/packages/tests/src/selection.ts +++ b/packages/tests/src/selection.ts @@ -285,6 +285,11 @@ namespace SelectionTests { .join(['foo', 'bar'], (foo, bar) => $.eq(foo.value, bar.value)) .execute() ).to.eventually.have.length(2) + + await expect(database.select('foo') + .join('bar', database.select('bar'), (foo, bar) => $.eq(foo.value, bar.value)) + .execute() + ).to.eventually.have.length(2) }) it('left join', async () => { @@ -303,6 +308,30 @@ namespace SelectionTests { { foo: { value: 2, id: 2 }, bar: {} }, { foo: { value: 2, id: 3 }, bar: {} }, ]) + + await expect(database.select('foo') + .join('bar', database.select('bar'), (foo, bar) => $.eq(foo.value, bar.value), 'left') + .execute() + ).to.eventually.have.shape([ + { + value: 0, id: 1, + bar: { uid: 1, pid: 1, value: 0, id: 1 }, + }, + { + value: 0, id: 1, + bar: { uid: 1, pid: 2, value: 0, id: 3 }, + }, + { value: 2, id: 2, bar: {} }, + { value: 2, id: 3, bar: {} }, + ]) + }) + + it('duplicate', async () => { + await expect(database.select('foo') + .project(['value']) + .join('bar', database.select('bar'), (foo, bar) => $.eq(foo.value, bar.uid)) + .execute() + ).to.eventually.have.length(4) }) it('group', async () => { @@ -344,6 +373,12 @@ namespace SelectionTests { }, ({ t1, t2, t3 }) => $.gt($.add(t1.id, t2.id, t3.id), 14)) .execute() ).to.eventually.have.length(4) + + await expect(database.select('bar').where(row => $.gt(row.pid, 1)) + .join('t2', database.select('bar').where(row => $.gt(row.uid, 1))) + .join('t3', database.select('bar').where(row => $.gt(row.id, 4)), (self, t3) => $.gt($.add(self.id, self.t2.id, t3.id), 14)) + .execute() + ).to.eventually.have.length(4) }) it('aggregate', async () => {