From 8b4f76fd6895257e03507553081e1a77154a7532 Mon Sep 17 00:00:00 2001 From: Yahya JIRARI Date: Sun, 13 Apr 2025 23:58:42 +0200 Subject: [PATCH 1/2] feat(common): Add support for custom SQL in powersync database schema --- .../src/client/AbstractPowerSyncDatabase.ts | 39 ++++++++++++------- packages/common/src/db/schema/Schema.ts | 9 ++++- .../common/tests/db/schema/Schema.test.ts | 10 +++++ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/common/src/client/AbstractPowerSyncDatabase.ts b/packages/common/src/client/AbstractPowerSyncDatabase.ts index 2e71083e7..2c2b78c8b 100644 --- a/packages/common/src/client/AbstractPowerSyncDatabase.ts +++ b/packages/common/src/client/AbstractPowerSyncDatabase.ts @@ -402,6 +402,15 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver this.schema.getTableInternalName(tableName) ?? tableName + }) ?? []) { + await this.database.execute(sql); + } + } catch (ex) { + this.options.logger?.error('Error executing custom SQL', ex); + } await this.database.refreshSchema(); this.iterateListeners(async (cb) => cb.schemaChanged?.(schema)); } @@ -555,7 +564,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver { @@ -633,7 +642,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver { @@ -661,7 +670,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver { readonly props: S; readonly tables: Table[]; - constructor(tables: Table[] | S) { + constructor( + tables: Table[] | S, + readonly getCustomSQL?: ({ getInternalName }: { getInternalName: (tableName: string) => string }) => string[] + ) { if (Array.isArray(tables)) { /* We need to validate that the tables have a name here because a user could pass in an array @@ -64,4 +67,8 @@ export class Schema { return convertedTable; }); } + + getTableInternalName(tableName: string) { + return this.tables.find((t) => t.name === tableName)?.internalName; + } } diff --git a/packages/common/tests/db/schema/Schema.test.ts b/packages/common/tests/db/schema/Schema.test.ts index 51bbf5aa9..240b254d0 100644 --- a/packages/common/tests/db/schema/Schema.test.ts +++ b/packages/common/tests/db/schema/Schema.test.ts @@ -110,4 +110,14 @@ describe('Schema', () => { ] }); }); + + it('should get the internal name of a table', () => { + const schema = new Schema({ + users: new Table({ name: column.text }), + posts: new Table({ name: column.text }) + }); + + expect(schema.getTableInternalName('users')).toBe('ps_data__users'); + expect(schema.getTableInternalName('posts')).toBe('ps_data__posts'); + }); }); From 8a135d16ed3305858456553b172b7be7e862d5d0 Mon Sep 17 00:00:00 2001 From: Yahya JIRARI Date: Mon, 14 Apr 2025 00:00:20 +0200 Subject: [PATCH 2/2] feat: Add example-vite-custom-sql demo project for custom SQL --- demos/example-vite-custom-sql/CHANGELOG.md | 3 + demos/example-vite-custom-sql/README.md | 9 ++ demos/example-vite-custom-sql/package.json | 22 +++++ demos/example-vite-custom-sql/src/index.html | 9 ++ demos/example-vite-custom-sql/src/index.js | 88 ++++++++++++++++++++ demos/example-vite-custom-sql/vite.config.ts | 27 ++++++ 6 files changed, 158 insertions(+) create mode 100644 demos/example-vite-custom-sql/CHANGELOG.md create mode 100644 demos/example-vite-custom-sql/README.md create mode 100644 demos/example-vite-custom-sql/package.json create mode 100644 demos/example-vite-custom-sql/src/index.html create mode 100644 demos/example-vite-custom-sql/src/index.js create mode 100644 demos/example-vite-custom-sql/vite.config.ts diff --git a/demos/example-vite-custom-sql/CHANGELOG.md b/demos/example-vite-custom-sql/CHANGELOG.md new file mode 100644 index 000000000..b245f3300 --- /dev/null +++ b/demos/example-vite-custom-sql/CHANGELOG.md @@ -0,0 +1,3 @@ +# example-vite-custom-sql + +## 0.0.1 diff --git a/demos/example-vite-custom-sql/README.md b/demos/example-vite-custom-sql/README.md new file mode 100644 index 000000000..82a2c8fbb --- /dev/null +++ b/demos/example-vite-custom-sql/README.md @@ -0,0 +1,9 @@ +# PowerSync Vite custom SQL demo + +This is a minimal example demonstrating how to use custom SQL with PowerSync. + +To see it in action: + +1. Make sure to run `pnpm install` and `pnpm build:packages` in the root directory of this repo. +2. `cd` into this directory, and run `pnpm start`. +3. Open the localhost URL displayed in the terminal output in your browser. diff --git a/demos/example-vite-custom-sql/package.json b/demos/example-vite-custom-sql/package.json new file mode 100644 index 000000000..bd579d15c --- /dev/null +++ b/demos/example-vite-custom-sql/package.json @@ -0,0 +1,22 @@ +{ + "name": "example-vite-custom-sql", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "start": "pnpm build && pnpm preview", + "test:build": "pnpm build" + }, + "dependencies": { + "@powersync/web": "workspace:*" + }, + "devDependencies": { + "@swc/core": "~1.6.0", + "vite": "^5.0.12", + "vite-plugin-top-level-await": "^1.4.1", + "vite-plugin-wasm": "^3.3.0" + } +} diff --git a/demos/example-vite-custom-sql/src/index.html b/demos/example-vite-custom-sql/src/index.html new file mode 100644 index 000000000..c7c4ba9aa --- /dev/null +++ b/demos/example-vite-custom-sql/src/index.html @@ -0,0 +1,9 @@ + + + + + + + Custom SQL demo: Check the console to see it in action! + + diff --git a/demos/example-vite-custom-sql/src/index.js b/demos/example-vite-custom-sql/src/index.js new file mode 100644 index 000000000..27d2a100d --- /dev/null +++ b/demos/example-vite-custom-sql/src/index.js @@ -0,0 +1,88 @@ +import { column, Schema, Table, PowerSyncDatabase } from '@powersync/web'; +import Logger from 'js-logger'; + +Logger.useDefaults(); + +/** + * A placeholder connector which doesn't do anything. + * This is just used to verify that the sync workers can be loaded + * when connecting. + */ +class DummyConnector { + async fetchCredentials() { + return { + endpoint: '', + token: '' + }; + } + + async uploadData(database) {} +} + +const customers = new Table({ first_name: column.text, last_name: column.text, full_name: column.text }); + +export const AppSchema = new Schema({ customers }, ({ getInternalName }) => [ + `DROP TRIGGER IF EXISTS compute_full_name`, + `DROP TRIGGER IF EXISTS update_full_name`, + + ` + CREATE TRIGGER compute_full_name + AFTER INSERT ON ${getInternalName('customers')} + BEGIN + UPDATE customers + SET full_name = first_name || ' ' || last_name + WHERE id = NEW.id; + END; + `, + ` + CREATE TRIGGER update_full_name + AFTER UPDATE OF data ON ${getInternalName('customers')} + BEGIN + UPDATE customers + SET full_name = first_name || ' ' || last_name + WHERE id = NEW.id AND full_name != (first_name || ' ' || last_name); + END; + ` +]); + +let PowerSync; + +const openDatabase = async () => { + PowerSync = new PowerSyncDatabase({ + schema: AppSchema, + database: { dbFilename: 'test.sqlite' } + }); + + await PowerSync.init(); + + await PowerSync.execute('DELETE FROM customers'); + + await PowerSync.execute('INSERT INTO customers(id, first_name, last_name) VALUES(uuid(), ?, ?)', ['John', 'Doe']); + + const result = await PowerSync.getAll('SELECT * FROM customers'); + + console.log('Contents of customers after insert: ', result); + + await PowerSync.execute('UPDATE customers SET first_name = ?', ['Jane']); + + const result2 = await PowerSync.getAll('SELECT * FROM customers'); + + console.log('Contents of customers after update: ', result2); + + console.log( + `Attempting to connect in order to verify web workers are correctly loaded. + This doesn't use any actual network credentials. + Network errors will be shown: these can be ignored.` + ); + + /** + * Try and connect, this will setup shared sync workers + * This will fail due to not having a valid endpoint, + * but it will try - which is all that matters. + */ + await PowerSync.connect(new DummyConnector()); +}; + +document.addEventListener('DOMContentLoaded', (event) => { + openDatabase(); +}); diff --git a/demos/example-vite-custom-sql/vite.config.ts b/demos/example-vite-custom-sql/vite.config.ts new file mode 100644 index 000000000..a1f909b03 --- /dev/null +++ b/demos/example-vite-custom-sql/vite.config.ts @@ -0,0 +1,27 @@ +import wasm from 'vite-plugin-wasm'; +import topLevelAwait from 'vite-plugin-top-level-await'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + root: 'src', + build: { + outDir: '../dist', + rollupOptions: { + input: 'src/index.html' + }, + emptyOutDir: true + }, + envDir: '..', // Use this dir for env vars, not 'src'. + optimizeDeps: { + // Don't optimize these packages as they contain web workers and WASM files. + // https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673 + exclude: ['@journeyapps/wa-sqlite', '@powersync/web'], + include: ['@powersync/web > js-logger'] + }, + plugins: [wasm(), topLevelAwait()], + worker: { + format: 'es', + plugins: () => [wasm(), topLevelAwait()] + } +});