Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ import {
TableName,
TableByNameSpec,
TableUpdateFieldNameSpec,
TableUpdateViewColumnMetaSpec,
TableSortKey,
ViewColumnMeta,
createSingleSelectField,
v2CoreTokens,
domainError,
Expand Down Expand Up @@ -1989,4 +1991,100 @@ describe('PostgresTableRepository (pg)', () => {
await db.destroy();
}
});

it('increments view version on view column meta updates', async () => {
const c = container.createChildContainer();
const db = await createPgDb(pgContainer.getConnectionUri());
await registerV2PostgresStateAdapter(c, {
db,
ensureSchema: true,
});
const repo = c.resolve<ITableRepository>(v2CoreTokens.tableRepository);

try {
const baseId = BaseId.generate()._unsafeUnwrap();
const actorId = ActorId.create('system')._unsafeUnwrap();
const context = { actorId };
const spaceId = `spc${getRandomString(16)}`;

await db
.insertInto('space')
.values({ id: spaceId, name: 'View Version Space', created_by: actorId.toString() })
.execute();

await db
.insertInto('base')
.values({
id: baseId.toString(),
space_id: spaceId,
name: 'View Version Base',
order: 1,
created_by: actorId.toString(),
})
.execute();

const builder = Table.builder()
.withBaseId(baseId)
.withName(TableName.create('View Version Table')._unsafeUnwrap());
builder
.field()
.singleLineText()
.withName(FieldName.create('Title')._unsafeUnwrap())
.primary()
.done();
builder.view().defaultGrid().done();
const table = builder.build()._unsafeUnwrap();

const inserted = (await repo.insert(context, table))._unsafeUnwrap();
const view = inserted.views()[0];
expect(view).toBeDefined();
if (!view) return;

const fieldId = inserted.primaryFieldId();
const fieldKey = fieldId.toString();
const currentMeta = view.columnMeta()._unsafeUnwrap().toDto();
const nextMeta = ViewColumnMeta.create({
...currentMeta,
[fieldKey]: {
...(currentMeta[fieldKey] ?? {}),
width: 320,
},
})._unsafeUnwrap();

const persistResult = (
await repo.updateOne(
context,
inserted,
TableUpdateViewColumnMetaSpec.create([
{
viewId: view.id(),
fieldId,
columnMeta: nextMeta,
},
])
)
)._unsafeUnwrap();

expect(persistResult).toEqual({
viewVersionChanges: [
{
viewId: view.id().toString(),
oldVersion: 1,
newVersion: 2,
},
],
});

const row = await db
.selectFrom('view')
.select(['version', 'column_meta'])
.where('id', '=', view.id().toString())
.executeTakeFirst();

expect(row?.version).toBe(2);
expect(row?.column_meta).toContain('"width":320');
} finally {
await db.destroy();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -813,24 +813,47 @@ export class PostgresTableRepository implements core.ITableRepository {
);

const fieldVersionTouchOrder = updateVisitor.fieldVersionTouchOrder();
if (fieldVersionTouchOrder.length === 0) {
const viewVersionTouchOrder = updateVisitor.viewVersionTouchOrder();
if (fieldVersionTouchOrder.length === 0 && viewVersionTouchOrder.length === 0) {
return ok(undefined);
}

const fieldVersionsResult = await this.loadFieldVersionsByIds(
db,
tableId,
fieldVersionTouchOrder
);
if (fieldVersionsResult.isErr()) {
return err(fieldVersionsResult.error);
let fieldVersionChanges: ReadonlyArray<core.FieldVersionChange> | undefined;
if (fieldVersionTouchOrder.length > 0) {
const fieldVersionsResult = await this.loadFieldVersionsByIds(
db,
tableId,
fieldVersionTouchOrder
);
if (fieldVersionsResult.isErr()) {
return err(fieldVersionsResult.error);
}
fieldVersionChanges = this.buildFieldVersionChanges(
fieldVersionTouchOrder,
fieldVersionsResult.value
);
}
const fieldVersionChanges = this.buildFieldVersionChanges(
fieldVersionTouchOrder,
fieldVersionsResult.value
);

return ok({ fieldVersionChanges });
let viewVersionChanges: ReadonlyArray<core.ViewVersionChange> | undefined;
if (viewVersionTouchOrder.length > 0) {
const viewVersionsResult = await this.loadViewVersionsByIds(
db,
tableId,
viewVersionTouchOrder
);
if (viewVersionsResult.isErr()) {
return err(viewVersionsResult.error);
}
viewVersionChanges = this.buildViewVersionChanges(
viewVersionTouchOrder,
viewVersionsResult.value
);
}

return ok({
...(fieldVersionChanges ? { fieldVersionChanges } : {}),
...(viewVersionChanges ? { viewVersionChanges } : {}),
});
} catch (error) {
return err(
domainError.infrastructure({ message: `Failed to update table: ${describeError(error)}` })
Expand Down Expand Up @@ -872,6 +895,41 @@ export class PostgresTableRepository implements core.ITableRepository {
}
}

private async loadViewVersionsByIds(
db: Kysely<V1TeableDatabase> | Transaction<V1TeableDatabase>,
tableId: string,
viewIds: ReadonlyArray<string>
): Promise<Result<ReadonlyMap<string, number>, DomainError>> {
const uniqueViewIds = [...new Set(viewIds)];
if (uniqueViewIds.length === 0) {
return ok(new Map());
}

try {
const rows = await db
.selectFrom('view')
.select(['id', 'version'])
.where('table_id', '=', tableId)
.where('deleted_time', 'is', null)
.where('id', 'in', uniqueViewIds)
.execute();

const versions = new Map(rows.map((row) => [row.id, Number(row.version ?? 0)]));
for (const viewId of uniqueViewIds) {
if (!versions.has(viewId)) {
return err(domainError.notFound({ message: `View not found: ${viewId}` }));
}
}
return ok(versions);
} catch (error) {
return err(
domainError.infrastructure({
message: `Failed to load view versions: ${describeError(error)}`,
})
);
}
}

private buildFieldVersionChanges(
fieldVersionTouchOrder: ReadonlyArray<string>,
finalVersionByFieldId: ReadonlyMap<string, number>
Expand All @@ -897,6 +955,31 @@ export class PostgresTableRepository implements core.ITableRepository {
});
}

private buildViewVersionChanges(
viewVersionTouchOrder: ReadonlyArray<string>,
finalVersionByViewId: ReadonlyMap<string, number>
): ReadonlyArray<core.ViewVersionChange> {
const countByViewId = new Map<string, number>();
for (const viewId of viewVersionTouchOrder) {
countByViewId.set(viewId, (countByViewId.get(viewId) ?? 0) + 1);
}

const indexByViewId = new Map<string, number>();
return viewVersionTouchOrder.map((viewId) => {
const totalCount = countByViewId.get(viewId) ?? 0;
const finalVersion = finalVersionByViewId.get(viewId) ?? 0;
const currentIndex = indexByViewId.get(viewId) ?? 0;
indexByViewId.set(viewId, currentIndex + 1);

const oldVersion = Math.max(finalVersion - totalCount + currentIndex, 0);
return {
viewId,
oldVersion,
newVersion: oldVersion + 1,
};
});
}

@core.TraceSpan()
async delete(
context: core.IExecutionContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ export class TableMetaUpdateVisitor
{
private readonly fieldRowBuilder: TableFieldPersistenceBuilder;
private readonly fieldVersionIncrement: RawBuilder<number> = sql<number>`coalesce(version, 0) + 1`;
private readonly viewVersionIncrement: RawBuilder<number> = sql<number>`coalesce(version, 0) + 1`;
private readonly fieldVersionTouches: string[] = [];
private readonly viewVersionTouches: string[] = [];

constructor(private readonly params: TableMetaUpdateVisitorParams) {
super();
Expand All @@ -128,6 +130,10 @@ export class TableMetaUpdateVisitor
return [...this.fieldVersionTouches];
}

viewVersionTouchOrder(): ReadonlyArray<string> {
return [...this.viewVersionTouches];
}

visitTableByBaseId(_: TableByBaseIdSpec): Result<ReadonlyArray<TableUpdateBuilder>, DomainError> {
return err(
domainError.validation({ message: 'TableByBaseIdSpec is not supported for table updates' })
Expand Down Expand Up @@ -231,11 +237,16 @@ export class TableMetaUpdateVisitor
spec: TableUpdateViewColumnMetaSpec
): Result<ReadonlyArray<TableUpdateBuilder>, DomainError> {
const updates = spec.updates();
for (const update of updates) {
this.trackViewVersionTouch(update.viewId.toString());
}

const statements: ReadonlyArray<TableUpdateBuilder> = updates.map((update) =>
this.params.db
.updateTable('view')
.set({
column_meta: JSON.stringify(update.columnMeta.toDto()),
version: this.viewVersionIncrement,
last_modified_time: this.params.now,
last_modified_by: this.params.actorId,
})
Expand All @@ -249,6 +260,10 @@ export class TableMetaUpdateVisitor
visitTableUpdateViewQueryDefaults(
spec: TableUpdateViewQueryDefaultsSpec
): Result<ReadonlyArray<TableUpdateBuilder>, DomainError> {
for (const update of spec.updates()) {
this.trackViewVersionTouch(update.viewId.toString());
}

const statements: ReadonlyArray<TableUpdateBuilder> = spec
.updates()
.map((update: TableViewQueryDefaultsUpdate) => {
Expand All @@ -270,6 +285,7 @@ export class TableMetaUpdateVisitor
: this.stringifyLegacyFilter(this.mapRecordFilterToLegacy(query.filter)),
sort: sortPayload ? JSON.stringify(sortPayload) : null,
group: query.group ? JSON.stringify(query.group) : null,
version: this.viewVersionIncrement,
last_modified_time: this.params.now,
last_modified_by: this.params.actorId,
})
Expand Down Expand Up @@ -959,4 +975,8 @@ export class TableMetaUpdateVisitor
private trackFieldVersionTouch(fieldId: FieldId): void {
this.fieldVersionTouches.push(fieldId.toString());
}

private trackViewVersionTouch(viewId: string): void {
this.viewVersionTouches.push(viewId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -526,13 +526,33 @@ describe('Realtime projections', () => {
tableId: table.id(),
viewId,
fieldId: table.primaryFieldId(),
oldVersion: 7,
newVersion: 8,
});

const result = await projection.handle(createContext(), event);
result._unsafeUnwrap();

expect(engine.ensures).toHaveLength(1);
expect(engine.changes).toHaveLength(1);
expect(engine.ensures).toHaveLength(2);
expect(engine.ensures[0]?.docId.toString()).toBe(
`tbl_${table.baseId().toString()}/${table.id().toString()}`
);
expect(engine.ensures[1]?.docId.toString()).toBe(
`viw_${table.id().toString()}/${viewId.toString()}`
);
expect(engine.changes).toHaveLength(2);
expect(engine.changes[0]?.docId.toString()).toBe(
`tbl_${table.baseId().toString()}/${table.id().toString()}`
);
expect(engine.changes[1]?.docId.toString()).toBe(
`viw_${table.id().toString()}/${viewId.toString()}`
);
expect(engine.changes[1]?.change).toEqual({
type: 'set',
path: ['columnMeta'],
value: buildTableDto(table).views[0]?.columnMeta,
});
expect(engine.changes[1]?.options).toEqual({ version: 7 });
});

it('ignores missing views', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { v2CoreTokens } from '../../ports/tokens';
import { ProjectionHandler } from './Projection';

const tableCollectionPrefix = 'tbl';
const viewCollectionPrefix = 'viw';

@ProjectionHandler(ViewColumnMetaUpdated)
@injectable()
Expand Down Expand Up @@ -56,12 +57,35 @@ export class ViewColumnMetaUpdatedRealtimeProjection
// Ensure table document exists first (for tables created before realtime was enabled)
yield* (await realtimeEngine.ensure(context, docId, snapshot)).safeUnwrap();

// Apply incremental change to update view columnMeta
return realtimeEngine.applyChange(context, docId, {
type: 'set',
path: ['views', viewIndex, 'columnMeta'],
value: viewDto.columnMeta,
});
// Keep the table snapshot in sync for table-level consumers.
yield* (
await realtimeEngine.applyChange(context, docId, {
type: 'set',
path: ['views', viewIndex, 'columnMeta'],
value: viewDto.columnMeta,
})
).safeUnwrap();

// Keep the standalone view document in sync for ShareDB/SDK view subscriptions.
const viewCollection = `${viewCollectionPrefix}_${event.tableId.toString()}`;
const viewDocId = yield* RealtimeDocId.fromParts(
viewCollection,
event.viewId.toString()
).safeUnwrap();
yield* (await realtimeEngine.ensure(context, viewDocId, viewDto)).safeUnwrap();

return realtimeEngine.applyChange(
context,
viewDocId,
{
type: 'set',
path: ['columnMeta'],
value: viewDto.columnMeta,
},
{
version: event.oldVersion,
}
);
});
}
}
Loading
Loading