From 02b48c4171ff8eede7e80a2917bb4a131332caa6 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Sun, 20 Sep 2026 13:03:40 +0100 Subject: [PATCH 1/2] fix(sqlite): preserve persistence type contracts --- .../fix-sqlite-persistence-type-contracts.md | 6 + .../src/persisted.ts | 57 ++++++++- .../persisted-options-type-oracle.test-d.ts | 108 ++++++++++++++++++ .../tests/persisted.test-d.ts | 2 +- .../src/expo-sqlite-driver.ts | 62 ++++++---- .../tests/expo-sqlite-driver.test-d.ts | 37 ++++++ .../tests/expo-sqlite-driver.test.ts | 53 +++++++++ .../helpers/expo-emulator-database-factory.ts | 24 ++-- .../tests/helpers/expo-sqlite-test-db.ts | 3 +- 9 files changed, 311 insertions(+), 41 deletions(-) create mode 100644 .changeset/fix-sqlite-persistence-type-contracts.md create mode 100644 packages/db-sqlite-persistence-core/tests/persisted-options-type-oracle.test-d.ts create mode 100644 packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test-d.ts diff --git a/.changeset/fix-sqlite-persistence-type-contracts.md b/.changeset/fix-sqlite-persistence-type-contracts.md new file mode 100644 index 0000000000..3266f71d72 --- /dev/null +++ b/.changeset/fix-sqlite-persistence-type-contracts.md @@ -0,0 +1,6 @@ +--- +'@tanstack/db-sqlite-persistence-core': patch +'@tanstack/expo-db-sqlite-persistence': patch +--- + +Preserve schema input and output inference when persisted collection options are passed to `createCollection`. Accept Expo's native SQLite database type and preserve transaction callback results while validating bind values against Expo's supported domain. diff --git a/packages/db-sqlite-persistence-core/src/persisted.ts b/packages/db-sqlite-persistence-core/src/persisted.ts index 0f1e4112ac..9ee0c810f7 100644 --- a/packages/db-sqlite-persistence-core/src/persisted.ts +++ b/packages/db-sqlite-persistence-core/src/persisted.ts @@ -20,6 +20,7 @@ import type { CollectionConfig, CollectionIndexMetadata, DeleteMutationFnParams, + InferSchemaOutput, InsertMutationFnParams, LoadSubsetFn, LoadSubsetOptions, @@ -2739,13 +2740,59 @@ function createLoopbackSyncConfig< } } +export function persistedCollectionOptions< + TSchema extends StandardSchemaV1, + TKey extends string | number, + TUtils extends UtilsRecord = UtilsRecord, +>( + options: PersistedSyncWrappedOptions< + InferSchemaOutput, + TKey, + TSchema, + TUtils + > & { + schema: TSchema + }, +): PersistedSyncOptionsResult< + InferSchemaOutput, + TKey, + TSchema, + TUtils +> & { + schema: TSchema +} + +export function persistedCollectionOptions< + TSchema extends StandardSchemaV1, + TKey extends string | number, + TUtils extends UtilsRecord = UtilsRecord, +>( + options: PersistedLocalOnlyOptions< + InferSchemaOutput, + TKey, + TSchema, + TUtils + > & { + schema: TSchema + }, +): PersistedLocalOnlyOptionsResult< + InferSchemaOutput, + TKey, + TSchema, + TUtils +> & { + schema: TSchema +} + export function persistedCollectionOptions< T extends object, TKey extends string | number, TSchema extends StandardSchemaV1 = never, TUtils extends UtilsRecord = UtilsRecord, >( - options: PersistedSyncWrappedOptions, + options: PersistedSyncWrappedOptions & { + schema?: never + }, ): PersistedSyncOptionsResult export function persistedCollectionOptions< @@ -2754,7 +2801,9 @@ export function persistedCollectionOptions< TSchema extends StandardSchemaV1 = never, TUtils extends UtilsRecord = UtilsRecord, >( - options: PersistedLocalOnlyOptions, + options: PersistedLocalOnlyOptions & { + schema?: never + }, ): PersistedLocalOnlyOptionsResult export function persistedCollectionOptions< @@ -2815,7 +2864,7 @@ export function persistedCollectionOptions< persistedCollectionOptions({ ...options, id: collectionId, - }) as typeof result, + } as never) as typeof result, ) } @@ -2923,7 +2972,7 @@ export function persistedCollectionOptions< persistedCollectionOptions({ ...options, id: collectionId, - }) as typeof result, + } as never) as typeof result, ) } diff --git a/packages/db-sqlite-persistence-core/tests/persisted-options-type-oracle.test-d.ts b/packages/db-sqlite-persistence-core/tests/persisted-options-type-oracle.test-d.ts new file mode 100644 index 0000000000..28293d38b3 --- /dev/null +++ b/packages/db-sqlite-persistence-core/tests/persisted-options-type-oracle.test-d.ts @@ -0,0 +1,108 @@ +import { describe, it } from 'vitest' +import { createCollection } from '@tanstack/db' +import { persistedCollectionOptions } from '../src' +import type { PersistenceAdapter } from '../src' +import type { StandardSchemaV1 } from '@standard-schema/spec' + +const adapter: PersistenceAdapter = { + loadSubset: () => Promise.resolve([]), + applyCommittedTx: () => Promise.resolve(), + ensureIndex: () => Promise.resolve(), +} + +type RowInput = { id: string; rank: string; label?: string } +type RowOutput = { id: string; rank: number; label: string } + +const rowSchema = null as unknown as StandardSchemaV1 + +// Law: persisted options preserve createCollection's schema input/output roles, +// inferred key, and own-sync-key mode split. The hostile controls reject the +// output-as-input and present-but-undefined-sync fractures found in the old API. +describe(`persisted collection option type oracle`, () => { + it(`composes an inferred transforming schema with createCollection`, () => { + const localOptions = persistedCollectionOptions({ + id: `local-schema`, + schema: rowSchema, + schemaVersion: 1, + getKey: (row) => row.id, + persistence: { adapter }, + }) + + const localCollection = createCollection(localOptions) + localCollection.insert({ id: `row`, rank: `1` } satisfies RowInput) + const localOutput: RowOutput | undefined = localCollection.get(`row`) + void localOutput + + // @ts-expect-error getKey inferred string keys from the schema output + localCollection.get(1) + + const syncedOptions = persistedCollectionOptions({ + id: `synced-schema`, + schema: rowSchema, + schemaVersion: 1, + getKey: (row) => row.id, + sync: { + sync: ({ markReady }) => { + markReady() + }, + }, + persistence: { adapter }, + }) + + const syncedCollection = createCollection(syncedOptions) + syncedCollection.insert({ id: `row`, rank: `1` } satisfies RowInput) + const syncedOutput: RowOutput | undefined = syncedCollection.get(`row`) + void syncedOutput + }) + + it(`keeps schema input and output roles distinct`, () => { + const collection = createCollection( + persistedCollectionOptions({ + id: `schema-roles`, + schema: rowSchema, + getKey: (row) => row.id, + persistence: { adapter }, + }), + ) + + collection.insert({ id: `row`, rank: `1` }) + + // @ts-expect-error transformed output values are not valid schema input + collection.insert({ id: `row`, rank: 1, label: `output` }) + + const output = collection.get(`row`) + if (output) { + const rank: number = output.rank + const label: string = output.label + void rank + void label + } + }) + + it(`discriminates sync mode from the presence of the sync key`, () => { + persistedCollectionOptions({ + id: `local`, + getKey: (row: RowOutput) => row.id, + persistence: { adapter }, + }) + + persistedCollectionOptions({ + id: `synced`, + getKey: (row: RowOutput) => row.id, + sync: { + sync: ({ markReady }) => { + markReady() + }, + }, + persistence: { adapter }, + }) + + persistedCollectionOptions({ + id: `invalid-undefined-sync`, + getKey: (row: RowOutput) => row.id, + // @ts-expect-error a present sync key must hold a SyncConfig + sync: undefined, + persistence: { adapter }, + }) + }) +}) diff --git a/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts b/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts index b07f9f787d..9019334c39 100644 --- a/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts +++ b/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts @@ -87,9 +87,9 @@ describe(`persisted collection types`, () => { }) it(`requires persistence config`, () => { - // @ts-expect-error persistedCollectionOptions requires a persistence config persistedCollectionOptions({ getKey: (item: Todo) => item.id, + // @ts-expect-error persistedCollectionOptions requires persistence when sync is provided sync: { sync: ({ markReady }: { markReady: () => void }) => { markReady() diff --git a/packages/expo-db-sqlite-persistence/src/expo-sqlite-driver.ts b/packages/expo-db-sqlite-persistence/src/expo-sqlite-driver.ts index e85df20aac..71e06b4766 100644 --- a/packages/expo-db-sqlite-persistence/src/expo-sqlite-driver.ts +++ b/packages/expo-db-sqlite-persistence/src/expo-sqlite-driver.ts @@ -1,33 +1,33 @@ import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core' import type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core' +import type { + SQLiteBindParams, + SQLiteBindValue, + SQLiteRunResult, +} from 'expo-sqlite' -export type ExpoSQLiteBindParams = - | ReadonlyArray - | Record +export type ExpoSQLiteBindParams = SQLiteBindParams -export type ExpoSQLiteRunResult = { - changes: number - lastInsertRowId: number -} +export type ExpoSQLiteRunResult = SQLiteRunResult export type ExpoSQLiteQueryable = { execAsync: (sql: string) => Promise - getAllAsync: ( - sql: string, - params?: ExpoSQLiteBindParams, - ) => Promise> - runAsync: ( - sql: string, - params?: ExpoSQLiteBindParams, - ) => Promise + getAllAsync: { + (sql: string, params: SQLiteBindParams): Promise> + (sql: string): Promise> + } + runAsync: { + (sql: string, params: SQLiteBindParams): Promise + (sql: string): Promise + } } export type ExpoSQLiteTransaction = ExpoSQLiteQueryable export type ExpoSQLiteDatabaseLike = ExpoSQLiteQueryable & { - withExclusiveTransactionAsync: ( - task: (transaction: ExpoSQLiteTransaction) => Promise, - ) => Promise + withExclusiveTransactionAsync: ( + task: (transaction: ExpoSQLiteTransaction) => Promise, + ) => Promise closeAsync?: () => Promise } @@ -144,10 +144,12 @@ export class ExpoSQLiteDriver implements SQLiteDriver { ): Promise { return this.enqueue(async () => { const database = await this.getDatabase() - return database.withExclusiveTransactionAsync(async (transaction) => { + let result: T | undefined + await database.withExclusiveTransactionAsync(async (transaction) => { const transactionDriver = this.createTransactionDriver(transaction) - return fn(transactionDriver) + result = await fn(transactionDriver) }) + return result as T }) } @@ -225,10 +227,22 @@ export class ExpoSQLiteDriver implements SQLiteDriver { } } -function normalizeParams( - params: ReadonlyArray, -): ExpoSQLiteBindParams | undefined { - return params.length > 0 ? [...params] : undefined +function normalizeParams(params: ReadonlyArray): SQLiteBindParams { + return params.map((value) => { + if ( + value === null || + typeof value === `string` || + typeof value === `number` || + typeof value === `boolean` || + value instanceof Uint8Array + ) { + return value satisfies SQLiteBindValue + } + + throw new TypeError( + `Expo SQLite bind parameters must be strings, numbers, booleans, null, or Uint8Array values`, + ) + }) } export function createExpoSQLiteDriver( diff --git a/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test-d.ts b/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test-d.ts new file mode 100644 index 0000000000..20383dc2d7 --- /dev/null +++ b/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test-d.ts @@ -0,0 +1,37 @@ +import { describe, it } from 'vitest' +import { createExpoSQLitePersistence } from '../src' +import { + ExpoSQLiteDriver, + createExpoSQLiteDriver, +} from '../src/expo-sqlite-driver' +import type { SQLiteDatabase } from 'expo-sqlite' +import type { ExpoSQLiteDatabaseLike } from '../src' + +// Law: the adapter's structural boundary accepts the installed Expo vendor +// database itself while retaining the exclusive-transaction requirement. +describe(`Expo SQLite driver types`, () => { + it(`accepts the vendor database returned by expo-sqlite`, () => { + const database = null as unknown as SQLiteDatabase + + createExpoSQLitePersistence({ database }) + createExpoSQLiteDriver({ database }) + new ExpoSQLiteDriver({ database }) + + const compatible: ExpoSQLiteDatabaseLike = database + void compatible + }) + + it(`rejects databases without an exclusive transaction boundary`, () => { + const database = { + execAsync: (_sql: string) => Promise.resolve(), + getAllAsync: (_sql: string) => Promise.resolve([] as Array), + runAsync: (_sql: string) => + Promise.resolve({ changes: 0, lastInsertRowId: 0 }), + } + + createExpoSQLitePersistence({ + // @ts-expect-error persistence requires exclusive transactions + database, + }) + }) +}) diff --git a/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test.ts b/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test.ts index cff37d7c56..ddfc602b23 100644 --- a/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test.ts +++ b/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test.ts @@ -5,6 +5,10 @@ import { afterEach, expect, it } from 'vitest' import { ExpoSQLiteDriver } from '../src/expo-sqlite-driver' import { InvalidPersistedCollectionConfigError } from '../../db-sqlite-persistence-core/src' import { createExpoSQLiteTestDatabase } from './helpers/expo-sqlite-test-db' +import type { + ExpoSQLiteDatabaseLike, + ExpoSQLiteTransaction, +} from '../src/expo-sqlite-driver' const activeCleanupFns: Array<() => void | Promise> = [] @@ -193,6 +197,55 @@ it(`throws when transaction callback omits transaction driver argument`, async ( ).rejects.toThrow(`transaction driver argument`) }) +it(`returns callback values when Expo's transaction boundary returns void`, async () => { + const transaction: ExpoSQLiteTransaction = { + execAsync: () => Promise.resolve(), + getAllAsync: () => Promise.resolve([] as Array), + runAsync: () => + Promise.resolve({ + changes: 0, + lastInsertRowId: 0, + }), + } + const database: ExpoSQLiteDatabaseLike = { + ...transaction, + withExclusiveTransactionAsync: async (task) => { + await task(transaction) + }, + } + const driver = new ExpoSQLiteDriver({ database }) + + await expect( + driver.transaction((transactionDriver) => { + void transactionDriver + return Promise.resolve({ status: `committed` as const }) + }), + ).resolves.toEqual({ status: `committed` }) +}) + +it(`rejects values outside Expo's SQLite bind domain`, async () => { + const transaction: ExpoSQLiteTransaction = { + execAsync: () => Promise.resolve(), + getAllAsync: () => Promise.resolve([] as Array), + runAsync: () => + Promise.resolve({ + changes: 0, + lastInsertRowId: 0, + }), + } + const database: ExpoSQLiteDatabaseLike = { + ...transaction, + withExclusiveTransactionAsync: async (task) => { + await task(transaction) + }, + } + const driver = new ExpoSQLiteDriver({ database }) + + await expect(driver.run(`SELECT ?`, [{ unsupported: true }])).rejects.toThrow( + `Expo SQLite bind parameters`, + ) +}) + it(`throws config error when expo database methods are missing`, () => { expect(() => new ExpoSQLiteDriver({ database: {} as never })).toThrowError( InvalidPersistedCollectionConfigError, diff --git a/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-database-factory.ts b/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-database-factory.ts index de347ee076..f93983aca2 100644 --- a/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-database-factory.ts +++ b/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-database-factory.ts @@ -3,10 +3,8 @@ import type { ExpoSQLiteTestDatabase, ExpoSQLiteTestDatabaseFactory, } from './expo-sqlite-test-db' -import type { - ExpoSQLiteBindParams, - ExpoSQLiteTransaction, -} from '../../src/expo-sqlite-driver' +import type { ExpoSQLiteTransaction } from '../../src/expo-sqlite-driver' +import type { SQLiteBindParams } from 'expo-sqlite' function resolvePlatform(): `ios` | `android` { const platform = process.env.TANSTACK_DB_EXPO_RUNTIME_PLATFORM?.trim() @@ -46,13 +44,17 @@ export function createMobileSQLiteTestDatabaseFactory(): ExpoSQLiteTestDatabaseF execAsync: async (sql: string) => { await (await getDatabase()).execAsync(sql) }, - getAllAsync: async (sql: string, params?: ExpoSQLiteBindParams) => - (await getDatabase()).getAllAsync(sql, params), - runAsync: async (sql: string, params?: ExpoSQLiteBindParams) => - (await getDatabase()).runAsync(sql, params), - withExclusiveTransactionAsync: async ( - task: (transaction: ExpoSQLiteTransaction) => Promise, - ): Promise => + getAllAsync: async (sql: string, params?: SQLiteBindParams) => + params === undefined + ? (await getDatabase()).getAllAsync(sql) + : (await getDatabase()).getAllAsync(sql, params), + runAsync: async (sql: string, params?: SQLiteBindParams) => + params === undefined + ? (await getDatabase()).runAsync(sql) + : (await getDatabase()).runAsync(sql, params), + withExclusiveTransactionAsync: async ( + task: (transaction: ExpoSQLiteTransaction) => Promise, + ): Promise => (await getDatabase()).withExclusiveTransactionAsync(task), closeAsync: async () => { if (!databasePromise) { diff --git a/packages/expo-db-sqlite-persistence/tests/helpers/expo-sqlite-test-db.ts b/packages/expo-db-sqlite-persistence/tests/helpers/expo-sqlite-test-db.ts index 70cc5ede58..0690d8630c 100644 --- a/packages/expo-db-sqlite-persistence/tests/helpers/expo-sqlite-test-db.ts +++ b/packages/expo-db-sqlite-persistence/tests/helpers/expo-sqlite-test-db.ts @@ -5,6 +5,7 @@ import type { ExpoSQLiteRunResult, ExpoSQLiteTransaction, } from '../../src/expo-sqlite-driver' +import type { SQLiteBindValue } from 'expo-sqlite' export type ExpoSQLiteTestDatabase = ExpoSQLiteDatabaseLike & { closeAsync: () => Promise @@ -35,7 +36,7 @@ function normalizeRunResult( function hasNamedParameters( params: ExpoSQLiteBindParams | undefined, -): params is Record { +): params is Record { return params !== undefined && !Array.isArray(params) } From 6fd845c8fd7c3e470bf63c8eb7df5c6e428e9bbe Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Mon, 21 Sep 2026 09:59:35 +0100 Subject: [PATCH 2/2] test(sqlite): document persistence oracle contracts --- .../persisted-options-type-oracle.test-d.ts | 24 ++++++++++++++++--- .../tests/expo-sqlite-driver.test-d.ts | 24 +++++++++++++++++-- .../tests/expo-sqlite-driver.test.ts | 6 +++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/packages/db-sqlite-persistence-core/tests/persisted-options-type-oracle.test-d.ts b/packages/db-sqlite-persistence-core/tests/persisted-options-type-oracle.test-d.ts index 28293d38b3..673ebd0c9d 100644 --- a/packages/db-sqlite-persistence-core/tests/persisted-options-type-oracle.test-d.ts +++ b/packages/db-sqlite-persistence-core/tests/persisted-options-type-oracle.test-d.ts @@ -15,9 +15,27 @@ type RowOutput = { id: string; rank: number; label: string } const rowSchema = null as unknown as StandardSchemaV1 -// Law: persisted options preserve createCollection's schema input/output roles, -// inferred key, and own-sync-key mode split. The hostile controls reject the -// output-as-input and present-but-undefined-sync fractures found in the old API. +/** + * # What does `persistedCollectionOptions` preserve? + * + * Law and source: the public `persistedCollectionOptions` utility must preserve + * `createCollection`'s Standard Schema input, output, and inferred key contract. + * Its own-key `sync` split comes from the persisted local-only and sync-wrapped + * option overloads; a present `sync` key must contain a `SyncConfig`. + * + * Legal forms here are transforming-schema collections with and without + * external sync, plus schema-free local and synced option objects. The public + * type path is `persistedCollectionOptions(...)` into `createCollection(...)`. + * The checkpoint is the resulting `insert`, `get`, and inferred key types. + * + * Positive observations accept schema input on insert, expose schema output on + * read, infer string keys, and accept both sync modes. Hostile observations + * reject output-as-input, numeric keys, and a present-but-undefined `sync`. + * The valid local and synced cells control against rejecting every option. + * + * This oracle does not exercise schema parsing, adapter I/O, the returned + * `PersistedCollectionUtils`, sync execution, or schema-validation failures. + */ describe(`persisted collection option type oracle`, () => { it(`composes an inferred transforming schema with createCollection`, () => { const localOptions = persistedCollectionOptions({ diff --git a/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test-d.ts b/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test-d.ts index 20383dc2d7..761c235a5f 100644 --- a/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test-d.ts +++ b/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test-d.ts @@ -7,8 +7,16 @@ import { import type { SQLiteDatabase } from 'expo-sqlite' import type { ExpoSQLiteDatabaseLike } from '../src' -// Law: the adapter's structural boundary accepts the installed Expo vendor -// database itself while retaining the exclusive-transaction requirement. +/** + * The public driver boundary accepts Expo's installed `SQLiteDatabase` while + * retaining its exclusive-transaction requirement. It also preserves the + * generic `SQLiteDriver.transaction` result even though Expo's native + * `withExclusiveTransactionAsync` boundary returns `Promise`. + * + * These compile-time checkpoints cover structural database compatibility and + * the returned `Promise`. The paired runtime test observes the callback value + * only after the native exclusive transaction boundary settles. + */ describe(`Expo SQLite driver types`, () => { it(`accepts the vendor database returned by expo-sqlite`, () => { const database = null as unknown as SQLiteDatabase @@ -21,6 +29,18 @@ describe(`Expo SQLite driver types`, () => { void compatible }) + it(`preserves the transaction callback result type`, () => { + const database = null as unknown as SQLiteDatabase + const driver = new ExpoSQLiteDriver({ database }) + + const result = driver.transaction(async (transactionDriver) => { + void transactionDriver + return { status: `committed` as const } + }) + const expected: Promise<{ readonly status: `committed` }> = result + void expected + }) + it(`rejects databases without an exclusive transaction boundary`, () => { const database = { execAsync: (_sql: string) => Promise.resolve(), diff --git a/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test.ts b/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test.ts index ddfc602b23..65b8c1c11c 100644 --- a/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test.ts +++ b/packages/expo-db-sqlite-persistence/tests/expo-sqlite-driver.test.ts @@ -197,6 +197,12 @@ it(`throws when transaction callback omits transaction driver argument`, async ( ).rejects.toThrow(`transaction driver argument`) }) +/** + * Runtime checkpoint for the paired type assertion: `transaction` resolves + * to the callback's value after Expo's void-returning exclusive transaction + * boundary settles. This test does not claim an intermediate publication or + * independently recheck commit and rollback behavior. + */ it(`returns callback values when Expo's transaction boundary returns void`, async () => { const transaction: ExpoSQLiteTransaction = { execAsync: () => Promise.resolve(),