Skip to content

Commit 83e4c62

Browse files
committed
Schema generators for Drift, SQLDelight and Room
1 parent 9c13cb9 commit 83e4c62

File tree

6 files changed

+168
-2
lines changed

6 files changed

+168
-2
lines changed

.changeset/hot-peaches-lay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/service-sync-rules': minor
3+
---
4+
5+
Add schema generator for Drift, SQLDelight and Room.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { SqlSyncRules } from '../SqlSyncRules.js';
2+
import { SourceSchema } from '../types.js';
3+
import { SchemaGenerator } from './SchemaGenerator.js';
4+
5+
/**
6+
* Generates a schema to use with the [Room library](https://docs.powersync.com/client-sdk-references/kotlin-multiplatform/libraries/room).
7+
*/
8+
export class RoomSchemaGenerator extends SchemaGenerator {
9+
readonly key = 'kotlin-room';
10+
readonly label = 'Room (Kotlin Multiplatform)';
11+
readonly mediaType = 'text/x-kotlin';
12+
readonly fileName = 'Entities.kt';
13+
14+
generate(source: SqlSyncRules, schema: SourceSchema): string {
15+
let buffer = `import androidx.room.ColumnInfo
16+
import androidx.room.Entity
17+
import androidx.room.PrimaryKey
18+
`;
19+
const tables = super.getAllTables(source, schema);
20+
for (const table of tables) {
21+
// @Entity(tableName = "todo_list_items") data class TodoListItems(
22+
buffer += `\n@Entity(tableName = "${table.name}")\n`;
23+
buffer += `data class ${snakeCaseToKotlin(table.name, true)}(\n`;
24+
25+
// Id column
26+
buffer += ' @PrimaryKey val id: String,\n';
27+
28+
for (const column of table.columns) {
29+
const sqliteType = this.columnType(column);
30+
const kotlinType = {
31+
text: 'String',
32+
real: 'Double',
33+
integer: 'Long'
34+
}[sqliteType];
35+
36+
// @ColumnInfo(name = "author_id") val authorId: String,
37+
buffer += ` @ColumnInfo("${column.name}") val ${snakeCaseToKotlin(column.name, false)}: ${kotlinType},\n`;
38+
}
39+
40+
buffer += ')\n';
41+
}
42+
43+
return buffer;
44+
}
45+
}
46+
47+
function snakeCaseToKotlin(source: string, initialUpper: boolean): string {
48+
let result = '';
49+
for (const chunk of source.split('_')) {
50+
if (chunk.length == 0) continue;
51+
52+
const firstCharUpper = result.length > 0 || initialUpper;
53+
if (firstCharUpper) {
54+
result += chunk.charAt(0).toUpperCase();
55+
result += chunk.substring(1);
56+
} else {
57+
result += chunk;
58+
}
59+
}
60+
61+
return result;
62+
}

