Skip to content

Commit 4a70624

Browse files
Chriztiaanbenitav
andauthored
feat: Converting Drizzle tables to Powersync tables (#408)
Co-authored-by: benitav <[email protected]>
1 parent 77a9ed2 commit 4a70624

File tree

6 files changed

+448
-8
lines changed

6 files changed

+448
-8
lines changed

.changeset/tender-mugs-deliver.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/drizzle-driver': minor
3+
---
4+
5+
Added helper `toPowersyncTable` function and `DrizzleAppSchema` constructor to convert a Drizzle schema into a PowerSync app schema.

packages/common/src/db/schema/Schema.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { RowType, Table } from './Table.js';
22

33
type SchemaType = Record<string, Table<any>>;
44

5-
type SchemaTableType<S extends SchemaType> = {
5+
export type SchemaTableType<S extends SchemaType> = {
66
[K in keyof S]: RowType<S[K]>;
77
};
88

packages/drizzle-driver/README.md

+85-6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { wrapPowerSyncWithDrizzle } from '@powersync/drizzle-driver';
1515
import { PowerSyncDatabase } from '@powersync/web';
1616
import { relations } from 'drizzle-orm';
1717
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
18-
import { appSchema } from './schema';
18+
import { AppSchema } from './schema';
1919

2020
export const lists = sqliteTable('lists', {
2121
id: text('id'),
@@ -47,24 +47,99 @@ export const drizzleSchema = {
4747
todosRelations
4848
};
4949

50+
// As an alternative to manually defining a PowerSync schema, generate the local PowerSync schema from the Drizzle schema with the `DrizzleAppSchema` constructor:
51+
// import { DrizzleAppSchema } from '@powersync/drizzle-driver';
52+
// export const AppSchema = new DrizzleAppSchema(drizzleSchema);
53+
//
54+
// This is optional, but recommended, since you will only need to maintain one schema on the client-side
55+
// Read on to learn more.
56+
5057
export const powerSyncDb = new PowerSyncDatabase({
5158
database: {
5259
dbFilename: 'test.sqlite'
5360
},
54-
schema: appSchema
61+
schema: AppSchema
5562
});
5663

64+
// This is the DB you will use in queries
5765
export const db = wrapPowerSyncWithDrizzle(powerSyncDb, {
5866
schema: drizzleSchema
5967
});
6068
```
6169

62-
## Known limitations
70+
## Schema Conversion
6371

64-
- The integration does not currently support nested transactions (also known as `savepoints`).
65-
- The Drizzle schema needs to be created manually, and it should match the table definitions of your PowerSync schema.
72+
The `DrizzleAppSchema` constructor 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.
73+
74+
As the PowerSync schema only supports SQLite types (`text`, `integer`, and `real`), the same limitation extends to the Drizzle table definitions.
75+
76+
To use it, define your Drizzle tables and supply the schema to the `DrizzleAppSchema` function:
77+
78+
```js
79+
import { DrizzleAppSchema } from '@powersync/drizzle-driver';
80+
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
81+
82+
// Define a Drizzle table
83+
const lists = sqliteTable('lists', {
84+
id: text('id').primaryKey().notNull(),
85+
created_at: text('created_at'),
86+
name: text('name').notNull(),
87+
owner_id: text('owner_id')
88+
});
89+
90+
export const drizzleSchema = {
91+
lists
92+
};
93+
94+
// Infer the PowerSync schema from your Drizzle schema
95+
export const AppSchema = new DrizzleAppSchema(drizzleSchema);
96+
```
97+
98+
### Defining PowerSync Options
6699

67-
### Compilable queries
100+
The PowerSync table definition allows additional options supported by PowerSync's app schema beyond that which are supported by Drizzle.
101+
They can be specified as follows. Note that these options exclude indexes as they can be specified in a Drizzle table.
102+
103+
```js
104+
import { DrizzleAppSchema } from '@powersync/drizzle-driver';
105+
// import { DrizzleAppSchema, type DrizzleTableWithPowerSyncOptions} from '@powersync/drizzle-driver'; for TypeScript
106+
107+
const listsWithOptions = { tableDefinition: logs, options: { localOnly: true } };
108+
// const listsWithOptions: DrizzleTableWithPowerSyncOptions = { tableDefinition: logs, options: { localOnly: true } }; for TypeScript
109+
110+
export const drizzleSchemaWithOptions = {
111+
lists: listsWithOptions
112+
};
113+
114+
export const AppSchema = new DrizzleAppSchema(drizzleSchemaWithOptions);
115+
```
116+
117+
### Converting a Single Table From Drizzle to PowerSync
118+
119+
Drizzle tables can also be converted on a table-by-table basis with `toPowerSyncTable`.
120+
121+
```js
122+
import { toPowerSyncTable } from '@powersync/drizzle-driver';
123+
import { Schema } from '@powersync/web';
124+
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
125+
126+
// Define a Drizzle table
127+
const lists = sqliteTable('lists', {
128+
id: text('id').primaryKey().notNull(),
129+
created_at: text('created_at'),
130+
name: text('name').notNull(),
131+
owner_id: text('owner_id')
132+
});
133+
134+
const psLists = toPowerSyncTable(lists); // converts the Drizzle table to a PowerSync table
135+
// toPowerSyncTable(lists, { localOnly: true }); - allows for PowerSync table configuration
136+
137+
export const AppSchema = new Schema({
138+
lists: psLists // names the table `lists` in the PowerSync schema
139+
});
140+
```
141+
142+
## Compilable queries
68143

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

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

78153
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).
154+
155+
## Known limitations
156+
157+
- The integration does not currently support nested transactions (also known as `savepoints`).

packages/drizzle-driver/src/index.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
11
import { wrapPowerSyncWithDrizzle, type DrizzleQuery, type PowerSyncSQLiteDatabase } from './sqlite/db';
22
import { toCompilableQuery } from './utils/compilableQuery';
3+
import {
4+
DrizzleAppSchema,
5+
toPowerSyncTable,
6+
type DrizzleTablePowerSyncOptions,
7+
type DrizzleTableWithPowerSyncOptions,
8+
type Expand,
9+
type ExtractPowerSyncColumns,
10+
type TableName,
11+
type TablesFromSchemaEntries
12+
} from './utils/schema';
313

4-
export { wrapPowerSyncWithDrizzle, toCompilableQuery, DrizzleQuery, PowerSyncSQLiteDatabase };
14+
export {
15+
DrizzleAppSchema,
16+
DrizzleTablePowerSyncOptions,
17+
DrizzleTableWithPowerSyncOptions,
18+
DrizzleQuery,
19+
Expand,
20+
ExtractPowerSyncColumns,
21+
PowerSyncSQLiteDatabase,
22+
TableName,
23+
TablesFromSchemaEntries,
24+
toCompilableQuery,
25+
toPowerSyncTable,
26+
wrapPowerSyncWithDrizzle
27+
};
+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import {
2+
column,
3+
IndexShorthand,
4+
Schema,
5+
SchemaTableType,
6+
Table,
7+
type BaseColumnType,
8+
type TableV2Options
9+
} from '@powersync/common';
10+
import { InferSelectModel, isTable, Relations } from 'drizzle-orm';
11+
import {
12+
getTableConfig,
13+
SQLiteInteger,
14+
SQLiteReal,
15+
SQLiteText,
16+
type SQLiteTableWithColumns,
17+
type TableConfig
18+
} from 'drizzle-orm/sqlite-core';
19+
20+
export type ExtractPowerSyncColumns<T extends SQLiteTableWithColumns<any>> = {
21+
[K in keyof InferSelectModel<T> as K extends 'id' ? never : K]: BaseColumnType<InferSelectModel<T>[K]>;
22+
};
23+
24+
export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
25+
26+
export function toPowerSyncTable<T extends SQLiteTableWithColumns<any>>(
27+
table: T,
28+
options?: Omit<TableV2Options, 'indexes'>
29+
): Table<Expand<ExtractPowerSyncColumns<T>>> {
30+
const { columns: drizzleColumns, indexes: drizzleIndexes } = getTableConfig(table);
31+
32+
const columns: { [key: string]: BaseColumnType<number | string | null> } = {};
33+
for (const drizzleColumn of drizzleColumns) {
34+
// Skip the id column
35+
if (drizzleColumn.name === 'id') {
36+
continue;
37+
}
38+
39+
let mappedType: BaseColumnType<number | string | null>;
40+
switch (drizzleColumn.columnType) {
41+
case SQLiteText.name:
42+
mappedType = column.text;
43+
break;
44+
case SQLiteInteger.name:
45+
mappedType = column.integer;
46+
break;
47+
case SQLiteReal.name:
48+
mappedType = column.real;
49+
break;
50+
default:
51+
throw new Error(`Unsupported column type: ${drizzleColumn.columnType}`);
52+
}
53+
columns[drizzleColumn.name] = mappedType;
54+
}
55+
const indexes: IndexShorthand = {};
56+
57+
for (const index of drizzleIndexes) {
58+
index.config;
59+
if (!index.config.columns.length) {
60+
continue;
61+
}
62+
const columns: string[] = [];
63+
for (const indexColumn of index.config.columns) {
64+
columns.push((indexColumn as { name: string }).name);
65+
}
66+
67+
indexes[index.config.name] = columns;
68+
}
69+
return new Table(columns, { ...options, indexes }) as Table<Expand<ExtractPowerSyncColumns<T>>>;
70+
}
71+
72+
export type DrizzleTablePowerSyncOptions = Omit<TableV2Options, 'indexes'>;
73+
74+
export type DrizzleTableWithPowerSyncOptions = {
75+
tableDefinition: SQLiteTableWithColumns<any>;
76+
options?: DrizzleTablePowerSyncOptions | undefined;
77+
};
78+
79+
export type TableName<T> =
80+
T extends SQLiteTableWithColumns<any>
81+
? T['_']['name']
82+
: T extends DrizzleTableWithPowerSyncOptions
83+
? T['tableDefinition']['_']['name']
84+
: never;
85+
86+
export type TablesFromSchemaEntries<T> = {
87+
[K in keyof T as T[K] extends Relations
88+
? never
89+
: T[K] extends SQLiteTableWithColumns<any> | DrizzleTableWithPowerSyncOptions
90+
? TableName<T[K]>
91+
: never]: T[K] extends SQLiteTableWithColumns<any>
92+
? Table<Expand<ExtractPowerSyncColumns<T[K]>>>
93+
: T[K] extends DrizzleTableWithPowerSyncOptions
94+
? Table<Expand<ExtractPowerSyncColumns<T[K]['tableDefinition']>>>
95+
: never;
96+
};
97+
98+
function toPowerSyncTables<
99+
T extends Record<string, SQLiteTableWithColumns<any> | Relations | DrizzleTableWithPowerSyncOptions>
100+
>(schemaEntries: T) {
101+
const tables: Record<string, Table> = {};
102+
for (const schemaEntry of Object.values(schemaEntries)) {
103+
let maybeTable: SQLiteTableWithColumns<any> | Relations | undefined = undefined;
104+
let maybeOptions: DrizzleTablePowerSyncOptions | undefined = undefined;
105+
106+
if (typeof schemaEntry === 'object' && 'tableDefinition' in schemaEntry) {
107+
const tableWithOptions = schemaEntry as DrizzleTableWithPowerSyncOptions;
108+
maybeTable = tableWithOptions.tableDefinition;
109+
maybeOptions = tableWithOptions.options;
110+
} else {
111+
maybeTable = schemaEntry;
112+
}
113+
114+
if (isTable(maybeTable)) {
115+
const { name } = getTableConfig(maybeTable);
116+
tables[name] = toPowerSyncTable(maybeTable as SQLiteTableWithColumns<TableConfig>, maybeOptions);
117+
}
118+
}
119+
120+
return tables;
121+
}
122+
123+
export class DrizzleAppSchema<
124+
T extends Record<string, SQLiteTableWithColumns<any> | Relations | DrizzleTableWithPowerSyncOptions>
125+
> extends Schema {
126+
constructor(drizzleSchema: T) {
127+
super(toPowerSyncTables(drizzleSchema));
128+
// This is just used for typing
129+
this.types = {} as SchemaTableType<Expand<TablesFromSchemaEntries<T>>>;
130+
}
131+
132+
readonly types: SchemaTableType<Expand<TablesFromSchemaEntries<T>>>;
133+
}

0 commit comments

Comments
 (0)