@rikishi/watermelondb
Version:
Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast
4,195 lines • 269 kB
HTML
<!DOCTYPE HTML>
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>WatermelonDB documentation</title>
<meta name="robots" content="noindex" />
<!-- Custom HTML head -->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="favicon.svg">
<link rel="shortcut icon" href="favicon.png">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" href="css/chrome.css">
<link rel="stylesheet" href="css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<link rel="stylesheet" href="ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">
var path_to_root = "";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded "><a href="ch01-00-get-excited.html"><strong aria-hidden="true">1.</strong> Get excited</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="index.html"><strong aria-hidden="true">1.1.</strong> Check out the README</a></li><li class="chapter-item expanded "><a href="Demo.html"><strong aria-hidden="true">1.2.</strong> See the demo</a></li></ol></li><li class="chapter-item expanded "><a href="ch02-00-learn-to-use.html"><strong aria-hidden="true">2.</strong> Learn to use Watermelon</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="Installation.html"><strong aria-hidden="true">2.1.</strong> Installation</a></li><li class="chapter-item expanded "><a href="Setup.html"><strong aria-hidden="true">2.2.</strong> Setup</a></li><li class="chapter-item expanded "><a href="Schema.html"><strong aria-hidden="true">2.3.</strong> Schema</a></li><li class="chapter-item expanded "><a href="Model.html"><strong aria-hidden="true">2.4.</strong> Defining Models</a></li><li class="chapter-item expanded "><a href="CRUD.html"><strong aria-hidden="true">2.5.</strong> Create, Read, Update, Delete</a></li><li class="chapter-item expanded "><a href="Components.html"><strong aria-hidden="true">2.6.</strong> Connecting to React Components</a></li><li class="chapter-item expanded "><a href="Query.html"><strong aria-hidden="true">2.7.</strong> Querying</a></li><li class="chapter-item expanded "><a href="Relation.html"><strong aria-hidden="true">2.8.</strong> Relations</a></li><li class="chapter-item expanded "><a href="Writers.html"><strong aria-hidden="true">2.9.</strong> Writers, Readers, batching</a></li></ol></li><li class="chapter-item expanded "><a href="ch03-00-advanced.html"><strong aria-hidden="true">3.</strong> Advanced guides</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="Advanced/Migrations.html"><strong aria-hidden="true">3.1.</strong> Migrations</a></li><li class="chapter-item expanded "><a href="Advanced/Sync.html"><strong aria-hidden="true">3.2.</strong> Sync</a></li><li class="chapter-item expanded "><a href="Advanced/CreateUpdateTracking.html"><strong aria-hidden="true">3.3.</strong> Automatic create/update tracking</a></li><li class="chapter-item expanded "><a href="Advanced/AdvancedFields.html"><strong aria-hidden="true">3.4.</strong> Advanced fields</a></li><li class="chapter-item expanded "><a href="Advanced/Flow.html"><strong aria-hidden="true">3.5.</strong> Flow</a></li><li class="chapter-item expanded "><a href="Advanced/LocalStorage.html"><strong aria-hidden="true">3.6.</strong> LocalStorage</a></li><li class="chapter-item expanded "><a href="Advanced/ProTips.html"><strong aria-hidden="true">3.7.</strong> Pro tips</a></li><li class="chapter-item expanded "><a href="Advanced/Performance.html"><strong aria-hidden="true">3.8.</strong> Performance tips</a></li><li class="chapter-item expanded "><a href="Advanced/SharingDatabaseAcrossTargets.html"><strong aria-hidden="true">3.9.</strong> iOS - Sharing database across targets</a></li></ol></li><li class="chapter-item expanded "><a href="ch04-00-deeper.html"><strong aria-hidden="true">4.</strong> Dig deeper into WatermelonDB</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="Implementation/Architecture.html"><strong aria-hidden="true">4.1.</strong> Architecture</a></li><li class="chapter-item expanded "><a href="Implementation/Adapters.html"><strong aria-hidden="true">4.2.</strong> Adapters</a></li><li class="chapter-item expanded "><a href="Implementation/SyncImpl.html"><strong aria-hidden="true">4.3.</strong> Sync implementation</a></li></ol></li><li class="chapter-item expanded "><a href="ch04-00-deeper.html"><strong aria-hidden="true">5.</strong> Other</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="Roadmap.html"><strong aria-hidden="true">5.1.</strong> Roadmap</a></li><li class="chapter-item expanded "><a href="CONTRIBUTING.html"><strong aria-hidden="true">5.2.</strong> Contributing</a></li><li class="chapter-item expanded "><a href="CHANGELOG.html"><strong aria-hidden="true">5.3.</strong> Changelog</a></li></ol></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">WatermelonDB documentation</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h3 id="get-excited"><a class="header" href="#get-excited">Get excited</a></h3>
<p align="center">
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/logo-horizontal2.png" alt="WatermelonDB" width="539" />
</p>
<h4 align="center">
A reactive database framework
</h4>
<p align="center">
Build powerful React and React Native apps that scale from hundreds to tens of thousands of records and remain <em>fast</em> ⚡️
</p>
<p align="center">
<a href="https://github.com/Nozbe/WatermelonDB/blob/master/LICENSE">
<img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License">
</a>
<a href="https://www.npmjs.com/package/@rikishi/watermelondb">
<img src="https://img.shields.io/npm/v/@rikishi/watermelondb.svg" alt="npm">
</a>
</p>
<table><thead><tr><th></th><th>WatermelonDB</th></tr></thead><tbody>
<tr><td>⚡️</td><td><strong>Launch your app instantly</strong> no matter how much data you have</td></tr>
<tr><td>📈</td><td><strong>Highly scalable</strong> from hundreds to tens of thousands of records</td></tr>
<tr><td>😎</td><td><strong>Lazy loaded</strong>. Only load data when you need it</td></tr>
<tr><td>🔄</td><td><strong>Offline-first.</strong> <a href="https://nozbe.github.io/WatermelonDB/Advanced/Sync.html">Sync</a> with your own backend</td></tr>
<tr><td>📱</td><td><strong>Multiplatform</strong>. iOS, Android, web, and Node.js</td></tr>
<tr><td>⚛️</td><td><strong>Optimized for React.</strong> Easily plug data into components</td></tr>
<tr><td>🧰</td><td><strong>Framework-agnostic.</strong> Use JS API to plug into other UI frameworks</td></tr>
<tr><td>⏱</td><td><strong>Fast.</strong> And getting faster with every release!</td></tr>
<tr><td>✅</td><td><strong>Proven.</strong> Powers <a href="https://nozbe.com/teams">Nozbe Teams</a> since 2017 (and <a href="index.html#who-uses-watermelondb">many others</a>)</td></tr>
<tr><td>✨</td><td><strong>Reactive.</strong> (Optional) <a href="https://github.com/ReactiveX/rxjs">RxJS</a> API</td></tr>
<tr><td>🔗</td><td><strong>Relational.</strong> Built on rock-solid <a href="https://www.sqlite.org">SQLite</a> foundation</td></tr>
<tr><td>⚠️</td><td><strong>Static typing</strong> with <a href="https://flow.org">Flow</a> or <a href="https://typescriptlang.org">TypeScript</a></td></tr>
</tbody></table>
<h2 id="why-watermelon"><a class="header" href="#why-watermelon">Why Watermelon?</a></h2>
<p><strong>WatermelonDB</strong> is a new way of dealing with user data in React Native and React web apps.</p>
<p>It's optimized for building <strong>complex applications</strong> in React Native, and the number one goal is <strong>real-world performance</strong>. In simple words, <em>your app must launch fast</em>.</p>
<p>For simple apps, using Redux or MobX with a persistence adapter is the easiest way to go. But when you start scaling to thousands or tens of thousands of database records, your app will now be slow to launch (especially on slower Android devices). Loading a full database into JavaScript is expensive!</p>
<p>Watermelon fixes it <strong>by being lazy</strong>. Nothing is loaded until it's requested. And since all querying is performed directly on the rock-solid <a href="https://www.sqlite.org/index.html">SQLite database</a> on a separate native thread, most queries resolve in an instant.</p>
<p>But unlike using SQLite directly, Watermelon is <strong>fully observable</strong>. So whenever you change a record, all UI that depends on it will automatically re-render. For example, completing a task in a to-do app will re-render the task component, the list (to reorder), and all relevant task counters. <a href="https://www.youtube.com/watch?v=UlZ1QnFF4Cw"><strong>Learn more</strong></a>.</p>
<table><thead><tr><th><a href="https://www.youtube.com/watch?v=UlZ1QnFF4Cw"><img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/watermelon-talk-thumbnail.jpg" alt="React Native EU: Next-generation React Databases" width="300" /></a></th><th><a href="https://watermelondb.now.sh/"><img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/watermelon-demo-thumbnail.png" alt="WatermelonDB Demo" width="300" /></a></th></tr></thead><tbody>
<tr><td><p align="center"><a href="https://www.youtube.com/watch?v=UlZ1QnFF4Cw">📺 <strong>Next-generation React databases</strong><br>(a talk about WatermelonDB)</a></p></td><td><p align="center"><a href="https://watermelondb.now.sh/">✨ <strong>Check out web Demo</strong></a></p></td></tr>
</tbody></table>
<h2 id="usage"><a class="header" href="#usage">Usage</a></h2>
<p><strong>Quick (over-simplified) example:</strong> an app with posts and comments.</p>
<p>First, you define Models:</p>
<pre><code class="language-js">class Post extends Model {
@field('name') name
@field('body') body
@children('comments') comments
}
class Comment extends Model {
@field('body') body
@field('author') author
}
</code></pre>
<p>Then, you connect components to the data:</p>
<pre><code class="language-js">const Comment = ({ comment }) => (
<View style={styles.commentBox}>
<Text>{comment.body} — by {comment.author}</Text>
</View>
)
// This is how you make your app reactive! ✨
const enhance = withObservables(['comment'], ({ comment }) => ({
comment,
}))
const EnhancedComment = enhance(Comment)
</code></pre>
<p>And now you can render the whole Post:</p>
<pre><code class="language-js">const Post = ({ post, comments }) => (
<View>
<Text>{post.name}</Text>
<Text>Comments:</Text>
{comments.map(comment =>
<EnhancedComment key={comment.id} comment={comment} />
)}
</View>
)
const enhance = withObservables(['post'], ({ post }) => ({
post,
comments: post.comments
}))
</code></pre>
<p>The result is fully reactive! Whenever a post or comment is added, changed, or removed, the right components <strong>will automatically re-render</strong> on screen. Doesn't matter if a change occurred in a totally different part of the app, it all just works out of the box!</p>
<h3 id="-learn-more-a-hrefhttpsnozbegithubiowatermelondbsee-full-documentationa"><a class="header" href="#-learn-more-a-hrefhttpsnozbegithubiowatermelondbsee-full-documentationa">➡️ <strong>Learn more:</strong> <a href="https://nozbe.github.io/WatermelonDB/">see full documentation</a></a></h3>
<h2 id="who-uses-watermelondb"><a class="header" href="#who-uses-watermelondb">Who uses WatermelonDB</a></h2>
<a href="https://nozbe.com/teams/">
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/apps/nozbe-teams.png" alt="Nozbe Teams" width="300" />
</a>
<br>
<a href="https://capmo.de">
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/apps/capmo.png" alt="CAPMO" width="300" />
</a>
<br>
<a href="https://steady.health">
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/apps/steady.png" alt="Steady" width="150">
</a>
<br>
<a href="https://aerobotics.com">
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/apps/aerobotics.png" alt="Aerobotics" width="300" />
</a>
<br>
<a href="https://smashappz.com">
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/apps/smashappz.jpg" alt="Smash Appz" width="300" />
</a>
<br>
<a href="https://rocket.chat/">
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/apps/rocketchat.png" alt="Rocket Chat" width="300" />
</a>
<br>
<a href="https://halogo.com.au/">
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/apps/halogo_logo.png" alt="HaloGo" width="300" />
</a>
<br>
<a href="https://sportsrecruits.com/">
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/apps/sportsrecruits-logo.png" alt="SportsRecruits" width="300" />
</a>
<br>
<a href="https://chatable.io/">
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/apps/chatable_logo.png" alt="Chatable" width="300" />
</a>
<br>
<p><em>Does your company or app use 🍉? Open a pull request and add your logo/icon with link here!</em></p>
<h2 id="contributing"><a class="header" href="#contributing">Contributing</a></h2>
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/needyou.jpg" alt="We need you" width="220" />
<p><strong>WatermelonDB is an open-source project and it needs your help to thrive!</strong></p>
<p>If there's a missing feature, a bug, or other improvement you'd like, we encourage you to contribute! Feel free to open an issue to get some guidance and see <a href="./CONTRIBUTING.html">Contributing guide</a> for details about project setup, testing, etc.</p>
<p>If you're just getting started, see <a href="https://github.com/Nozbe/WatermelonDB/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22">good first issues</a> that are easy to contribute to. If you make a non-trivial contribution, email me, and I'll send you a nice 🍉 sticker!</p>
<p>If you make or are considering making an app using WatermelonDB, please let us know!</p>
<h2 id="author-and-license"><a class="header" href="#author-and-license">Author and license</a></h2>
<p><strong>WatermelonDB</strong> was created by <a href="https://github.com/Nozbe">@Nozbe</a>. Main author and maintainer is <a href="https://github.com/radex">Radek Pietruszewski</a>.</p>
<p><strong>Contributors:</strong> <a href="https://github.com/mobily">@mobily</a>, <a href="https://github.com/kokusGr">@kokusGr</a>, <a href="https://github.com/rozPierog">@rozPierog</a>, <a href="https://github.com/rkrajewski">@rkrajewski</a>, <a href="https://github.com/domeknn">@domeknn</a>, <a href="https://github.com/Tereszkiewicz">@Tereszkiewicz</a> and <a href="https://github.com/Nozbe/WatermelonDB/graphs/contributors">more</a>.</p>
<p>WatermelonDB is available under the MIT license. See the <a href="./LICENSE">LICENSE file</a> for more info.</p>
<h1 id="demo"><a class="header" href="#demo">Demo</a></h1>
<p>See how WatermelonDB performs at large scales in the demo app.</p>
<h2 id="online-demo"><a class="header" href="#online-demo">Online demo</a></h2>
<h3>
<a href="https://watermelondb.now.sh">
<img src="https://github.com/Nozbe/WatermelonDB/raw/master/assets/watermelon-demo-medium.png" alt="WatermelonDB Demo" width="600" /><br>
Check out WatermelonDB demo online
</a>
</h3>
<p>Note that where Watermelon really shines is in React Native apps — see instructions below ⬇️</p>
<h2 id="running-react-native-demo"><a class="header" href="#running-react-native-demo">Running React Native demo</a></h2>
<p>To compile the WatermelonDB demo on your own machine:</p>
<ol>
<li>Install <a href="https://facebook.github.io/react-native/docs/getting-started.html">React Native toolkit</a> if you haven't already</li>
<li>Download this project
<pre><code class="language-bash">git clone https://github.com/Nozbe/WatermelonDB.git
cd WatermelonDB/examples/native
yarn
</code></pre>
</li>
<li>Run the React Native packager:
<pre><code class="language-bash">yarn dev
</code></pre>
</li>
<li>Run the app on iOS or Android:
<pre><code class="language-bash">yarn start:ios # or:
yarn start:android
</code></pre>
</li>
</ol>
<p>⚠️ Note that for accurate measurement of performance, you need to compile the demo app in Release mode and run it on a real device, not the simulator.</p>
<p>⚠️ If iOS app doesn't compile, try running it from Xcode instead of the terminal first</p>
<p>⚠️ You might want to <code>git checkout</code> the <a href="https://github.com/Nozbe/WatermelonDB/releases">latest stable tag</a> if the demo app doesn't work</p>
<h2 id="running-web-demo"><a class="header" href="#running-web-demo">Running web demo</a></h2>
<p>To compile the WatermelonDB demo on your own machine:</p>
<ol>
<li>Download this project
<pre><code class="language-bash">git clone https://github.com/Nozbe/WatermelonDB.git
cd WatermelonDB/examples/web
yarn
</code></pre>
</li>
<li>Run the server:
<pre><code class="language-bash">yarn dev
</code></pre>
</li>
<li>Webpack will point you to the right URL to open in the browser</li>
</ol>
<p>You can also <a href="https://zeit.co/now">use Now</a> to deploy the demo app (requires a Zeit account):</p>
<pre><code class="language-bash">now
</code></pre>
<p>⚠️ You might want to <code>git checkout</code> the <a href="https://github.com/Nozbe/WatermelonDB/releases">latest stable tag</a> if the demo app doesn't work</p>
<h3 id="learn-to-use-watermelon"><a class="header" href="#learn-to-use-watermelon">Learn to use Watermelon</a></h3>
<p>Learn the basics of how to use WatermelonDB</p>
<h1 id="installation"><a class="header" href="#installation">Installation</a></h1>
<p>First, add Watermelon to your project:</p>
<pre><code class="language-bash">yarn add @rikishi/watermelondb
# (or with npm:)
npm install @rikishi/watermelondb
</code></pre>
<h2 id="react-native-setup"><a class="header" href="#react-native-setup">React Native setup</a></h2>
<ol>
<li>
<p>Install the Babel plugin for decorators if you haven't already:</p>
<pre><code class="language-bash">yarn add --dev @babel/plugin-proposal-decorators
# (or with npm:)
npm install -D @babel/plugin-proposal-decorators
</code></pre>
</li>
<li>
<p>Add ES6 decorators support to your <code>.babelrc</code> file:</p>
<pre><code class="language-json">{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }]
]
}
</code></pre>
</li>
<li>
<p>Set up your iOS or Android project — see instructions below</p>
</li>
</ol>
<h3 id="ios-react-native"><a class="header" href="#ios-react-native">iOS (React Native)</a></h3>
<ol>
<li>
<p><strong>Set up Babel config in your project</strong></p>
<p>See instructions above ⬆️</p>
</li>
<li>
<p><strong>Add Swift support to your Xcode project</strong>:</p>
<ul>
<li>Open <code>ios/YourAppName.xcodeproj</code> in Xcode</li>
<li>Right-click on <strong>(your app name)</strong> in the Project Navigator on the left, and click <strong>New File…</strong></li>
<li>Create a single empty Swift file (<code>wmelon.swift</code>) to the project (make sure that <strong>Your App Name</strong> target is selected when adding), and when Xcode asks, press <strong>Create Bridging Header</strong> and <strong>do not remove</strong> the Swift file afterwards</li>
</ul>
</li>
<li>
<p><strong>Link WatermelonDB's native library using CocoaPods</strong></p>
<p>Add this to your <code>Podfile</code>:</p>
<pre><code class="language-ruby"># If you're using autolinking, this line might not be needed
pod 'WatermelonDB', :path => '../node_modules/@rikishi/watermelondb'
# NOTE: Do not remove, needed to keep WatermelonDB compiling:
pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi', :modular_headers => true
# NOTE: This is required as of v0.23
pod 'simdjson', path: '../node_modules/@nozbe/simdjson'
</code></pre>
<p>Note that as of WatermelonDB 0.22, manual (non-CocoaPods) linking is not supported.</p>
<p>At least Xcode 12.2 and iOS 13 are recommended (earlier versions are not tested for compatibility).</p>
</li>
<li>
<p><strong>Fix up your Bridging Header</strong></p>
<p>You will likely see that the iOS build fails to compile. If this happens, locate the Swift Bridging Header (likely <code>ios/YourAppName/YourAppName-Bridging-Header.h</code>), and paste this:</p>
<pre><code class="language-objc">#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTViewManager.h>
#import <React/RCTBridgeModule.h>
// Silence warning
#import "../../node_modules/@rikishi/watermelondb/native/ios/WatermelonDB/SupportingFiles/Bridging.h"
</code></pre>
<p>You might have to tweak the import path to correctly locate Watermelon's bridging header.</p>
</li>
</ol>
<h3 id="android-react-native"><a class="header" href="#android-react-native">Android (React Native)</a></h3>
<p><strong>Set up Babel config in your project</strong></p>
<p>See instructions above ⬆️</p>
<p>On RN60+, auto linking should work.</p>
<details>
<summary>Linking Manually</summary>
<p>Users on React Native 0.60+ automatically have access to "autolinking", requiring no further manual installation steps. If you are on React Native 0.60+ please skip this section. If you are on React Native < 0.60 please do the following in <strong>addition</strong> to the previous steps:</p>
<ol>
<li>In <code>android/settings.gradle</code>, add:</li>
</ol>
<pre><code class="language-gradle">include ':watermelondb'
project(':watermelondb').projectDir =
new File(rootProject.projectDir, '../node_modules/@rikishi/watermelondb/native/android')
</code></pre>
<ol start="2">
<li>In <code>android/app/build.gradle</code>, add:</li>
</ol>
<pre><code class="language-gradle">// ...
dependencies {
// ...
implementation project(':watermelondb') // ⬅️ This!
}
</code></pre>
<ol start="3">
<li>And finally, in <code>android/app/src/main/java/{YOUR_APP_PACKAGE}/MainApplication.java</code>, add:</li>
</ol>
<pre><code class="language-java">// ...
import com.nozbe.watermelondb.WatermelonDBPackage; // ⬅️ This!
// ...
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new WatermelonDBPackage() // ⬅️ Here!
);
}
</code></pre>
</details>
<details>
<summary>Custom Kotlin Version ⚠️</summary>
Make sure the kotlin version is set to 1.3.50 or above. Just set ext properties `kotlinVersion` in `android/build.gradle`, and WatermelonDB will use the specified kotlin version.
<pre><code class="language-gradle">buildscript {
ext.kotlinVersion = '1.3.50'
}
</code></pre>
</details>
<details>
<summary>Troubleshooting</summary>
If you get this error:
<blockquote>
<p><code>Can't find variable: Symbol</code></p>
</blockquote>
<p>You're using an ancient version of JSC. Install <a href="https://github.com/react-community/jsc-android-buildscripts"><code>jsc-android</code></a> or Hermes.</p>
</details>
<details>
<summary>JSI Installation (Optional)</summary>
<p>To enable fast, highly performant, synchronous JSI operation on Android, you need to take a few
additional steps manually.</p>
<ol>
<li>
<p>Make sure you have NDK installed (version <code>20.1.5948944</code> has been tested to work when writing this guide)</p>
</li>
<li>
<p>In <code>android/settings.gradle</code>, add:</p>
<pre><code class="language-gradle">include ':watermelondb-jsi'
project(':watermelondb-jsi').projectDir =
new File(rootProject.projectDir, '../node_modules/@rikishi/watermelondb/native/android-jsi')
</code></pre>
</li>
<li>
<p>In <code>android/app/build.gradle</code>, add:</p>
<pre><code class="language-gradle">// ...
android {
// ...
packagingOptions {
pickFirst '**/libc++_shared.so' // ⬅️ This (if missing)
}
}
dependencies {
// ...
implementation project(':watermelondb-jsi') // ⬅️ This!
}
</code></pre>
</li>
<li>
<p>And finally, in <code>android/app/src/main/java/{YOUR_APP_PACKAGE}/MainApplication.java</code>, add:</p>
<pre><code class="language-java">// ...
import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage; // ⬅️ This!
import com.facebook.react.bridge.JSIModulePackage; // ⬅️ This!
// ...
private final ReactNativeHost mReactNativeHost =
new ReactNativeHost(this) {
// ...
@Override
protected JSIModulePackage getJSIModulePackage() {
return new WatermelonDBJSIPackage(); // ⬅️ This!
}
}
</code></pre>
<p>or if you have <strong>multiple</strong> JSI Packages:</p>
<pre><code class="language-java">// ...
import java.util.Arrays; // ⬅️ This!
import com.facebook.react.bridge.JSIModuleSpec; // ⬅️ This!
import com.facebook.react.bridge.JSIModulePackage; // ⬅️ This!
import com.facebook.react.bridge.ReactApplicationContext; // ⬅️ This!
import com.facebook.react.bridge.JavaScriptContextHolder; // ⬅️ This!
import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage; // ⬅️ This!
// ...
private final ReactNativeHost mReactNativeHost =
new ReactNativeHost(this) {
// ...
@Override
protected JSIModulePackage getJSIModulePackage() {
return new JSIModulePackage() {
@Override
public List<JSIModuleSpec> getJSIModules(
final ReactApplicationContext reactApplicationContext,
final JavaScriptContextHolder jsContext
) {
List<JSIModuleSpec> modules = Arrays.asList();
modules.addAll(new WatermelonDBJSIPackage().getJSIModules(reactApplicationContext, jsContext)); // ⬅️ This!
// ⬅️ add more JSI packages here by conventions above
return modules;
}
};
}
}
</code></pre>
</li>
</ol>
</details>
<h2 id="web-setup"><a class="header" href="#web-setup">Web setup</a></h2>
<p>This guide assumes you use Webpack as your bundler.</p>
<ol>
<li>If you haven't already, install Babel plugins for decorators, static class properties, and async/await to get the most out of Watermelon. This assumes you use Babel 7 and already support ES6 syntax.
<pre><code class="language-bash">yarn add --dev @babel/plugin-proposal-decorators
yarn add --dev @babel/plugin-proposal-class-properties
yarn add --dev @babel/plugin-transform-runtime
# (or with npm:)
npm install -D @babel/plugin-proposal-decorators
npm install -D @babel/plugin-proposal-class-properties
npm install -D @babel/plugin-transform-runtime
</code></pre>
</li>
<li>Add ES7 support to your <code>.babelrc</code> file:
<pre><code class="language-json">{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
[
"@babel/plugin-transform-runtime",
{
"helpers": true,
"regenerator": true
}
]
]
}
</code></pre>
</li>
</ol>
<h2 id="nodejs-sqlite-setup"><a class="header" href="#nodejs-sqlite-setup">NodeJS (SQLite) setup</a></h2>
<p>You only need this if you want to use WatermelonDB in NodeJS with SQLite (e.g. for scripts that share code with your web/React Native app)</p>
<ol>
<li>Install <a href="https://github.com/JoshuaWise/better-sqlite3">better-sqlite3</a> peer dependency
<pre><code class="language-sh">yarn add --dev better-sqlite3
# (or with npm:)
npm install -D better-sqlite3
</code></pre>
</li>
</ol>
<hr />
<h2 id="next-steps"><a class="header" href="#next-steps">Next steps</a></h2>
<p>➡️ After Watermelon is installed, <a href="./Setup.html"><strong>set it up</strong></a></p>
<h1 id="set-up-your-app-for-watermelondb"><a class="header" href="#set-up-your-app-for-watermelondb">Set up your app for WatermelonDB</a></h1>
<p>Make sure you <a href="./Installation.html">installed Watermelon</a> before proceeding.</p>
<p>Create <code>model/schema.js</code> in your project. You'll need it for <a href="./Schema.html">the next step</a>.</p>
<pre><code class="language-js">import { appSchema, tableSchema } from '@rikishi/watermelondb'
export default appSchema({
version: 1,
tables: [
// We'll add tableSchemas here later
]
})
</code></pre>
<p>Similarly, create <code>model/migrations.js</code>. (<a href="./Advanced/Migrations.html">More information about migrations</a>):</p>
<pre><code class="language-js">import { schemaMigrations } from '@rikishi/watermelondb/Schema/migrations'
export default schemaMigrations({
migrations: [
// We'll add migration definitions here later
],
})
</code></pre>
<p>Now, in your <code>index.native.js</code>:</p>
<pre><code class="language-js">import { Platform } from 'react-native'
import { Database } from '@rikishi/watermelondb'
import SQLiteAdapter from '@rikishi/watermelondb/adapters/sqlite'
import schema from './model/schema'
import migrations from './model/migrations'
// import Post from './model/Post' // ⬅️ You'll import your Models here
// First, create the adapter to the underlying database:
const adapter = new SQLiteAdapter({
schema,
// (You might want to comment it out for development purposes -- see Migrations documentation)
migrations,
// (optional database name or file system path)
// dbName: 'myapp',
// (recommended option, should work flawlessly out of the box on iOS. On Android,
// additional installation steps have to be taken - disable if you run into issues...)
jsi: true, /* Platform.OS === 'ios' */
// (optional, but you should implement this method)
onSetUpError: error => {
// Database failed to load -- offer the user to reload the app or log out
}
})
// Then, make a Watermelon database from it!
const database = new Database({
adapter,
modelClasses: [
// Post, // ⬅️ You'll add Models to Watermelon here
],
})
</code></pre>
<p>The above will work on React Native (iOS/Android) and NodeJS. For the web, instead of <code>SQLiteAdapter</code> use <code>LokiJSAdapter</code>:</p>
<pre><code class="language-js">import LokiJSAdapter from '@rikishi/watermelondb/adapters/lokijs'
const adapter = new LokiJSAdapter({
schema,
// (You might want to comment out migrations for development purposes -- see Migrations documentation)
migrations,
useWebWorker: false,
useIncrementalIndexedDB: true,
// dbName: 'myapp', // optional db name
// --- Optional, but recommended event handlers:
onQuotaExceededError: (error) => {
// Browser ran out of disk space -- offer the user to reload the app or log out
},
onSetUpError: (error) => {
// Database failed to load -- offer the user to reload the app or log out
},
extraIncrementalIDBOptions: {
onDidOverwrite: () => {
// Called when this adapter is forced to overwrite contents of IndexedDB.
// This happens if there's another open tab of the same app that's making changes.
// Try to synchronize the app now, and if user is offline, alert them that if they close this
// tab, some data may be lost
},
onversionchange: () => {
// database was deleted in another browser tab (user logged out), so we must make sure we delete
// it in this tab as well - usually best to just refresh the page
if (checkIfUserIsLoggedIn()) {
window.location.reload()
}
},
}
})
// The rest is the same!
</code></pre>
<hr />
<h2 id="next-steps-1"><a class="header" href="#next-steps-1">Next steps</a></h2>
<p>➡️ After Watermelon is installed, <a href="./Schema.html"><strong>define your app's schema</strong></a></p>
<h1 id="schema"><a class="header" href="#schema">Schema</a></h1>
<p>When using WatermelonDB, you're dealing with <strong>Models</strong> and <strong>Collections</strong>. However, underneath Watermelon sits an <strong>underlying database</strong> (SQLite or LokiJS) which speaks a different language: <strong>tables and columns</strong>. Together, those are called a <strong>database schema</strong> and we must define it first.</p>
<h2 id="defining-a-schema"><a class="header" href="#defining-a-schema">Defining a Schema</a></h2>
<p>Say you want Models <code>Post</code>, <code>Comment</code> in your app. For each of those Models, you define a table. And for every field of a Model (e.g. name of the blog post, author of the comment) you define a column. For example:</p>
<pre><code class="language-js">// model/schema.js
import { appSchema, tableSchema } from '@rikishi/watermelondb'
export const mySchema = appSchema({
version: 1,
tables: [
tableSchema({
name: 'posts',
columns: [
{ name: 'title', type: 'string' },
{ name: 'subtitle', type: 'string', isOptional: true },
{ name: 'body', type: 'string' },
{ name: 'is_pinned', type: 'boolean' },
]
}),
tableSchema({
name: 'comments',
columns: [
{ name: 'body', type: 'string' },
{ name: 'post_id', type: 'string', isIndexed: true },
]
}),
]
})
</code></pre>
<p><strong>Note:</strong> It is database convention to use plural and snake_case names for table names. Column names are also snake_case. So <code>Post</code> become <code>posts</code> and <code>createdAt</code> becomes <code>created_at</code>.</p>
<h3 id="column-types"><a class="header" href="#column-types">Column types</a></h3>
<p>Columns have one of three types: <code>string</code>, <code>number</code>, or <code>boolean</code>.</p>
<p>Fields of those types will default to <code>''</code>, <code>0</code>, or <code>false</code> respectively, if you create a record with a missing field.</p>
<p>To allow fields to be <code>null</code>, mark the column as <code>isOptional: true</code>.</p>
<h3 id="naming-conventions"><a class="header" href="#naming-conventions">Naming conventions</a></h3>
<p>To add a relation to a table (e.g. <code>Post</code> where a <code>Comment</code> was published, or author of a comment), add a string column ending with <code>_id</code>:</p>
<pre><code class="language-js">{ name: 'post_id', type: 'string' },
{ name: 'author_id', type: 'string' },
</code></pre>
<p>Boolean columns should have names starting with <code>is_</code>:</p>
<pre><code class="language-js">{ name: 'is_pinned', type: 'boolean' }
</code></pre>
<p>Date fields should be <code>number</code> (dates are stored as Unix timestamps) and have names ending with <code>_at</code>:</p>
<pre><code class="language-js">{ name: 'last_seen_at', type: 'number', isOptional: true }
</code></pre>
<h3 id="special-columns"><a class="header" href="#special-columns">Special columns</a></h3>
<p>All tables <em>automatically</em> have a string column <code>id</code> (of <code>string</code> type) to uniquely identify records -- therefore you cannot declare a column named <code>id</code> yourself. (There are also special <code>_status</code> and <code>_changed</code> columns used for <a href="./Advanced/Sync.html">synchronization</a> - you shouldn't touch them yourself).</p>
<p>You can add special <code>created_at</code> / <code>updated_at</code> columns to enable <a href="./Advanced/CreateUpdateTracking.html">automatic create/update tracking</a>.</p>
<h3 id="modifying-schema"><a class="header" href="#modifying-schema">Modifying Schema</a></h3>
<p>Watermelon cannot automatically detect Schema changes. Therefore, whenever you change the Schema, you must increment its version number (<code>version:</code> field).</p>
<p>During early development, this is all you need to do - on app reload, this will cause the database to be cleared completely.</p>
<p>To seamlessly update the schema (without deleting user data), use <a href="./Advanced/Migrations.html">Migrations</a>.</p>
<p>⚠️ Always use Migrations if you already shipped your app.</p>
<h3 id="indexing"><a class="header" href="#indexing">Indexing</a></h3>
<p>To enable database indexing, add <code>isIndexed: true</code> to a column.</p>
<p>Indexing makes querying by a column faster, at the expense of create/update speed and database size.</p>
<p>For example, if you often query all comments belonging to a post (that is, query comments by its <code>post_id</code> column), you should mark the <code>post_id</code> column as indexed.</p>
<p>However, if you rarely query all comments by its author, indexing <code>author_id</code> is probably not worth it.</p>
<p>In general, most <code>_id</code> fields are indexed. Occasionally, <code>boolean</code> fields are worth indexing (but it's a "low quality index"). However, you should almost never index date (<code>_at</code>) columns or <code>string</code> columns. You definitely do not want to index long-form user text.</p>
<p>⚠️ Do not mark all columns as indexed to "make Watermelon faster". Indexing has a real performance cost and should be used only when appropriate.</p>
<h2 id="advanced"><a class="header" href="#advanced">Advanced</a></h2>
<h3 id="unsafe-sql-schema"><a class="header" href="#unsafe-sql-schema">Unsafe SQL schema</a></h3>
<p>If you want to modify the SQL used to set up the SQLite database, you can pass <code>unsafeSql</code> parameter
to <code>tableSchema</code> and <code>appSchema</code>. This parameter is a function that receives SQL generated by Watermelon,
and you can return whatever you want - so you can append, prepend, replace parts of SQL, or return
your own SQL altogether. When passed to <code>tableSchema</code>, it receives SQL generated for just that table,
and when to <code>appSchema</code> - the entire schema SQL.</p>
<p>⚠️ Note that SQL generated by WatermelonDB is not considered to be a stable API, so be careful about your transforms as they can break at any time.</p>
<pre><code class="language-js">appSchema({
...
tables: [
tableSchema({
name: 'tasks',
columns: [...],
unsafeSql: sql => sql.replace(/create table [^)]+\)/, '$& without rowid'),
}),
],
unsafeSql: (sql, kind) => {
// Note that this function is called not just when first setting up the database
// Additionally, when running very large batches, all database indices may be dropped and later
// recreated as an optimization. More kinds may be added in the future.
switch (kind) {
case 'setup':
return `create blabla;${sql}`
case 'create_indices':
case 'drop_indices':
return sql
default:
throw new Error('unexpected unsafeSql kind')
}
},
})
</code></pre>
<hr />
<h2 id="next-steps-2"><a class="header" href="#next-steps-2">Next steps</a></h2>
<p>➡️ After you define your schema, go ahead and <a href="./Model.html"><strong>define your Models</strong></a></p>
<h1 id="defining-models"><a class="header" href="#defining-models">Defining Models</a></h1>
<p>A <strong>Model</strong> class represents a type of thing in your app. For example, <code>Post</code>, <code>Comment</code>, <code>User</code>.</p>
<p>Before defining a Model, make sure you <a href="./Schema.html">defined its schema</a>.</p>
<h2 id="create-a-model"><a class="header" href="#create-a-model">Create a Model</a></h2>
<p>Let's define the <code>Post</code> model:</p>
<pre><code class="language-js">// model/Post.js
import { Model } from '@rikishi/watermelondb'
export default class Post extends Model {
static table = 'posts'
}
</code></pre>
<p>Specify the table name for this Model — the same you defined <a href="./Schema.html">in the schema</a>.</p>
<p>Now add the new Model to <code>Database</code>:</p>
<pre><code class="language-js">// index.js
import Post from 'model/Post'
const database = new Database({
// ...
modelClasses: [Post],
})
</code></pre>
<h3 id="associations"><a class="header" href="#associations">Associations</a></h3>
<p>Many models relate to one another. A <code>Post</code> has many <code>Comment</code>s. And every <code>Comment</code> belongs to a <code>Post</code>. (Every relation is double-sided). Define those associations like so:</p>
<pre><code class="language-js">class Post extends Model {
static table = 'posts'
static associations = {
comments: { type: 'has_many', foreignKey: 'post_id' },
}
}
class Comment extends Model {
static table = 'comments'
static associations = {
posts: { type: 'belongs_to', key: 'post_id' },
}
}
</code></pre>
<p>On the "child" side (<code>comments</code>) you define a <code>belongs_to</code> association, and pass a column name (key) that points to the parent (<code>post_id</code> is the ID of the post the comment belongs to).</p>
<p>On the "parent" side (<code>posts</code>) you define an equivalent <code>has_many</code> association and pass the same column name (⚠️ note that the name here is <code>foreignKey</code>).</p>
<h2 id="add-fields"><a class="header" href="#add-fields">Add fields</a></h2>
<p>Next, define the Model's <em>fields</em> (properties). Those correspond to <a href="./Schema.html">table columns</a> defined earlier in the schema.</p>
<pre><code class="language-js">import { field, text } from '@rikishi/watermelondb/decorators'
class Post extends Model {
static table = 'posts'
static associations = {
comments: { type: 'has_many', foreignKey: 'post_id' },
}
@text('title') title
@text('body') body
@field('is_pinned') isPinned
}
</code></pre>
<p>Fields are defined using ES6 decorators. Pass <strong>column name</strong> you defined in Schema as the argument to <code>@field</code>.</p>
<p><strong>Field types</strong>. Fields are guaranteed to be the same type (string/number/boolean) as the column type defined in Schema. If column is marked <code>isOptional: true</code>, fields may also be null.</p>
<p><strong>User text fields</strong>. For fields that contain arbitrary text specified by the user (e.g. names, titles, comment bodies), use <code>@text</code> - a simple extension of <code>@field</code> that also trims whitespace.</p>
<p><strong>Note:</strong> Why do I have to type the field/column name twice? The database convention is to use <code>snake_case</code> for names, and the JavaScript convention is to use camelCase. So for any multi-word name, the two differ. Also, for resiliency, we believe it's better to be explicit, because over time, you might want to refactor how you name your JavaScript field names, but column names must stay the same for backward compatibility.</p>
<h3 id="date-fields"><a class="header" href="#date-fields">Date fields</a></h3>
<p>For date fields, use <code>@date</code> instead of <code>@field</code>. This will return a JavaScript <code>Date</code> object (instead of Unix timestamp integer).</p>
<pre><code class="language-js">import { date } from '@rikishi/watermelondb/decorators'
class Post extends Model {
// ...
@date('last_event_at') lastEventAt
}
</code></pre>
<h3 id="derived-fields"><a class="header" href="#derived-fields">Derived fields</a></h3>
<p>Use ES6 getters to define model properties that can be calculated based on database fields:</p>
<pre><code class="language-js">import { field, text } from '@rikishi/watermelondb/decorators'
class Post extends Model {
static table = 'posts'
@date('archived_at') archivedAt
get isRecentlyArchived() {
// in the last 7 days
return this.archivedAt &&
this.archivedAt.getTime() > Date.now() - 7 * 24 * 3600 * 1000
}
}
</code></pre>
<h3 id="to-one-relation-fields"><a class="header" href="#to-one-relation-fields">To-one relation fields</a></h3>
<p>To point to a related record, e.g. <code>Post</code> a <code>Comment</code> belongs to, or author (<code>User</code>) of a <code>Comment</code>, use <code>@relation</code> or <code>@immutableRelation</code>:</p>
<pre><code class="language-js">import { relation, immutableRelation } from '@rikishi/watermelondb/decorators'
class Comment extends Model {
// ...
@relation('posts', 'post_id') post
@immutableRelation('users', 'author_id') author
}
</code></pre>
<p><strong>➡️ Learn more:</strong> <a href="./Relation.html">Relation API</a></p>
<h3 id="children-to-many-relation-fields"><a class="header" href="#children-to-many-relation-fields">Children (to-many relation fields)</a></h3>
<p>To point to a list of records that belong to this Model, e.g. all <code>Comment</code>s that belong to a <code>Post</code>, you can define a simple <code>Query</code> using <code>@children</code>:</p>
<pre><code class="language-js">import { children } from '@rikishi/watermelondb/decorators'
class Post extends Model {
static table = 'posts'
static associations = {
comments: { type: 'has_many', foreignKey: 'post_id' },
}
@children('comments') comments
}
</code></pre>
<p>Pass the <em>table name</em> of the related records as an argument to <code>@children</code>. The resulting property will be a <code>Query</code> you can fetch, observe, or count.</p>
<p><strong>Note:</strong> You must define a <code>has_many</code> association in <code>static associations</code> for this to work</p>
<p><strong>➡️ Learn more:</strong> <a href="./Query.html">Queries</a></p>
<h3 id="custom-queries"><a class="header" href="#custom-queries">Custom Queries</a></h3>
<p>In addition to <code>@children</code>, you can define custom Queries or extend existing ones, for example:</p>
<pre><code class="language-js">import { children } from '@rikishi/watermelondb/decorators'
import { Q } from '@rikishi/watermelondb'
class Post extends Model {
static table = 'posts'
static associations = {
comments: { type: 'has_many', foreignKey: 'post_id' },
}
@children('comments') comments
@lazy verifiedComments = this.comments.extend(
Q.where('is_verified', true)
)
}
</code></pre>
<p><strong>➡️ Learn more:</strong> <a href="./Query.html">Queries</a></p>
<h3 id="writer-methods"><a class="header" href="#writer-methods">Writer methods</a></h3>
<p>Define <strong>writers</strong> to simplify creating and updating records, for example:</p>
<pre><code class="language-js">import { writer } from '@rikishi/watermelondb/decorators'
class Comment extends Model {
static table = 'comments'
@field('is_spam') isSpam
@writer async markAsSpam() {
await this.update(comment => {
comment.isSpam = true
})
}
}
</code></pre>
<p>Methods must be marked as <code>@writer</code> to be able to modify the database.</p>
<p><strong>➡️ Learn more:</strong> <a href="./Writers.html">Writers</a></p>
<h2 id="advanced-fields"><a class="header" href="#advanced-fields">Advanced fields</a></h2>
<p>You can also use these decorators:</p>
<ul>
<li><code>@json</code> for complex serialized data</li>
<li><code>@readonly</code> to make the field read-only</li>
<li><code>@nochange</code> to disallow changes to the field <em>after the first creation</em></li>
</ul>
<p>And you can make observable compound properties using RxJS...</p>
<p><strong>➡️ Learn more:</strong> <a href="./Advanced/AdvancedFields.html">Advanced fields</a></p>
<hr />
<h2 id="next-steps-3"><a class="header" href="#next-steps-3">Next steps</a></h2>
<p>➡️ After you define some Models, learn the <a href="./CRUD.html"><strong>Create / Read / Update / Delete API</strong></a></p>
<h1 id="create-read-update-delete"><a class="header" href="#create-read-update-delete">Create, Read, Update, Delete</a></h1>
<p>When you have your <a href="./Schema.html">Schema</a> and <a href="./Model.html">Models</a> defined, learn how to manipulate them!</p>
<h2 id="reading"><a class="header" href="#reading">Reading</a></h2>
<h4 id="get-a-collection"><a class="header" href="#get-a-collection">Get a collection</a></h4>
<p>The <code>Collection</code> object is how you find, query, and create new records of a given type.</p>
<pre><code class="language-js">const postsCollection = database.get('posts')
</code></pre>
<p>Pass the <a href="./Schema.html">table name</a> as the argument.</p>
<h4 id="find-a-record-by-id"><a class="header" href="#find-a-record-by-id">Find a record (by ID)</a></h4>
<pre><code class="language-js">const postId = 'abcdefgh'
const post = await database.get('posts').find(postId)
</code></pre>
<p><code>find()</code> returns a Promise. If the record cannot be found, the Promise will be rejected.</p>
<h4 id="query-records"><a class="header" href="#query-records">Query records</a></h4>
<p>Find a list of records matching given conditions by making a Query and then fetching it:</p>
<pre><code class="language-js">const allPosts = await database.get('posts').query().fetch()
const numberOfStarredPosts = await database.get('posts').query(
Q.where('is_starred', true)
).fetchCount()
</code></pre>
<p><strong>➡️ Learn more:</strong> <a href="./Query.html">Queries</a></p>
<h2 id="modifying-the-database"><a class="header" href="#modifying-the-database">Modifying the database</a></h2>
<p>All modifications to the database (like creating, updating, deleting records) must be done <strong>in a Writer</strong>, either by wrapping your work in <code>database.write()</code>:</p>
<pre><code class="language-js">await database.write(async () => {
const comment = await database.get('comments').find(commentId)
await comment.update(() => {
comment.isSpam = true
})
})
</code></pre>
<p>Or by defining a <code>@writer</code> method on a Model:</p>
<pre><code class="language-js">import { writer } from '@rikishi/watermelondb/decorators'
class Comment extends Model {
// (...)
@writer async markAsSpam() {
await this.update(comment => {
comment.isSpam = true
})
}
}
</code></pre>
<p><strong>➡️ Learn more:</strong> <a href="./Writers.html">Writers</a></p>
<h3 id="create-a-new-record"><a class="header" href="#create-a-new-record">Create a new record</a></h3>
<pre><code class="language-js">const newPost = await database.get('posts').create(post => {
post.title = 'New post'
post.body = 'Lorem ipsum...'
})
</code></pre>
<p><code>.create()</code> takes a "builder function". In the example above, the builder will get a <code>Post</code> object as an argument. Use this object to set values for <a href="./Model.html">fields you defined</a>.</p>
<p><strong>Note:</strong> Always <code>await</code> the Promise returned by <code>create</code> before you access the created record.</p>
<p><strong>Note:</strong> You can only set fields inside <code>create()</code> or <code>update()</code> builder functions.</p>
<h3 id="update-a-record"><a class="header" href="#update-a-record">Update a record</a></h3>
<pre><code class="language-js">await somePost.update(post => {
post.title = 'Updated title'
})
</code></pre>
<p>Like creating, updating takes a builder function, where you can use field setters.</p>
<p><strong>Note:</strong> Always <code>await</code> the Promise returned by <code>update</code> before you access the modified record.</p>
<h3 id="delete-a-record"><a class="header" href="#delete-a-record">Delete a record</a></h3>
<p>There are two ways of deleting records: syncable (mark as deleted), and permanent.</p>
<p>If you only use Watermelon as a local database, destroy records permanently, if you <a href="./Advanced/Sync.html">synchronize</a>, mark as deleted instead.</p>
<pre><code class="language-js">await somePost.markAsDeleted() // syncable
await somePost.destroyPermanently() // permanent
</code></pre>
<p><strong>Note:</strong> Do not access, update, or observe records after they're deleted.</p>
<h2 id="advanced-1"><a class="header" href="#advanced-1">Advanced</a></h2>
<ul>
<li><code>Model.observe()</code> - usually you only use this <a href="./Components.html">when connecting records to components</a>, but you can manually observe a record outside of React components. The returned <a href="https://github.com/reactivex/rxjs">RxJS</a> <code>Observable</code> will emit the record immediately upon subscription, and then every time the record is updated. If the record is deleted, the Observable will complete.</li>
<li><code>Query.observe()</code>, <code>Relation.observe()</code> — analagous to the above, but for <a href="./Query.html">Queries</a> and <a href="./Relation.html">Relations</a></li>
<li><code>Query.observeWithColumns()</code> - used for <a href="./Components.html">sorted lists</a></li>
<li><code>Collection.findAndObserve(id)</code> — same as using <code>.find(id)</code> and then calling <code>record.observe()</code></li>
<li><code>Model.prepareUpdate()</code>, <code>Collection.prepareCreate</code>, <code>Database.batch</code> — used for <a href="./Writers.html">batch updates</a></li>
<li><code>Database.unsafeResetDatabase()</code> destroys the whole database - <a href="https://github.com/Nozbe/WatermelonDB/blob/22188ee5b6e3af08e48e8af52d14e0d90db72925/src/Database/index.js#L131">be sure to see this comment before using it</a></li>
<li>To override the <code>record.id</code> during the creation, e.g. to sync with a remote database, you can do it by <code>record._raw</code> property. Be aware that the <code>id</code> must be of type <code>string</code>.
<pre><code class="language-js">await database.get('posts').create(post => {
post._raw.id = serverId
})
</code></pre>
</li>
</ul>
<h3 id="advanced-unsafe-raw-execute"><a class="header" href="#advanced-unsafe-raw-execute">Advanced: Unsafe raw execute</a></h3>
<p>⚠️ Do not use this if you don't know what you're doing...</p>
<p>There is an escape hatch to drop down from WatermelonDB to underlying database level to execute arbitrary commands. Use as a last resort tool:</p>
<pre><code class="language-js">await database.write(() => {
// sqlite:
await database.adapter.unsafeExecute({
sqls: [
// [sql_query, [placeholder arguments, ...]]
['create table temporary_test (id, foo, bar)', []],
['insert into temporary_test (id, foo, bar) values (?, ?, ?)', ['t1', true, 3.14]],
]
})
// lokijs:
await database.adapter.unsafeExecute({
loki: loki => {
loki.addCollection('temporary_test', { unique: ['id'], indices: [], disableMeta: true })
loki.getCollection('temporary_test').insert({ id: 't1', foo: true, bar: 3.14 })
}
})
})
</code></pre>
<hr />
<h2 id="next-steps-4"><a class="header" href="#next-steps-4">Next steps</a></h2>
<p>➡️ Now that you can create and update records, <a href="./Components.html"><strong>connect them to React components</strong></a></p>
<h1 id="connecting-to-components"><a class="header" href="#connecting-to-components">Connecting to Components</a></h1>
<p>After you <a href="./Model.html">define some Models</a>, it's time to connect Watermelon to your app's interface. We're using React in this guide, however WatermelonDB can be used with any UI framework.</p>
<p><strong>Note:</strong> If you're not familiar with higher-order components, read <a href="https://reactjs.org/docs/higher-order-components.html">React documentation</a>, check out <a href="https://github.com/acdlite/recompose"><code>recompose</code></a>… or just read the examples below to see it in practice!</p>
<h2 id="reactive-components"><a class="header" href="#reactive-components">Reactive components</a></h2>
<p>Here's a very simple React component rendering a <code>Comment</code> record:</p>
<pre><code class="language-jsx">const Comment = ({ comment }) => (
<div>
<p>{comment.body}</p>
</div>
)
</code></pre>
<p>Now we can fetch a comment: <code>const comment = await commentsCollection.find(id)</code> and then render it: <code><Comment comment={comment} /></code>. The only problem is that this is <strong>not reactive</strong>. If the Comment is updated or deleted, the component will not re-render to reflect the changes. (Unless an update is forced manually or the parent component re-renders).</p>
<p>Let's enhance the component to make it <em>observe</em> the <code>Comment</code> automatically:</p>
<pre><code class="language-jsx">import withObservables from '@nozbe/with-observables'
const enhance = withObservables(['comment'], ({ comment }) => ({
comment // shortcut syntax for `comment: comment.observe()`
}))
const EnhancedComment = enhance(Comment)
export default EnhancedComment
</code></pre>
<p>Now, if we render <code><EnhancedComment comment={comment} /></code>, it <strong>will</strong> update every time the comment changes.</p>
<h3 id="reactive-lists"><a class="header" href="#reactive-lists">Reactive lists</a></h3>
<p>Let's render the whole <code>Post</code> with comments:</p>
<pre><code class="language-jsx">import withObservables from '@nozbe/with-observables'
import EnhancedComment from 'components/Comment'
const Post = ({ post, comments }) => (
<article>
<h1>{post.name}</h1>
<p>{post.body}</p>
<h2>Comments</h2>
{comments.map(comment =>
<EnhancedComment key={comment.id} comment={comment} />
)}
</article>
)
const enhance = withObservables(['post'], ({ post }) => ({
post,
comments: post.comments, // Shortcut syntax for `post.comments.observe()`
}))
const EnhancedPost = enhance(Post)
export default EnhancedPost
</code></pre>
<p>Notice a couple of things:</p>
<ol>
<li>
<p>We're starting with a simple non-reactive <code>Post</code> component</p>
</li>
<li>
<p>Like before, we enhance it by observing the <code>Post</code>. If the post name or body changes, it will re-render.</p>
</li>
<li>
<p>To access comments, we fetch them from the database and observe using <code>post.comments.observe()</code> and inject a new prop <code>comments</code>. (<code>post.comments</code> is a Query created using <code>@children</code>).</p>
<p>Note that we can skip <code>.observe()</code> and just pass <code>post.comments</code> for convenience — <code>withObservables</code> will call observe for us</p>
</li>
<li>
<p>By <strong>observing the Query</strong>, the <code><Post></code> component will re-render if a comment is created or deleted</p>
</li>
<li>
<p>However, observing the comments Query will not re-render <code><Post></code> if a comment is <em>updated</em> — we render the <code><EnhancedComment></code> so that <em>it</em> observes the comment and re-renders if necessary.</p>
</li>
</ol>
<h3 id="reactive-relations"><a class="header" href="#reactive-relations">Reactive relations</a></h3>
<p>The <code><Comment></code> component we made previously only renders the body of the comment but doesn't say who posted it.</p>
<p>Assume the <code>Comment</code> model has a <code>@relation('users', 'author_id') author</code> field. Let's render it:</p>
<pre><code class="language-jsx">const Comment = ({ comment, author }) => (
<div>
<p>{comment.body} — by {author.name}</p>
</div>
)
const enhance = withObservables(['comment'], ({ comment }) => ({
comment,
author: comment.author, // shortcut syntax for `comment.author.observe()`
}))
const EnhancedComment = enhance(Comment)
</code></pre>
<p><code>comment.author</code> is a <a href="./Relation.html">Relation object</a>, and we can call <code>.observe()</code> on it to fetch the <code>User</code> and then observe changes to it. If author's name changes, the component will re-render.</p>
<p><strong>Note</strong> again that we can also pass <code>Relation</code> objects directly for convenience, skipping <code>.observe()</code></p>
<h3 id="reactive-counters"><a class="header" href="#reactive-counters">Reactive counters</a></h3>
<p>Let's make a <code><PostExcerpt></code> component to display on a <em>list</em> of Posts, with only a brief summary of the contents and only the number of comments it has:</p>
<pre><code class="language-jsx">const PostExcerpt = ({ post, commentCount }) => (
<div>
<h1>{post.name}</h1>
<p>{getExcerpt(post.body)}</p>
<span>{commentCount} comments</span>
</div>
)
const enhance = withObservables(['post'], ({ post }) => ({
post,
commentCount: post.comments.observeCount()
}))
const EnhancedPostExcerpt = enhance(PostExcerpt)
</code></pre>
<p>This is very similar to normal <code><Post></code>. We take the <code>Query</code> for post's comments, but instead of observing the <em>list</em> of comments, we call <code>observeCount()</code>. This is far more efficient. And as always, if a new comment is posted, or one is deleted, the component will re-render with the updated count.</p>
<h2 id="hey-what-about-react-hooks"><a class="header" href="#hey-what-about-react-hooks">Hey, what about React Hooks?</a></h2>
<p>We get it — HOCs are so 2017, and Hooks are the future! And we agree.</p>
<p>However, Hooks are not compatible with WatermelonDB's asynchronous API. You <em>could</em> use alternative open-source Hooks for Rx Observables, however we don't recommend that. They won't work correctly in all cases and won't be as optimized for performance with WatermelonDB as <code>withObservables</code>. In the future, once Concurrent React is fully developed and published, WatermelonDB will have official hooks.</p>
<p><strong><a href="https://github.com/Nozbe/withObservables/issues/16">See discussion about official <code>useObservables</code> Hook</a></strong></p>
<h2 id="understanding-withobservables"><a class="header" href="#understanding-withobservables">Understanding <code>withObservables</code></a></h2>
<p>Let's unpack this:</p>
<pre><code class="language-js">withObservables(['post'], ({ post }) => ({
post: post.observe(),
commentCount: post.comments.observeCount()
}))
</code></pre>
<ol>
<li>Starting from the second argument, <code>({ post })</code> are the input props for the component. Here, we receive <code>post</code> prop with a <code>Post</code> object.</li>
<li>These:
<pre><code class="language-js">({
post: post.observe(),
commentCount: post.comments.observeCount()
})
</code></pre>
are the enhanced props we inject. The keys are props' names, and values are <code>Observable</code> objects. Here, we override the <code>post</code> prop with an observable version, and create a new <code>commentCount</code> prop.</li>
<li>The first argument: <code>['post']</code> is a list of props that trigger observation restart. So if a different <code>post</code> is passed, that new post will be observed. If you pass <code>[]</code>, the rendered Post will not change. You can pass multiple prop names if any of them should cause observation to re-start. Think of it the same way as the <code>deps</code> argument you pass to <code>useEffect</code> hook.</li>
<li><strong>Rule of thumb</strong>: If you want to use a prop in the second arg function, pass its name in the first arg array</li>
</ol>
<h2 id="advanced-2"><a class="header" href="#advanced-2">Advanced</a></h2>
<ol>
<li><strong>findAndObserve</strong>. If you have, say, a post ID from your Router (URL in the browser), you can use:
<pre><code class="language-js">withObservables(['postId'], ({ postId, database }) => ({
post: database.get('posts').findAndObserve(postId)
}))
</code></pre>
</li>
<li><strong>RxJS transformations</strong>. The values returned by <code>Model.observe()</code>, <code>Query.observe()</code>, <code>Relation.observe()</code> are <a href="https://github.com/ReactiveX/rxjs">RxJS Observables</a>. You can use standard transforms like mapping, filtering, throttling, startWith to change when and how the component is re-rendered.</li>
<li><strong>Custom Observables</strong>. <code>withObservables</code> is a general-purpose HOC for Observables, not just Watermelon. You can create new props from any <code>Observable</code>.</li>
</ol>
<h3 id="advanced-observing-sorted-lists"><a class="header" href="#advanced-observing-sorted-lists">Advanced: observing sorted lists</a></h3>
<p>If you have a list that's dynamically sorted (e.g. sort comments by number of likes), use <code>Query.observeWithColumns</code> to ensure the list is re-rendered when its order changes:</p>
<pre><code class="language-jsx">// This is a function that sorts an array of comments according to its `likes` field
// I'm using `ramda` functions for this example, but you can do sorting however you like
const sortComments = sortWith([
descend(prop('likes'))
])
const CommentList = ({ comments }) => (
<div>
{sortComments(comments).map(comment =>
<EnhancedComment key={comment.id} comment={comment} />
)}
</div>
)
const enhance = withObservables(['post'], ({ post }) => ({
comments: post.comments.observeWithColumns(['likes'])
}))
const EnhancedCommentList = enhance(CommentList)
</code></pre>
<p>If you inject <code>post.comments.observe()</code> into the component, the list will not re-render to change its order, only if comments are added or removed. Instead, use <code>query.observeWithColumns()</code> with an array of <a href="./Schema.html"><strong>column names</strong></a> you use for sorting to re-render whenever a record on the list has any of those fields changed.</p>
<h3 id="advanced-observing-2nd-level-relations"><a class="header" href="#advanced-observing-2nd-level-relations">Advanced: observing 2nd level relations</a></h3>
<p>If you have 2nd level relations, like author's <code>Contact</code> info, and want to connect it to a component as well, you cannot simply use <code>post.author.contact.observe()</code> in <code>withObservables</code>. Remember, <code>post.author</code> is not a <code>User</code> object, but a <code>Relation</code> that has to be asynchronously fetched.</p>
<p>Before accessing and observing the <code>Contact</code> relation, you need to resolve the <code>author</code> itself. Here is the simplest way to do it:</p>
<pre><code class="language-js">import { compose } from 'recompose'
const enhance = compose(
withObservables(['post'], ({ post }) => ({
post,
author: post.author,
})),
withObservables(['author'], ({ author }) => ({
contact: author.contact,
})),
)
const EnhancedPost = enhance(PostComponent);
</code></pre>
<p>This is using a <code>compose</code> function from <a href="https://github.com/acdlite/recompose"><code>recompose</code></a>. If you're not familiar with function composition, read the <code>enhance</code> function from top to bottom:</p>
<ul>
<li>first, the PostComponent is enhanced by changing the incoming <code>post</code> prop into its observable version, and by adding a new <code>author</code> prop that will contain the fetched contents of <code>post.author</code></li>
<li>then, the enhanced component is enhanced once again, by adding a <code>contact</code> prop containing the fetched contents of <code>author.contact</code>.</li>
</ul>
<h4 id="alternative-method-of-observing-2nd-level-relations"><a class="header" href="#alternative-method-of-observing-2nd-level-relations">Alternative method of observing 2nd level relations</a></h4>
<p>If you are familiar with <code>rxjs</code>, another way to achieve the same result is using <code>switchMap</code> operator:</p>
<pre><code class="language-js">import { switchMap } from 'rxjs/operators'
const enhance = withObservables(['post'], ({post}) => ({
post: post,
author: post.author,
contact: post.author.observe().pipe(switchMap(author => author.contact.observe()))
}))
const EnhancedPost = enhance(PostComponent)
</code></pre>
<p>Now <code>PostComponent</code> will have <code>Post</code>, <code>Author</code> and <code>Contact</code> props.</p>
<h4 id="2nd-level-optional-relations"><a class="header" href="#2nd-level-optional-relations">2nd level optional relations</a></h4>
<p>If you have an optional relation between <code>Post</code> and <code>Author</code>, the enhanced component might receive <code>null</code> as <code>author</code> prop. As you must always return an observable for the <code>contact</code> prop, you can use <code>rxjs</code>'s <code>of</code> function to create a default or empty <code>Contact</code> prop:</p>
<pre><code class="language-js">import { of as of$ } from 'rxjs'
import { compose } from 'recompose'
const enhance = compose(
withObservables(['post'], ({ post }) => ({
post,
author: post.author,
})),
withObservables(['author'], ({ author }) => ({
contact: author ? author.contact.observe() : of$(null),
})),
)
</code></pre>
<p>With the <code>switchMap</code> approach, you can do:</p>
<pre><code class="language-js">const enhance = withObservables(['post'], ({post}) => ({
post: post,
author: post.author,
contact: post.autor.observe().pipe(
switchMap(author => author ? autor.contact : of$(null))
)
}))
</code></pre>
<h2 id="database-provider"><a class="header" href="#database-provider">Database Provider</a></h2>
<p>To prevent prop drilling you can use the Database Provider and the <code>withDatabase</code> Higher-Order Component.</p>
<pre><code class="language-jsx">import DatabaseProvider from '@rikishi/watermelondb/DatabaseProvider'
// ...
const database = new Database({
adapter,
modelClasses: [Blog, Post, Comment],
})
render(
<DatabaseProvider database={database}>
<Root />
</DatabaseProvider>, document.getElementById('application')
)
</code></pre>
<p>To consume the database in your components you just wrap your component like so:</p>
<pre><code class="language-jsx">import { withDatabase } from '@rikishi/watermelondb/DatabaseProvider'
import { compose } from 'recompose'
// ...
export default compose(
withDatabase,
withObservables([], ({ database }) => ({
blogs: database.get('blogs').query(),
}),
)(BlogList)
</code></pre>
<p>The database prop in the <code>withObservables</code> Higher-Order Component is provided by the database provider.</p>
<h3 id="usedatabase"><a class="header" href="#usedatabase"><code>useDatabase</code></a></h3>
<p>You can also consume <code>Database</code> object using React Hooks syntax:</p>
<pre><code class="language-js">import { useDatabase } from '@rikishi/watermelondb/hooks'
const Component = () => {
const database = useDatabase()
}
</code></pre>
<hr />
<h2 id="next-steps-5"><a class="header" href="#next-steps-5">Next steps</a></h2>
<p>➡️ Next, learn more about <a href="./Query.html"><strong>custom Queries</strong></a></p>
<h1 id="query-api"><a class="header" href="#query-api">Query API</a></h1>
<p><strong>Querying</strong> is how you find records that match certain conditions, for example:</p>
<ul>
<li>Find all comments that belong to a certain post</li>
<li>Find all <em>verified</em> comments made by John</li>
<li>Count all verified comments made by John or Lucy published under posts made in the last two weeks</li>
</ul>
<p>Because queries are executed on the database, and not in JavaScript, they're really fast. It's also how Watermelon can be fast even at large scales, because even with tens of thousands of records <em>total</em>, you rarely need to load more than a few dozen records at app launch.</p>
<h2 id="defining-queries"><a class="header" href="#defining-queries">Defining Queries</a></h2>
<h3 id="children"><a class="header" href="#children">@children</a></h3>
<p>The simplest query is made using <code>@children</code>. This defines a <code>Query</code> for all comments that belong to a <code>Post</code>:</p>
<pre><code class="language-js">class Post extends Model {
// ...
@children('comments') comments
}
</code></pre>
<p><strong>➡️ Learn more:</strong> <a href="./Model.html">Defining Models</a></p>
<h3 id="extended-query"><a class="header" href="#extended-query">Extended Query</a></h3>
<p>To <strong>narrow down</strong> a <code>Query</code> (add <a href="Query.html#query-conditions">extra conditions</a> to an existing Query), use <code>.extend()</code>:</p>
<pre><code class="language-js">import { Q } from '@rikishi/watermelondb'
import { children, lazy } from '@rikishi/watermelondb/decorators'
class Post extends Model {
// ...
@children('comments') comments
@lazy verifiedComments = this.comments.extend(
Q.where('is_verified', true)
)
@lazy verifiedAwesomeComments = this.verifiedComments.extend(
Q.where('is_awesome', true)
)
}
</code></pre>
<p><strong>Note:</strong> Use <code>@lazy</code> when extending or defining new Queries for performance</p>
<h3 id="custom-queries-1"><a class="header" href="#custom-queries-1">Custom Queries</a></h3>
<p>You can query any table like so:</p>
<pre><code class="language-js">import { Q } from '@rikishi/watermelondb'
const users = await database.get('users').query(
// conditions that a user must match:
Q.on('comments', 'post_id', somePostId)
).fetch()
</code></pre>
<p>This fetches all users that made a comment under a post with <code>id = somePostId</code>.</p>
<p>You can define custom queries on a Model like so:</p>
<pre><code class="language-js">class Post extends Model {
// ...
@lazy commenters = this.collections.get('users').query(
Q.on('comments', 'post_id', this.id)
)
}
</code></pre>
<h2 id="executing-queries"><a class="header" href="#executing-queries">Executing Queries</a></h2>
<p>Most of the time, you execute Queries by connecting them to React Components like so:</p>
<pre><code class="language-js">withObservables(['post'], ({ post }) => ({
post,
comments: post.comments,
verifiedCommentCount: post.verifiedComments.observeCount(),
}))
</code></pre>
<p><strong>➡️ Learn more:</strong> <a href="./Components.html">Connecting to Components</a></p>
<h4 id="fetch"><a class="header" href="#fetch">Fetch</a></h4>
<p>To simply get the current list or current count (without observing future changes), use <code>fetch</code> / <code>fetchCount</code>.</p>
<pre><code class="language-js">const comments = await post.comments.fetch()
const verifiedCommentCount = await post.verifiedComments.fetchCount()
// Shortcut syntax:
const comments = await post.comments
const verifiedCommentCount = await post.verifiedComments.count
</code></pre>
<h2 id="query-conditions"><a class="header" href="#query-conditions">Query conditions</a></h2>
<pre><code class="language-js">import { Q } from '@rikishi/watermelondb'
// ...
database.get('comments').query(
Q.where('is_verified', true)
)
</code></pre>
<p>This will query <strong>all</strong> comments that are verified (all comments with one condition: the <code>is_verified</code> column of a comment must be <code>true</code>).</p>
<p>When making conditions, you refer to <a href="./Schema.html"><strong>column names</strong></a> of a table (i.e. <code>is_verified</code>, not <code>isVerified</code>). This is because queries are executed directly on the underlying database.</p>
<p>The second argument is the value we want to query for. Note that the passed argument must be the same type as the column (<code>string</code>, <code>number</code>, or <code>boolean</code>; <code>null</code> is allowed only if the column is marked as <code>isOptional: true</code> in the schema).</p>
<h4 id="empty-query"><a class="header" href="#empty-query">Empty query</a></h4>
<pre><code class="language-js">const allComments = await database.get('comments').query().fetch()
</code></pre>
<p>A Query with no conditions will find <strong>all</strong> records in the collection.</p>
<p><strong>Note:</strong> Don't do this unless necessary. It's generally more efficient to only query the exact records you need.</p>
<h4 id="multiple-conditions"><a class="header" href="#multiple-conditions">Multiple conditions</a></h4>
<pre><code class="language-js">database.get('comments').query(
Q.where('is_verified', true),
Q.where('is_awesome', true)
)
</code></pre>
<p>This queries all comments that are <strong>both</strong> verified <strong>and</strong> awesome.</p>
<h3 id="conditions-with-other-operators"><a class="header" href="#conditions-with-other-operators">Conditions with other operators</a></h3>
<table><thead><tr><th>Query</th><th>JavaScript equivalent</th></tr></thead><tbody>
<tr><td><code>Q.where('is_verified', true)</code></td><td><code>is_verified === true</code> (shortcut syntax)</td></tr>
<tr><td><code>Q.where('is_verified', Q.eq(true))</code></td><td><code>is_verified === true</code></td></tr>
<tr><td><code>Q.where('archived_at', Q.notEq(null))</code></td><td><code>archived_at !== null</code></td></tr>
<tr><td><code>Q.where('likes', Q.gt(0))</code></td><td><code>likes > 0</code></td></tr>
<tr><td><code>Q.where('likes', Q.weakGt(0))</code></td><td><code>likes > 0</code> (slightly different semantics — <a href="Query.html#null-behavior">see "null behavior"</a> for details)</td></tr>
<tr><td><code>Q.where('likes', Q.gte(100))</code></td><td><code>likes >= 100</code></td></tr>
<tr><td><code>Q.where('dislikes', Q.lt(100))</code></td><td><code>dislikes < 100</code></td></tr>
<tr><td><code>Q.where('dislikes', Q.lte(100))</code></td><td><code>dislikes <= 100</code></td></tr>
<tr><td><code>Q.where('likes', Q.between(10, 100))</code></td><td><code>likes >= 10 && likes <= 100</code></td></tr>
<tr><td><code>Q.where('status', Q.oneOf(['published', 'draft']))</code></td><td><code>['published', 'draft'].includes(status)</code></td></tr>
<tr><td><code>Q.where('status', Q.notIn(['archived', 'deleted']))</code></td><td><code>status !== 'archived' && status !== 'deleted'</code></td></tr>
<tr><td><code>Q.where('status', Q.like('%bl_sh%'))</code></td><td><code>/.*bl.sh.*/i</code> (See note below!)</td></tr>
<tr><td><code>Q.where('status', Q.notLike('%bl_sh%'))</code></td><td><code>/^((!?.*bl.sh.*).)*$/i</code> (Inverse regex match) (See note below!)</td></tr>
</tbody></table>
<p><strong>Note:</strong> It's NOT SAFE to use <code>Q.like</code> and <code>Q.notLike</code> with user input directly, because special characters like <code>%</code> or <code>_</code> are not escaped. Always sanitize user input like so:</p>
<pre><code class="language-js">Q.like(`%${Q.sanitizeLikeString(userInput)}%`)
Q.notLike(`%${Q.sanitizeLikeString(userInput)}%`)
</code></pre>
<p>You can use <code>Q.like</code> for search-related tasks. For example, to find all users whose username start with "jas" (case-insensitive) you can write</p>
<pre><code class="language-js">usersCollection.query(
Q.where("username", Q.like(`${Q.sanitizeLikeString("jas")}%`)
)
</code></pre>
<p>where <code>"jas"</code> can be changed dynamically with user input.</p>
<h3 id="andor-nesting"><a class="header" href="#andor-nesting">AND/OR nesting</a></h3>
<p>You can nest multiple conditions using <code>Q.and</code> and <code>Q.or</code>:</p>
<pre><code class="language-js">database.get('comments').query(
Q.where('archived_at', Q.notEq(null)),
Q.or(
Q.where('is_verified', true),
Q.and(
Q.where('likes', Q.gt(10)),
Q.where('dislikes', Q.lt(5))
)
)
)
</code></pre>
<p>This is equivalent to <code>archivedAt !== null && (isVerified || (likes > 10 && dislikes < 5))</code>.</p>
<h3 id="conditions-on-related-tables-join-queries"><a class="header" href="#conditions-on-related-tables-join-queries">Conditions on related tables ("JOIN queries")</a></h3>
<p>For example: query all comments under posts published by John:</p>
<pre><code class="language-js">// Shortcut syntax:
database.get('comments').query(
Q.on('posts', 'author_id', john.id),
)
// Full syntax:
database.get('comments').query(
Q.on('posts', Q.where('author_id', Q.eq(john.id))),
)
</code></pre>
<p>Normally you set conditions on the table you're querying. Here we're querying <strong>comments</strong>, but we have a condition on the <strong>post</strong> the comment belongs to.</p>
<p>The first argument for <code>Q.on</code> is the table name you're making a condition on. The other two arguments are same as for <code>Q.where</code>.</p>
<p><strong>Note:</strong> The two tables <a href="./Model.html">must be associated</a> before you can use <code>Q.on</code>.</p>
<h4 id="multiple-conditions-on-a-related-table"><a class="header" href="#multiple-conditions-on-a-related-table">Multiple conditions on a related table</a></h4>
<p>For example: query all comments under posts that are written by John <em>and</em> are either published or belong to <code>draftBlog</code></p>
<pre><code class="language-js">database.get('comments').query(
Q.on('posts', [
Q.where('author_id', john.id)
Q.or(
Q.where('published', true),
Q.where('blog_id', draftBlog.id),
)
]),
)
</code></pre>
<p>Instead of an array of conditions, you can also pass <code>Q.and</code>, <code>Q.or</code>, <code>Q.where</code>, or <code>Q.on</code> as the second argument to <code>Q.on</code>.</p>
<h4 id="nesting-qon-within-andor"><a class="header" href="#nesting-qon-within-andor">Nesting <code>Q.on</code> within AND/OR</a></h4>
<p>If you want to place <code>Q.on</code> nested within <code>Q.and</code> and <code>Q.or</code>, you must explicitly define all tables you're joining on. (NOTE: The <code>Q.experimentalJoinTables</code> API is subject to change)</p>
<pre><code class="language-js">tasksCollection.query(
Q.experimentalJoinTables(['projects']),
Q.or(
Q.where('is_followed', true),
Q.on('projects', 'is_followed', true),
),
)
</code></pre>
<h4 id="deep-qons"><a class="header" href="#deep-qons">Deep <code>Q.on</code>s</a></h4>
<p>You can also nest <code>Q.on</code> within <code>Q.on</code>, e.g. to make a condition on a grandparent. You must explicitly define the tables you're joining on. (NOTE: The <code>Q.experimentalNestedJoin</code> API is subject to change). Multiple levels of nesting are allowed.</p>
<pre><code class="language-js">// this queries tasks that are inside projects that are inside teams where team.foo == 'bar'
tasksCollection.query(
Q.experimentalNestedJoin('projects', 'teams'),
Q.on('projects', Q.on('teams', 'foo', 'bar')),
)
</code></pre>
<h2 id="advanced-queries"><a class="header" href="#advanced-queries">Advanced Queries</a></h2>
<h3 id="advanced-observing"><a class="header" href="#advanced-observing">Advanced observing</a></h3>
<p>Call <code>query.observeWithColumns(['foo', 'bar'])</code> to create an Observable that emits a value not only when the list of matching records changes (new records/deleted records), but also when any of the matched records changes its <code>foo</code> or <code>bar</code> column. <a href="./Components.html">Use this for observing sorted lists</a></p>
<h4 id="count-throttling"><a class="header" href="#count-throttling">Count throttling</a></h4>
<p>By default, calling <code>query.observeCount()</code> returns an Observable that is throttled to emit at most once every 250ms. You can disable throttling using <code>query.observeCount(false)</code>.</p>
<h3 id="column-comparisons"><a class="header" href="#column-comparisons">Column comparisons</a></h3>
<p>This queries comments that have more likes than dislikes. Note that we're comparing <code>likes</code> column to another column instead of a value.</p>
<pre><code class="language-js">database.get('comments').query(
Q.where('likes', Q.gt(Q.column('dislikes')))
)
</code></pre>
<h3 id="sortby-take-skip"><a class="header" href="#sortby-take-skip">sortBy, take, skip</a></h3>
<p>You can use these clauses to sort the query by one or more columns. Note that only simple ascending/descending criteria for columns are supported.</p>
<pre><code class="language-js">database.get('comments').query(
// sorts by number of likes from the most likes to the fewest
Q.sortBy('likes', Q.desc),
// if two comments have the same number of likes, the one with fewest dislikes will be at the top
Q.sortBy('dislikes', Q.asc),
// limit number of comments to 100, skipping the first 50
Q.skip(50),
Q.take(100),
)
</code></pre>
<p>It isn't <em>necessarily</em> better or more efficient to sort on query level instead of in JavaScript, <strong>however</strong> the most important use case for <code>Q.sortBy</code> is when used alongside <code>Q.skip</code> and <code>Q.take</code> to implement paging - to limit the number of records loaded from database to memory on very long lists</p>
<h3 id="fetch-ids"><a class="header" href="#fetch-ids">Fetch IDs</a></h3>
<p>If you only need IDs of records matching a query, you can optimize the query by calling <code>await query.fetchIds()</code> instead of <code>await query.fetch()</code></p>
<h3 id="security"><a class="header" href="#security">Security</a></h3>
<p>Remember that Queries are a sensitive subject, security-wise. Never trust user input and pass it directly into queries. In particular:</p>
<ul>
<li>Never pass into queries values you don't know for sure are the right type (e.g. value passed to <code>Q.eq()</code> should be a string, number, boolean, or null -- but not an Object. If the value comes from JSON, you must validate it before passing it!)</li>
<li>Never pass column names (without whitelisting) from user input</li>
<li>Values passed to <code>oneOf</code>, <code>notIn</code> should be arrays of simple types - be careful they don't contain objects</li>
<li>Do not use <code>Q.like</code> / <code>Q.notLike</code> without <code>Q.sanitizeLikeString</code></li>
<li>Do not use <code>unsafe raw queries</code> without knowing what you're doing and sanitizing all user input</li>
</ul>
<h3 id="unsafe-sql-queries"><a class="header" href="#unsafe-sql-queries">Unsafe SQL queries</a></h3>
<pre><code class="language-js">const records = await database.get('comments').query(
Q.unsafeSqlQuery(`select * from comments where foo is not ? and _status is not 'deleted'`, ['bar'])
).fetch()
const recordCount = await database.get('comments').query(
Q.unsafeSqlQuery(`select count(*) as count from comments where foo is not ? and _status is not 'deleted'`, ['bar'])
).fetchCount()
</code></pre>
<p>You can also observe unsafe raw SQL queries, however, if it contains <code>JOIN</code> statements, you must explicitly specify all other tables using <code>Q.experimentalJoinTables</code> and/or <code>Q.experimentalNestedJoin</code>, like so:</p>
<pre><code class="language-js">const records = await database.get('comments').query(
Q.experimentalJoinTables(['posts']),
Q.experimentalNestedJoin('posts', 'blogs'),
Q.unsafeSqlQuery(
'select comments.* from comments ' +
'left join posts on comments.post_id is posts.id ' +
'left join blogs on posts.blog_id is blogs.id' +
'where ...',
),
).observe()
</code></pre>
<p>⚠️ Please note:</p>
<ul>
<li>Do not use this if you don't know what you're doing</li>
<li>Do not pass user input directly to avoid SQL Injection - use <code>?</code> placeholders and pass array of placeholder values</li>
<li>You must filter out deleted record using <code>where _status is not 'deleted'</code> clause</li>
<li>If you're going to fetch count of the query, use <code>count(*) as count</code> as the select result</li>
</ul>
<h3 id="unsafe-fetch-raw"><a class="header" href="#unsafe-fetch-raw">Unsafe fetch raw</a></h3>
<p>In addition to <code>.fetch()</code> and <code>.fetchIds()</code>, there is also <code>.unsafeFetchRaw()</code>. Instead of returning an array of <code>Model</code> class instances, it returns an array of raw objects.</p>
<p>You can use it as an unsafe optimization, or alongside <code>Q.unsafeSqlQuery</code>/<code>Q.unsafeLokiTransform</code> to create an advanced query that either skips fetching unnecessary columns or includes extra computed columns. For example:</p>
<pre><code class="language-js">const rawData = await database.get('posts').query(
Q.unsafeSqlQuery(
'select posts.text1, count(tag_assignments.id) as tag_count, sum(tag_assignments.rank) as tag_rank from posts' +
' left join tag_assignments on posts.id = tag_assignments.post_id' +
' group by posts.id' +
' order by posts.position desc',
)
).unsafeFetchRaw()
</code></pre>
<p>⚠️ You MUST NOT mutate returned objects. Doing so will corrupt the database.</p>
<h3 id="unsafe-sqlloki-expressions"><a class="header" href="#unsafe-sqlloki-expressions">Unsafe SQL/Loki expressions</a></h3>
<p>You can also include smaller bits of SQL and Loki expressions so that you can still use as much of Watermelon query builder as possible:</p>
<pre><code class="language-js">// SQL example:
postsCollection.query(
Q.where('is_published', true),
Q.unsafeSqlExpr('tasks.num1 not between 1 and 5'),
)
// LokiJS example:
postsCollection.query(
Q.where('is_published', true),
Q.unsafeLokiExpr({ text1: { $contains: 'hey' } })
)
</code></pre>
<p>For SQL, be sure to prefix column names with table name when joining with other tables.</p>
<p>⚠️ Please do not use this if you don't know what you're doing. Do not pass user input directly to avoid SQL injection.</p>
<h3 id="multi-table-column-comparisons-and-qunsafelokitransform"><a class="header" href="#multi-table-column-comparisons-and-qunsafelokitransform">Multi-table column comparisons and <code>Q.unsafeLokiTransform</code></a></h3>
<p>Example: we want to query comments posted more than 14 days after the post it belongs to was published.</p>
<p>There's sadly no built-in syntax for this, but can be worked around using unsafe expressions like so:</p>
<pre><code class="language-js">// SQL example:
commentsCollection.query(
Q.on('posts', 'published_at', Q.notEq(null)),
Q.unsafeSqlExpr(`comments.createad_at > posts.published_at + ${14 * 24 * 3600 * 1000}`)
)
// LokiJS example:
commentsCollection.query(
Q.on('posts', 'published_at', Q.notEq(null)),
Q.unsafeLokiTransform((rawRecords, loki) => {
return rawRecords.filter(rawRecord => {
const post = loki.getCollection('posts').by('id', rawRecord.post_id)
return post && rawRecord.created_at > post.published_at + 14 * 24 * 3600 * 1000
})
}),
)
</code></pre>
<p>For LokiJS, remember that <code>rawRecord</code> is an unsanitized, unsafe object and must not be mutated. <code>Q.unsafeLokiTransform</code> only works when using <code>LokiJSAdapter</code> with <code>useWebWorkers: false</code>. There can only be one <code>Q.unsafeLokiTransform</code> clause per query.</p>
<h3 id="null-behavior"><a class="header" href="#null-behavior"><code>null</code> behavior</a></h3>
<p>There are some gotchas you should be aware of. The <code>Q.gt</code>, <code>gte</code>, <code>lt</code>, <code>lte</code>, <code>oneOf</code>, <code>notIn</code>, <code>like</code> operators match the semantics of SQLite in terms of how they treat <code>null</code>. Those are different from JavaScript.</p>
<p><strong>Rule of thumb:</strong> No null comparisons are allowed.</p>
<p>For example, if you query <code>comments</code> for <code>Q.where('likes', Q.lt(10))</code>, a comment with 8 likes and 0 likes will be included, but a comment with <code>null</code> likes will not! In Watermelon queries, <code>null</code> is not less than any number. That's why you should avoid <a href="./Schema.html">making table columns optional</a> unless you actually need it.</p>
<p>Similarly, if you query with a column comparison, like <code>Q.where('likes', Q.gt(Q.column('dislikes')))</code>, only comments where both <code>likes</code> and <code>dislikes</code> are not null will be compared. A comment with 5 likes and <code>null</code> dislikes will NOT be included. 5 is not greater than <code>null</code> here.</p>
<p><strong><code>Q.oneOf</code> operator</strong>: It is not allowed to pass <code>null</code> as an argument to <code>Q.oneOf</code>. Instead of <code>Q.oneOf([null, 'published', 'draft'])</code> you need to explicitly allow <code>null</code> as a value like so:</p>
<pre><code class="language-js">postsCollection.query(
Q.or(
Q.where('status', Q.oneOf(['published', 'draft'])),
Q.where('status', null)
)
)
</code></pre>
<p><strong><code>Q.notIn</code> operator</strong>: If you query, say, posts with <code>Q.where('status', Q.notIn(['published', 'draft']))</code>, it will match posts with a status different than <code>published</code> or <code>draft</code>, however, it will NOT match posts with <code>status == null</code>. If you want to include such posts, query for that explicitly like with the example above.</p>
<p><strong><code>Q.weakGt</code> operator</strong>: This is weakly typed version of <code>Q.gt</code> — one that allows null comparisons. So if you query <code>comments</code> with <code>Q.where('likes', Q.weakGt(Q.column('dislikes')))</code>, it WILL match comments with 5 likes and <code>null</code> dislikes. (For <code>weakGt</code>, unlike standard operators, any number is greater than <code>null</code>).</p>
<h2 id="contributing-improvements-to-watermelon-query-language"><a class="header" href="#contributing-improvements-to-watermelon-query-language">Contributing improvements to Watermelon query language</a></h2>
<p>Here are files that are relevant. This list may look daunting, but adding new matchers is actually quite simple and multiple first-time contributors made these improvements (including like, sort, take, skip). The implementation is just split into multiple files (and their test files), but when you look at them, it'll be easy to add matchers by analogy.</p>
<p>We recommend starting from writing tests first to check expected behavior, then implement the actual behavior.</p>
<ul>
<li><code>src/QueryDescription/test.js</code> - Test clause builder (<code>Q.myThing</code>) output and test that it rejects bad/unsafe parameters</li>
<li><code>src/QueryDescription/index.js</code> - Add clause builder and type definition</li>
<li><code>src/__tests__/databaseTests.js</code> - Add test ("join" if it requires conditions on related tables; "match" otherwise) that checks that the new clause matches expected records. From this, tests running against SQLite, LokiJS, and Matcher are generated. (If one of those is not supported, add <code>skip{Loki,Sql,Count,Matcher}: true</code> to your test)</li>
<li><code>src/adapters/sqlite/encodeQuery/test.js</code> - Test that your query generates SQL you expect. (If your clause is Loki-only, test that error is thrown)</li>
<li><code>src/adapters/sqlite/encodeQuery/index.js</code> - Generate SQL</li>
<li><code>src/adapters/lokijs/worker/encodeQuery/test.js</code> - Test that your query generates the Loki query you expect (If your clause is SQLite-only, test that an error is thrown)</li>
<li><code>src/adapters/lokijs/worker/encodeQuery/index.js</code> - Generate Loki query</li>
<li><code>src/adapters/lokijs/worker/{performJoins/*.js,executeQuery.js}</code> - May be relevant for some Loki queries, but most likely you don't need to look here.</li>
<li><code>src/observation/encodeMatcher/</code> - If your query can be checked against a record in JavaScript (e.g. you're adding new "by regex" matcher), implement this behavior here (<code>index.js</code>, <code>operators.js</code>). This is used for efficient "simple observation". You don't need to write tests - <code>databaseTests</code> are used automatically. If you can't or won't implement encodeMatcher for your query, add a check to <code>canEncode.js</code> so that it returns <code>false</code> for your query (Less efficient "reloading observation" will be used then). Add your query to <code>test.js</code>'s "unencodable queries" then.</li>
</ul>
<hr />
<h2 id="next-steps-6"><a class="header" href="#next-steps-6">Next steps</a></h2>
<p>➡️ Now that you've mastered Queries, <a href="./Relation.html"><strong>make more Relations</strong></a></p>
<h1 id="relations"><a class="header" href="#relations">Relations</a></h1>
<p>A <code>Relation</code> object represents one record pointing to another — such as the author (<code>User</code>) of a <code>Comment</code>, or the <code>Post</code> the comment belongs to.</p>
<h3 id="defining-relations"><a class="header" href="#defining-relations">Defining Relations</a></h3>
<p>There's two steps to defining a relation:</p>
<ol>
<li>
<p>A <a href="./Schema.html"><strong>table column</strong></a> for the related record's ID</p>
<pre><code class="language-js">tableSchema({
name: 'comments',
columns: [
// ...
{ name: 'author_id', type: 'string' },
]
}),
</code></pre>
</li>
<li>
<p>A <code>@relation</code> field <a href="./Model.html">defined on a <code>Model</code></a> class:</p>
<pre><code class="language-js">import { relation } from '@rikishi/watermelondb/decorators'
class Comment extends Model {
// ...
@relation('users', 'author_id') author
}
</code></pre>
<p>The first argument is the <em>table name</em> of the related record, and the second is the <em>column name</em> with an ID for the related record.</p>
</li>
</ol>
<h3 id="immutablerelation"><a class="header" href="#immutablerelation">immutableRelation</a></h3>
<p>If you have a relation that cannot change (for example, a comment can't change its author), use <code>@immutableRelation</code> for extra protection and performance:</p>
<pre><code class="language-js">import { immutableRelation } from '@rikishi/watermelondb/decorators'
class Comment extends Model {
// ...
@immutableRelation('posts', 'post_id') post
@immutableRelation('users', 'author_id') author
}
</code></pre>
<h2 id="relation-api"><a class="header" href="#relation-api">Relation API</a></h2>
<p>In the example above, <code>comment.author</code> returns a <code>Relation</code> object.</p>
<blockquote>
<p>Remember, WatermelonDB is a lazily-loaded database, so you don't get the related <code>User</code> record immediately, only when you explicitly fetch it</p>
</blockquote>
<h3 id="observing"><a class="header" href="#observing">Observing</a></h3>
<p>Most of the time, you <a href="./Components.html">connect Relations to Components</a> by using <code>observe()</code> (the same <a href="./Query.html">as with Queries</a>):</p>
<pre><code class="language-js">withObservables(['comment'], ({ comment }) => ({
comment,
author: comment.author, // shortcut syntax for `author: comment.author.observe()`
}))
</code></pre>
<p>The component will now have an <code>author</code> prop containing a <code>User</code>, and will re-render both when the user changes (e.g. comment's author changes its name), but also when a new author is assigned to the comment (if that was possible).</p>
<h3 id="fetching"><a class="header" href="#fetching">Fetching</a></h3>
<p>To simply get the related record, use <code>fetch</code>. You might need it <a href="./Writers.html">in a Writer</a></p>
<pre><code class="language-js">const author = await comment.author.fetch()
// Shortcut syntax:
const author = await comment.author
</code></pre>
<p><strong>Note</strong>: If the relation column (in this example, <code>author_id</code>) is marked as <code>isOptional: true</code>, <code>fetch()</code> might return <code>null</code>.</p>
<h3 id="id"><a class="header" href="#id">ID</a></h3>
<p>If you only need the ID of a related record (e.g. to use in an URL or for the <code>key=</code> React prop), use <code>id</code>.</p>
<pre><code class="language-js">const authorId = comment.author.id
</code></pre>
<h3 id="assigning"><a class="header" href="#assigning">Assigning</a></h3>
<p>Use <code>set()</code> to assign a new record to the relation</p>
<pre><code class="language-js">await database.get('comments').create(comment => {
comment.author.set(someUser)
// ...
})
</code></pre>
<p><strong>Note</strong>: you can only do this in the <code>.create()</code> or <code>.update()</code> block.</p>
<p>You can also use <code>set id</code> if you only have the ID for the record to assign</p>
<pre><code class="language-js">await comment.update(() => {
comment.author.id = userId
})
</code></pre>
<h2 id="advanced-relations"><a class="header" href="#advanced-relations">Advanced relations</a></h2>
<h3 id="many-to-many-relation"><a class="header" href="#many-to-many-relation">Many-To-Many Relation</a></h3>
<p>If for instance, our app <code>Post</code>s can be authored by many <code>User</code>s and a user can author many <code>Post</code>s. We would create such a relation following these steps:-</p>
<ol>
<li>Create a pivot schema and model that both the <code>User</code> model and <code>Post</code> model has association to; say <code>PostAuthor</code></li>
<li>Create has_many association on both <code>User</code> and <code>Post</code> pointing to <code>PostAuthor</code> Model</li>
<li>Create belongs_to association on <code>PostAuthor</code> pointing to both <code>User</code> and <code>Post</code></li>
<li>Retrieve all <code>Posts</code> for a user by defining a query that uses the pivot <code>PostAuthor</code> to infer the <code>Post</code>s that were authored by the User.</li>
</ol>
<pre><code class="language-js">import { lazy } from '@rikishi/watermelondb/decorators'
class Post extends Model {
static table = 'posts'
static associations = {
post_authors: { type: 'has_many', foreignKey: 'post_id' },
}
@lazy
authors = this.collections
.get('users')
.query(Q.on('post_authors', 'post_id', this.id));
}
</code></pre>
<pre><code class="language-js">import { field } from '@rikishi/watermelondb/decorators'
class PostAuthor extends Model {
static table = 'post_authors'
static associations = {
posts: { type: 'belongs_to', key: 'post_id' },
users: { type: 'belongs_to', key: 'user_id' },
}
@immutableRelation('posts', 'post_id') post
@immutableRelation('users', 'user_id') user
}
</code></pre>
<pre><code class="language-js">import { lazy } from '@rikishi/watermelondb/decorators'
class User extends Model {
static table = 'users'
static associations = {
post_authors: { type: 'has_many', foreignKey: 'user_id' },
}
@lazy
posts = this.collections
.get('posts')
.query(Q.on('post_authors', 'user_id', this.id));
}
</code></pre>
<pre><code class="language-js">withObservables(['post'], ({ post }) => ({
authors: post.authors,
}))
</code></pre>
<hr />
<h2 id="next-steps-7"><a class="header" href="#next-steps-7">Next steps</a></h2>
<p>➡️ Now the last step of this guide: <a href="./Writers.html"><strong>understand Writers (and Readers)</strong></a></p>
<h1 id="writers-readers-and-batching"><a class="header" href="#writers-readers-and-batching">Writers, Readers, and batching</a></h1>
<p>Think of this guide as a part two of <a href="./CRUD.html">Create, Read, Update, Delete</a>.</p>
<p>As mentioned previously, you can't just modify WatermelonDB's database anywhere. All changes must be done within a <strong>Writer</strong>.</p>
<p>There are two ways of defining a writer: inline and by defining a <strong>writer method</strong>.</p>
<h3 id="inline-writers"><a class="header" href="#inline-writers">Inline writers</a></h3>
<p>Here is an inline writer, you can invoke it anywhere you have access to the <code>database</code> object:</p>
<pre><code class="language-js">// Note: function passed to `database.write()` MUST be asynchronous
const newPost = await database.write(async => {
const post = await database.get('posts').create(post => {
post.title = 'New post'
post.body = 'Lorem ipsum...'
})
const comment = await database.get('comments').create(comment => {
comment.post.set(post)
comment.author.id = someUserId
comment.body = 'Great post!'
})
// Note: Value returned from the wrapped function will be returned to `database.write` caller
return post
})
</code></pre>
<h3 id="writer-methods-1"><a class="header" href="#writer-methods-1">Writer methods</a></h3>
<p>Writer methods can be defined on <code>Model</code> subclasses by using the <code>@writer</code> decorator:</p>
<pre><code class="language-js">import { writer } from '@rikishi/watermelondb/decorators'
class Post extends Model {
// ...
@writer async addComment(body, author) {
const newComment = await this.collections.get('comments').create(comment => {
comment.post.set(this)
comment.author.set(author)
comment.body = body
})
return newComment
}
}
</code></pre>
<p>We highly recommend defining writer methods on <code>Models</code> to organize all code that changes the database in one place, and only use inline writers sporadically.</p>
<p>Note that this is the same as defining a simple method that wraps all work in <code>database.write()</code> - using <code>@writer</code> is simply more convenient.</p>
<p><strong>Note:</strong></p>
<ul>
<li>Always mark actions as <code>async</code> and remember to <code>await</code> on <code>.create()</code> and <code>.update()</code></li>
<li>You can use <code>this.collections</code> to access <code>Database.collections</code></li>
</ul>
<p><strong>Another example</strong>: updater action on <code>Comment</code>:</p>
<pre><code class="language-js">class Comment extends Model {
// ...
@field('is_spam') isSpam
@writer async markAsSpam() {
await this.update(comment => {
comment.isSpam = true
})
}
}
</code></pre>
<p>Now we can create a comment and immediately mark it as spam:</p>
<pre><code class="language-js">const comment = await post.addComment('Lorem ipsum', someUser)
await comment.markAsSpam()
</code></pre>
<h2 id="batch-updates"><a class="header" href="#batch-updates">Batch updates</a></h2>
<p>When you make multiple changes in a writer, it's best to <strong>batch them</strong>.</p>
<p>Batching means that the app doesn't have to go back and forth with the database (sending one command, waiting for the response, then sending another), but instead sends multiple commands in one big batch. This is faster, safer, and can avoid subtle bugs in your app</p>
<p>Take an action that changes a <code>Post</code> into spam:</p>
<pre><code class="language-js">class Post extends Model {
// ...
@writer async createSpam() {
await this.update(post => {
post.title = `7 ways to lose weight`
})
await this.collections.get('comments').create(comment => {
comment.post.set(this)
comment.body = "Don't forget to comment, like, and subscribe!"
})
}
}
</code></pre>
<p>Let's modify it to use batching:</p>
<pre><code class="language-js">class Post extends Model {
// ...
@writer async createSpam() {
await this.batch(
this.prepareUpdate(post => {
post.title = `7 ways to lose weight`
}),
this.collections.get('comments').prepareCreate(comment => {
comment.post.set(this)
comment.body = "Don't forget to comment, like, and subscribe!"
})
)
}
}
</code></pre>
<p><strong>Note</strong>:</p>
<ul>
<li>You can call <code>await this.batch</code> within <code>@writer</code> methods only. You can also call <code>database.batch()</code> within a <code>database.write()</code> block.</li>
<li>Pass the list of <strong>prepared operations</strong> as arguments:
<ul>
<li>Instead of calling <code>await record.update()</code>, pass <code>record.prepareUpdate()</code> — note lack of <code>await</code></li>
<li>Instead of <code>await collection.create()</code>, use <code>collection.prepareCreate()</code></li>
<li>Instead of <code>await record.markAsDeleted()</code>, use <code>record.prepareMarkAsDeleted()</code></li>
<li>Instead of <code>await record.destroyPermanently()</code>, use <code>record.prepareDestroyPermanently()</code></li>
<li>Advanced: you can pass <code>collection.prepareCreateFromDirtyRaw({ put your JSON here })</code></li>
<li>You can pass falsy values (null, undefined, false) to batch — they will simply be ignored.</li>
<li>You can also pass a single array argument instead of a list of arguments</li>
</ul>
</li>
</ul>
<h2 id="delete-action"><a class="header" href="#delete-action">Delete action</a></h2>
<p>When you delete, say, a <code>Post</code>, you generally want all <code>Comment</code>s that belong to it to be deleted as well.</p>
<p>To do this, override <code>markAsDeleted()</code> (or <code>destroyPermanently()</code> if you don't sync) to explicitly delete all children as well.</p>
<pre><code class="language-js">class Post extends Model {
static table = 'posts'
static associations = {
comments: { type: 'has_many', foreignKey: 'post_id' },
}
@children('comments') comments
async markAsDeleted() {
await this.comments.destroyAllPermanently()
await super.markAsDeleted()
}
}
</code></pre>
<p>Then to actually delete the post:</p>
<pre><code class="language-js">database.write(async () => {
await post.markAsDeleted()
})
</code></pre>
<p><strong>Note:</strong></p>
<ul>
<li>Use <code>Query.destroyAllPermanently()</code> on all dependent <code>@children</code> you want to delete</li>
<li>Remember to call <code>super.markAsDeleted</code> — at the end of the method!</li>
</ul>
<h2 id="advanced-why-are-readers-and-writers-necessary"><a class="header" href="#advanced-why-are-readers-and-writers-necessary">Advanced: Why are readers and writers necessary?</a></h2>
<p>WatermelonDB is highly asynchronous, which is a BIG challange in terms of achieving consistent data. Read this only if you are curious:</p>
<details>
<summary>Why are readers and writers necessary?</summary>
<p>Consider a function <code>markCommentsAsSpam</code> that fetches a list of comments on a post, and then marks them all as spam. The two operations (fetching, and then updating) are asynchronous, and some other operation that modifies the database could run in between. And it could just happen to be a function that adds a new comment on this post. Even though the function completes <em>successfully</em>, it wasn't <em>actually</em> successful at its job.</p>
<p>This example is trivial. But others may be far more dangerous. If a function fetches a record to perform an update on, this very record could be deleted midway through, making the action fail (and potentially causing the app to crash, if not handled properly). Or a function could have invariants determining whether the user is allowed to perform an action, that would be invalidated during action's execution. Or, in a collaborative app where access permissions are represented by another object, parallel execution of different actions could cause those access relations to be left in an inconsistent state.</p>
<p>The worst part is that analyzing all <em>possible</em> interactions for dangers is very hard, and having sync that runs automatically makes them very likely.</p>
<p>Solution? Group together related reads and writes together in an Writer, enforce that all writes MUST occur in a Writer, and only allow one Writer to run at the time. This way, it's guaranteed that in a Writer, you're looking at a consistent view of the world. Most simple reads are safe to do without groupping them, however if you have multiple related reads, you also need to wrap them in a Reader.</p>
</details>
<h2 id="advanced-readers"><a class="header" href="#advanced-readers">Advanced: Readers</a></h2>
<p>Readers are an advanced feature you'll rarely need.</p>
<p>Because WatermelonDB is asynchronous, if you make multiple separate queries, normally you have no guarantee that no records were created, updated, or deleted between fetching these queries.</p>
<p>Code within a Reader, however, has a guarantee that for the duration of the Reader, no changes will be made to the database (more precisely, no Writer can execute during Reader's work).</p>
<p>For example, if you were writing a custom XML data export feature for your app, you'd want the information there to be fully consistent. Therefore, you'd wrap all queries within a Reader:</p>
<pre><code class="language-js">database.read(async () => {
// no changes will happen to the database until this function exits
})
// alternatively:
class Blog extends Model {
// ...
@reader async exportBlog() {
const posts = await this.posts.fetch()
const comments = await this.allComments.fetch()
// ...
}
}
</code></pre>
<h2 id="advanced-nesting-writers-or-readers"><a class="header" href="#advanced-nesting-writers-or-readers">Advanced: nesting writers or readers</a></h2>
<p>If you try to call a Writer from another Writer, you'll notice that it won't work. This is because while a Writer is running, no other Writer can run simultaneously. To override this behavior, wrap the Writer call in <code>this.callWriter</code>:</p>
<pre><code class="language-js">class Comment extends Model {
// ...
@writer async appendToPost() {
const post = await this.post.fetch()
// `appendToBody` is an `@writer` on `Post`, so we call callWriter to allow it
await this.callWriter(() => post.appendToBody(this.body))
}
}
// alternatively:
database.write(async writer => {
const post = await database.get('posts').find('abcdef')
await writer.callWriter(() => post.appendToBody('Lorem ipsum...')) // appendToBody is a @writer
})
</code></pre>
<p>The same is true with Readers - use <code>callReader</code> to nest readers.</p>
<hr />
<h2 id="next-steps-8"><a class="header" href="#next-steps-8">Next steps</a></h2>
<p>➡️ Now that you've mastered all basics of Watermelon, go create some powerful apps — or keep reading <a href="./README.html"><strong>advanced guides</strong></a></p>
<h3 id="advanced-guides"><a class="header" href="#advanced-guides">Advanced guides</a></h3>
<p>Advanced guides for using WatermelonDB</p>
<h1 id="migrations"><a class="header" href="#migrations">Migrations</a></h1>
<p><strong>Schema migrations</strong> is the mechanism by which you can add new tables and columns to the database in a backward-compatible way.</p>
<p>Without migrations, if a user of your app upgrades from one version to another, their local database will be cleared at launch, and they will lose all their data.</p>
<p>⚠️ Always use migrations!</p>
<h2 id="migrations-setup"><a class="header" href="#migrations-setup">Migrations setup</a></h2>
<ol>
<li>
<p>Add a new file for migrations:</p>
<pre><code class="language-js">// app/model/migrations.js
import { schemaMigrations } from '@rikishi/watermelondb/Schema/migrations'
export default schemaMigrations({
migrations: [
// We'll add migration definitions here later
],
})
</code></pre>
</li>
<li>
<p>Hook up migrations to the Database adapter setup:</p>
<pre><code class="language-js">// index.js
import migrations from 'model/migrations'
const adapter = new SQLiteAdapter({
schema: mySchema,
migrations,
})
</code></pre>
</li>
</ol>
<h2 id="migrations-workflow"><a class="header" href="#migrations-workflow">Migrations workflow</a></h2>
<p>When you make schema changes when you use migrations, be sure to do this in this specific order, to minimize the likelihood of making an error.</p>
<h3 id="step-1-add-a-new-migration"><a class="header" href="#step-1-add-a-new-migration">Step 1: Add a new migration</a></h3>
<p>First, define the migration - that is, define the <strong>change</strong> that occurs between two versions of schema (such as adding a new table, or a new table column).</p>
<p>Don't change the schema file yet!</p>
<pre><code class="language-js">// app/model/migrations.js
import { schemaMigrations, createTable } from '@rikishi/watermelondb/Schema/migrations'
export default schemaMigrations({
migrations: [
{
// ⚠️ Set this to a number one larger than the current schema version
toVersion: 2,
steps: [
// See "Migrations API" for more details
createTable({
name: 'comments',
columns: [
{ name: 'post_id', type: 'string', isIndexed: true },
{ name: 'body', type: 'string' },
],
}),
],
},
],
})
</code></pre>
<p>Refresh your simulator/browser. You should see this error:</p>
<blockquote>
<p>Migrations can't be newer than schema. Schema is version 1 and migrations cover range from 1 to 2</p>
</blockquote>
<p>If so, good, move to the next step!</p>
<p>But you might also see an error like "Missing table name in schema", which means you made an error in defining migrations. See <a href="Advanced/Migrations.html#migrations-api">"Migrations API" below</a> for details.</p>
<h3 id="step-2-make-matching-changes-in-schema"><a class="header" href="#step-2-make-matching-changes-in-schema">Step 2: Make matching changes in schema</a></h3>
<p>Now it's time to make the actual changes to the schema file — add the same tables or columns as in your migration definition</p>
<p>⚠️ Please double and triple check that your changes to schema match exactly the change you defined in the migration. Otherwise you risk that the app will work when the user migrates, but will fail if it's a fresh install — or vice versa.</p>
<p>⚠️ Don't change the schema version yet</p>
<pre><code class="language-js">// model/schema.js
export default appSchema({
version: 1,
tables: [
// This is our new table!
tableSchema({
name: 'comments',
columns: [
{ name: 'post_id', type: 'string', isIndexed: true },
{ name: 'body', type: 'string' },
],
}),
// ...
]
})
</code></pre>
<p>Refresh the simulator. You should again see the same "Migrations can't be newer than schema" error. If you see a different error, you made a syntax error.</p>
<h3 id="step-3-bump-schema-version"><a class="header" href="#step-3-bump-schema-version">Step 3: Bump schema version</a></h3>
<p>Now that we made matching changes in the schema (source of truth about tables and columns) and migrations (the change in tables and columns), it's time to commit the change by bumping the version:</p>
<pre><code class="language-js">// model/schema.js
export default appSchema({
version: 2,
tables: [
// ...
]
})
</code></pre>
<p>If you refresh again, your app should show up without issues — but now you can use the new tables/columns</p>
<h3 id="step-4-test-your-migrations"><a class="header" href="#step-4-test-your-migrations">Step 4: Test your migrations</a></h3>
<p>Before shipping a new version of the app, please check that your database changes are all compatible:</p>
<ol>
<li>Migrations test: Install the previous version of your app, then update to the version you're about to ship, and make sure it still works</li>
<li>Fresh schema install test: Remove the app, and then install the <em>new</em> version of the app, and make sure it works</li>
</ol>
<h3 id="why-is-this-order-important"><a class="header" href="#why-is-this-order-important">Why is this order important</a></h3>
<p>It's simply because React Native simulator (and often React web projects) are configured to automatically refresh when you save a file. You don't want to database to accidentally migrate (upgrade) with changes that have a mistake, or changes you haven't yet completed making. By making migrations first, and bumping version last, you can double check you haven't made a mistake.</p>
<h2 id="migrations-api"><a class="header" href="#migrations-api">Migrations API</a></h2>
<p>Each migration must migrate to a version one above the previous migration, and have multiple <em>steps</em> (such as adding a new table, or new columns). Larger example:</p>
<pre><code class="language-js">schemaMigrations({
migrations: [
{
toVersion: 3,
steps: [
createTable({
name: 'comments',
columns: [
{ name: 'post_id', type: 'string', isIndexed: true },
{ name: 'body', type: 'string' },
],
}),
addColumns({
table: 'posts',
columns: [
{ name: 'subtitle', type: 'string', isOptional: true },
{ name: 'is_pinned', type: 'boolean' },
],
}),
],
},
{
toVersion: 2,
steps: [
// ...
],
},
],
})
</code></pre>
<h3 id="migration-steps"><a class="header" href="#migration-steps">Migration steps:</a></h3>
<ul>
<li><code>createTable({ name: 'table_name', columns: [ ... ] })</code> - same API as <code>tableSchema()</code></li>
<li><code>addColumns({ table: 'table_name', columns: [ ... ] })</code> - you can add one or multiple columns to an existing table. The columns table has the same format as in schema definitions</li>
<li>Other types of migrations (e.g. deleting or renaming tables and columns) are not yet implemented. See <a href="https://github.com/Nozbe/WatermelonDB/blob/master/src/Schema/migrations/index.js"><code>migrations/index.js</code></a>. Please contribute!</li>
</ul>
<h2 id="database-reseting-and-other-edge-cases"><a class="header" href="#database-reseting-and-other-edge-cases">Database reseting and other edge cases</a></h2>
<ol>
<li>When you're <strong>not</strong> using migrations, the database will reset (delete all its contents) whenever you change the schema version.</li>
<li>If the migration fails, the database will fail to initialize, and will roll back to previous version. This is unlikely, but could happen if you, for example, create a migration that tries to create the same table twice. The reason why the database will fail instead of reset is to avoid losing user data (also it's less confusing in development). You can notice the problem, fix the migration, and ship it again without data loss.</li>
<li>When database in the running app has <em>newer</em> database version than the schema version defined in code, the database will reset (clear its contents). This is useful in development</li>
<li>If there's no available migrations path (e.g. user has app with database version 4, but oldest migration is from version 10 to 11), the database will reset.</li>
</ol>
<h3 id="rolling-back-changes"><a class="header" href="#rolling-back-changes">Rolling back changes</a></h3>
<p>There's no automatic "rollback" feature in Watermelon. If you make a mistake in migrations during development, roll back in this order:</p>
<ol>
<li>Comment out any changes made to schema.js</li>
<li>Comment out any changes made to migrations.js</li>
<li>Decrement schema version number (bring back the original number)</li>
</ol>
<p>After refreshing app, the database should reset to previous state. Now you can correct your mistake and apply changes again (please do it in order described in "Migrations workflow").</p>
<h3 id="unsafe-sql-migrations"><a class="header" href="#unsafe-sql-migrations">Unsafe SQL migrations</a></h3>
<p>Similar to <a href="Advanced/../Schema.html">Schema</a>, you can add <code>unsafeSql</code> parameter to every migration step to modify or replace SQL generated by WatermelonDB to perform the migration. There is also an <code>unsafeExecuteSql('some sql;')</code> step you can use to append extra SQL. Those are ignored with LokiJSAdapter and for the purposes of <a href="Advanced/./Sync.html">migration syncs</a>.</p>
<h1 id="synchronization"><a class="header" href="#synchronization">Synchronization</a></h1>
<p>WatermelonDB has been designed from scratch to be able to seamlessly synchronize with a remote database (and, therefore, keep multiple copies of data synced with each other).</p>
<p>Note that Watermelon is only a local database — you need to <strong>bring your own backend</strong>. What Watermelon provides are:</p>
<ul>
<li><strong>Synchronization primitives</strong> — information about which records were created, updated, or deleted locally since the last sync — and which columns exactly were modified. You can build your own custom sync engine using those primitives</li>
<li><strong>Built-in sync adapter</strong> — You can use the sync engine Watermelon provides out of the box, and you only need to provide two API endpoints on your backend that conform to Watermelon sync protocol</li>
</ul>
<h2 id="using-synchronize-in-your-app"><a class="header" href="#using-synchronize-in-your-app">Using <code>synchronize()</code> in your app</a></h2>
<p>To synchronize, you need to pass <code>pullChanges</code> and <code>pushChanges</code> <em>(optional)</em> that talk to your backend and are compatible with Watermelon Sync Protocol. The frontend code will look something like this:</p>
<pre><code class="language-js">import { synchronize } from '@rikishi/watermelondb/sync'
async function mySync() {
await synchronize({
database,
pullChanges: async ({ lastPulledAt, schemaVersion, migration }) => {
const urlParams = `last_pulled_at=${lastPulledAt}&schema_version=${schemaVersion}&migration=${encodeURIComponent(JSON.stringify(migration))}`
const response = await fetch(`https://my.backend/sync?${urlParams}`)
if (!response.ok) {
throw new Error(await response.text())
}
const { changes, timestamp } = await response.json()
return { changes, timestamp }
},
pushChanges: async ({ changes, lastPulledAt }) => {
const response = await fetch(`https://my.backend/sync?last_pulled_at=${lastPulledAt}`, {
method: 'POST',
body: JSON.stringify(changes)
})
if (!response.ok) {
throw new Error(await response.text())
}
},
migrationsEnabledAtVersion: 1,
})
}
</code></pre>
<h4 id="who-calls-synchronize"><a class="header" href="#who-calls-synchronize">Who calls <code>synchronize()</code>?</a></h4>
<p>Upon looking at the example above, one question that may arise is who will call <code>synchronize()</code> -- or, in the example above <code>mySync()</code>. WatermelonDB does not manage the moment of invocation of the <code>synchronize()</code> function in any way. The database assumes every call of <code>pullChanges</code> will return <em>all</em> the changes that haven't yet been replicated (up to <code>last_pulled_at</code>). The application code is responsible for calling <code>synchronize()</code> in the frequence it deems necessary.</p>
<h3 id="troubleshooting"><a class="header" href="#troubleshooting">Troubleshooting</a></h3>
<p><strong>⚠️ Note about a React Native / UglifyES bug</strong>. When you import Watermelon Sync, your app might fail to compile in release mode. To fix this, configure Metro bundler to use Terser instead of UglifyES. Run:</p>
<pre><code class="language-bash">yarn add metro-minify-terser
</code></pre>
<p>Then, update <code>metro.config.js</code>:</p>
<pre><code class="language-js">module.exports = {
// ...
transformer: {
// ...
minifierPath: 'metro-minify-terser',
},
}
</code></pre>
<p>You might also need to switch to Terser in Webpack if you use Watermelon for web.</p>
<h3 id="implementing-pullchanges"><a class="header" href="#implementing-pullchanges">Implementing <code>pullChanges()</code></a></h3>
<p>Watermelon will call this function to ask for changes that happened on the server since the last pull.</p>
<p>Arguments:</p>
<ul>
<li><code>lastPulledAt</code> is a timestamp for the last time client pulled changes from server (or <code>null</code> if first sync)</li>
<li><code>schemaVersion</code> is the current schema version of the local database</li>
<li><code>migration</code> is an object representing schema changes since last sync (or <code>null</code> if up to date or not supported)</li>
</ul>
<p>This function should fetch from the server the list of ALL changes in all collections since <code>lastPulledAt</code>.</p>
<ol>
<li>You MUST pass an async function or return a Promise that eventually resolves or rejects</li>
<li>You MUST pass <code>lastPulledAt</code>, <code>schemaVersion</code>, and <code>migration</code> to an endpoint that conforms to Watermelon Sync Protocol</li>
<li>You MUST return a promise resolving to an object of this shape (your backend SHOULD return this shape already):
<pre><code class="language-js">{
changes: { ... }, // valid changes object
timestamp: 100000, // integer with *server's* current time
}
</code></pre>
</li>
<li>You MUST NOT store the object returned in <code>pullChanges()</code>. If you need to do any processing on it, do it before returning the object. Watermelon treats this object as "consumable" and can mutate it (for performance reasons)</li>
</ol>
<h3 id="implementing-pushchanges"><a class="header" href="#implementing-pushchanges">Implementing <code>pushChanges()</code></a></h3>
<p>Watermelon will call this function with a list of changes that happened locally since the last push so you can post it to your backend.</p>
<p>Arguments passed:</p>
<pre><code class="language-js">{
changes: { ... }, // valid changes object
lastPulledAt: 10000, // the timestamp of the last successful pull (timestamp returned in pullChanges)
}
</code></pre>
<ol>
<li>You MUST pass <code>changes</code> and <code>lastPulledAt</code> to a push sync endpoint conforming to Watermelon Sync Protocol</li>
<li>You MUST pass an async function or return a Promise from <code>pushChanges()</code></li>
<li><code>pushChanges()</code> MUST resolve after and only after the backend confirms it successfully received local changes</li>
<li><code>pushChanges()</code> MUST reject if backend failed to apply local changes</li>
<li>You MUST NOT resolve sync prematurely or in case of backend failure</li>
<li>You MUST NOT mutate or store arguments passed to <code>pushChanges()</code>. If you need to do any processing on it, do it before returning the object. Watermelon treats this object as "consumable" and can mutate it (for performance reasons)</li>
</ol>
<h3 id="checking-unsynced-changes"><a class="header" href="#checking-unsynced-changes">Checking unsynced changes</a></h3>
<p>WatermelonDB has a built in function to check whether there are any unsynced changes. The frontend code will look something like this</p>
<pre><code class="language-js">import { hasUnsyncedChanges } from '@rikishi/watermelondb/sync'
async function checkUnsyncedChanges() {
await hasUnsyncedChanges({
database
})
}
</code></pre>
<h3 id="general-information-and-tips"><a class="header" href="#general-information-and-tips">General information and tips</a></h3>
<ol>
<li>You MUST NOT connect to backend endpoints you don't control using <code>synchronize()</code>. WatermelonDB assumes pullChanges/pushChanges are friendly and correct and does not guarantee secure behavior if data returned is malformed.</li>
<li>You SHOULD NOT call <code>synchronize()</code> while synchronization is already in progress (it will safely abort)</li>
<li>You MUST NOT reset local database while synchronization is in progress (push to server will be safely aborted, but consistency of the local database may be compromised)</li>
<li>You SHOULD wrap <code>synchronize()</code> in a "retry once" block - if sync fails, try again once. This will resolve push failures due to server-side conflicts by pulling once again before pushing.</li>
<li>You can use <code>database.withChangesForTables</code> to detect when local changes occured to call sync. If you do this, you should debounce (or throttle) this signal to avoid calling <code>synchronize()</code> too often.</li>
</ol>
<h3 id="adopting-migration-syncs"><a class="header" href="#adopting-migration-syncs">Adopting Migration Syncs</a></h3>
<p>For Watermelon Sync to maintain consistency after <a href="Advanced/./Migrations.html">migrations</a>, you must support Migration Syncs (introduced in WatermelonDB v0.17). This allows Watermelon to request from backend the tables and columns it needs to have all the data.</p>
<ol>
<li>For new apps, pass <code>{migrationsEnabledAtVersion: 1}</code> to <code>synchronize()</code> (or the first schema version that shipped / the oldest schema version from which it's possible to migrate to the current version)</li>
<li>To enable migration syncs, the database MUST be configured with <a href="Advanced/./Migrations.html">migrations spec</a> (even if it's empty)</li>
<li>For existing apps, set <code>migrationsEnabledAtVersion</code> to the current schema version before making any schema changes. In other words, this version should be the last schema version BEFORE the first migration that should support migration syncs.</li>
<li>Note that for apps that shipped before WatermelonDB v0.17, it's not possible to determine what was the last schema version at which the sync happened. <code>migrationsEnabledAtVersion</code> is used as a placeholder in this case. It's not possible to guarantee that all necessary tables and columns will be requested. (If user logged in when schema version was lower than <code>migrationsEnabledAtVersion</code>, tables or columns were later added, and new records in those tables/changes in those columns occured on the server before user updated to an app version that has them, those records won't sync). To work around this, you may specify <code>migrationsEnabledAtVersion</code> to be the oldest schema version from which it's possible to migrate to the current version. However, this means that users, after updating to an app version that supports Migration Syncs, will request from the server all the records in new tables. This may be unacceptably inefficient.</li>
<li>WatermelonDB >=0.17 will note the schema version at which the user logged in, even if migrations are not enabled, so it's possible for app to request from backend changes from schema version lower than <code>migrationsEnabledAtVersion</code></li>
<li>You MUST NOT delete old <a href="Advanced/./Migrations.html">migrations</a>, otherwise it's possible that the app is permanently unable to sync.</li>
</ol>
<h3 id="adopting-turbo-login"><a class="header" href="#adopting-turbo-login">Adopting Turbo Login</a></h3>
<p>WatermelonDB v0.23 introduced an experimental optimization called "Turbo Login". Syncing using Turbo is up to 5.3x faster than the traditional method and uses a lot less memory, so it's suitable for even very large syncs. Keep in mind:</p>
<ol>
<li>This can only be used for the initial (login) sync, not for incremental syncs. It is a serious programmer error to run sync in Turbo mode if the database is not empty. Syncs with <code>deleted: []</code> fields not empty will fail.</li>
<li>This only withs with SQLiteAdapter with JSI enabled and running - it does not work on web, or if e.g. Chrome Remote Debugging is enabled</li>
<li>As of writing this, Turbo Login is considered experimental, so the exact API may change in a future version</li>
</ol>
<p>Here's basic usage:</p>
<pre><code class="language-js">const isFirstSync = ...
const useTurbo = isFirstSync
await synchronize({
database,
pullChanges: async ({ lastPulledAt, schemaVersion, migration }) => {
const response = await fetch(`https://my.backend/sync?${...}`)
if (!response.ok) {
throw new Error(await response.text())
}
if (useTurbo) {
// NOTE: DO NOT parse JSON, we want raw text
const json = await response.text()
return { syncJson: json }
} else {
const { changes, timestamp } = await response.json()
return { changes, timestamp }
}
},
unsafeTurbo: useTurbo,
// ...
})
</code></pre>
<p>Raw JSON text is required, so it is not expected that you need to do any processing in pullChanges() - doing that defeats much of the point of using Turbo Login!</p>
<p>If you're using pullChanges to send additional data to your app other than Watermelon Sync's <code>changes</code> and <code>timestamp</code>, you won't be able to process it in pullChanges. However, WatermelonDB can still pass extra keys in sync response back to the app - you can process them using <code>onDidPullChanges</code>. This works both with and without turbo mode:</p>
<pre><code class="language-js">await synchronize({
database,
pullChanges: async ({ lastPulledAt, schemaVersion, migration }) => {
// ...
},
unsafeTurbo: useTurbo,
onDidPullChanges: async ({ messages }) => {
if (messages) {
messages.forEach(message => {
alert(message)
})
}
}
// ...
})
</code></pre>
<p>There's a way to make Turbo Login even more <em>turbo</em>! However, it requires native development skills. You need to develop your own networking native code, so that raw JSON can go straight from your native code to WatermelonDB's native code - skipping JavaScript processing altogether.</p>
<pre><code class="language-js">await synchronize({
database,
pullChanges: async ({ lastPulledAt, schemaVersion, migration }) => {
// NOTE: You need the standard JS code path for incremental syncs
// Create a unique id for this sync request
const syncId = Math.floor(Math.random() * 1000000000)
await NativeModules.MyNetworkingPlugin.pullSyncChanges(
// Pass the id
syncId,
// Pass whatever information your plugin needs to make the request
lastPulledAt, schemaVersion, migration
)
// If successful, return the sync id
return { syncJsonId: syncId }
},
unsafeTurbo: true,
// ...
})
</code></pre>
<p>In native code, perform network request and if successful, extract raw response body data - <code>NSData *</code> on iOS, <code>byte[]</code> on Android. Avoid extracting the response as a string or parsing the JSON. Then pass it to WatermelonDB's native code:</p>
<pre><code class="language-java">// On Android (Java):
import com.nozbe.watermelondb.jsi.WatermelonJSI;
WatermelonJSI.provideSyncJson(/* id */ syncId, /* byte[] */ data);
</code></pre>
<pre><code class="language-objc">// On iOS (Objective-C):
extern void watermelondbProvideSyncJson(int id, NSData *json, NSError **errorPtr);
watermelondbProvideSyncJson(syncId, data, &error)
</code></pre>
<h3 id="adding-logging-to-your-sync"><a class="header" href="#adding-logging-to-your-sync">Adding logging to your sync</a></h3>
<p>You can add basic sync logs to the sync process by passing an empty object to <code>synchronize()</code>. Sync will then mutate the object, populating it with diagnostic information (start/finish time, resolved conflicts, number of remote/local changes, any errors that occured, and more):</p>
<pre><code class="language-js">// Using built-in SyncLogger
import SyncLogger from '@rikishi/watermelondb/sync/SyncLogger'
const logger = new SyncLogger(10 /* limit of sync logs to keep in memory */ )
await synchronize({ database, log: logger.newLog(), ... })
// this returns all logs (censored and safe to use in production code)
console.log(logger.logs)
// same, but pretty-formatted to a string (a user can easy copy this for diagnostic purposes)
console.log(logger.formattedLogs)
// You don't have to use SyncLogger, just pass a plain object to synchronize()
const log = {}
await synchronize({ database, log, ... })
console.log(log.startedAt)
console.log(log.finishedAt)
</code></pre>
<p>⚠️ Remember to act responsibly with logs, since they might contain your user's private information. Don't display, save, or send the log unless you censor the log.</p>
<h3 id="debugging-changes"><a class="header" href="#debugging-changes">Debugging <code>changes</code></a></h3>
<p>If you want to conveniently see incoming and outgoing changes in sync in the console, add these lines to your pullChanges/pushChanges:</p>
<p>⚠️ Leaving such logging committed and running in production is a huge security vulnerability and a performance hog.</p>
<pre><code class="language-js">// UNDER NO CIRCUMSTANCES SHOULD YOU COMMIT THESE LINES UNCOMMENTED!!!
require('@rikishi/watermelondb/sync/debugPrintChanges').default(changes, isPush)
</code></pre>
<p>Pass <code>true</code> for second parameter if you're checking outgoing changes (pushChanges), <code>false</code> otherwise. Make absolutely sure you don't commit this debug tool. For best experience, run this on web (Chrome) -- the React Native experience is not as good.</p>
<h3 id="additional-synchronize-flags"><a class="header" href="#additional-synchronize-flags">Additional <code>synchronize()</code> flags</a></h3>
<ul>
<li><code>_unsafeBatchPerCollection: boolean</code> - if true, changes will be saved to the database in multiple batches. This is unsafe and breaks transactionality, however may be required for very large syncs due to memory issues</li>
<li><code>sendCreatedAsUpdated: boolean</code> - if your backend can't differentiate between created and updated records, set this to <code>true</code> to supress warnings. Sync will still work well, however error reporting, and some edge cases will not be handled as well.</li>
<li><code>conflictResolver: (TableName, local: DirtyRaw, remote: DirtyRaw, resolved: DirtyRaw) => DirtyRaw</code> - can be passed to customize how records are updated when they change during sync. See <code>src/sync/index.js</code> for details.</li>
</ul>
<h2 id="implementing-your-sync-backend"><a class="header" href="#implementing-your-sync-backend">Implementing your Sync backend</a></h2>
<h3 id="understanding-changes-objects"><a class="header" href="#understanding-changes-objects">Understanding <code>changes</code> objects</a></h3>
<p>Synchronized changes (received by the app in <code>pullChanges</code> and sent to the backend in <code>pushChanges</code>) are represented as an object with <em>raw records</em>. Those only use raw table and column names, and raw values (strings/numbers/booleans) — the same as in <a href="Advanced/../Schema.html">Schema</a>.</p>
<p>Deleted objects are always only represented by their IDs.</p>
<p>Example:</p>
<pre><code class="language-js">{
projects: {
created: [
{ id: 'aaaa', name: 'Foo', is_favorite: true },
{ id: 'bbbb', name: 'Bar', is_favorite: false },
],
updated: [
{ id: 'ccc', name: 'Baz', is_favorite: true },
],
deleted: ['ddd'],
},
tasks: {
created: [],
updated: [
{ id: 'tttt', name: 'Buy eggs' },
],
deleted: [],
},
...
}
</code></pre>
<p>Again, notice the properties returned have the format defined in the <a href="Advanced/../Schema.html">Schema</a> (e.g. <code>is_favorite</code>, not <code>isFavorite</code>).</p>
<p>Valid changes objects MUST conform to this shape:</p>
<pre><code class="language-js">Changes = {
[table_name: string]: {
created: RawRecord[],
updated: RawRecord[],
deleted: string[],
}
}
</code></pre>
<h3 id="implementing-pull-endpoint"><a class="header" href="#implementing-pull-endpoint">Implementing pull endpoint</a></h3>
<p>Expected parameters:</p>
<pre><code class="language-js">{
lastPulledAt: Timestamp,
schemaVersion: int,
migration: null | { from: int, tables: string[], columns: { table: string, columns: string[] }[] }
}
</code></pre>
<p>Expected response:</p>
<pre><code class="language-js">{ changes: Changes, timestamp: Timestamp }
</code></pre>
<ol>
<li>The pull endpoint SHOULD take parameters and return a response matching the shape specified above.
This shape MAY be different if negotiated with the frontend (however, frontend-side <code>pullChanges()</code> MUST conform to this)</li>
<li>The pull endpoint MUST return all record changes in all collections since <code>lastPulledAt</code>, specifically:
<ul>
<li>all records that were created on the server since <code>lastPulledAt</code></li>
<li>all records that were updated on the server since <code>lastPulledAt</code></li>
<li>IDs of all records that were deleted on the server since <code>lastPulledAt</code></li>
<li>record IDs MUST NOT be duplicated</li>
</ul>
</li>
<li>If <code>lastPulledAt</code> is null or 0, you MUST return all accessible records (first sync)</li>
<li>The timestamp returned by the server MUST be a value that, if passed again to <code>pullChanges()</code> as <code>lastPulledAt</code>, will return all changes that happened since this moment.</li>
<li>The pull endpoint MUST provide a consistent view of changes since <code>lastPulledAt</code>
<ul>
<li>You should perform all queries synchronously or in a write lock to ensure that returned changes are consistent</li>
<li>You should also mark the current server time synchronously with the queries</li>
<li>This is to ensure that no changes are made to the database while you're fetching changes (otherwise some records would never be returned in a pull query)</li>
<li>If it's absolutely not possible to do so, and you have to query each collection separately, be sure to return a <code>lastPulledAt</code> timestamp marked BEFORE querying starts. You still risk inconsistent responses (that may break app's consistency assumptions), but the next pull will fetch whatever changes occured during previous pull.</li>
<li>An alternative solution is to check for the newest change before and after all queries are made, and if there's been a change during the pull, return an error code, or retry.</li>
</ul>
</li>
<li>If <code>migration</code> is not null, you MUST include records needed to get a consistent view after a local database migration
<ul>
<li>Specifically, you MUST include all records in tables that were added to the local database between the last user sync and <code>schemaVersion</code></li>
<li>For all columns that were added to the local app database between the last sync and <code>schemaVersion</code>, you MUST include all records for which the added column has a value other than the default value (<code>0</code>, <code>''</code>, <code>false</code>, or <code>null</code> depending on column type and nullability)</li>
<li>You can determine what schema changes were made to the local app in two ways:
<ul>
<li>You can compare <code>migration.from</code> (local schema version at the time of the last sync) and <code>schemaVersion</code> (current local schema version). This requires you to negotiate with the frontend what schema changes are made at which schema versions, but gives you more control</li>
<li>Or you can ignore <code>migration.from</code> and only look at <code>migration.tables</code> (which indicates which tables were added to the local database since the last sync) and <code>migration.columns</code> (which indicates which columns were added to the local database to which tables since last sync).</li>
<li>If you use <code>migration.tables</code> and <code>migration.columns</code>, you MUST whitelist values a client can request. Take care not to leak any internal fields to the client.</li>
</ul>
</li>
</ul>
</li>
<li>Returned raw records MUST match your app's <a href="Advanced/../Schema.html">Schema</a></li>
<li>Returned raw records MUST NOT not contain special <code>_status</code>, <code>_changed</code> fields.</li>
<li>Returned raw records MAY contain fields (columns) that are not yet present in the local app (at <code>schemaVersion</code> -- but added in a later version). They will be safely ignored.</li>
<li>Returned raw records MUST NOT contain arbitrary column names, as they may be unsafe (e.g. <code>__proto__</code> or <code>constructor</code>). You should whitelist acceptable column names.</li>
<li>Returned record IDs MUST only contain safe characters
<ul>
<li>Default WatermelonDB IDs conform to <code>/^[a-zA-Z0-9]{16}$/</code></li>
<li><code>_-.</code> are also allowed if you override default ID generator, but <code>'"\/$</code> are unsafe</li>
</ul>
</li>
<li>Changes SHOULD NOT contain collections that are not yet present in the local app (at <code>schemaVersion</code>). They will, however, be safely ignored.
<ul>
<li>NOTE: This is true for WatermelonDB v0.17 and above. If you support clients using earlier versions, you MUST NOT return collections not known by them.</li>
</ul>
</li>
<li>Changes MUST NOT contain collections with arbitrary names, as they may be unsafe. You should whitelist acceptable collection names.</li>
</ol>
<h3 id="implementing-push-endpoint"><a class="header" href="#implementing-push-endpoint">Implementing push endpoint</a></h3>
<ol>
<li>The push endpoint MUST apply local changes (passed as a <code>changes</code> object) to the database. Specifically:
<ul>
<li>create new records as specified by the changes object</li>
<li>update existing records as specified by the changes object</li>
<li>delete records by the specified IDs</li>
</ul>
</li>
<li>If the <code>changes</code> object contains a new record with an ID that already exists, you MUST update it, and MUST NOT return an error code.
<ul>
<li>(This happens if previous push succeeded on the backend, but not on frontend)</li>
</ul>
</li>
<li>If the <code>changes</code> object contains an update to a record that does not exist, then:
<ul>
<li>If you can determine that this record no longer exists because it was deleted, you SHOULD return an error code (to force frontend to pull the information about this deleted ID)</li>
<li>Otherwise, you MUST create it, and MUST NOT return an error code. (This scenario should not happen, but in case of frontend or backend bugs, it would keep sync from ever succeeding.)</li>
</ul>
</li>
<li>If the <code>changes</code> object contains a record to delete that doesn't exist, you MUST ignore it and MUST NOT return an error code
<ul>
<li>(This may happen if previous push succeeded on the backend, but not on frontend, or if another user deleted this record in between user's pull and push calls)</li>
</ul>
</li>
<li>If the <code>changes</code> object contains a record that has been modified on the server after <code>lastPulledAt</code>, you MUST abort push and return an error code
<ul>
<li>This scenario means that there's a conflict, and record was updated remotely between user's pull and push calls. Returning an error forces frontend to call pull endpoint again to resolve the conflict</li>
</ul>
</li>
<li>If application of all local changes succeeds, the endpoint MUST return a success status code.</li>
<li>The push endpoint MUST be fully transactional. If there is an error, all local changes MUST be reverted on the server, and en error code MUST be returned.</li>
<li>You MUST ignore <code>_status</code> and <code>_changed</code> fields contained in records in <code>changes</code> object</li>
<li>You SHOULD validate data passed to the endpoint. In particular, collection and column names ought to be whitelisted, as well as ID format — and of course any application-specific invariants, such as permissions to access and modify records</li>
<li>You SHOULD sanitize record fields passed to the endpoint. If there's something slightly wrong with the contents (but not shape) of the data (e.g. <code>user.role</code> should be <code>owner</code>, <code>admin</code>, or <code>member</code>, but user sent empty string or <code>abcdef</code>), you SHOULD NOT send an error code. Instead, prefer to "fix" errors (sanitize to correct format).
<ul>
<li>Rationale: Synchronization should be reliable, and should not fail other than transiently, or for serious programming errors. Otherwise, the user will have a permanently unsyncable app, and may have to log out/delete it and lose unsynced data. You don't want a bug 5 versions ago to create a persistently failing sync.</li>
</ul>
</li>
<li>You SHOULD delete all descendants of deleted records
<ul>
<li>Frontend should ask the push endpoint to do so as well, but if it's buggy, you may end up with permanent orphans</li>
</ul>
</li>
</ol>
<h3 id="tips-on-implementing-server-side-changes-tracking"><a class="header" href="#tips-on-implementing-server-side-changes-tracking">Tips on implementing server-side changes tracking</a></h3>
<p>If you're wondering how to <em>actually</em> implement consistent pulling of all changes since the last pull, or how to detect that a record being pushed by the user changed after <code>lastPulledAt</code>, here's what we recommend:</p>
<ul>
<li>Add a <code>last_modified</code> field to all your server database tables, and bump it to <code>NOW()</code> every time you create or update a record.</li>
<li>This way, when you want to get all changes since <code>lastPulledAt</code>, you query records whose <code>last_modified > lastPulledAt</code>.</li>
<li>The timestamp should be at least millisecond resolution, and you should add (for extra safety) a MySQL/PostgreSQL procedure that will ensure <code>last_modified</code> uniqueness and monotonicity
<ul>
<li>Specificaly, check that there is no record with a <code>last_modified</code> equal to or greater than <code>NOW()</code>, and if there is, increment the new timestamp by 1 (or however much you need to ensure it's the greatest number)</li>
<li><a href="https://github.com/Kinto/kinto/blob/814c30c5dd745717b8ea50d708d9163a38d2a9ec/kinto/core/storage/postgresql/schema.sql#L64-L116">An example of this for PostgreSQL can be found in Kinto</a></li>
<li>This protects against weird edge cases - such as records being lost due to server clock time changes (NTP time sync, leap seconds, etc.)</li>
</ul>
</li>
<li>Of course, remember to ignore <code>last_modified</code> from the user if you do it this way.</li>
<li>An alternative to using timestamps is to use an auto-incrementing counter sequence, but you must ensure that this sequence is consistent across all collections. You also leak to users the amount of traffic to your sync server (number of changes in the sequence)</li>
<li>To distinguish between <code>created</code> and <code>updated</code> records, you can also store server-side <code>server_created_at</code> timestamp (if it's greater than <code>last_pulled_at</code> supplied to sync, then record is to be <code>created</code> on client, if less than — client already has it and it is to be <code>updated</code> on client). Note that this timestamp must be consistent with last_modified — and you must not use client-created <code>created_at</code> field, since you can never trust local timestamps.
<ul>
<li>Alternatively, you can send all non-deleted records as all <code>updated</code> and Watermelon will do the right thing in 99% of cases (you will be slightly less protected against weird edge cases — treatment of locally deleted records is different). If you do this, pass <code>sendCreatedAsUpdated: true</code> to <code>synchronize()</code> to supress warnings about records to be updated not existing locally.</li>
</ul>
</li>
<li>You do need to implement a mechanism to track when records were deleted on the server, otherwise you wouldn't know to push them
<ul>
<li>One possible implementation is to not fully delete records, but mark them as DELETED=true</li>
<li>Or, you can have a <code>deleted_xxx</code> table with just the record ID and timestamp (consistent with last_modified)</li>
<li>Or, you can treat it the same way as "revoked permissions"</li>
</ul>
</li>
<li>If you have a collaborative app with any sort of permissions, you also need to track granting and revoking of permissions the same way as changes to records
<ul>
<li>If permission to access records has been granted, the pull endpoint must add those records to <code>created</code></li>
<li>If permission to access records has been revoked, the pull endpoint must add those records to <code>deleted</code></li>
<li>Remember to also return all descendants of a record in those cases</li>
</ul>
</li>
</ul>
<h2 id="local-vs-remote-ids"><a class="header" href="#local-vs-remote-ids">Local vs Remote IDs</a></h2>
<p>WatermelonDB has been designed with the assumption that there is no difference between Local IDs (IDs of records and their relations in a WatermelonDB database) and Remote IDs (IDs on the backend server). So a local app can create new records, generating their IDs, and the backend server will use this ID as the true ID. This greatly simplifies synchronization, as you don't have to replace local with remote IDs on the record and all records that point to it.</p>
<p>We highly recommend that you adopt this practice.</p>
<p>Some people are skeptical about this approach due to conflicts, since backend can guarantee unique IDs, and the local app can't. However, in practice, a standard Watermelon ID has 8,000,000,000,000,000,000,000,000 possible combinations. That's enough entropy to make conflicts extremely unlikely. At <a href="https://nozbe.com">Nozbe</a>, we've done it this way at scale for more than a decade, and not once did we encounter a genuine ID conflict or had other issues due to this approach.</p>
<blockquote>
<p>Using the birthday problem, we can calculate that for 36^16 possible IDs, if your system grows to a billion records, the probability of a single conflict is 6e-8. At 100B records, the probability grows to 0.06%. But if you grow to that many records, you're probably a very rich company and can start worrying about things like this <em>then</em>.</p>
</blockquote>
<p>If you absolutely can't adopt this practice, there's a number of production apps using WatermelonDB that keep local and remote IDs separate — however, more work is required this way. Search Issues to find discussions about this topic — and consider contributing to WatermelonDB to make managing separate local IDs easier for everyone!</p>
<h2 id="existing-backend-implementations-for-watermelondb"><a class="header" href="#existing-backend-implementations-for-watermelondb">Existing backend implementations for WatermelonDB</a></h2>
<p>Note that those are not maintained by WatermelonDB, and we make no endorsements about quality of these projects:</p>
<ul>
<li><a href="https://fahri.id/posts/how-to-build-watermelondb-sync-backend-in-elixir/">How to Build WatermelonDB Sync Backend in Elixir</a></li>
<li><a href="https://github.com/AliAllaf/firemelon">Firemelon</a></li>
<li><a href="https://github.com/nathanheffley/laravel-watermelon">Laravel Watermelon</a></li>
<li>Did you make one? Please contribute a link!</li>
</ul>
<h2 id="current-sync-limitations"><a class="header" href="#current-sync-limitations">Current Sync limitations</a></h2>
<ol>
<li>If a record being pushed changes between pull and push, push will just fail. It would be better if it failed with a list of conflicts, so that <code>synchronize()</code> can automatically respond. Alternatively, sync could only send changed fields and server could automatically always just apply those changed fields to the server version (since that's what per-column client-wins resolver will do anyway)</li>
<li>During next sync pull, changes we've just pushed will be pulled again, which is unnecessary. It would be better if server, during push, also pulled local changes since <code>lastPulledAt</code> and responded with NEW timestamp to be treated as <code>lastPulledAt</code>.</li>
<li>It shouldn't be necessary to push the whole updated record — just changed fields + ID should be enough</li>
</ol>
<blockquote>
<p>Note: That might conflict with "If client wants to update a record that doesn’t exist, create it"</p>
</blockquote>
<p>You don't like these limitations? Good, neither do we! Please contribute - we'll give you guidance.</p>
<h2 id="contributing-1"><a class="header" href="#contributing-1">Contributing</a></h2>
<ol>
<li>If you implement Watermelon sync but found this guide confusing, please contribute improvements!</li>
<li>Please help out with solving the current limitations!</li>
<li>If you write server-side code made to be compatible with Watermelon, especially for popular platforms (Node, Ruby on Rails, Kinto, etc.) - please open source it and let us know! This would dramatically simplify implementing sync for people</li>
<li>If you find Watermelon sync bugs, please report the issue! And if possible, write regression tests to make sure it never happens again</li>
</ol>
<h2 id="sync-primitives-and-implementing-your-own-sync-entirely-from-scratch"><a class="header" href="#sync-primitives-and-implementing-your-own-sync-entirely-from-scratch">Sync primitives and implementing your own sync entirely from scratch</a></h2>
<p>See: <a href="Advanced/../Implementation/SyncImpl.html">Sync implementation details</a></p>
<h1 id="createupdate-tracking"><a class="header" href="#createupdate-tracking">Create/Update tracking</a></h1>
<p>You can add per-table support for create/update tracking. When you do this, the Model will have information about when it was created, and when it was last updated.</p>
<p>:warning: <strong>Note:</strong> WatermelonDB automatically sets and persists the <code>created_at</code>/<code>updated_at</code> fields if they are present as <em>millisecond</em> epoch's. If you intend to interact with these properties in any way you should always treat them as such.</p>
<h3 id="when-to-use-this"><a class="header" href="#when-to-use-this">When to use this</a></h3>
<p><strong>Use create tracking</strong>:</p>
<ul>
<li>When you display to the user when a thing (e.g. a Post, Comment, Task) was created</li>
<li>If you sort created items chronologically (Note that Record IDs are random strings, not auto-incrementing integers, so you need create tracking to sort chronologically)</li>
</ul>
<p><strong>Use update tracking</strong>:</p>
<ul>
<li>When you display to the user when a thing (e.g. a Post) was modified</li>
</ul>
<p><strong>Notes</strong>:</p>
<ul>
<li>you <em>don't have to</em> enable both create and update tracking. You can do either, both, or none.</li>
<li>In your model, these fields need to be called createdAt and updatedAt respectively.</li>
</ul>
<h3 id="how-to-do-this"><a class="header" href="#how-to-do-this">How to do this</a></h3>
<p><strong>Step 1:</strong> Add to the <a href="Advanced/../Schema.html">schema</a>:</p>
<pre><code class="language-js">tableSchema({
name: 'posts',
columns: [
// other columns
{ name: 'created_at', type: 'number' },
{ name: 'updated_at', type: 'number' },
]
}),
</code></pre>
<p><strong>Step 2:</strong> Add this to the Model definition:</p>
<pre><code class="language-js">import { date, readonly } from '@rikishi/watermelondb/decorators'
class Post extends Model {
// ...
@readonly @date('created_at') createdAt
@readonly @date('updated_at') updatedAt
}
</code></pre>
<p>Again, you can add just <code>created_at</code> column and field if you don't need update tracking.</p>
<h3 id="how-this-behaves"><a class="header" href="#how-this-behaves">How this behaves</a></h3>
<p>If you have the magic <code>createdAt</code> field defined on the Model, the current timestamp will be set when you first call <code>collection.create()</code> or <code>collection.prepareCreate()</code>. It will never be modified again.</p>
<p>If the magic <code>updatedAt</code> field is also defined, then after creation, <code>model.updatedAt</code> will have the same value as <code>model.createdAt</code>. Then every time you call <code>model.update()</code> or <code>model.prepareUpdate()</code>, <code>updatedAt</code> will be changed to the current timestamp.</p>
<h1 id="advanced-fields-1"><a class="header" href="#advanced-fields-1">Advanced Fields</a></h1>
<h2 id="json"><a class="header" href="#json"><code>@json</code></a></h2>
<p>If you have a lot of metadata about a record (say, an object with many keys, or an array of values), you can use a <code>@json</code> field to contain that information in a single string column (serialized to JSON) instead of adding multiple columns or a relation to another table.</p>
<p>⚠️ This is an advanced feature that comes with downsides — make sure you really need it</p>
<p>First, add a string column to <a href="Advanced/../Schema.html">the schema</a>:</p>
<pre><code class="language-js">tableSchema({
name: 'comments',
columns: [
{ name: 'reactions', type: 'string' }, // You can add isOptional: true, if appropriate
],
})
</code></pre>
<p>Then in the Model definition:</p>
<pre><code class="language-js">import { json } from '@rikishi/watermelondb/decorators'
class Comment extends Model {
// ...
@json('reactions', sanitizeReactions) reactions
}
</code></pre>
<p>Now you can set complex JSON values to a field:</p>
<pre><code class="language-js">comment.update(() => {
comment.reactions = ['up', 'down', 'down']
})
</code></pre>
<p>As the second argument, pass a <strong>sanitizer function</strong>. This is a function that receives whatever <code>JSON.parse()</code> returns for the serialized JSON, and returns whatever type you expect in your app. In other words, it turns raw, dirty, untrusted data (that might be missing, or malformed by a bug in previous version of the app) into trusted format.</p>
<p>The sanitizer might also receive <code>null</code> if the column is nullable, or <code>undefined</code> if the field doesn't contain valid JSON.</p>
<p>For example, if you need the field to be an array of strings, you can ensure it like so:</p>
<pre><code class="language-js">const sanitizeReactions = rawReactions => {
return Array.isArray(rawReactions) ? rawReactions.map(String) : []
}
</code></pre>
<p>If you don't want to sanitize JSON, pass an identity function:</p>
<pre><code class="language-js">const sanitizeReactions = json => json
</code></pre>
<p>The sanitizer function takes an optional second argument, which is a reference to the model. This is useful is your sanitization logic depends on the other fields in the model.</p>
<p><strong>Warning about JSON fields</strong>:</p>
<p>JSON fields go against relational, lazy nature of Watermelon, because <strong>you can't query or count by the contents of JSON fields</strong>. If you need or might need in the future to query records by some piece of data, don't use JSON.</p>
<p>Only use JSON fields when you need the flexibility of complex freeform data, or the speed of having metadata without querying another table, and you are sure that you won't need to query by those metadata.</p>
<h2 id="nochange"><a class="header" href="#nochange"><code>@nochange</code></a></h2>
<p>For extra protection, you can mark fields as <code>@nochange</code> to ensure they can't be modified. Always put <code>@nochange</code> before <code>@field</code> / <code>@date</code> / <code>@text</code></p>
<pre><code class="language-js">import { field, nochange } from '@rikishi/watermelondb/decorators'
class User extends Model {
// ...
@nochange @field('is_owner') isOwner
}
</code></pre>
<p><code>user.isOwner</code> can only be set in the <code>collection.create()</code> block, but will throw an error if you try to set a new value in <code>user.update()</code> block.</p>
<h3 id="readonly"><a class="header" href="#readonly"><code>@readonly</code></a></h3>
<p>Similar to <code>@nochange</code>, you can use the <code>@readonly</code> decorator to ensure a field cannot be set at all. Use this for <a href="Advanced/./CreateUpdateTracking.html">create/update tracking</a>, but it might also be useful if you use Watermelon with a <a href="Advanced/../Advanced/Sync.html">Sync engine</a> and a field can only be set by the server.</p>
<h2 id="custom-observable-fields"><a class="header" href="#custom-observable-fields">Custom observable fields</a></h2>
<p>You're in advanced <a href="https://github.com/ReactiveX/rxjs">RxJS</a> territory now! You have been warned.</p>
<p>Say, you have a Post model that has many Comments. And a Post is considered to be "popular" if it has more than 10 comments.</p>
<p>You can add a "popular" badge to a Post component in two ways.</p>
<p>One is to simply observe how many comments there are <a href="Advanced/../Components.html">in the component</a>:</p>
<pre><code class="language-js">const enhance = withObservables(['post'], ({ post }) => ({
post: post.observe(),
commentCount: post.comments.observeCount()
}))
</code></pre>
<p>And in the <code>render</code> method, if <code>props.commentCount > 10</code>, show the badge.</p>
<p>Another way is to define an observable property on the Model layer, like so:</p>
<pre><code class="language-js">import { distinctUntilChanged, map as map$ } from 'rxjs/operators'
import { lazy } from '@rikishi/watermelondb/decorators'
class Post extends Model {
@lazy isPopular = this.comments.observeCount().pipe(
map$(comments => comments > 10),
distinctUntilChanged()
)
}
</code></pre>
<p>And then you can directly connect this to the component:</p>
<pre><code class="language-js">const enhance = withObservables(['post'], ({ post }) => ({
isPopular: post.isPopular,
}))
</code></pre>
<p><code>props.isPopular</code> will reflect whether or not the Post is popular. Note that this is fully observable, i.e. if the number of comments rises above/falls below the popularity threshold, the component will re-render. Let's break it down:</p>
<ul>
<li><code>this.comments.observeCount()</code> - take the Observable number of comments</li>
<li><code>map$(comments => comments > 10)</code> - transform this into an Observable of boolean (popular or not)</li>
<li><code>distinctUntilChanged()</code> - this is so that if the comment count changes, but the popularity doesn't (it's still below/above 10), components won't be unnecessarily re-rendered</li>
<li><code>@lazy</code> - also for performance (we only define this Observable once, so we can re-use it for free)</li>
</ul>
<p>Let's make this example more complicated. Say the post is <strong>always</strong> popular if it's marked as starred. So if <code>post.isStarred</code>, then we don't have to do unnecessary work of fetching comment count:</p>
<pre><code class="language-js">import { of as of$ } from 'rxjs/observable/of'
import { distinctUntilChanged, map as map$ } from 'rxjs/operators'
import { lazy } from '@rikishi/watermelondb/decorators'
class Post extends Model {
@lazy isPopular = this.observe().pipe(
distinctUntilKeyChanged('isStarred'),
switchMap(post =>
post.isStarred ?
of$(true) :
this.comments.observeCount().pipe(map$(comments => comments > 10))
),
distinctUntilChanged(),
)
}
</code></pre>
<ul>
<li><code>this.observe()</code> - if the Post changes, it might change its popularity status, so we observe it</li>
<li><code>this.comments.observeCount().pipe(map$(comments => comments > 10))</code> - this part is the same, but we only observe it if the post is starred</li>
<li><code>switchMap(post => post.isStarred ? of$(true) : ...)</code> - if the post is starred, we just return an Observable that emits <code>true</code> and never changes.</li>
<li><code>distinctUntilKeyChanged('isStarred')</code> - for performance, so that we don't re-subscribe to comment count Observable if the post changes (only if the <code>isStarred</code> field changes)</li>
<li><code>distinctUntilChanged()</code> - again, don't emit new values, if popularity doesn't change</li>
</ul>
<h1 id="watermelon--flow"><a class="header" href="#watermelon--flow">Watermelon ❤️ Flow</a></h1>
<p>Watermelon was developed with <a href="https://flow.org">Flow</a> in mind.</p>
<p>If you're a Flow user yourself (and we highly recommend it!), here's some things you need to keep in mind:</p>
<h2 id="setup"><a class="header" href="#setup">Setup</a></h2>
<p>Add this to your <code>.flowconfig</code> file so that Flow can see Watermelon's types.</p>
<pre><code class="language-ini">[options]
module.name_mapper='^@rikishi/watermelondb\(.*\)$' -> '<PROJECT_ROOT>/node_modules/@rikishi/watermelondb/src\1'
</code></pre>
<p>Note that this won't work if you put the entire <code>node_modules/</code> folder under the <code>[ignore]</code> section. In that case, change it to only ignore the specific node modules that throw errors in your app, so that Flow can scan Watermelon files.</p>
<h2 id="tables-and-columns"><a class="header" href="#tables-and-columns">Tables and columns</a></h2>
<p>Table and column names are <strong>opaque types</strong> in Flow.</p>
<p>So if you try to use simple strings, like so:</p>
<pre><code class="language-js">class Comment extends Model {
static table = 'comments'
@text('body') body
}
</code></pre>
<p>You'll get errors, because you're passing <code>'comments'</code> (a <code>string</code>) where <code>TableName<Comment></code> is expected, and <code>'body'</code> (again, a <code>string</code>) where <code>ColumnName</code> is expected.</p>
<p>When using Watermelon with Flow, you must pre-define all your table and column names in one place, then only use those symbols (and not strings) in all other places.</p>
<p>We recommend defining symbols like this:</p>
<pre><code class="language-js">// File: model/schema.js
// @flow
import { tableName, columnName, type TableName, appSchema, tableSchema } from '@rikishi/watermelondb'
import type Comment from './Comment.js'
export const Tables = {
comments: (tableName('comments'): TableName<Comment>),
// ...
}
export const Columns = {
comments: {
body: columnName('body'),
// ...
}
}
export const appSchema = appSchema({
version: 1,
tables: [
tableSchema({
name: Tables.comments,
columns: [
{ name: Columns.comments.body, type: 'string' },
],
}),
// ...
]
})
</code></pre>
<p>And then using them like so:</p>
<pre><code class="language-js">// File: model/Comment.js
// @flow
import { Model } from '@rikishi/watermelondb'
import { text } from '@rikishi/watermelondb/decorators'
import { Tables, Columns } from './schema.js'
const Column = Columns.comments
export default class Comment extends Model {
static table = Tables.comments
@text(Column.body) body: string
}
</code></pre>
<h3 id="but-isnt-that-a-lot-of-boilerplate"><a class="header" href="#but-isnt-that-a-lot-of-boilerplate">But isn't that a lot of boilerplate?</a></h3>
<p>Yes, it looks more boilerplate'y than the non-Flow examples, however:</p>
<ul>
<li>you're protected from typos — strings are defined once</li>
<li>easier refactoring — you only change column name in one place</li>
<li>no orphan columns or tables — no way to accidentally refer to a column or table that was removed from the schema</li>
<li><code>TableName</code> is typed with the model class it refers to, which allows Flow to find other mistakes in your code</li>
</ul>
<p>In general, we find that untyped string constants lead to bugs, and defining typed constants is a good practice.</p>
<h3 id="associations-1"><a class="header" href="#associations-1">associations</a></h3>
<p>When using Flow, you define model associations like this:</p>
<pre><code class="language-js">import { Model, associations } from '@rikishi/watermelondb'
import { Tables, Columns } from './schema.js'
const Column = Columns.posts
class Post extends Model {
static table = Tables.posts
static associations = associations(
[Tables.comments, { type: 'has_many', foreignKey: Columns.comments.postId }],
[Tables.users, { type: 'belongs_to', key: Column.authorId }],
)
}
</code></pre>
<h2 id="common-types"><a class="header" href="#common-types">Common types</a></h2>
<p>Many types are tagged with the model class the type refers to:</p>
<pre><code class="language-js">TableName<Post> // a table name referring to posts
Collection<Post> // the Collection for posts
Relation<Comment> // a relation that can fetch a Comment
Relation<?Comment> // a relation that can fetch a Comment or `null`
Query<Comment> // a query that can fetch many Comments
</code></pre>
<p>Always mark the type of model fields. Remember to include <code>?</code> if the underlying table column is optional. Flow can't check if model fields match the schema or if they match the decorator's signature.</p>
<pre><code class="language-js">@text(Column.body) body: string
@date(Column.createdAt) createdAt: Date
@date(Column.archivedAt) archivedAt: ?Date
</code></pre>
<p>If you need to refer to an ID of a record, always use the <code>RecordId</code> type alias, not <code>string</code> (they're the same, but the former is self-documenting).</p>
<p>If you ever access the record's raw data (DON'T do that unless you <em>really</em> know what you're doing), use <code>DirtyRaw</code> to refer to raw data from external sources (database, server), and <code>RawRecord</code> after it was passed through <code>sanitizedRaw</code>.</p>
<h1 id="local-storage"><a class="header" href="#local-storage">Local storage</a></h1>
<p>WatermelonDB has a simple key/value store, similar to <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage">localStorage</a>:</p>
<pre><code class="language-js">// setting a value
await database.localStorage.set("user_id", "abcdef")
// retrieving a value
const userId = await database.localStorage.get("user_id") // string or undefined if no value for this key
// removing a value
await database.localStorage.remove("user_id")
</code></pre>
<p><strong>When to use it</strong>. For things like the ID of the logged-in user, or the route to the last-viewed screen in the app. You should generally avoid it and stick to standard Watermelon records.</p>
<p><strong>This is a low-level API</strong>. You can't do things like observe changes of a value over time. If you need that, just use standard WatermelonDB records. You can only store JSON-serializable values</p>
<p><strong>What to be aware of</strong>. DO NOT let the local storage key be a user-supplied value. Only allow predefined/whitelisted keys. Key names starting with <code>__</code> are reserved for WatermelonDB use (e.g. used by Sync to remember time of last sync)</p>
<p><strong>Why not use localStorage/AsyncStorage?</strong> Because this way, you have only one source of truth — one database that, say, stores the logged-in user ID and the information about all users. So there's a lower risk that the two sets of values get out of sync.</p>
<h1 id="various-pro-tips"><a class="header" href="#various-pro-tips">Various Pro Tips</a></h1>
<h2 id="database-viewer"><a class="header" href="#database-viewer">Database viewer</a></h2>
<p><a href="https://github.com/Nozbe/WatermelonDB/issues/710">See discussion</a></p>
<p><strong>Android</strong> - you can use the new <a href="https://medium.com/androiddevelopers/database-inspector-9e91aa265316">App Inspector</a> in modern versions of Android Studio.</p>
<p><strong>Via Flipper</strong> You can also use Facebook Flipper <a href="https://github.com/panz3r/react-native-flipper-databases#readme">with a plugin</a>. See <a href="https://github.com/Nozbe/WatermelonDB/issues/653">discussion</a>.</p>
<p><strong>iOS</strong> - check open database path in iOS System Log (via Console for plugged-in device, or Xcode logs, or <a href="https://github.com/Nozbe/WatermelonDB/issues/710#issuecomment-776255654">by using <code>find</code></a>), then open it via <code>sqlite3</code> in the console, or an external tool like <a href="https://sqlitebrowser.org">sqlitebrowser</a></p>
<h2 id="prepopulating-database-on-native"><a class="header" href="#prepopulating-database-on-native">Prepopulating database on native</a></h2>
<p>There's no built-in support for this. One way is to generate a SQLite DB (you can use the the Node SQLite support in 0.19.0-2 pre-release or extract it from an ios/android app), bundle it with the app, and then use a bit of code to check if the DB you're expecting it available, and if not, making a copy of the default DB — before you attempt loading DB from JS side. <a href="https://github.com/Nozbe/WatermelonDB/issues/774#issuecomment-667981361">See discussion</a></p>
<h1 id="performance"><a class="header" href="#performance">Performance</a></h1>
<p>Performance tips — TODO</p>
<h1 id="ios---sharing-database-across-targets"><a class="header" href="#ios---sharing-database-across-targets">iOS - Sharing database across targets</a></h1>
<p>In case you have multiple Xcode targets and want to share your WatermelonDB instance across them, there are 2 options to be followed: via JS or via native Swift / Objective-C.</p>
<h3 id="when-to-use-this-1"><a class="header" href="#when-to-use-this-1">When to use this</a></h3>
<p>When you want to access the same database data in 2 or more Xcode targets (Notification Service Extension, Share Extension, iMessage stickers, etc).</p>
<h3 id="how-to-do-this-1"><a class="header" href="#how-to-do-this-1">How to do this</a></h3>
<p><strong>Step 1:</strong> Setting up an App Group</p>
<p>Through Xcode, repeat this process for your <strong>main target</strong> and <strong>every other target</strong> that you want to share the database with:</p>
<ul>
<li>Click on target name</li>
<li>Click <strong>Signing and Capabilities</strong></li>
<li>Click <strong>+ Capability</strong></li>
<li>Select <strong>App Groups</strong></li>
<li>Provide your App Group name, usually <code>group.$(PRODUCT_BUNDLE_IDENTIFIER)</code> (e.g.: <code>group.com.example.MyAwesomeApp</code>)</li>
</ul>
<blockquote>
<p>Note: the App Group name must be the <strong>exact same</strong> for every target</p>
</blockquote>
<p>This tells iOS to share storage directories between your targets, and in this case, also the Watermelon database.</p>
<p><strong>Step 2</strong>: Setting up <code>dbName</code>:</p>
<p><strong>Option A</strong>: Via JS</p>
<blockquote>
<p>Note: although this method is simpler, it has the disadvantage of breaking Chrome remote debugging</p>
</blockquote>
<ol>
<li>
<p>Install <a href="https://github.com/joltup/rn-fetch-blob#installation">rn-fetch-blob</a></p>
</li>
<li>
<p>In your JS, when creating the database, get the App Group path using <code>rn-fetch-blob</code>:</p>
<pre><code class="language-ts">import { NativeModules, Platform } from 'react-native';
import { Database } from '@rikishi/watermelondb';
import SQLiteAdapter from '@rikishi/watermelondb/adapters/sqlite';
import schema from './schema';
import RNFetchBlob from 'rn-fetch-blob';
const getAppGroupPath = (): string => {
let path = '';
if (Platform.OS === 'ios') {
path = `${RNFetchBlob.fs.syncPathAppGroup('group.com.example.MyAwesomeApp')}/`;
}
return path;
}
const adapter = new SQLiteAdapter({
dbName: `${getAppGroupPath()}default.db`,
schema,
});
const database = new Database({
adapter,
modelClasses: [
...
],
});
export default database;
</code></pre>
</li>
</ol>
<p><strong>Option B</strong>: Via native Swift / Objective-C</p>
<ol>
<li>
<p>Through Xcode, repeat this process for your <strong>main target</strong> and <strong>every other target</strong> that you want to share the database with:</p>
<ul>
<li>Edit <code>Info.plist</code></li>
<li>Add a new row with <code>AppGroup</code> as key and <code>group.$(PRODUCT_BUNDLE_IDENTIFIER)</code> (set up in Step 1) as value.</li>
</ul>
</li>
<li>
<p>Right-click your project name and click <strong>New Group</strong>.</p>
</li>
<li>
<p>Add a file named <code>AppGroup.m</code> and paste the following:</p>
<pre><code>#import "React/RCTBridgeModule.h"
@interface RCT_EXTERN_MODULE(AppGroup, NSObject)
@end
</code></pre>
</li>
<li>
<p>Add a file named <code>AppGroup.swift</code> and paste the following:</p>
<pre><code>import Foundation
@objc(AppGroup)
class AppGroup: NSObject {
@objc
func constantsToExport() -> [AnyHashable : Any]! {
var path = ""
if let suiteName = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String {
if let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: suiteName) {
path = directory.path
}
}
return ["path": "\(path)/"]
}
}
</code></pre>
<p>This reads your new <code>Info.plist</code> row and exports a constant called <code>path</code> with your App Group path (shared directory path), to be used in your JS code.</p>
</li>
<li>
<p>In your JS, when creating the database, import the <code>path</code> constant from your new <code>AppGroup</code> module and prepend to your <code>dbName</code>:</p>
<pre><code class="language-ts">import { NativeModules, Platform } from 'react-native';
import { Database } from '@rikishi/watermelondb';
import SQLiteAdapter from '@rikishi/watermelondb/adapters/sqlite';
import schema from './schema';
const getAppGroupPath = (): string => {
let path = '';
if (Platform.OS === 'ios') {
path = NativeModules.AppGroup.path;
}
return path;
}
const adapter = new SQLiteAdapter({
dbName: `${getAppGroupPath()}default.db`,
schema,
});
const database = new Database({
adapter,
modelClasses: [
...
],
});
export default database;
</code></pre>
</li>
</ol>
<p>This way you're telling Watermelon to store your database into the shared directories, you're ready to go!</p>
<h2 id="dig-deeper-into-watermelondb"><a class="header" href="#dig-deeper-into-watermelondb">Dig deeper into WatermelonDB</a></h2>
<p>Details about how Watermelon works, how to hack and contribute</p>
<p><a href="https://www.youtube.com/watch?v=uFvHURTRLxQ">📺 Digging deeper into WatermelonDB</a> — more architectural info about caching, observation, and sync</p>
<h1 id="architecture"><a class="header" href="#architecture">Architecture</a></h1>
<h2 id="base-objects"><a class="header" href="#base-objects">Base objects</a></h2>
<p><code>Database</code> is the root object of Watermelon. It owns:</p>
<ul>
<li>a <code>DatabaseAdapter</code></li>
<li>a map of <code>Collection</code>s</li>
</ul>
<p><code>DatabaseAdapter</code> connects Watermelon's reactive world to low-level imperative world of databases. See <a href="Implementation/./Adapters.html">Adapters</a>.</p>
<p><code>Collection</code> manages all records of a given kind:</p>
<ul>
<li>it has a cache of records already fetched from the database (<code>RecordCache</code>)</li>
<li>it has the public API to <code>find</code>, <code>query</code> and <code>create</code> existing records</li>
<li>it implements fetch/update/delete operations on records</li>
</ul>
<p><code>Model</code> is an instance of a collection record. A model <em>class</em> describes a <em>kind</em> of a record. <code>Model</code> is the base class for your concrete models (e.g. <code>Post</code>, <code>Comment</code>, <code>Task</code>):</p>
<ul>
<li>it describes the specific instance - <code>id</code> + all custom fields and actions</li>
<li>it has public API to <code>update</code>, <code>markAsDeleted</code> and <code>destroyPermanently</code></li>
<li>implements record-level observation <code>observe()</code></li>
<li>static fields describe base information about a model (<code>table</code>, <code>associations</code>) - See <a href="Implementation/../Model.html">Defining models</a></li>
</ul>
<p>As a general rule, <code>Model</code> manages the state of a specific instance, and <code>Collection</code> of the entire collection of records. So for example, <code>model.markAsDeleted()</code> changes the local state of called record, but then delegates to its collection to notify collection observers and actually remove from the database</p>
<p><code>Query</code> is a helper object that gives us a nice API to perform queries (<code>query.observe()</code>, <code>query.fetchCount()</code>):</p>
<ul>
<li>created via <code>collection.query()</code></li>
<li>encapsulates a <code>QueryDescription</code> structure which actually describes the query conditions</li>
<li>fetch/observe methods actually delegate to <code>Collection</code> to perform database operations</li>
<li>caches <code>Observable</code>s created by <code>observe/observeCount</code> methods so they can be reused and shared</li>
</ul>
<h2 id="helper-functions"><a class="header" href="#helper-functions">Helper functions</a></h2>
<p>Watermelon's objects and classes are meant to be as minimal as possible — only manage their own state and be an API for your app. Most logic should be stateless, and implemented as pure functions:</p>
<p><code>QueryDescription</code> is a structure (object) describing the query, built using <code>Q.*</code> helper functions</p>
<p><code>encodeMatcher()</code>, <code>simpleObserver()</code>, <code>reloadingObserver()</code>, <code>fieldObserver()</code> implement query observation logic (See <a href="Implementation/./Observation.html">Observation</a>.)</p>
<p>Model decorators transform simple class properties into Watermelon-aware record fields.</p>
<p>Much of Adapters' logic is implemented as pure functions too. See <a href="Implementation/./Adapters.html">Adapters</a>.</p>
<h1 id="database-adapters"><a class="header" href="#database-adapters">Database adapters</a></h1>
<p>The idea for the <a href="Implementation/./Architecture.html">Watermelon architecture</a> is to be database-agnostic. <code>Watermelon</code> is a cross-platform high-level layer for dealing with data, but can be plugged in to any underlying database, depending on platform needs.</p>
<p>Think of it this way:</p>
<ul>
<li>Collection/Model/Query is the <strong>reactive</strong> layer</li>
<li><code>DatabaseAdapter</code> is the <strong>imperative</strong> layer</li>
</ul>
<p>The adapter merely performs simple CRUD (create/read/update/delete) operations.</p>
<p><code>DatabaseAdapter</code> is a Flow <em>interface</em>. Watermelon comes with two concrete implementations:</p>
<h2 id="react-native"><a class="header" href="#react-native">React Native</a></h2>
<p><code>SQLiteAdapter</code> is an adapter for React Native, based on SQLite:</p>
<ul>
<li>Queries are converted to SQL on app thread using <code>adapters/sqlite/encodeQuery</code></li>
<li>Communication happens over <code>NativeModules</code> with a native-side bridge</li>
<li>Native database handling happens on a separate thread</li>
<li><code>DatabaseBridge</code> is the React Native bridge stub</li>
<li><code>DatabaseDriver</code> implements Watermelon-specific logic (caching, etc.)</li>
<li><code>Database</code> is a simple SQLite abstraction layer (over <a href="https://github.com/ccgus/fmdb">FMDB</a> on iOS and built-in <code>sqlite.SQLiteDatabase</code> on Android)</li>
</ul>
<h2 id="web"><a class="header" href="#web">Web</a></h2>
<p><code>LokiJSAdapter</code> is an adapter for the web, based around <a href="http://techfort.github.io/LokiJS/">LokiJS</a>:</p>
<ul>
<li>Why LokiJS? WebSQL would be a perfect fit for Watermelon, but sadly is a dead API, so we must use IndexedDB, but it's too low-level. LokiJS implements a fast querying API on top of IndexedDB.</li>
<li><code>LokiJSAdapter</code> delegates everything to a separate thread over <code>LokiDispatcher</code></li>
<li><code>LokiDispatcher</code> spins up a worker thread running <code>DatabaseBridge</code></li>
<li><code>DatabaseBridge</code> maintains a queue of operations and executes them on <code>DatabaseDriver</code></li>
<li><code>DatabaseDriver</code> actually implements the Adapter operations</li>
<li><code>encodeQuery</code> translates <code>QueryDescription</code> objects to Loki query objects</li>
<li><code>executeQuery</code> implements join queries (<code>Q.on</code>), which Loki does not support</li>
</ul>
<h2 id="writing-your-own-adapter"><a class="header" href="#writing-your-own-adapter">Writing your own adapter</a></h2>
<p>If you want to write a new adapter, please contact <a href="https://github.com/radex">@radex</a> for more information.</p>
<p>⚠️ TODO: This section needs more concrete tips</p>
<h1 id="sync-implementation-details"><a class="header" href="#sync-implementation-details">Sync implementation details</a></h1>
<p>If you're looking for a guide to implement Watermelon Sync in your app, see <a href="Implementation/../Advanced/Sync.html"><strong>Synchronization</strong></a>.</p>
<p>If you want to contribute to Watermelon Sync, or implement your own synchronization engine from scratch, read this.</p>
<h2 id="implementing-your-own-sync-from-scratch"><a class="header" href="#implementing-your-own-sync-from-scratch">Implementing your own sync from scratch</a></h2>
<p>For basic details about how changes tracking works, see: <a href="https://www.youtube.com/watch?v=uFvHURTRLxQ">📺 Digging deeper into WatermelonDB</a></p>
<p>Why you might want to implement a custom sync engine? If you have an existing remote server architecture that's difficult to adapt to Watermelon sync protocol, or you specifically want a different architecture (e.g. single HTTP request -- server resolves conflicts). Be warned, however, that <strong>implementing sync that works reliably</strong> is a hard problem, so we recommend sticking to Watermelon Sync and tweaking it as needed.</p>
<p>The rest of this document contains details about how Watermelon Sync works - you can use that as a blueprint for your own work.</p>
<p>If possible, please use sync implementation helpers from <code>sync/*.js</code> to keep your custom sync implementation have as much commonality as possible with the standard implementation. This is good both for you and for the rest of WatermelonDB community, as we get to share improvements and bug fixes. If the helpers are <em>almost</em> what you need, but not quite, please send pull requests with improvements!</p>
<h2 id="watermelon-sync----details"><a class="header" href="#watermelon-sync----details">Watermelon Sync -- Details</a></h2>
<h3 id="general-design"><a class="header" href="#general-design">General design</a></h3>
<ul>
<li>master/replica - server is the source of truth, client has a full copy and syncs back to server (no peer-to-peer syncs)</li>
<li>two phase sync: first pull remote changes to local app, then push local changes to server</li>
<li>client resolves conflicts</li>
<li>content-based, not time-based conflict resolution</li>
<li>conflicts are resolved using per-column client-wins strategy: in conflict, server version is taken
except for any column that was changed locally since last sync.</li>
<li>local app tracks its changes using a _status (synced/created/updated/deleted) field and _changes
field (which specifies columns changed since last sync)</li>
<li>server only tracks timestamps (or version numbers) of every record, not specific changes</li>
<li>sync is performed for the entire database at once, not per-collection</li>
<li>eventual consistency (client and server are consistent at the moment of successful pull if no
local changes need to be pushed)</li>
<li>non-blocking: local database writes (but not reads) are only momentarily locked when writing data
but user can safely make new changes throughout the process</li>
</ul>
<h3 id="sync-procedure"><a class="header" href="#sync-procedure">Sync procedure</a></h3>
<ol>
<li>Pull phase</li>
</ol>
<ul>
<li>get <code>lastPulledAt</code> timestamp locally (null if first sync)</li>
<li>call <code>pullChanges</code> function, passing <code>lastPulledAt</code>
<ul>
<li>server responds with all changes (create/update/delete) that occured since <code>lastPulledAt</code></li>
<li>server serves us with its current timestamp</li>
</ul>
</li>
<li>IN ACTION (lock local writes):
<ul>
<li>ensure no concurrent syncs</li>
<li>apply remote changes locally
<ul>
<li>insert new records
<ul>
<li>if already exists (error), update</li>
<li>if locally marked as deleted (error), un-delete and update</li>
</ul>
</li>
<li>update records
<ul>
<li>if synced, just replace contents with server version</li>
<li>if locally updated, we have a conflict!
<ul>
<li>take remote version, apply local fields that have been changed locally since last sync
(per-column client wins strategy)</li>
<li>record stays marked as updated, because local changes still need to be pushed</li>
</ul>
</li>
<li>if locally marked as deleted, ignore (deletion will be pushed later)</li>
<li>if doesn't exist locally (error), create</li>
</ul>
</li>
<li>destroy records
<ul>
<li>if alredy deleted, ignore</li>
<li>if locally changed, destroy anyway</li>
<li>ignore children (server ought to schedule children to be destroyed)</li>
</ul>
</li>
</ul>
</li>
<li>if successful, save server's timestamp as new <code>lastPulledAt</code></li>
</ul>
</li>
</ul>
<ol start="2">
<li>Push phase</li>
</ol>
<ul>
<li>Fetch local changes
<ul>
<li>Find all locally changed records (created/updated record + deleted IDs) for all collections</li>
<li>Strip _status, _changed</li>
</ul>
</li>
<li>Call <code>pushChanges</code> function, passing local changes object, and the new <code>lastPulledAt</code> timestamp
<ul>
<li>Server applies local changes to database, and sends OK</li>
<li>If one of the pushed records has changed <em>on the server</em> since <code>lastPulledAt</code>, push is aborted,
all changes reverted, and server responds with an error</li>
</ul>
</li>
<li>IN ACTION (lock local writes):
<ul>
<li>markLocalChangesAsSynced:
<ul>
<li>take local changes fetched in previous step, and:</li>
<li>permanently destroy records marked as deleted</li>
<li>mark created/updated records as synced and reset their _changed field</li>
<li>note: <em>do not</em> mark record as synced if it changed locally since <code>fetch local changes</code> step
(user could have made new changes that need syncing)</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="notes"><a class="header" href="#notes">Notes</a></h3>
<ul>
<li>This procedure is designed such that if sync fails at any moment, and even leaves local app in
inconsistent (not fully synced) state, we should still achieve consistency with the next sync:
<ul>
<li>applyRemoteChanges is designed such that if all changes are applied, but <code>lastPulledAt</code> doesn't get
saved — so during next pull server will serve us the same changes, second applyRemoteChanges will
arrive at the same result</li>
<li>local changes before "fetch local changes" step don't matter at all - user can do anything</li>
<li>local changes between "fetch local changes" and "mark local changes as synced" will be ignored
(won't be marked as synced) - will be pushed during next sync</li>
<li>if changes don't get marked as synced, and are pushed again, server should apply them the same way</li>
<li>remote changes between pull and push phase will be locally ignored (will be pulled next sync)
unless there's a per-record conflict (then push fails, but next sync resolves both pull and push)</li>
</ul>
</li>
</ul>
<h3 id="migration-syncs"><a class="header" href="#migration-syncs">Migration Syncs</a></h3>
<p>Schema versioning and migrations complicate sync, because a client might not be able to sync some tables and columns, but after upgrade to the newest version, it should be able to get consistent sync. To be able
to do that, we need to know what's the schema version at which the last sync occured. Unfortunately,
Watermelon Sync didn't track that from the first version, so backwards-compat is required.</p>
<pre><code>synchronize({ migrationsEnabledAtVersion: XXX })
. . . .
LPA = last pulled at
MEA = migrationsEnabledAtVersion, schema version at which future migration support was introduced
LS = last synced schema version (may be null due to backwards compat)
CV = current schema version
LPA MEA LS CV migration set LS=CV? comment
null X X 10 null YES first sync. regardless of whether the app
is migration sync aware, we can note LS=CV
to fetch all migrations once available
100 null X X null NO indicates app is not migration sync aware so
we're not setting LS to allow future migration sync
100 X 10 10 null NO up to date, no migration
100 9 9 10 {9-10} YES correct migration sync
100 9 null 10 {9-10} YES fallback migration. might not contain all
necessary migrations, since we can't know for sure
that user logged in at then-current-version==MEA
100 9 11 10 ERROR NO LS > CV indicates programmer error
100 11 X 10 ERROR NO MEA > CV indicates programmer error
</code></pre>
<h3 id="reference"><a class="header" href="#reference">Reference</a></h3>
<p>This design has been informed by:</p>
<ul>
<li>10 years of experience building synchronization at Nozbe</li>
<li>Kinto & Kinto.js
<ul>
<li>https://github.com/Kinto/kinto.js/blob/master/src/collection.js</li>
<li>https://kintojs.readthedocs.io/en/latest/api/#fetching-and-publishing-changes</li>
</ul>
</li>
<li>Histo - https://github.com/mirkokiefer/syncing-thesis</li>
</ul>
<h2 id="dig-deeper-into-watermelondb-1"><a class="header" href="#dig-deeper-into-watermelondb-1">Dig deeper into WatermelonDB</a></h2>
<p>Details about how Watermelon works, how to hack and contribute</p>
<p><a href="https://www.youtube.com/watch?v=uFvHURTRLxQ">📺 Digging deeper into WatermelonDB</a> — more architectural info about caching, observation, and sync</p>
<h1 id="watermelondb-roadmap"><a class="header" href="#watermelondb-roadmap">WatermelonDB Roadmap</a></h1>
<p>Despite being called 0.xx, WatermelonDB is essentially feature-complete and relatively API stable. It's used in production by <a href="https://nozbe.com">Nozbe Teams</a> and many others.</p>
<p>We don't call it 1.0 mostly out of convenience, to allow rapid development without incrementing <code>major</code> version counter (as dictated by SemVer). We do intend to call WatermelonDB a 1.0 once we can reach a long-term stable API.</p>
<h3 id="v10"><a class="header" href="#v10">v1.0</a></h3>
<ul>
<li>Optimized tree deleting</li>
<li>Long term stable API</li>
</ul>
<h3 id="beyond-10"><a class="header" href="#beyond-10">Beyond 1.0</a></h3>
<ul>
<li>Full transactionality (atomicity) support?</li>
<li>Field sanitizers</li>
</ul>
<h1 id="contributing-guidelines"><a class="header" href="#contributing-guidelines">Contributing guidelines</a></h1>
<h2 id="before-you-send-a-pull-request"><a class="header" href="#before-you-send-a-pull-request">Before you send a pull request</a></h2>
<ol>
<li>
<p><strong>Did you add or changed some functionality?</strong></p>
<p>Add (or modify) tests!</p>
</li>
<li>
<p><strong>Check if the automated tests pass</strong></p>
<pre><code class="language-bash">yarn ci:check
</code></pre>
</li>
<li>
<p><strong>Format the files you changed</strong></p>
<pre><code class="language-bash">yarn prettier
</code></pre>
</li>
<li>
<p><strong>Mark your changes in CHANGELOG</strong></p>
<p>Put a one-line description of your change under Added/Changed section. See <a href="https://keepachangelog.com/en/1.0.0/">Keep a Changelog</a>.</p>
</li>
</ol>
<h2 id="running-watermelon-in-development"><a class="header" href="#running-watermelon-in-development">Running Watermelon in development</a></h2>
<h3 id="download-source-and-dependencies"><a class="header" href="#download-source-and-dependencies">Download source and dependencies</a></h3>
<pre><code class="language-bash">git clone https://github.com/Nozbe/WatermelonDB.git
cd WatermelonDB
yarn
</code></pre>
<h3 id="developing-watermelon-alongside-your-app"><a class="header" href="#developing-watermelon-alongside-your-app">Developing Watermelon alongside your app</a></h3>
<p>To work on Watermelon code in the sandbox of your app:</p>
<pre><code class="language-bash">yarn dev
</code></pre>
<p>This will create a <code>dev/</code> folder in Watermelon and observe changes to source files (only JavaScript files) and recompile them as needed.</p>
<p>Then in your app:</p>
<pre><code class="language-bash">cd node_modules/@nozbe
rm -fr watermelondb
ln -s path-to-watermelondb/dev watermelondb
</code></pre>
<p><strong>This will work in Webpack but not in Metro</strong> (React Native). Metro doesn't follow symlinks. Instead, you can compile WatermelonDB directly to your project:</p>
<pre><code class="language-bash">DEV_PATH="/path/to/your/app/node_modules/@rikishi/watermelondb" yarn dev
</code></pre>
<h3 id="running-tests"><a class="header" href="#running-tests">Running tests</a></h3>
<p>This runs Jest, ESLint and Flow:</p>
<pre><code class="language-bash">yarn ci:check
</code></pre>
<p>You can also run them separately:</p>
<pre><code class="language-bash">yarn test
yarn eslint
yarn flow
</code></pre>
<h3 id="editing-files"><a class="header" href="#editing-files">Editing files</a></h3>
<p>We recommend VS Code with ESLint, Flow, and Prettier (with prettier-eslint enabled) plugins for best development experience. (To see lint/type issues inline + have automatic reformatting of code)</p>
<h2 id="editing-native-code"><a class="header" href="#editing-native-code">Editing native code</a></h2>
<p>In <code>native/ios</code> and <code>native/android</code> you'll find the native bridge code for React Native.</p>
<p>It's recommended to use the latest stable version of Xcode / Android Studio to work on that code.</p>
<h3 id="integration-tests"><a class="header" href="#integration-tests">Integration tests</a></h3>
<p>If you change native bridge code or <code>adapter/sqlite</code> code, it's recommended to run integration tests that run the entire Watermelon code with SQLite and React Native in the loop:</p>
<pre><code class="language-bash">yarn test:ios
yarn test:android
</code></pre>
<h3 id="running-tests-manualy"><a class="header" href="#running-tests-manualy">Running tests manualy</a></h3>
<ul>
<li>For iOS open the <code>native/iosTest/WatermelonTester.xcworkspace</code> project and hit Cmd+U.</li>
<li>For Android open <code>native/androidTest</code> in AndroidStudio navigate to <code>app/src/androidTest/java/com.nozbe.watermelonTest/BridgeTest</code> and click green arrow near <code>class BridgeTest</code></li>
</ul>
<h3 id="native-linting"><a class="header" href="#native-linting">Native linting</a></h3>
<p>Make sure the native code you're editing conforms to Watermelon standards:</p>
<pre><code class="language-bash">yarn swiftlint
yarn ktlint
</code></pre>
<h3 id="native-code-troubleshooting"><a class="header" href="#native-code-troubleshooting">Native code troubleshooting</a></h3>
<ol>
<li>If <code>test:ios</code> fails in terminal:</li>
</ol>
<ul>
<li>Run tests in Xcode first before running from terminal</li>
<li>Make sure you have the right version of Xcode CLI tools set in Preferences -> Locations</li>
</ul>
<ol>
<li>Make sure you're on the most recent stable version of Xcode / Android Studio</li>
<li>Remove native caches:</li>
</ol>
<ul>
<li>Xcode: <code>~/Library/Developer/Xcode/DerivedData</code>:</li>
<li>Android: <code>.gradle</code> and <code>build</code> folders in <code>native/android</code> and <code>native/androidTest</code></li>
<li><code>node_modules</code> (because of React Native precompiled third party libraries)</li>
</ul>
<h1 id="changelog"><a class="header" href="#changelog">Changelog</a></h1>
<p>All notable changes to this project will be documented in this file.</p>
<p>Contributors: Please add your changes to CHANGELOG-Unreleased.md</p>
<h2 id="024---2021-10-28"><a class="header" href="#024---2021-10-28">0.24 - 2021-10-28</a></h2>
<h3 id="breaking-changes"><a class="header" href="#breaking-changes">BREAKING CHANGES</a></h3>
<ul>
<li><code>Q.experimentalSortBy</code>, <code>Q.experimentalSkip</code>, <code>Q.experimentalTake</code> have been renamed to <code>Q.sortBy</code>, <code>Q.skip</code>, <code>Q.take</code> respectively</li>
<li><strong>RxJS has been updated to 7.3.0</strong>. If you're not importing from <code>rxjs</code> in your app, this doesn't apply to you. If you are, read RxJS 7 breaking changes: https://rxjs.dev/deprecations/breaking-changes</li>
</ul>
<h3 id="new-features"><a class="header" href="#new-features">New features</a></h3>
<ul>
<li><strong>LocalStorage</strong>. <code>database.localStorage</code> is now available</li>
<li><strong>sortBy, skip, take</strong> are now available in LokiJSAdapter as well</li>
<li><strong>Disposable records</strong>. Read-only records that cannot be saved in the database, updated, or deleted and only exist for as long as you keep a reference to them in memory can now be created using <code>collection.disposableFromDirtyRaw()</code>. This is useful when you're adding online-only features to an otherwise offline-first app.</li>
<li>[Sync] <code>experimentalRejectedIds</code> parameter now available in push response to allow partial rejection of an otherwise successful sync</li>
</ul>
<h3 id="fixes"><a class="header" href="#fixes">Fixes</a></h3>
<ul>
<li>Fixes an issue when using Headless JS on Android with JSI mode enabled - pass <code>usesExclusiveLocking: true</code> to SQLiteAdapter to enable</li>
<li>Fixes Typescript annotations for Collection and adapters/sqlite</li>
</ul>
<h2 id="023---2021-07-22"><a class="header" href="#023---2021-07-22">0.23 - 2021-07-22</a></h2>
<p>This is a big release to WatermelonDB with new advanced features, great performance improvements, and important fixes to JSI on Android.</p>
<p>Please don't get scared off the long list of breaking changes - they are all either simple Find&Replace renames or changes to internals you probably don't use. It shouldn't take you more than 15 minutes to upgrade to 0.23.</p>
<h3 id="breaking-changes-1"><a class="header" href="#breaking-changes-1">BREAKING CHANGES</a></h3>
<ul>
<li><strong>iOS Installation change</strong>. You need to add this line to your Podfile: <code>pod 'simdjson', path: '../node_modules/@nozbe/simdjson'</code></li>
<li>Deprecated <code>new Database({ actionsEnabled: false })</code> options is now removed. Actions are always enabled.</li>
<li>Deprecated <code>new SQLiteAdapter({ synchronous: true })</code> option is now removed. Use <code>{ jsi: true }</code> instead.</li>
<li>Deprecated <code>Q.unsafeLokiFilter</code> is now removed. Use <code>Q.unsafeLokiTransform((raws, loki) => raws.filter(raw => ...))</code> instead.</li>
<li>Deprecated <code>Query.hasJoins</code> is now removed</li>
<li>Changes to <code>LokiJSAdapter</code> constructor options:
<ul>
<li><code>indexedDBSerializer</code> -> <code>extraIncrementalIDBOptions: { serializeChunk, deserializeChunk }</code></li>
<li><code>onIndexedDBFetchStart</code> -> <code>extraIncrementalIDBOptions: { onFetchStart }</code></li>
<li><code>onIndexedDBVersionChange</code> -> <code>extraIncrementalIDBOptions: { onversionchange }</code></li>
<li><code>autosave: false</code> -> <code>extraLokiOptions: { autosave: false }</code></li>
</ul>
</li>
<li>Changes to Internal APIs. These were never meant to be public, and so are unlikely to affect you:
<ul>
<li><code>Model._isCommited</code>, <code>._hasPendingUpdate</code>, <code>._hasPendingDelete</code> have been removed and changed to <code>Model._pendingState</code></li>
<li><code>Collection.unsafeClearCache()</code> is no longer exposed</li>
</ul>
</li>
<li>Values passed to <code>adapter.setLocal()</code> are now validated to be strings. This is technically a bug fix, since local storage was always documented to only accept strings, however applications may have relied on this lack of validation. Adding this validation was necessary to achieve consistent behavior between SQLiteAdapter and LokiJSAdapter</li>
<li><code>unsafeSql</code> passed to <code>appSchema</code> will now also be called when dropping and later recreating all database indices on large batches. A second argument was added so you can distinguish between these cases. See Schema docs for more details.</li>
<li><strong>Changes to sync change tracking</strong>. The behavior of <code>record._raw._changed</code> and <code>record._raw._status</code> (a.k.a. <code>record.syncStatus</code>) has changed. This is unlikely to be a breaking change to you, unless you're writing your own sync engine or rely on these low-level details.
<ul>
<li>Previously, _changed was always empty when _status=created. Now, _changed is not populated during initial creation of a record, but a later update will add changed fields to _changed. This change was necessary to fix a long-standing Sync bug.</li>
</ul>
</li>
</ul>
<h3 id="deprecations"><a class="header" href="#deprecations">Deprecations</a></h3>
<ul>
<li><code>database.action(() => {})</code> is now deprecated. Use <code>db.write(() => {})</code> instead (or <code>db.read(() => {})</code> if you only need consistency but are not writing any changes to DB)</li>
<li><code>@action</code> is now deprecated. Use <code>@writer</code> or <code>@reader</code> instead</li>
<li><code>.subAction()</code> is now deprecated. Use <code>.callReader()</code> or <code>.callWriter()</code> instead</li>
<li><code>Collection.unsafeFetchRecordsWithSQL()</code> is now deprecated. Use <code>collection.query(Q.unsafeSqlQuery("select * from...")).fetch()</code> instead.</li>
</ul>
<h3 id="new-features-1"><a class="header" href="#new-features-1">New features</a></h3>
<ul>
<li><code>db.write(writer => { ... writer.batch() })</code> - you can now call batch on the interface passed to a writer block</li>
<li><strong>Fetching record IDs and unsafe raws.</strong> You can now optimize fetching of queries that only require IDs, not full cached records:
<ul>
<li><code>await query.fetchIds()</code> will return an array of record ids</li>
<li><code>await query.unsafeFetchRaw()</code> will return an array of unsanitized, unsafe raw objects (use alongside <code>Q.unsafeSqlQuery</code> to exclude unnecessary or include extra columns)</li>
<li>advanced <code>adapter.queryIds()</code>, <code>adapter.unsafeQueryRaw</code> are also available</li>
</ul>
</li>
<li><strong>Raw SQL queries</strong>. New syntax for running unsafe raw SQL queries:
<ul>
<li><code>collection.query(Q.unsafeSqlQuery("select * from tasks where foo = ?", ['bar'])).fetch()</code></li>
<li>You can now also run <code>.fetchCount()</code>, <code>.fetchIds()</code> on SQL queries</li>
<li>You can now safely pass values for SQL placeholders by passing an array</li>
<li>You can also observe an unsafe raw SQL query -- with some caveats! refer to documentation for more details</li>
</ul>
</li>
<li><strong>Unsafe raw execute</strong>. You can now execute arbitrary SQL queries (SQLiteAdapter) or access Loki object directly (LokiJSAdapter) using <code>adapter.unsafeExecute</code> -- see docs for more details</li>
<li><strong>Turbo Login</strong>. You can now speed up the initial (login) sync by up to 5.3x with Turbo Login. See Sync docs for more details.</li>
<li>New diagnostic tool - <strong>debugPrintChanges</strong>. See Sync documentation for more details</li>
</ul>
<h3 id="performance-1"><a class="header" href="#performance-1">Performance</a></h3>
<ul>
<li>The order of Q. clauses in a query is now preserved - previously, the clauses could get rearranged and produce a suboptimal query</li>
<li>[SQLite] <code>adapter.batch()</code> with large numbers of created/updated/deleted records is now between 16-48% faster</li>
<li>[LokiJS] Querying and finding is now faster - unnecessary data copy is skipped</li>
<li>[jsi] 15-30% faster querying on JSC (iOS) when the number of returned records is large</li>
<li>[jsi] up to 52% faster batch creation (yes, that's on top of the improvement listed above!)</li>
<li>Fixed a performance bug that caused observed items on a list observer with <code>.observeWithColumns()</code> to be unnecessarily re-rendered just before they were removed from the list</li>
</ul>
<h3 id="changes"><a class="header" href="#changes">Changes</a></h3>
<ul>
<li>All Watermelon console logs are prepended with a 🍉 tag</li>
<li>Extra protections against improper use of writers/readers (formerly actions) have been added</li>
<li>Queries with multiple top-level <code>Q.on('table', ...)</code> now produce a warning. Use <code>Q.on('table', [condition1, condition2, ...])</code> syntax instead.</li>
<li>[jsi] WAL mode is now used</li>
</ul>
<h3 id="fixes-1"><a class="header" href="#fixes-1">Fixes</a></h3>
<ul>
<li>[jsi] Fix a race condition where commands sent to the database right after instantiating SQLiteAdapter would fail</li>
<li>[jsi] Fix incorrect error reporting on some sqlite errors</li>
<li>[jsi] Fix issue where app would crash on Android/Hermes on reload</li>
<li>[jsi] Fix IO errors on Android</li>
<li>[sync] Fixed a long-standing bug that would cause records that are created before a sync and updated during sync's push to lose their most recent changes on a subsequent sync</li>
</ul>
<h3 id="internal"><a class="header" href="#internal">Internal</a></h3>
<ul>
<li>Internal changes to SQLiteAdapter:
<ul>
<li>.batch is no longer available on iOS implementation</li>
<li>.batch/.batchJSON internal format has changed</li>
<li>.getDeletedRecords, destroyDeletedRecords, setLocal, removeLocal is no longer available</li>
</ul>
</li>
<li>encoded SQLiteAdapter schema has changed</li>
<li>LokiJSAdapter has had many internal changes</li>
</ul>
<h2 id="022---2021-05-07"><a class="header" href="#022---2021-05-07">0.22 - 2021-05-07</a></h2>
<h3 id="breaking-changes-2"><a class="header" href="#breaking-changes-2">BREAKING CHANGES</a></h3>
<ul>
<li>[SQLite] <code>experimentalUseJSI: true</code> option has been renamed to <code>jsi: true</code></li>
</ul>
<h3 id="deprecations-1"><a class="header" href="#deprecations-1">Deprecations</a></h3>
<ul>
<li>[LokiJS] <code>Q.unsafeLokiFilter</code> is now deprecated and will be removed in a future version.
Use <code>Q.unsafeLokiTransform((raws, loki) => raws.filter(raw => ...))</code> instead.</li>
</ul>
<h3 id="new-features-2"><a class="header" href="#new-features-2">New features</a></h3>
<ul>
<li>[SQLite] [JSI] <code>jsi: true</code> now works on Android - see docs for installation info</li>
</ul>
<h3 id="performance-2"><a class="header" href="#performance-2">Performance</a></h3>
<ul>
<li>Removed dependency on rambdax and made the util library smaller</li>
<li>Faster withObservables</li>
</ul>
<h3 id="changes-1"><a class="header" href="#changes-1">Changes</a></h3>
<ul>
<li>Synchronization: <code>pushChanges</code> is optional, will not calculate local changes if not specified.</li>
<li>withObservables is now a dependency of WatermelonDB for simpler installation and consistent updates. You can (and generally should) delete <code>@nozbe/with-observables</code> from your app's package.json</li>
<li>[Docs] Add advanced tutorial to share database across iOS targets - @thiagobrez</li>
<li>[SQLite] Allowed callbacks (within the migrationEvents object) to be passed so as to track the migration events status ( onStart, onSuccess, onError ) - @avinashlng1080</li>
<li>[SQLite] Added a dev-only <code>Query._sql()</code> method for quickly extracting SQL from Queries for debugging purposes</li>
</ul>
<h3 id="fixes-2"><a class="header" href="#fixes-2">Fixes</a></h3>
<ul>
<li>Non-react statics hoisting in <code>withDatabase()</code></li>
<li>Fixed incorrect reference to <code>process</code>, which can break apps in some environments (e.g. webpack5)</li>
<li>[SQLite] [JSI] Fixed JSI mode when running on Hermes</li>
<li>Fixed a race condition when using standard fetch methods alongside <code>Collection.unsafeFetchRecordsWithSQL</code> - @jspizziri</li>
<li>withObservables shouldn't cause any RxJS issues anymore as it no longer imports RxJS</li>
<li>[Typescript] Added <code>onSetUpError</code> and <code>onIndexedDBFetchStart</code> fields to <code>LokiAdapterOptions</code>; fixes TS error - @3DDario</li>
<li>[Typescript] Removed duplicated identifiers <code>useWebWorker</code> and <code>useIncrementalIndexedDB</code> in <code>LokiAdapterOptions</code> - @3DDario</li>
<li>[Typescript] Fix default export in logger util</li>
</ul>
<h2 id="021---2021-03-24"><a class="header" href="#021---2021-03-24">0.21 - 2021-03-24</a></h2>
<h3 id="breaking-changes-3"><a class="header" href="#breaking-changes-3">BREAKING CHANGES</a></h3>
<ul>
<li>[LokiJS] <code>useWebWorker</code> and <code>useIncrementalIndexedDB</code> options are now required (previously, skipping them would only trigger a warning)</li>
</ul>
<h3 id="new-features-3"><a class="header" href="#new-features-3">New features</a></h3>
<ul>
<li>[Model] <code>Model.update</code> method now returns updated record</li>
<li>[adapters] <code>onSetUpError: Error => void</code> option is added to both <code>SQLiteAdapter</code> and <code>LokiJSAdapter</code>. Supply this option to catch initialization errors and offer the user to reload or log out</li>
<li>[LokiJS] new <code>extraLokiOptions</code> and <code>extraIncrementalIDBOptions</code> options</li>
<li>[Android] Autolinking is now supported.
<ul>
<li>If You upgrade to <code><= v0.21.0</code> <strong>AND</strong> are on a version of React Native which supports Autolinking, you will need to remove the config manually linking WatermelonDB.</li>
<li>You can resolve this issue by <strong>REMOVING</strong> the lines of config from your project which are <em>added</em> in the <code>Manual Install ONLY</code> section of the <a href="https://nozbe.github.io/WatermelonDB/Installation.html#android-react-native">Android Install docs</a>.</li>
</ul>
</li>
</ul>
<h3 id="performance-3"><a class="header" href="#performance-3">Performance</a></h3>
<ul>
<li>[LokiJS] Improved performance of launching the app</li>
</ul>
<h3 id="changes-2"><a class="header" href="#changes-2">Changes</a></h3>
<ul>
<li>[LokiJS] <code>useWebWorker: true</code> and <code>useIncrementalIndexedDB: false</code> options are now deprecated. If you rely on these features, please file an issue!</li>
<li>[Sync] Optional <code>log</code> passed to sync now has more helpful diagnostic information</li>
<li>[Sync] Open-sourced a simple SyncLogger you can optionally use. See docs for more info.</li>
<li>[SQLiteAdapter] <code>synchronous:true</code> option is now deprecated and will be replaced with <code>experimentalUseJSI: true</code> in the future. Please test if your app compiles and works well with <code>experimentalUseJSI: true</code>, and if not - file an issue!</li>
<li>[LokiJS] Changed default autosave interval from 250 to 500ms</li>
<li>[Typescript] Add <code>experimentalNestedJoin</code> definition and <code>unsafeSqlExpr</code> clause</li>
</ul>
<h3 id="fixes-3"><a class="header" href="#fixes-3">Fixes</a></h3>
<ul>
<li>[LokiJS] Fixed a case where IndexedDB could get corrupted over time</li>
<li>[Resilience] Added extra diagnostics for when you encounter the <code>Record ID aa#bb was sent over the bridge, but it's not cached</code> error and a recovery path (LokiJSAdapter-only). Please file an issue if you encounter this issue!</li>
<li>[Typescript] Fixed type on OnFunction to accept <code>and</code> in join</li>
<li>[Typescript] Fixed type <code>database#batch(records)</code>'s argument <code>records</code> to accept mixed types</li>
</ul>
<h3 id="internal-1"><a class="header" href="#internal-1">Internal</a></h3>
<ul>
<li>Added an experimental mode where a broken database state is detected, further mutations prevented, and the user notified</li>
</ul>
<h2 id="020---2020-10-05"><a class="header" href="#020---2020-10-05">0.20 - 2020-10-05</a></h2>
<h3 id="breaking-changes-4"><a class="header" href="#breaking-changes-4">BREAKING CHANGES</a></h3>
<p>This release has unintentionally broken RxJS for some apps using <code>with-observables</code>. If you have this issue, please update <code>@nozbe/with-observables</code> to the latest version.</p>
<h3 id="new-features-4"><a class="header" href="#new-features-4">New features</a></h3>
<ul>
<li>[Sync] Conflict resolution can now be customized. See docs for more details</li>
<li>[Android] Autolinking is now supported</li>
<li>[LokiJS] Adapter autosave option is now configurable</li>
</ul>
<h3 id="changes-3"><a class="header" href="#changes-3">Changes</a></h3>
<ul>
<li>Interal RxJS imports have been refactor such that rxjs-compat should never be used now</li>
<li>[Performance] Tweak Babel config to produce smaller code</li>
<li>[Performance] LokiJS-based apps will now take up to 30% less time to load the database (id and unique indicies are generated lazily)</li>
</ul>
<h3 id="fixes-4"><a class="header" href="#fixes-4">Fixes</a></h3>
<ul>
<li>[iOS] Fixed crash on database reset in apps linked against iOS 14 SDK</li>
<li>[LokiJS] Fix <code>Q.like</code> being broken for multi-line strings on web</li>
<li>Fixed warn "import cycle" from DialogProvider (#786) by @gmonte.</li>
<li>Fixed cache date as instance of Date (#828) by @djorkaeffalexandre.</li>
</ul>
<h2 id="019---2020-08-17"><a class="header" href="#019---2020-08-17">0.19 - 2020-08-17</a></h2>
<h3 id="new-features-5"><a class="header" href="#new-features-5">New features</a></h3>
<ul>
<li>[iOS] Added CocoaPods support - @leninlin</li>
<li>[NodeJS] Introducing a new SQLite Adapter based integration to NodeJS. This requires a
peer dependency on <a href="https://github.com/JoshuaWise/better-sqlite3">better-sqlite3</a>
and should work with the same configuration as iOS/Android - @sidferreira</li>
<li>[Android] <code>exerimentalUseJSI</code> option has been enabled on Android. However, it requires some app-specific setup which is not yet documented - stay tuned for upcoming releases</li>
<li>[Schema] [Migrations] You can now pass <code>unsafeSql</code> parameters to schema builder and migration steps to modify SQL generated to set up the database or perform migrations. There's also new <code>unsafeExecuteSql</code> migration step. Please use this only if you know what you're doing — you shouldn't need this in 99% of cases. See Schema and Migrations docs for more details</li>
<li>[LokiJS] [Performance] Added experimental <code>onIndexedDBFetchStart</code> and <code>indexedDBSerializer</code> options to <code>LokiJSAdapter</code>. These can be used to improve app launch time. See <code>src/adapters/lokijs/index.js</code> for more details.</li>
</ul>
<h3 id="changes-4"><a class="header" href="#changes-4">Changes</a></h3>
<ul>
<li>[Performance] findAndObserve is now able to emit a value synchronously. By extension, this makes Relations put into withObservables able to render the child component in one shot. Avoiding the extra unnecessary render cycles avoids a lot of DOM and React commit-phase work, which can speed up loading some views by 30%</li>
<li>[Performance] LokiJS is now faster (refactored encodeQuery, skipped unnecessary clone operations)</li>
</ul>
<h2 id="018---2020-06-30"><a class="header" href="#018---2020-06-30">0.18 - 2020-06-30</a></h2>
<p>Another WatermelonDB release after just a week? Yup! And it's jam-packed full of features!</p>
<h3 id="new-features-6"><a class="header" href="#new-features-6">New features</a></h3>
<ul>
<li>
<p>[Query] <code>Q.on</code> queries are now far more flexible. Previously, they could only be placed at the top
level of a query. See Docs for more details. Now, you can:</p>
<ul>
<li>
<p>Pass multiple conditions on the related query, like so:</p>
<pre><code class="language-js">collection.query(
Q.on('projects', [
Q.where('foo', 'bar'),
Q.where('bar', 'baz'),
])
)
</code></pre>
</li>
<li>
<p>You can place <code>Q.on</code> deeper inside the query (nested inside <code>Q.and()</code>, <code>Q.or()</code>). However, you
must explicitly list all tables you're joining on at the beginning of a query, using:
<code>Q.experimentalJoinTables(['join_table1', 'join_table2'])</code>.</p>
</li>
<li>
<p>You can nest <code>Q.on</code> conditions inside <code>Q.on</code>, e.g. to make a condition on a grandchild.
To do so, it's required to pass <code>Q.experimentalNestedJoin('parent_table', 'grandparent_table')</code> at the beginning
of a query</p>
</li>
</ul>
</li>
<li>
<p>[Query] <code>Q.unsafeSqlExpr()</code> and <code>Q.unsafeLokiExpr()</code> are introduced to allow adding bits of queries
that are not supported by the WatermelonDB query language without having to use <code>unsafeFetchRecordsWithSQL()</code>.
See docs for more details</p>
</li>
<li>
<p>[Query] <code>Q.unsafeLokiFilter((rawRecord, loki) => boolean)</code> can now be used as an escape hatch to make
queries with LokiJSAdapter that are not otherwise possible (e.g. multi-table column comparisons).
See docs for more details</p>
</li>
</ul>
<h3 id="changes-5"><a class="header" href="#changes-5">Changes</a></h3>
<ul>
<li>[Performance] [LokiJS] Improved performance of queries containing query comparisons on LokiJSAdapter</li>
<li>[Docs] Added Contributing guide for Query language improvements</li>
<li>[Deprecation] <code>Query.hasJoins</code> is deprecated</li>
<li>[DX] Queries with bad associations now show more helpful error message</li>
<li>[Query] Counting queries that contain <code>Q.experimentalTake</code> / <code>Q.experimentalSkip</code> is currently broken - previously it would return incorrect results, but
now it will throw an error to avoid confusion. Please contribute to fix the root cause!</li>
</ul>
<h3 id="fixes-5"><a class="header" href="#fixes-5">Fixes</a></h3>
<ul>
<li>[Typescript] Fixed types of Relation</li>
</ul>
<h3 id="internal-2"><a class="header" href="#internal-2">Internal</a></h3>
<ul>
<li><code>QueryDescription</code> structure has been changed.</li>
</ul>
<h2 id="0171---2020-06-24"><a class="header" href="#0171---2020-06-24">0.17.1 - 2020-06-24</a></h2>
<ul>
<li>Fixed broken iOS build - @mlecoq</li>
</ul>
<h2 id="017---2020-06-22"><a class="header" href="#017---2020-06-22">0.17 - 2020-06-22</a></h2>
<h3 id="new-features-7"><a class="header" href="#new-features-7">New features</a></h3>
<ul>
<li>
<p>[Sync] Introducing Migration Syncs - this allows fully consistent synchronization when migrating
between schema versions. Previously, there was no mechanism to incrementally fetch all remote changes in
new tables and columns after a migration - so local copy was likely inconsistent, requiring a re-login.
After adopting migration syncs, Watermelon Sync will request from backend all missing information.
See Sync docs for more details.</p>
</li>
<li>
<p>[iOS] Introducing a new native SQLite database integration, rewritten from scratch in C++, based
on React Native's JSI (JavaScript Interface). It is to be considered experimental, however
we intend to make it the default (and eventually, the only) implementation. In a later release,
Android version will be introduced.</p>
<pre><code> The new adapter is up to 3x faster than the previously fastest `synchronous: true` option,
however this speedup is only achieved with some unpublished React Native patches.
To try out JSI, add `experimentalUseJSI: true` to `SQLiteAdapter` constructor.
</code></pre>
</li>
<li>
<p>[Query] Added <code>Q.experimentalSortBy(sortColumn, sortOrder)</code>, <code>Q.experimentalTake(count)</code>,
<code>Q.experimentalSkip(count)</code> methods (only availble with SQLiteAdapter) - @Kenneth-KT</p>
</li>
<li>
<p><code>Database.batch()</code> can now be called with a single array of models</p>
</li>
<li>
<p>[DX] <code>Database.get(tableName)</code> is now a shortcut for <code>Database.collections.get(tableName)</code></p>
</li>
<li>
<p>[DX] Query is now thenable - you can now use <code>await query</code> and <code>await query.count</code> instead of <code>await query.fetch()</code> and <code>await query.fetchCount()</code></p>
</li>
<li>
<p>[DX] Relation is now thenable - you can now use <code>await relation</code> instead of <code>await relation.fetch()</code></p>
</li>
<li>
<p>[DX] Exposed <code>collection.db</code> and <code>model.db</code> as shortcuts to get to their Database object</p>
</li>
</ul>
<h3 id="changes-6"><a class="header" href="#changes-6">Changes</a></h3>
<ul>
<li>[Hardening] Column and table names starting with <code>__</code>, Object property names (e.g. <code>constructor</code>), and some reserved keywords are now forbidden</li>
<li>[DX] [Hardening] QueryDescription builder methods do tighter type checks, catching more bugs, and
preventing users from unwisely passing unsanitized user data into Query builder methods</li>
<li>[DX] [Hardening] Adapters check early if table names are valid</li>
<li>[DX] Collection.find reports an error more quickly if an obviously invalid ID is passed</li>
<li>[DX] Intializing Database with invalid model classes will now show a helpful error</li>
<li>[DX] DatabaseProvider shows a more helpful error if used improperly</li>
<li>[Sync] Sync no longer fails if pullChanges returns collections that don't exist on the frontend - shows a warning instead. This is to make building backwards-compatible backends less error-prone</li>
<li>[Sync] [Docs] Sync documentation has been rewritten, and is now closer in detail to a formal specification</li>
<li>[Hardening] database.collections.get() better validates passed value</li>
<li>[Hardening] Prevents unsafe strings from being passed as column name/table name arguments in QueryDescription</li>
</ul>
<h3 id="fixes-6"><a class="header" href="#fixes-6">Fixes</a></h3>
<ul>
<li>[Sync] Fixed <code>RangeError: Maximum call stack size exceeded</code> when syncing large amounts of data - @leninlin</li>
<li>[iOS] Fixed a bug that could cause a database operation to fail with an (6) SQLITE_LOCKED error</li>
<li>[iOS] Fixed 'jsi/jsi.h' file not found when building at the consumer level. Added path <code>$(SRCROOT)/../../../../../ios/Pods/Headers/Public/React-jsi</code> to Header Search Paths (issue #691) - @victorbutler</li>
<li>[Native] SQLite keywords used as table or column names no longer crash</li>
<li>Fixed potential issues when subscribing to database, collection, model, queries passing a subscriber function with the same identity more than once</li>
</ul>
<h3 id="internal-3"><a class="header" href="#internal-3">Internal</a></h3>
<ul>
<li>Fixed broken adapter tests</li>
</ul>
<h2 id="0151-0161-fix-0162---2020-06-03"><a class="header" href="#0151-0161-fix-0162---2020-06-03">0.15.1, 0.16.1-fix, 0.16.2 - 2020-06-03</a></h2>
<p>This is a security patch for a vulnerability that could cause maliciously crafted record IDs to
cause all or some of user's data to be deleted. More information available via GitHub security advisory</p>
<h2 id="0161---2020-05-18"><a class="header" href="#0161---2020-05-18">0.16.1 - 2020-05-18</a></h2>
<h3 id="changes-7"><a class="header" href="#changes-7">Changes</a></h3>
<ul>
<li><code>Database.unsafeResetDatabase()</code> is now less unsafe — more application bugs are being caught</li>
</ul>
<h3 id="fixes-7"><a class="header" href="#fixes-7">Fixes</a></h3>
<ul>
<li>[iOS] Fix build in apps using Flipper</li>
<li>[Typescript] Added type definition for <code>setGenerator</code>.</li>
<li>[Typescript] Fixed types of decorators.</li>
<li>[Typescript] Add Tests to test Types.</li>
<li>Fixed typo in learn-to-use docs.</li>
<li>[Typescript] Fixed types of changes.</li>
</ul>
<h3 id="internal-4"><a class="header" href="#internal-4">Internal</a></h3>
<ul>
<li>[SQLite] Infrastruture for a future JSI adapter has been added</li>
</ul>
<h2 id="016---2020-03-06"><a class="header" href="#016---2020-03-06">0.16 - 2020-03-06</a></h2>
<h3 id="-breaking"><a class="header" href="#-breaking">⚠️ Breaking</a></h3>
<ul>
<li><code>experimentalUseIncrementalIndexedDB</code> has been renamed to <code>useIncrementalIndexedDB</code></li>
</ul>
<h4 id="low-breakage-risk"><a class="header" href="#low-breakage-risk">Low breakage risk</a></h4>
<ul>
<li>[adapters] Adapter API has changed from returning Promise to taking callbacks as the last argument. This won't affect you unless you call on adapter methods directly. <code>database.adapter</code> returns a new <code>DatabaseAdapterCompat</code> which has the same shape as old adapter API. You can use <code>database.adapter.underlyingAdapter</code> to get back <code>SQLiteAdapter</code> / <code>LokiJSAdapter</code></li>
<li>[Collection] <code>Collection.fetchQuery</code> and <code>Collection.fetchCount</code> are removed. Please use <code>Query.fetch()</code> and <code>Query.fetchCount()</code>.</li>
</ul>
<h3 id="new-features-8"><a class="header" href="#new-features-8">New features</a></h3>
<ul>
<li>[SQLiteAdapter] [iOS] Add new <code>synchronous</code> option to adapter: <code>new SQLiteAdapter({ ..., synchronous: true })</code>.
When enabled, database operations will block JavaScript thread. Adapter actions will resolve in the
next microtask, which simplifies building flicker-free interfaces. Adapter will fall back to async
operation when synchronous adapter is not available (e.g. when doing remote debugging)</li>
<li>[LokiJS] Added new <code>onQuotaExceededError?: (error: Error) => void</code> option to <code>LokiJSAdapter</code> constructor.
This is called when underlying IndexedDB encountered a quota exceeded error (ran out of allotted disk space for app)
This means that app can't save more data or that it will fall back to using in-memory database only
Note that this only works when <code>useWebWorker: false</code></li>
</ul>
<h3 id="changes-8"><a class="header" href="#changes-8">Changes</a></h3>
<ul>
<li>[Performance] Watermelon internals have been rewritten not to rely on Promises and allow some fetch/observe calls to resolve synchronously. Do not rely on this -- external API is still based on Rx and Promises and may resolve either asynchronously or synchronously depending on capabilities. This is meant as a internal performance optimization only for the time being.</li>
<li>[LokiJS] [Performance] Improved worker queue implementation for performance</li>
<li>[observation] Refactored observer implementations for performance</li>
</ul>
<h3 id="fixes-8"><a class="header" href="#fixes-8">Fixes</a></h3>
<ul>
<li>Fixed a possible cause for "Record ID xxx#yyy was sent over the bridge, but it's not cached" error</li>
<li>[LokiJS] Fixed an issue preventing database from saving when using <code>experimentalUseIncrementalIndexedDB</code></li>
<li>Fixed a potential issue when using <code>database.unsafeResetDatabase()</code></li>
<li>[iOS] Fixed issue with clearing database under experimental synchronous mode</li>
</ul>
<h3 id="new-features-experimental"><a class="header" href="#new-features-experimental">New features (Experimental)</a></h3>
<ul>
<li>[Model] Added experimental <code>model.experimentalSubscribe((isDeleted) => { ... })</code> method as a vanilla JS alternative to Rx based <code>model.observe()</code>. Unlike the latter, it does not notify the subscriber immediately upon subscription.</li>
<li>[Collection] Added internal <code>collection.experimentalSubscribe((changeSet) => { ... })</code> method as a vanilla JS alternative to Rx based <code>collection.changes</code> (you probably shouldn't be using this API anyway)</li>
<li>[Database] Added experimental <code>database.experimentalSubscribe(['table1', 'table2'], () => { ... })</code> method as a vanilla JS alternative to Rx-based <code>database.withChangesForTables()</code>. Unlike the latter, <code>experimentalSubscribe</code> notifies the subscriber only once after a batch that makes a change in multiple collections subscribed to. It also doesn't notify the subscriber immediately upon subscription, and doesn't send details about the changes, only a signal.</li>
<li>Added <code>experimentalDisableObserveCountThrottling()</code> to <code>@rikishi/watermelondb/observation/observeCount</code> that globally disables count observation throttling. We think that throttling on WatermelonDB level is not a good feature and will be removed in a future release - and will be better implemented on app level if necessary</li>
<li>[Query] Added experimental <code>query.experimentalSubscribe(records => { ... })</code>, <code>query.experimentalSubscribeWithColumns(['col1', 'col2'], records => { ... })</code>, and <code>query.experimentalSubscribeToCount(count => { ... })</code> methods</li>
</ul>
<h2 id="015---2019-11-08"><a class="header" href="#015---2019-11-08">0.15 - 2019-11-08</a></h2>
<h3 id="highlights"><a class="header" href="#highlights">Highlights</a></h3>
<p>This is a <strong>massive</strong> new update to WatermelonDB! 🍉</p>
<ul>
<li>
<p><strong>Up to 23x faster sync</strong>. You heard that right. We've made big improvements to performance.
In our tests, with a massive sync (first login, 45MB of data / 65K records) we got a speed up of:</p>
<ul>
<li>5.7s -> 1.2s on web (5x)</li>
<li>142s -> 6s on iOS (23x)</li>
</ul>
<p>Expect more improvements in the coming releases!</p>
</li>
<li>
<p><strong>Improved LokiJS adapter</strong>. Option to disable web workers, important Safari 13 fix, better performance,
and now works in Private Modes. We recommend adding <code>useWebWorker: false, experimentalUseIncrementalIndexedDB: true</code> options to the <code>LokiJSAdapter</code> constructor to take advantage of the improvements, but please read further changelog to understand the implications of this.</p>
</li>
<li>
<p><strong>Raw SQL queries</strong> now available on iOS and Android thanks to the community</p>
</li>
<li>
<p><strong>Improved TypeScript support</strong> — thanks to the community</p>
</li>
</ul>
<h3 id="-breaking-1"><a class="header" href="#-breaking-1">⚠️ Breaking</a></h3>
<ul>
<li>Deprecated <code>bool</code> schema column type is removed -- please change to <code>boolean</code></li>
<li>Experimental <code>experimentalSetOnlyMarkAsChangedIfDiffers(false)</code> API is now removed</li>
</ul>
<h3 id="new-featuers"><a class="header" href="#new-featuers">New featuers</a></h3>
<ul>
<li>
<p>[Collection] Add <code>Collection.unsafeFetchRecordsWithSQL()</code> method. You can use it to fetch record using
raw SQL queries on iOS and Android. Please be careful to avoid SQL injection and other pitfalls of
raw queries</p>
</li>
<li>
<p>[LokiJS] Introduces new <code>new LokiJSAdapter({ ..., experimentalUseIncrementalIndexedDB: true })</code> option.
When enabled, database will be saved to browser's IndexedDB using a new adapter that only saves the
changed records, instead of the entire database.</p>
<p><strong>This works around a serious bug in Safari 13</strong> (https://bugs.webkit.org/show_bug.cgi?id=202137) that causes large
databases to quickly balloon to gigabytes of temporary trash</p>
<p>This also improves performance of incremental saves, although initial page load or very, very large saves
might be slightly slower.</p>
<p>This is intended to become the new default option, but it's not backwards compatible (if enabled, old database
will be lost). <strong>You're welcome to contribute an automatic migration code.</strong></p>
<p>Note that this option is still experimental, and might change in breaking ways at any time.</p>
</li>
<li>
<p>[LokiJS] Introduces new <code>new LokiJSAdapter({ ..., useWebWorker: false })</code> option. Before, web workers
were always used with <code>LokiJSAdapter</code>. Although web workers may have some performance benefits, disabling them
may lead to lower memory consumption, lower latency, and easier debugging. YMMV.</p>
</li>
<li>
<p>[LokiJS] Added <code>onIndexedDBVersionChange</code> option to <code>LokiJSAdapter</code>. This is a callback that's called
when internal IDB version changed (most likely the database was deleted in another browser tab).
Pass a callback to force log out in this copy of the app as well. Note that this only works when
using incrementalIDB and not using web workers</p>
</li>
<li>
<p>[Model] Add <code>Model._dangerouslySetRawWithoutMarkingColumnChange()</code> method. You probably shouldn't use it,
but if you know what you're doing and want to live-update records from server without marking record as updated,
this is useful</p>
</li>
<li>
<p>[Collection] Add <code>Collection.prepareCreateFromDirtyRaw()</code></p>
</li>
<li>
<p>@json decorator sanitizer functions take an optional second argument, with a reference to the model</p>
</li>
</ul>
<h3 id="fixes-9"><a class="header" href="#fixes-9">Fixes</a></h3>
<ul>
<li>Pinned required <code>rambdax</code> version to 2.15.0 to avoid console logging bug. In a future release we will switch to our own fork of <code>rambdax</code> to avoid future breakages like this.</li>
</ul>
<h3 id="improvements"><a class="header" href="#improvements">Improvements</a></h3>
<ul>
<li>[Performance] Make large batches a lot faster (1.3s shaved off on a 65K insert sample)</li>
<li>[Performance] [iOS] Make large batch inserts an order of magnitude faster</li>
<li>[Performance] [iOS] Make encoding very large queries (with thousands of parameters) 20x faster</li>
<li>[Performance] [LokiJS] Make batch inserts faster (1.5s shaved off on a 65K insert sample)</li>
<li>[Performance] [LokiJS] Various performance improvements</li>
<li>[Performance] [Sync] Make Sync faster</li>
<li>[Performance] Make observation faster</li>
<li>[Performance] [Android] Make batches faster</li>
<li>Fix app glitches and performance issues caused by race conditions in <code>Query.observeWithColumns()</code></li>
<li>[LokiJS] Persistence adapter will now be automatically selected based on availability. By default,
IndexedDB is used. But now, if unavailable (e.g. in private mode), ephemeral memory adapter will be used.</li>
<li>Disabled console logs regarding new observations (it never actually counted all observations) and
time to query/count/batch (the measures were wildly inaccurate because of asynchronicity - actual
times are much lower)</li>
<li>[withObservables] Improved performance and debuggability (update withObservables package separately)</li>
<li>Improved debuggability of Watermelon -- shortened Rx stacks and added function names to aid in understanding
call stacks and profiles</li>
<li>[adapters] The adapters interface has changed. <code>query()</code> and <code>count()</code> methods now receive a <code>SerializedQuery</code>, and <code>batch()</code> now takes <code>TableName<any></code> and <code>RawRecord</code> or <code>RecordId</code> instead of <code>Model</code>.</li>
<li>[Typescript] Typing improvements
<ul>
<li>Added 3 missing properties <code>collections</code>, <code>database</code> and <code>asModel</code> in Model type definition.</li>
<li>Removed optional flag on <code>actionsEnabled</code> in the Database constructor options since its mandatory since 0.13.0.</li>
<li>fixed several further typing issues in Model, Relation and lazy decorator</li>
</ul>
</li>
<li>Changed how async functions are transpiled in the library. This could break on really old Android phones
but shouldn't matter if you use latest version of React Native. Please report an issue if you see a problem.</li>
<li>Avoid <code>database</code> prop drilling in the web demo</li>
</ul>
<h2 id="0141---2019-08-31"><a class="header" href="#0141---2019-08-31">0.14.1 - 2019-08-31</a></h2>
<p>Hotfix for rambdax crash</p>
<ul>
<li>[Schema] Handle invalid table schema argument in appSchema</li>
<li>[withObservables] Added TypeScript support (<a href="https://github.com/Nozbe/withObservables/blob/master/CHANGELOG.md">changelog</a>)</li>
<li>[Electron] avoid <code>Uncaught ReferenceError: global is not defined</code> in electron runtime (<a href="https://github.com/Nozbe/WatermelonDB/issues/453">#453</a>)</li>
<li>[rambdax] Replaces <code>contains</code> with <code>includes</code> due to <code>contains</code> deprecation https://github.com/selfrefactor/rambda/commit/1dc1368f81e9f398664c9d95c2efbc48b5cdff9b#diff-04c6e90faac2675aa89e2176d2eec7d8R2209</li>
</ul>
<h2 id="0140---2019-08-02"><a class="header" href="#0140---2019-08-02">0.14.0 - 2019-08-02</a></h2>
<h3 id="new-features-9"><a class="header" href="#new-features-9">New features</a></h3>
<ul>
<li>[Query] Added support for <code>notLike</code> queries 🎉</li>
<li>[Actions] You can now batch delete record with all descendants using experimental functions <code>experimentalMarkAsDeleted</code> or <code>experimentalDestroyPermanently</code></li>
</ul>
<h2 id="0130---2019-07-18"><a class="header" href="#0130---2019-07-18">0.13.0 - 2019-07-18</a></h2>
<h3 id="-breaking-2"><a class="header" href="#-breaking-2">⚠️ Breaking</a></h3>
<ul>
<li>
<p>[Database] It is now mandatory to pass <code>actionsEnabled:</code> option to Database constructor.
It is recommended that you enable this option:</p>
<pre><code class="language-js">const database = new Database({
adapter: ...,
modelClasses: [...],
actionsEnabled: true
})
</code></pre>
<p>See <code>docs/Actions.md</code> for more details about Actions. You can also pass <code>false</code> to maintain
backward compatibility, but this option <strong>will be removed</strong> in a later version</p>
</li>
<li>
<p>[Adapters] <code>migrationsExperimental</code> prop of <code>SQLiteAdapter</code> and <code>LokiJSAdapter</code> has been renamed
to <code>migrations</code>.</p>
</li>
</ul>
<h3 id="new-features-10"><a class="header" href="#new-features-10">New features</a></h3>
<ul>
<li>[Actions] You can now batch deletes by using <code>prepareMarkAsDeleted</code> or <code>prepareDestroyPermanently</code></li>
<li>[Sync] Performance: <code>synchronize()</code> no longer calls your <code>pushChanges()</code> function if there are no
local changes to push. This is meant to save unnecessary network bandwidth. ⚠️ Note that this
could be a breaking change if you rely on it always being called</li>
<li>[Sync] When setting new values to fields on a record, the field (and record) will no longer be
marked as changed if the field's value is the same. This is meant to improve performance and avoid
unnecessary code in the app. ⚠️ Note that this could be a breaking change if you rely on the old
behavior. For now you can import <code>experimentalSetOnlyMarkAsChangedIfDiffers</code> from
<code>@rikishi/watermelondb/Model/index</code> and call if with <code>(false)</code> to bring the old behavior back, but
this will be removed in the later version -- create a new issue explaining why you need this</li>
<li>[Sync] Small perf improvements</li>
</ul>
<h3 id="improvements-1"><a class="header" href="#improvements-1">Improvements</a></h3>
<ul>
<li>[Typescript] Improved types for SQLite and LokiJS adapters, migrations, models, the database and the logger.</li>
</ul>
<h2 id="0123---2019-05-06"><a class="header" href="#0123---2019-05-06">0.12.3 - 2019-05-06</a></h2>
<h3 id="changes-9"><a class="header" href="#changes-9">Changes</a></h3>
<ul>
<li>[Database] You can now update the random id schema by importing
<code>import { setGenerator } from '@rikishi/watermelondb/utils/common/randomId'</code> and then calling <code>setGenerator(newGenenerator)</code>.
This allows WatermelonDB to create specific IDs for example if your backend uses UUIDs.</li>
<li>[Typescript] Type improvements to SQLiteAdapter and Database</li>
<li>[Tests] remove cleanup for react-hooks-testing-library@0.5.0 compatibility</li>
</ul>
<h2 id="0122---2019-04-19"><a class="header" href="#0122---2019-04-19">0.12.2 - 2019-04-19</a></h2>
<h3 id="fixes-10"><a class="header" href="#fixes-10">Fixes</a></h3>
<ul>
<li>[TypeScript] 'Cannot use 'in' operator to search for 'initializer'; decorator fix</li>
</ul>
<h3 id="changes-10"><a class="header" href="#changes-10">Changes</a></h3>
<ul>
<li>[Database] You can now pass falsy values to <code>Database.batch(...)</code> (false, null, undefined). This is
useful in keeping code clean when doing operations conditionally. (Also works with <code>model.batch(...)</code>)</li>
<li>[Decorators]. You can now use <code>@action</code> on methods of any object that has a <code>database: Database</code>
property, and <code>@field @children @date @relation @immutableRelation @json @text @nochange</code> decorators on
any object with a <code>asModel: Model</code> property.</li>
<li>[Sync] Adds a temporary/experimental <code>_unsafeBatchPerCollection: true</code> flag to <code>synchronize()</code>. This
causes server changes to be committed to database in multiple batches, and not one. This is NOT preferred
for reliability and performance reasons, but it works around a memory issue that might cause your app
to crash on very large syncs (>20,000 records). Use this only if necessary. Note that this option
might be removed at any time if a better solution is found.</li>
</ul>
<h2 id="0121---2019-04-01"><a class="header" href="#0121---2019-04-01">0.12.1 - 2019-04-01</a></h2>
<h3 id="-hotfix"><a class="header" href="#-hotfix">⚠️ Hotfix</a></h3>
<ul>
<li>
<p>[iOS] Fix runtime crash when built with Xcode 10.2 (Swift 5 runtime).</p>
<p><strong>⚠️ Note</strong>: You need to upgrade to React Native 0.59.3 for this to work. If you can't upgrade
React Native yet, either stick to Xcode 10.1 or manually apply this patch:
https://github.com/Nozbe/WatermelonDB/pull/302/commits/aa4e08ad0fa55f434da2a94407c51fc5ff18e506</p>
</li>
</ul>
<h3 id="changes-11"><a class="header" href="#changes-11">Changes</a></h3>
<ul>
<li>[Sync] Adds basic sync logging capability to Sync. Pass an empty object to <code>synchronize()</code> to populate it with diagnostic information:
<pre><code class="language-js">const log = {}
await synchronize({ database, log, ...})
console.log(log.startedAt)
</code></pre>
See Sync documentation for more details.</li>
</ul>
<h2 id="0120---2019-03-18"><a class="header" href="#0120---2019-03-18">0.12.0 - 2019-03-18</a></h2>
<h3 id="added"><a class="header" href="#added">Added</a></h3>
<ul>
<li>[Hooks] new <code>useDatabase</code> hook for consuming the Database Context:
<pre><code class="language-js">import { useDatabase } from '@rikishi/watermelondb/hooks';
const Component = () => {
const database = useDatabase();
}
</code></pre>
</li>
<li>[TypeScript] added <code>.d.ts</code> files. Please note: TypeScript definitions are currently incomplete and should be used as a guide only. <strong>PRs for improvements would be greatly appreciated!</strong></li>
</ul>
<h3 id="performance-4"><a class="header" href="#performance-4">Performance</a></h3>
<ul>
<li>Improved UI performance by consolidating multiple observation emissions into a single per-collection batch emission when doing batch changes</li>
</ul>
<h2 id="0110---2019-03-12"><a class="header" href="#0110---2019-03-12">0.11.0 - 2019-03-12</a></h2>
<h3 id="breaking"><a class="header" href="#breaking">Breaking</a></h3>
<ul>
<li>⚠️ Potentially BREAKING fix: a <code>@date</code> field now returns a Jan 1, 1970 date instead of <code>null</code> if the field's raw value is <code>0</code>.
This is considered a bug fix, since it's unexpected to receive a <code>null</code> from a getter of a field whose column schema doesn't say <code>isOptional: true</code>.
However, if you relied on this behavior, this might be a breaking change.</li>
<li>⚠️ BREAKING: <code>Database.unsafeResetDatabase()</code> now requires that you run it inside an Action</li>
</ul>
<h3 id="bug-fixes"><a class="header" href="#bug-fixes">Bug fixes</a></h3>
<ul>
<li>[Sync] Fixed an issue where synchronization would continue running despite <code>unsafeResetDatabase</code> being called</li>
<li>[Android] fix compile error for kotlin 1.3+</li>
</ul>
<h3 id="other-changes"><a class="header" href="#other-changes">Other changes</a></h3>
<ul>
<li>Actions are now aborted when <code>unsafeResetDatabase()</code> is called, making reseting database a little bit safer</li>
<li>Updated demo dependencies</li>
<li>LokiJS is now a dependency of WatermelonDB (although it's only required for use on the web)</li>
<li>[Android] removed unused test class</li>
<li>[Android] updated ktlint to <code>0.30.0</code></li>
</ul>
<h2 id="0101---2019-02-12"><a class="header" href="#0101---2019-02-12">0.10.1 - 2019-02-12</a></h2>
<h3 id="changes-12"><a class="header" href="#changes-12">Changes</a></h3>
<ul>
<li>[Android] Changed <code>compile</code> to <code>implementation</code> in Library Gradle file
<ul>
<li>⚠️ might break build if you are using Android Gradle Plugin <3.X</li>
</ul>
</li>
<li>Updated <code>peerDependency</code> <code>react-native</code> to <code>0.57.0</code></li>
<li>[Sync] Added <code>hasUnsyncedChanges()</code> helper method</li>
<li>[Sync] Improved documentation for backends that can't distinguish between <code>created</code> and <code>updated</code> records</li>
<li>[Sync] Improved diagnostics / protection against edge cases</li>
<li>[iOS] Add missing <code>header search path</code> to support <strong>ejected</strong> expo project.</li>
<li>[Android] Fix crash on android < 5.0</li>
<li>[iOS] <code>SQLiteAdapter</code>'s <code>dbName</code> path now allows you to pass an absolute path to a file, instead of a name</li>
<li>[Web] Add adaptive layout for demo example with smooth scrolling for iOS</li>
</ul>
<h2 id="0100---2019-01-18"><a class="header" href="#0100---2019-01-18">0.10.0 - 2019-01-18</a></h2>
<h3 id="breaking-1"><a class="header" href="#breaking-1">Breaking</a></h3>
<ul>
<li><strong>BREAKING:</strong> Table column <code>last_modified</code> is no longer automatically added to all database tables. If
you don't use this column (e.g. in your custom sync code), you don't have to do anything.
If you do, manually add this column to all table definitions in your Schema:
<pre><code>{ name: 'last_modified', type: 'number', isOptional: true }
</code></pre>
<strong>Don't</strong> bump schema version or write a migration for this.</li>
</ul>
<h3 id="new"><a class="header" href="#new">New</a></h3>
<ul>
<li>
<p><strong>Actions API</strong>.</p>
<p>This was actually released in 0.8.0 but is now documented in <a href="./docs/CRUD.html">CRUD.md</a> and <a href="./docs/Actions.html">Actions.md</a>.
With Actions enabled, all create/update/delete/batch calls must be wrapped in an Action.</p>
<p>To use Actions, call <code>await database.action(async () => { /* perform writes here */ }</code>, and in
Model instance methods, you can just decorate the whole method with <code>@action</code>.</p>
<p>This is necessary for Watermelon Sync, and also to enable greater safety and consistency.</p>
<p>To enable actions, add <code>actionsEnabled: true</code> to <code>new Database({ ... })</code>. In a future release this
will be enabled by default, and later, made mandatory.</p>
<p>See documentation for more details.</p>
</li>
<li>
<p><strong>Watermelon Sync Adapter</strong> (Experimental)</p>
<p>Added <code>synchronize()</code> function that allows you to easily add full synchronization capabilities to
your Watermelon app. You only need to provide two fetch calls to your remote server that conforms
to Watermelon synchronization protocol, and all the client-side processing (applying remote changes,
resolving conflicts, finding local changes, and marking them as synced) is done by Watermelon.</p>
<p>See documentation for more details.</p>
</li>
<li>
<p><strong>Support caching for non-global IDs at Native level</strong></p>
</li>
</ul>
<h2 id="090---2018-11-23"><a class="header" href="#090---2018-11-23">0.9.0 - 2018-11-23</a></h2>
<h3 id="new-1"><a class="header" href="#new-1">New</a></h3>
<ul>
<li>Added <code>Q.like</code> - you can now make queries similar to SQL <code>LIKE</code></li>
</ul>
<h2 id="080---2018-11-16"><a class="header" href="#080---2018-11-16">0.8.0 - 2018-11-16</a></h2>
<h3 id="new-2"><a class="header" href="#new-2">New</a></h3>
<ul>
<li>Added <code>DatabaseProvider</code> and <code>withDatabase</code> Higher-Order Component to reduce prop drilling</li>
<li>Added experimental Actions API. This will be documented in a future release.</li>
</ul>
<h3 id="fixes-11"><a class="header" href="#fixes-11">Fixes</a></h3>
<ul>
<li>Fixes crash on older Android React Native targets without <code>jsc-android</code> installed</li>
</ul>
<h2 id="070---2018-10-31"><a class="header" href="#070---2018-10-31">0.7.0 - 2018-10-31</a></h2>
<h3 id="deprecations-2"><a class="header" href="#deprecations-2">Deprecations</a></h3>
<ul>
<li>[Schema] Column type 'bool' is deprecated — change to 'boolean'</li>
</ul>
<h3 id="new-3"><a class="header" href="#new-3">New</a></h3>
<ul>
<li>Added support for Schema Migrations. See documentation for more details.</li>
<li>Added fundaments for integration of Danger with Jest</li>
</ul>
<h3 id="changes-13"><a class="header" href="#changes-13">Changes</a></h3>
<ul>
<li>Fixed "dependency cycle" warning</li>
<li>[SQLite] Fixed rare cases where database could be left in an unusable state (added missing transaction)</li>
<li>[Flow] Fixes <code>oneOf()</code> typing and some other variance errors</li>
<li>[React Native] App should launch a little faster, because schema is only compiled on demand now</li>
<li>Fixed typos in README.md</li>
<li>Updated Flow to 0.85</li>
</ul>
<h2 id="062---2018-10-04"><a class="header" href="#062---2018-10-04">0.6.2 - 2018-10-04</a></h2>
<h3 id="deprecations-3"><a class="header" href="#deprecations-3">Deprecations</a></h3>
<ul>
<li>The <code>@rikishi/watermelondb/babel/cjs</code> / <code>@rikishi/watermelondb/babel/esm</code> Babel plugin that ships with Watermelon is deprecated and no longer necessary. Delete it from your Babel config as it will be removed in a future update</li>
</ul>
<h3 id="refactoring"><a class="header" href="#refactoring">Refactoring</a></h3>
<ul>
<li>Removed dependency on <code>async</code> (Web Worker should be ~30KB smaller)</li>
<li>Refactored <code>Collection</code> and <code>simpleObserver</code> for getting changes in an array and also adds CollectionChangeTypes for differentiation between different changes</li>
<li>Updated dependencies</li>
<li>Simplified build system by using relative imports</li>
<li>Simplified build package by outputting CJS-only files</li>
</ul>
<h2 id="061---2018-09-20"><a class="header" href="#061---2018-09-20">0.6.1 - 2018-09-20</a></h2>
<h3 id="added-1"><a class="header" href="#added-1">Added</a></h3>
<ul>
<li>Added iOS and Android integration tests and lint checks to TravisCI</li>
</ul>
<h3 id="changed"><a class="header" href="#changed">Changed</a></h3>
<ul>
<li>Changed Flow setup for apps using Watermelon - see docs/Advanced/Flow.md</li>
<li>Improved documentation, and demo code</li>
<li>Updated dependencies</li>
</ul>
<h3 id="fixed"><a class="header" href="#fixed">Fixed</a></h3>
<ul>
<li>Add quotes to all names in sql queries to allow keywords as table or column names</li>
<li>Fixed running model tests in apps with Watermelon in the loop</li>
<li>Fixed Flow when using Watermelon in apps</li>
</ul>
<h2 id="060---2018-09-05"><a class="header" href="#060---2018-09-05">0.6.0 - 2018-09-05</a></h2>
<p>Initial release of WatermelonDB</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
</nav>
</div>
<script type="text/javascript">
window.playground_copyable = true;
</script>
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
<script type="text/javascript">
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
</script>
</body>
</html>