Repository: Nozbe/WatermelonDB (JavaScript)

Issue:
Migrations failure when using `unsafeExecuteSql` with terminating expression with semicolon
We were experiencing a crash on app startup on a new app release which we hadn't picked up in testing. Upon inspecting the logs, the issue was found to be a syntax error in the generated migrations SQL. 

What had caused this SQL error was not terminating the SQL provided to the  `unsafeExecuteSql` with a semicolon. This meant that, when the migration was generated, the SQL statement had the `unsafeExecuteSql` migration step not terminated with a semicolon, causing a syntax error. 

Here's an example:

```js
const migrations = schemaMigrations({
  migrations: [
    {
      toVersion: 1,
      steps: [
        createTable({
          name: 'table_a',
          columns: [
            {
              name: 'column_a',
              type: 'string',
            },
          ],
        }),
      ],
    },
    {
      toVersion: 2,
      steps: [
        unsafeExecuteSql('DELETE FROM table_a'),
      ],
    },
    {
      toVersion: 3,
      steps: [
        createTable({
          name: 'table_b',
          columns: [
            {
              name: 'column_b',
              type: 'string',
            },
          ],
        }),
      ],
    },
  ],
});
```

When running migrations from v1 -> v2 or v2-v3, there are no issues since the generate migration SQL statement are just:

```sql
DELETE FROM table_a
```

```sql
CREATE TABLE table_b (
    column_b TEXT,
);
```

This is why we never picked this up in our testing.

However, when running migrations from v1 -> v3, a syntax error is thrown since the generated SQL is missing the termination semicolon in the first statement:

```sql
DELETE FROM ai_sizes_weekly_reports -- <-- missing semicolon
CREATE TABLE table_b (
    column_b TEXT,
); -- <-- end of statement
```
(not exactly what is generated, but it's the gist)

I'm creating this issue for two reasons:
1. to warn fellow application programmers using this library about this behaviour of the API
2. to propose a solution

I know the method used is rightfully named `unsafeExecuteSql`, but I still think programmers could be protected from this issue. Two simple ideas come to mind:

1. Add validation to the sql passed into `unsafeExecuteSql` to ensure it ends in a semicolon. Nice and simple.
2. Add validation to the migrations steps to ensure each step ends in a semicolon. Also should be nice and simple. 

I'm happy to implement them, just looking if anyone out there has any feedback or better solutions. 


Discussion:
Good catch -- I think validation makes sense, especially that running _only_ this migration will seem like it works, and it only breaks when combined with other statements. I'll look into it.
@nhanders Fixed in https://github.com/Nozbe/WatermelonDB/pull/1812



Patch:
diff --git a/CHANGELOG-Unreleased.md b/CHANGELOG-Unreleased.md
index 4dfdd1674..ec4b24869 100644
--- a/CHANGELOG-Unreleased.md
+++ b/CHANGELOG-Unreleased.md
@@ -14,6 +14,7 @@
 
 - [ts] Improved LocalStorage type definition
 - [ts] Add missing .d.ts for experimentalFailsafe decorator
+- [migrations] `unsafeExecuteSql` migration is now validate to ensure it ends with a semicolon (#1811)
 
 ### Performance
 
diff --git a/src/Schema/migrations/index.js b/src/Schema/migrations/index.js
index d6b8cd8ac..2c200b708 100644
--- a/src/Schema/migrations/index.js
+++ b/src/Schema/migrations/index.js
@@ -162,6 +162,10 @@ export function addColumns({
 export function unsafeExecuteSql(sql: string): SqlMigrationStep {
   if (process.env.NODE_ENV !== 'production') {
     invariant(typeof sql === 'string', `SQL passed to unsafeExecuteSql is not a string`)
+    invariant(
+      sql.trimEnd().endsWith(';'),
+      `SQL passed to unsafeExecuteSql must end with a semicolon (it would work when executed individually but break if multiple migration steps are executed)`,
+    )
   }
   return { type: 'sql', sql }
 }


Test patch:
diff --git a/src/Schema/migrations/getSyncChanges/test.js b/src/Schema/migrations/getSyncChanges/test.js
index ce0e87da2..d0422e607 100644
--- a/src/Schema/migrations/getSyncChanges/test.js
+++ b/src/Schema/migrations/getSyncChanges/test.js
@@ -152,7 +152,7 @@ describe('getSyncChanges', () => {
           ],
           unsafeSql: (sql) => sql,
         }),
-        unsafeExecuteSql(''),
+        unsafeExecuteSql(';'),
       ],
     },
     {
diff --git a/src/Schema/migrations/test.js b/src/Schema/migrations/test.js
index b47d24b65..13a0ec0cc 100644
--- a/src/Schema/migrations/test.js
+++ b/src/Schema/migrations/test.js
@@ -1,4 +1,4 @@
-import { createTable, addColumns, schemaMigrations } from './index'
+import { createTable, addColumns, unsafeExecuteSql, schemaMigrations } from './index'
 import { stepsForMigration } from './stepsForMigration'
 
 describe('schemaMigrations()', () => {
@@ -181,6 +181,11 @@ describe('migration step functions', () => {
       'type',
     )
   })
+  it('throws if unsafeExecuteSql() is malformed', () => {
+    expect(() => unsafeExecuteSql()).toThrow('not a string')
+    expect(() => unsafeExecuteSql('delete from table_a')).toThrow('semicolon')
+    expect(() => unsafeExecuteSql('delete from table_a;')).not.toThrow()
+  })
 })
 
 describe('stepsForMigration', () => {