Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Converting Drizzle tables to Powersync tables #408

Merged
merged 19 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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/tender-mugs-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/drizzle-driver': minor
---

Added `toPowersyncTable` and `toPowerSyncSchema` helper functions to convert a Drizzle schema into a PowerSync app schema
91 changes: 85 additions & 6 deletions packages/drizzle-driver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { wrapPowerSyncWithDrizzle } from '@powersync/drizzle-driver';
import { PowerSyncDatabase } from '@powersync/web';
import { relations } from 'drizzle-orm';
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { appSchema } from './schema';
import { AppSchema } from './schema';

export const lists = sqliteTable('lists', {
id: text('id'),
Expand Down Expand Up @@ -47,24 +47,99 @@ export const drizzleSchema = {
todosRelations
};

// As an alternative to manually defining a PowerSync schema, generate the local PowerSync schema from the Drizzle schema with `toPowerSyncSchema`:
// import { toPowerSyncSchema } from '@powersync/drizzle-driver';
// export const AppSchema = toPowerSyncSchema(drizzleSchema);
//
// This is optional, but recommended, since you will only need to maintain one schema on the client-side
// Read on to learn more.

export const powerSyncDb = new PowerSyncDatabase({
database: {
dbFilename: 'test.sqlite'
},
schema: appSchema
schema: AppSchema
});

// This is the DB you will use in queries
export const db = wrapPowerSyncWithDrizzle(powerSyncDb, {
schema: drizzleSchema
});
```

## Known limitations
## Schema Conversion

- The integration does not currently support nested transactions (also known as `savepoints`).
- The Drizzle schema needs to be created manually, and it should match the table definitions of your PowerSync schema.
The `toPowerSyncSchema` function simplifies the process of integrating Drizzle with PowerSync. It infers the local [PowerSync schema](https://docs.powersync.com/installation/client-side-setup/define-your-schema) from your Drizzle schema definition, providing a unified development experience.

As the PowerSync schema only supports SQLite types (`text`, `integer`, and `real`), the same limitation extends to the Drizzle table definitions.

To use it, define your Drizzle tables and supply the schema to the `toPowerSyncSchema` function:

```js
import { toPowerSyncSchema } from '@powersync/drizzle-driver';
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';

// Define a Drizzle table
const lists = sqliteTable('lists', {
id: text('id').primaryKey().notNull(),
created_at: text('created_at'),
name: text('name').notNull(),
owner_id: text('owner_id')
});

export const drizzleSchema = {
lists
};

// Infer the PowerSync schema from your Drizzle schema
export const AppSchema = toPowerSyncSchema(drizzleSchema);
```

### Defining PowerSync Options

### Compilable queries
The PowerSync table definition allows additional options supported by PowerSync's app schema beyond that which are supported by Drizzle.
They can be specified as follows. Note that these options exclude indexes as they can be specified in a Drizzle table.

```js
import { toPowerSyncSchema } from '@powersync/drizzle-driver';
// import { toPowerSyncSchema, type DrizzleTableWithPowerSyncOptions} from '@powersync/drizzle-driver'; for TypeScript

const listsWithOptions = { tableDefinition: logs, options: { localOnly: true } };
// const listsWithOptions: DrizzleTableWithPowerSyncOptions = { tableDefinition: logs, options: { localOnly: true } }; for TypeScript

export const drizzleSchemaWithOptions = {
lists: listsWithOptions
};

export const AppSchema = toPowerSyncSchema(drizzleSchemaWithOptions);
```

### Converting a Single Table From Drizzle to PowerSync

Drizzle tables can also be converted on a table-by-table basis with `toPowerSyncTable`.

```js
import { toPowerSyncTable } from '@powersync/drizzle-driver';
import { Schema } from '@powersync/web';
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';

// Define a Drizzle table
const lists = sqliteTable('lists', {
id: text('id').primaryKey().notNull(),
created_at: text('created_at'),
name: text('name').notNull(),
owner_id: text('owner_id')
});

const psLists = toPowerSyncTable(lists); // converts the Drizzle table to a PowerSync table
// toPowerSyncTable(lists, { localOnly: true }); - allows for PowerSync table configuration

export const AppSchema = new Schema({
lists: psLists // names the table `lists` in the PowerSync schema
});
```

## Compilable queries

To use Drizzle queries in your hooks and composables, queries need to be converted using `toCompilableQuery`.

Expand All @@ -76,3 +151,7 @@ const { data: listRecords, isLoading } = useQuery(toCompilableQuery(query));
```

For more information on how to use Drizzle queries in PowerSync, see [here](https://docs.powersync.com/client-sdk-references/javascript-web/javascript-orm/drizzle#usage-examples).

## Known limitations

- The integration does not currently support nested transactions (also known as `savepoints`).
3 changes: 3 additions & 0 deletions packages/drizzle-driver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"@powersync/common": "workspace:^1.19.0",
"drizzle-orm": "<1.0.0"
},
"dependencies": {
Chriztiaan marked this conversation as resolved.
Show resolved Hide resolved
"@powersync/common": "workspace:*"
},
"devDependencies": {
"@powersync/web": "workspace:*",
"@journeyapps/wa-sqlite": "^0.4.1",
Expand Down
24 changes: 23 additions & 1 deletion packages/drizzle-driver/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
import { wrapPowerSyncWithDrizzle, type PowerSyncSQLiteDatabase } from './sqlite/db';
import { toCompilableQuery } from './utils/compilableQuery';
import {
toPowerSyncSchema,
toPowerSyncTable,
type DrizzleTablePowerSyncOptions,
type DrizzleTableWithPowerSyncOptions,
type Expand,
type ExtractPowerSyncColumns,
type TableName,
type TablesFromSchemaEntries
} from './utils/schema';

export { wrapPowerSyncWithDrizzle, toCompilableQuery, PowerSyncSQLiteDatabase };
export {
DrizzleTablePowerSyncOptions,
DrizzleTableWithPowerSyncOptions,
Expand,
ExtractPowerSyncColumns,
PowerSyncSQLiteDatabase,
TableName,
TablesFromSchemaEntries,
toCompilableQuery,
toPowerSyncSchema,
toPowerSyncTable,
wrapPowerSyncWithDrizzle
};
113 changes: 113 additions & 0 deletions packages/drizzle-driver/src/utils/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { column, IndexShorthand, Schema, Table, type BaseColumnType, type TableV2Options } from '@powersync/common';
import { InferSelectModel, isTable, Relations } from 'drizzle-orm';
import {
getTableConfig,
SQLiteInteger,
SQLiteReal,
SQLiteText,
type SQLiteTableWithColumns,
type TableConfig
} from 'drizzle-orm/sqlite-core';

export type ExtractPowerSyncColumns<T extends SQLiteTableWithColumns<any>> = {
[K in keyof InferSelectModel<T> as K extends 'id' ? never : K]: BaseColumnType<InferSelectModel<T>[K]>;
};

export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

export function toPowerSyncTable<T extends SQLiteTableWithColumns<any>>(
table: T,
options?: Omit<TableV2Options, 'indexes'>
): Table<Expand<ExtractPowerSyncColumns<T>>> {
const { columns: drizzleColumns, indexes: drizzleIndexes } = getTableConfig(table);

const columns: { [key: string]: BaseColumnType<number | string | null> } = {};
for (const drizzleColumn of drizzleColumns) {
// Skip the id column
if (drizzleColumn.name === 'id') {
continue;
}

let mappedType: BaseColumnType<number | string | null>;
switch (drizzleColumn.columnType) {
case SQLiteText.name:
mappedType = column.text;
break;
case SQLiteInteger.name:
mappedType = column.integer;
break;
case SQLiteReal.name:
mappedType = column.real;
break;
default:
throw new Error(`Unsupported column type: ${drizzleColumn.columnType}`);
}
columns[drizzleColumn.name] = mappedType;
}
const indexes: IndexShorthand = {};

for (const index of drizzleIndexes) {
index.config;
if (!index.config.columns.length) {
continue;
}
const columns: string[] = [];
for (const indexColumn of index.config.columns) {
columns.push((indexColumn as { name: string }).name);
}

indexes[index.config.name] = columns;
}
return new Table(columns, { ...options, indexes }) as Table<Expand<ExtractPowerSyncColumns<T>>>;
}

export type DrizzleTablePowerSyncOptions = Omit<TableV2Options, 'indexes'>;

export type DrizzleTableWithPowerSyncOptions = {
tableDefinition: SQLiteTableWithColumns<any>;
options?: DrizzleTablePowerSyncOptions | undefined;
};

export type TableName<T> =
T extends SQLiteTableWithColumns<any>
? T['_']['name']
: T extends DrizzleTableWithPowerSyncOptions
? T['tableDefinition']['_']['name']
: never;

export type TablesFromSchemaEntries<T> = {
[K in keyof T as T[K] extends Relations
? never
: T[K] extends SQLiteTableWithColumns<any> | DrizzleTableWithPowerSyncOptions
? TableName<T[K]>
: never]: T[K] extends SQLiteTableWithColumns<any>
? Table<Expand<ExtractPowerSyncColumns<T[K]>>>
: T[K] extends DrizzleTableWithPowerSyncOptions
? Table<Expand<ExtractPowerSyncColumns<T[K]['tableDefinition']>>>
: never;
};

export function toPowerSyncSchema<
T extends Record<string, SQLiteTableWithColumns<any> | Relations | DrizzleTableWithPowerSyncOptions>
>(schemaEntries: T): Schema<Expand<TablesFromSchemaEntries<T>>> {
const tables: Record<string, Table> = {};
for (const schemaEntry of Object.values(schemaEntries)) {
let maybeTable: SQLiteTableWithColumns<any> | Relations | undefined = undefined;
let maybeOptions: DrizzleTablePowerSyncOptions | undefined = undefined;

if (typeof schemaEntry === 'object' && 'tableDefinition' in schemaEntry) {
const tableWithOptions = schemaEntry as DrizzleTableWithPowerSyncOptions;
maybeTable = tableWithOptions.tableDefinition;
maybeOptions = tableWithOptions.options;
} else {
maybeTable = schemaEntry;
}

if (isTable(maybeTable)) {
const { name } = getTableConfig(maybeTable);
tables[name] = toPowerSyncTable(maybeTable as SQLiteTableWithColumns<TableConfig>, maybeOptions);
}
}

return new Schema(tables) as Schema<Expand<TablesFromSchemaEntries<T>>>;
}
Loading