packages/sync-rules/src/schema-generators/SchemaGenerator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export abstract class SchemaGenerator {
3333
* @param def The column definition to generate the type for.
3434
* @returns The SDK column type for the given column definition.
3535
*/
36-
columnType(def: ColumnDefinition): string {
36+
columnType(def: ColumnDefinition): 'text' | 'real' | 'integer' {
3737
const { type } = def;
3838
if (type.typeFlags & TYPE_TEXT) {
3939
return 'text';
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { SqlSyncRules } from '../SqlSyncRules.js';
2+
import { SourceSchema } from '../types.js';
3+
import { GenerateSchemaOptions, SchemaGenerator } from './SchemaGenerator.js';
4+
5+
/**
6+
* Generates a schema as `CREATE TABLE` statements, useful for libraries like drift or SQLDelight which can generate
7+
* typed rows or generate queries based on that.
8+
*/
9+
export class SqlSchemaGenerator extends SchemaGenerator {
10+
readonly key = 'sql';
11+
readonly mediaType = 'application/sql';
12+
13+
constructor(
14+
readonly label: string,
15+
readonly fileName: string
16+
) {
17+
super();
18+
}
19+
20+
generate(source: SqlSyncRules, schema: SourceSchema, options?: GenerateSchemaOptions): string {
21+
let buffer =
22+
'-- Note: These definitions are only used to generate typed code. PowerSync manages the database schema.\n';
23+
const tables = super.getAllTables(source, schema);
24+
25+
for (const table of tables) {
26+
buffer += `CREATE TABLE ${table.name}(\n`;
27+
28+
buffer += ' id TEXT NOT NULL PRIMARY KEY';
29+
for (const column of table.columns) {
30+
const type = this.columnType(column).toUpperCase();
31+
buffer += `,\n ${column.name} ${type}`;
32+
}
33+
34+
buffer += '\n);\n';
35+
}
36+
37+
return buffer;
38+
}
39+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
import { SchemaGenerator } from './SchemaGenerator.js';
2+
import { SqlSchemaGenerator } from './SqlSchemaGenerator.js';
3+
14
export * from './DartSchemaGenerator.js';
25
export * from './DotNetSchemaGenerator.js';
36
export * from './generators.js';
47
export * from './JsLegacySchemaGenerator.js';
58
export * from './KotlinSchemaGenerator.js';
9+
export * from './RoomSchemaGenerator.js';
610
export * from './SchemaGenerator.js';
711
export * from './SwiftSchemaGenerator.js';
812
export * from './TsSchemaGenerator.js';
13+
14+
export const driftSchemaGenerator = new SqlSchemaGenerator('Drift', 'tables.drift');
15+
export const sqlDelightSchemaGenerator = new SqlSchemaGenerator('SQLDelight', 'tables.sq');

packages/sync-rules/test/src/generate_schema.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ import {
66
DotNetSchemaGenerator,
77
JsLegacySchemaGenerator,
88
KotlinSchemaGenerator,
9+
RoomSchemaGenerator,
910
SqlSyncRules,
1011
StaticSchema,
1112
SwiftSchemaGenerator,
12-
TsSchemaGenerator
13+
TsSchemaGenerator,
14+
driftSchemaGenerator,
15+
sqlDelightSchemaGenerator
1316
} from '../../src/index.js';
1417

1518
import { PARSE_OPTIONS } from './util.js';
@@ -231,6 +234,30 @@ val schema = Schema(
231234
)`);
232235
});
233236

237+
test('room', () => {
238+
expect(new RoomSchemaGenerator().generate(rules, schema)).toEqual(`import androidx.room.ColumnInfo
239+
import androidx.room.Entity
240+
import androidx.room.PrimaryKey
241+
242+
@Entity(tableName = "assets1")
243+
data class Assets1(
244+
@PrimaryKey val id: String,
245+
@ColumnInfo("name") val name: String,
246+
@ColumnInfo("count") val count: Long,
247+
@ColumnInfo("owner_id") val ownerId: String,
248+
)
249+
250+
@Entity(tableName = "assets2")
251+
data class Assets2(
252+
@PrimaryKey val id: String,
253+
@ColumnInfo("name") val name: String,
254+
@ColumnInfo("count") val count: Long,
255+
@ColumnInfo("other_id") val otherId: String,
256+
@ColumnInfo("foo") val foo: String,
257+
)
258+
`);
259+
});
260+
234261
test('swift', () => {
235262
expect(new SwiftSchemaGenerator().generate(rules, schema)).toEqual(`import PowerSync
236263
@@ -331,4 +358,30 @@ class AppSchema
331358
});
332359
}`);
333360
});
361+
362+
describe('sql', () => {
363+
const expected = `-- Note: These definitions are only used to generate typed code. PowerSync manages the database schema.
364+
CREATE TABLE assets1(
365+
id TEXT NOT NULL PRIMARY KEY,
366+
name TEXT,
367+
count INTEGER,
368+
owner_id TEXT
369+
);
370+
CREATE TABLE assets2(
371+
id TEXT NOT NULL PRIMARY KEY,
372+
name TEXT,
373+
count INTEGER,
374+
other_id TEXT,
375+
foo TEXT
376+
);
377+
`;
378+
379+
test('drift', () => {
380+
expect(driftSchemaGenerator.generate(rules, schema)).toEqual(expected);
381+
});
382+
383+
test('sqldelight', () => {
384+
expect(sqlDelightSchemaGenerator.generate(rules, schema)).toEqual(expected);
385+
});
386+
});
334387
});

0 commit comments

Comments
 (0)