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
5 changes: 5 additions & 0 deletions .changeset/fix-powersync-correctness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/powersync-db-collection': minor
---

Require PowerSync 2 and fix update conservation, declared-view rows, transformed-schema comparison, and portable inferred declarations.
14 changes: 7 additions & 7 deletions docs/collections/powersync-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ const db = new PowerSyncDatabase({
### 3. (optional) Configure Sync with a Backend

```ts
import {
AbstractPowerSyncDatabase,
import type {
CommonPowerSyncDatabase,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
PowerSyncBackendConnector,
PowerSyncCredentials,
} from "@powersync/web"
Expand All @@ -67,11 +67,11 @@ class Connector implements PowerSyncBackendConnector {

/** Upload local changes to the app backend.
*
* Use {@link AbstractPowerSyncDatabase.getCrudBatch} to get a batch of changes to upload.
* Use {@link CommonPowerSyncDatabase.getCrudBatch} to get a batch of changes to upload.
*
* Any thrown errors will result in a retry after the configured wait period (default: 5 seconds).
*/
uploadData: (database: AbstractPowerSyncDatabase) => Promise<void>
uploadData: (database: CommonPowerSyncDatabase) => Promise<void>
}

// Configure the client to connect to a PowerSync service and your backend
Expand Down Expand Up @@ -474,12 +474,12 @@ await documents.delete(docId, {
The metadata is available in PowerSync `CrudEntry` records when processing uploads in the connector:

```typescript
import { CrudEntry } from "@powersync/web"
import type { CommonPowerSyncDatabase } from "@powersync/web"

class Connector implements PowerSyncBackendConnector {
// ...

async uploadData(database: AbstractPowerSyncDatabase) {
async uploadData(database: CommonPowerSyncDatabase) {
const batch = await database.getCrudBatch()
if (!batch) return

Expand Down Expand Up @@ -1097,4 +1097,4 @@ const liveQuery = createLiveQueryCollection({
completed: todo.completed,
})),
})
```
```
6 changes: 3 additions & 3 deletions packages/powersync-db-collection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@
"p-defer": "^4.0.1"
},
"peerDependencies": {
"@powersync/common": "^1.41.0"
"@powersync/common": "^2.0.0"
},
"devDependencies": {
"@powersync/common": "1.49.0",
"@powersync/node": "0.18.1",
"@powersync/common": "2.0.0",
"@powersync/node": "0.20.0",
"@types/debug": "^4.1.12",
"@vitest/coverage-istanbul": "^3.2.4",
"better-sqlite3": "^12.6.2"
Expand Down
71 changes: 46 additions & 25 deletions packages/powersync-db-collection/src/PowerSyncTransactor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { sanitizeSQL } from '@powersync/common'
import { LogLevels, sanitizeSQL } from '@powersync/common'
import { LoadSubsetOperationAbortedError } from '@tanstack/db'
import DebugModule from 'debug'
import { PendingOperationStore } from './PendingOperationStore'
import { asPowerSyncRecord, mapOperationToPowerSync } from './helpers'
import type { AbstractPowerSyncDatabase, LockContext } from '@powersync/common'
import type { CommonPowerSyncDatabase, LockContext } from '@powersync/common'
import type { PendingMutation, Transaction } from '@tanstack/db'
import type { PendingOperation } from './PendingOperationStore'
import type {
Expand All @@ -14,7 +14,7 @@ import type {
const debug = DebugModule.debug(`ts/db:powersync`)

export type TransactorOptions = {
database: AbstractPowerSyncDatabase
database: CommonPowerSyncDatabase
}

/**
Expand Down Expand Up @@ -53,7 +53,7 @@ export type TransactorOptions = {
* @returns A promise that resolves when the mutations have been persisted to PowerSync
*/
export class PowerSyncTransactor {
database: AbstractPowerSyncDatabase
database: CommonPowerSyncDatabase
pendingOperationStore: PendingOperationStore

constructor(options: TransactorOptions) {
Expand All @@ -74,24 +74,29 @@ export class PowerSyncTransactor {
* The transaction might contain operations for different collections.
* We can do some optimizations for single-collection transactions.
*/
const mutationsCollectionIds = mutations.map(
(mutation) => mutation.collection.id,
)
const collectionIds = Array.from(new Set(mutationsCollectionIds))
const collectionsById = new Map<
string,
PendingMutation<any>[`collection`]
>()
const lastCollectionMutationIndexes = new Map<string, number>()
const allCollections = collectionIds
.map((id) => mutations.find((mutation) => mutation.collection.id == id)!)
.map((mutation) => mutation.collection)
for (const collectionId of collectionIds) {
lastCollectionMutationIndexes.set(
collectionId,
mutationsCollectionIds.lastIndexOf(collectionId),
)
for (const [index, mutation] of mutations.entries()) {
const collectionId = mutation.collection.id
if (!collectionsById.has(collectionId)) {
collectionsById.set(collectionId, mutation.collection)
}
const changesDatabase =
mutation.type != `update` ||
Object.keys(mutation.changes).some((key) => key != `id`) ||
(typeof mutation.metadata != `undefined` &&
this.getMutationCollectionMeta(mutation).metadataIsTracked)
if (changesDatabase) {
lastCollectionMutationIndexes.set(collectionId, index)
}
}

// Check all the observers are ready before taking a lock
await Promise.all(
allCollections.map(async (collection) => {
Array.from(collectionsById.values()).map(async (collection) => {
if (collection.isReady()) {
return
}
Expand Down Expand Up @@ -221,7 +226,8 @@ export class PowerSyncTransactor {
waitForCompletion,
// eslint-disable-next-line no-shadow
async (tableName, mutation, serializeValue) => {
const values = serializeValue(mutation.modified)
const { id: _id, ...changes } = mutation.changes
const values = serializeValue(changes)
const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)
const queryParameters = Object.values(values)

Expand All @@ -231,14 +237,20 @@ export class PowerSyncTransactor {
queryParameters.push(metadataValue)
}

if (keys.length == 0) {
return false
}

await context.execute(
`
UPDATE ${tableName}
SET ${keys.map((key) => `${key} = ?`).join(`, `)}
WHERE id = ?
`,
[...queryParameters, asPowerSyncRecord(mutation.modified).id],
[...queryParameters, asPowerSyncRecord(mutation.original).id],
)

return
},
)
}
Expand Down Expand Up @@ -294,12 +306,20 @@ export class PowerSyncTransactor {
tableName: string,
mutation: PendingMutation<any>,
serializeValue: (value: any) => Record<string, unknown>,
) => Promise<void>,
) => Promise<void | false>,
): Promise<PendingOperation | null> {
const { tableName, trackedTableName, serializeValue } =
this.getMutationCollectionMeta(mutation)

await handler(sanitizeSQL`${tableName}`, mutation, serializeValue)
const executed = await handler(
sanitizeSQL`${tableName}`,
mutation,
serializeValue,
)

if (executed === false) {
return null
}

if (!waitForCompletion) {
return null
Expand Down Expand Up @@ -344,10 +364,11 @@ export class PowerSyncTransactor {
// If it's not supported, we don't store metadata.
if (typeof mutation.metadata != `undefined`) {
// Log a warning if metadata is provided but not tracked.
this.database.logger.warn(
`Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`,
mutation.metadata,
)
this.database.logger.log({
level: LogLevels.warn,
message: `Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`,
error: mutation.metadata,
})
}
return null
} else if (typeof mutation.metadata == `undefined`) {
Expand Down
16 changes: 11 additions & 5 deletions packages/powersync-db-collection/src/definitions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AbstractPowerSyncDatabase, Table } from '@powersync/common'
import type { CommonPowerSyncDatabase, Table } from '@powersync/common'
import type { StandardSchemaV1 } from '@standard-schema/spec'
import type {
BaseCollectionConfig,
Expand All @@ -22,7 +22,9 @@ import type {
export type InferPowerSyncOutputType<
TTable extends Table = Table,
TSchema extends StandardSchemaV1<PowerSyncRecord> = never,
> = TSchema extends never ? ExtractedTable<TTable> : InferSchemaOutput<TSchema>
> = [TSchema] extends [never]
? ExtractedTable<TTable>
: InferSchemaOutput<TSchema>

/**
* A mapping type for custom serialization of object properties to SQLite-compatible values.
Expand Down Expand Up @@ -202,15 +204,19 @@ export type OnDemandSyncHooks = {

export type BasePowerSyncCollectionConfig<
TTable extends Table = Table,
TSchema extends StandardSchemaV1 = never,
TSchema extends StandardSchemaV1<any> = never,
> = Omit<
BaseCollectionConfig<ExtractedTable<TTable>, string, TSchema>,
BaseCollectionConfig<
InferPowerSyncOutputType<TTable, TSchema>,
string,
TSchema
>,
`onInsert` | `onUpdate` | `onDelete` | `getKey` | `syncMode`
> & {
/** The PowerSync schema Table definition */
table: TTable
/** The PowerSync database instance */
database: AbstractPowerSyncDatabase
database: CommonPowerSyncDatabase
/**
* The maximum number of documents to read from the SQLite table
* in a single batch during the initial sync between PowerSync and the
Expand Down
28 changes: 10 additions & 18 deletions packages/powersync-db-collection/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { DiffTriggerOperation } from '@powersync/common'
import type {
BaseColumnType,
ExtractColumnValueType,
Table,
} from '@powersync/common'
import type { ExtractColumnValueType, Table } from '@powersync/common'

/**
* All PowerSync table records include a UUID `id` column.
Expand All @@ -28,11 +24,12 @@ type OptionalIfUndefined<T> = {
/**
* Provides the base column types for a table. This excludes the `id` column.
*/
export type ExtractedTableColumns<TTable extends Table> = {
[K in keyof TTable[`columnMap`]]: ExtractColumnValueType<
TTable[`columnMap`][K]
>
}
export type ExtractedTableColumns<TTable extends Table> =
TTable extends Table<infer Columns>
? {
[K in keyof Columns]: ExtractColumnValueType<Columns[K]>
}
: never
/**
* Utility type that extracts the typed structure of a table based on its column definitions.
* Maps each column to its corresponding TypeScript type using ExtractColumnValueType.
Expand All @@ -54,8 +51,8 @@ export type ExtractedTable<TTable extends Table> =
}

export type OptionalExtractedTable<TTable extends Table> = OptionalIfUndefined<{
[K in keyof TTable[`columnMap`]]: WithUndefinedIfNull<
ExtractColumnValueType<TTable[`columnMap`][K]>
[K in keyof ExtractedTableColumns<TTable>]: WithUndefinedIfNull<
ExtractedTableColumns<TTable>[K]
>
}> & {
id: string
Expand All @@ -66,7 +63,7 @@ export type OptionalExtractedTable<TTable extends Table> = OptionalIfUndefined<{
* requires the keys be equal, but the values can have any value type.
*/
export type AnyTableColumnType<TTable extends Table> = {
[K in keyof TTable[`columnMap`]]: any
[K in keyof ExtractedTableColumns<TTable>]: unknown
} & { id: string }

export function asPowerSyncRecord(record: any): PowerSyncRecord {
Expand All @@ -76,11 +73,6 @@ export function asPowerSyncRecord(record: any): PowerSyncRecord {
return record as PowerSyncRecord
}

// Helper type to ensure the keys of TOutput match the Table columns
export type MapBaseColumnType<TOutput> = {
[Key in keyof TOutput]: BaseColumnType<any>
}

/**
* Maps {@link DiffTriggerOperation} to TanstackDB operations
*/
Expand Down
1 change: 1 addition & 0 deletions packages/powersync-db-collection/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './definitions'
export * from './powersync'
export * from './PowerSyncTransactor'
export * from './sqlite-compiler'
export type { OptionalExtractedTable } from './helpers'
Loading
Loading