Skip to content
Open
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/hot-peaches-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/service-sync-rules': minor
---

Add schema generator for Drift, SQLDelight and Room.
62 changes: 62 additions & 0 deletions packages/sync-rules/src/schema-generators/RoomSchemaGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { SqlSyncRules } from '../SqlSyncRules.js';
import { SourceSchema } from '../types.js';
import { SchemaGenerator } from './SchemaGenerator.js';

/**
* Generates a schema to use with the [Room library](https://docs.powersync.com/client-sdk-references/kotlin-multiplatform/libraries/room).
*/
export class RoomSchemaGenerator extends SchemaGenerator {
readonly key = 'kotlin-room';
readonly label = 'Room (Kotlin Multiplatform)';
readonly mediaType = 'text/x-kotlin';
readonly fileName = 'Entities.kt';

generate(source: SqlSyncRules, schema: SourceSchema): string {
let buffer = `import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
`;
const tables = super.getAllTables(source, schema);
for (const table of tables) {
// @Entity(tableName = "todo_list_items") data class TodoListItems(
buffer += `\n@Entity(tableName = "${table.name}")\n`;
buffer += `data class ${snakeCaseToKotlin(table.name, true)}(\n`;

// Id column
buffer += ' @PrimaryKey val id: String,\n';

for (const column of table.columns) {
const sqliteType = this.columnType(column);
const kotlinType = {
text: 'String',
real: 'Double',
integer: 'Long'
}[sqliteType];

// @ColumnInfo(name = "author_id") val authorId: String,
buffer += ` @ColumnInfo("${column.name}") val ${snakeCaseToKotlin(column.name, false)}: ${kotlinType},\n`;
}

buffer += ')\n';
}

return buffer;
}
}

function snakeCaseToKotlin(source: string, initialUpper: boolean): string {
let result = '';
for (const chunk of source.split('_')) {
if (chunk.length == 0) continue;

const firstCharUpper = result.length > 0 || initialUpper;
if (firstCharUpper) {
result += chunk.charAt(0).toUpperCase();
result += chunk.substring(1);
} else {
result += chunk;
}
}

return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export abstract class SchemaGenerator {
* @param def The column definition to generate the type for.
* @returns The SDK column type for the given column definition.
*/
columnType(def: ColumnDefinition): string {
columnType(def: ColumnDefinition): 'text' | 'real' | 'integer' {
const { type } = def;
if (type.typeFlags & TYPE_TEXT) {
return 'text';
Expand Down
39 changes: 39 additions & 0 deletions packages/sync-rules/src/schema-generators/SqlSchemaGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { SqlSyncRules } from '../SqlSyncRules.js';
import { SourceSchema } from '../types.js';
import { GenerateSchemaOptions, SchemaGenerator } from './SchemaGenerator.js';

/**
* Generates a schema as `CREATE TABLE` statements, useful for libraries like drift or SQLDelight which can generate
* typed rows or generate queries based on that.
*/
export class SqlSchemaGenerator extends SchemaGenerator {
readonly key = 'sql';
readonly mediaType = 'application/sql';

constructor(
readonly label: string,
readonly fileName: string
) {
super();
}

generate(source: SqlSyncRules, schema: SourceSchema, options?: GenerateSchemaOptions): string {
let buffer =
'-- Note: These definitions are only used to generate typed code. PowerSync manages the database schema.\n';
const tables = super.getAllTables(source, schema);

for (const table of tables) {
buffer += `CREATE TABLE ${table.name}(\n`;

buffer += ' id TEXT NOT NULL PRIMARY KEY';
for (const column of table.columns) {
const type = this.columnType(column).toUpperCase();
buffer += `,\n ${column.name} ${type}`;
}

buffer += '\n);\n';
}

return buffer;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { SchemaGenerator } from './SchemaGenerator.js';
import { SqlSchemaGenerator } from './SqlSchemaGenerator.js';

export * from './DartSchemaGenerator.js';
export * from './DotNetSchemaGenerator.js';
export * from './generators.js';
export * from './JsLegacySchemaGenerator.js';
export * from './KotlinSchemaGenerator.js';
export * from './RoomSchemaGenerator.js';
export * from './SchemaGenerator.js';
export * from './SwiftSchemaGenerator.js';
export * from './TsSchemaGenerator.js';

export const driftSchemaGenerator = new SqlSchemaGenerator('Drift', 'tables.drift');
export const sqlDelightSchemaGenerator = new SqlSchemaGenerator('SQLDelight', 'tables.sq');
55 changes: 54 additions & 1 deletion packages/sync-rules/test/src/generate_schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import {
DotNetSchemaGenerator,
JsLegacySchemaGenerator,
KotlinSchemaGenerator,
RoomSchemaGenerator,
SqlSyncRules,
StaticSchema,
SwiftSchemaGenerator,
TsSchemaGenerator
TsSchemaGenerator,
driftSchemaGenerator,
sqlDelightSchemaGenerator
} from '../../src/index.js';

import { PARSE_OPTIONS } from './util.js';
Expand Down Expand Up @@ -231,6 +234,30 @@ val schema = Schema(
)`);
});

test('room', () => {
expect(new RoomSchemaGenerator().generate(rules, schema)).toEqual(`import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "assets1")
data class Assets1(
@PrimaryKey val id: String,
@ColumnInfo("name") val name: String,
@ColumnInfo("count") val count: Long,
@ColumnInfo("owner_id") val ownerId: String,
)

@Entity(tableName = "assets2")
data class Assets2(
@PrimaryKey val id: String,
@ColumnInfo("name") val name: String,
@ColumnInfo("count") val count: Long,
@ColumnInfo("other_id") val otherId: String,
@ColumnInfo("foo") val foo: String,
)
`);
});

test('swift', () => {
expect(new SwiftSchemaGenerator().generate(rules, schema)).toEqual(`import PowerSync

Expand Down Expand Up @@ -331,4 +358,30 @@ class AppSchema
});
}`);
});

describe('sql', () => {
const expected = `-- Note: These definitions are only used to generate typed code. PowerSync manages the database schema.
CREATE TABLE assets1(
id TEXT NOT NULL PRIMARY KEY,
name TEXT,
count INTEGER,
owner_id TEXT
);
CREATE TABLE assets2(
id TEXT NOT NULL PRIMARY KEY,
name TEXT,
count INTEGER,
other_id TEXT,
foo TEXT
);
`;

test('drift', () => {
expect(driftSchemaGenerator.generate(rules, schema)).toEqual(expected);
});

test('sqldelight', () => {
expect(sqlDelightSchemaGenerator.generate(rules, schema)).toEqual(expected);
});
});
});