diff --git a/.changeset/hot-peaches-lay.md b/.changeset/hot-peaches-lay.md new file mode 100644 index 000000000..ba2b7807d --- /dev/null +++ b/.changeset/hot-peaches-lay.md @@ -0,0 +1,5 @@ +--- +'@powersync/service-sync-rules': minor +--- + +Add schema generator for Drift, SQLDelight and Room. diff --git a/packages/sync-rules/src/schema-generators/RoomSchemaGenerator.ts b/packages/sync-rules/src/schema-generators/RoomSchemaGenerator.ts new file mode 100644 index 000000000..082f89cf2 --- /dev/null +++ b/packages/sync-rules/src/schema-generators/RoomSchemaGenerator.ts @@ -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; +} diff --git a/packages/sync-rules/src/schema-generators/SchemaGenerator.ts b/packages/sync-rules/src/schema-generators/SchemaGenerator.ts index cde9585b2..e257f5726 100644 --- a/packages/sync-rules/src/schema-generators/SchemaGenerator.ts +++ b/packages/sync-rules/src/schema-generators/SchemaGenerator.ts @@ -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'; diff --git a/packages/sync-rules/src/schema-generators/SqlSchemaGenerator.ts b/packages/sync-rules/src/schema-generators/SqlSchemaGenerator.ts new file mode 100644 index 000000000..66a826d08 --- /dev/null +++ b/packages/sync-rules/src/schema-generators/SqlSchemaGenerator.ts @@ -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; + } +} diff --git a/packages/sync-rules/src/schema-generators/schema-generators.ts b/packages/sync-rules/src/schema-generators/schema-generators.ts index 29a89bfa2..af088e505 100644 --- a/packages/sync-rules/src/schema-generators/schema-generators.ts +++ b/packages/sync-rules/src/schema-generators/schema-generators.ts @@ -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'); diff --git a/packages/sync-rules/test/src/generate_schema.test.ts b/packages/sync-rules/test/src/generate_schema.test.ts index 15c419c75..a041f8483 100644 --- a/packages/sync-rules/test/src/generate_schema.test.ts +++ b/packages/sync-rules/test/src/generate_schema.test.ts @@ -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'; @@ -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 @@ -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); + }); + }); });