Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 44 additions & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,64 @@
name: Build and test
name: CI

on:
push:
pull_request:
branches:
- main

env:
CARGO_TERM_COLOR: always
permissions:
contents: read

jobs:
rust:
name: Build and test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)
uses: actions/cache@v4
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- run: rustup update stable && rustup default stable && rustup component add clippy
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)

- run: cargo build --verbose
name: Building project
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)
- name: Run tests
run: cargo test --all-features

- name: Build
run: cargo build --all-features

lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- run: cargo clippy
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)
- name: Check formatting
run: cargo fmt --check

- run: cargo test --verbose
name: Testing project
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)
- name: Lint with clippy
run: cargo clippy --all-targets --all-features -- -D warnings
58 changes: 58 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: PR

on:
pull_request:
branches:
- main

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Install cargo-deny
uses: taiki-e/install-action@cargo-deny

- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Format code
run: cargo fmt

- name: Lint with clippy
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Run tests
run: cargo test --all-features

- name: Check for security vulnerabilities
run: cargo deny check

- name: Build
run: cargo build --all-features

osv-scan:
uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.3.5
permissions:
actions: read
contents: read
security-events: write
pull-requests: write
9 changes: 9 additions & 0 deletions .hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/sh
set -e

echo "==> Running pre-commit checks..."

echo "==> Checking formatting..."
cargo fmt --check

echo "==> pre-commit checks passed"
12 changes: 12 additions & 0 deletions .hooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
set -e

echo "==> Running pre-push checks..."

echo "==> Running clippy..."
cargo clippy --all-targets --all-features -- -D warnings

echo "==> Running cargo-deny..."
cargo deny check

echo "==> pre-push checks passed"
65 changes: 65 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
[graph]
targets = []
all-features = false

[licenses]
allow = [
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"MPL-2.0",
"Zlib",
"Unicode-3.0",
"CDLA-Permissive-2.0",
"CC0-1.0",
"OFL-1.1",
"BSL-1.0",
"OpenSSL",
"Ubuntu-font-1.0",
]
exceptions = []
# Workspace crates are private/unlicensed
[[licenses.clarify]]
crate = "powersync"
expression = "Apache-2.0"
license-files = []
[[licenses.clarify]]
crate = "powersync_test_utils"
expression = "Apache-2.0"
license-files = []
# egui_todolist is an example app — treat as Apache-2.0
[[licenses.clarify]]
crate = "egui_todolist"
expression = "Apache-2.0"
license-files = []
# aws-lc-sys pulls in OpenSSL — add exception for ISC+OpenSSL combined license
[[licenses.clarify]]
crate = "aws-lc-sys"
expression = "ISC AND (Apache-2.0 OR ISC) AND OpenSSL"
license-files = []

[bans]
multiple-versions = "warn"
wildcards = "allow"

[advisories]
ignore = [
# aws-lc-sys transitive advisory — upstream is aws-lc
"RUSTSEC-2026-0047", # AWS-LC: CN wildcard bypass
"RUSTSEC-2026-0048", # AWS-LC: CRL DP scope check error
"RUSTSEC-2026-0044", # AWS-LC: CN wildcard bypass via Unicode
# rustls-webpki advisory — transitive via reqwest's rustls
"RUSTSEC-2026-0049", # rustls-webpki: CRL matching logic error
# remove_dir_all — transitive via powersync_test_utils dev-dependency tempdir
"RUSTSEC-2023-0018", # remove_dir_all: TOCTOU race condition
# tempdir unmaintained — transitive via powersync_test_utils
"RUSTSEC-2018-0017", # tempdir deprecated
]

[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-git = []
13 changes: 13 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
pre-commit:
parallel: true
commands:
cargo-fmt:
run: cargo fmt --check

pre-push:
parallel: true
commands:
cargo-clippy:
run: cargo clippy --all-targets --all-features -- -D warnings
cargo-deny:
run: cargo deny check
6 changes: 5 additions & 1 deletion powersync/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async-oneshot = "0.5.9"
atomic_enum = "0.3.0"
event-listener = "5.4.1"
futures-lite = "2.6.1"
reqwest = { version = "0.13.2", optional = true, features = ["stream"] }
reqwest = { version = "0.13.2", default-features = false, optional = true, features = ["stream", "rustls"] }
bytes = "1"
log = "0.4.28"
pin-project-lite = "0.2.16"
Expand All @@ -49,3 +49,7 @@ async-task = "4.7.1"
futures-lite = "2.6.1"
futures-test = "0.3.31"
powersync_test_utils = { path = "../powersync_test_utils" }

[lints.clippy]
clone_on_copy = "allow"
derivable_impls = "allow"
4 changes: 2 additions & 2 deletions powersync/src/db/internal.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use event_listener::EventListener;
use futures_lite::{FutureExt, Stream, StreamExt, ready};
use rusqlite::{Connection, params};
use rusqlite::{Connection, TransactionBehavior, params};
use std::sync::{Mutex, Weak};
use std::time::Duration;
use std::{
Expand Down Expand Up @@ -97,7 +97,7 @@ impl InnerPowerSyncState {
write_checkpoint: Option<i64>,
) -> Result<(), PowerSyncError> {
let mut writer = self.writer().await?;
let writer = writer.transaction()?;
let writer = writer.transaction_with_behavior(TransactionBehavior::Immediate)?;

writer.execute("DELETE FROM ps_crud WHERE id <= ?", params![last_client_id])?;
let mut target_op: i64 = MAX_OP_ID;
Expand Down
20 changes: 11 additions & 9 deletions powersync/src/db/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ impl ConnectionPool {
for _ in 0..5 {
let reader = Connection::open(&path)?;
reader.pragma_update(None, "query_only", true)?;
reader.pragma_update(None, "busy_timeout", 30_000)?;
readers.push(reader);
}

Expand Down Expand Up @@ -99,7 +100,7 @@ impl ConnectionPool {
LeasedConnection {
inner: OwnedConnectionLease::Reader {
connection: MaybeUninit::new(reader),
pool: self.clone(),
release: readers.release_reader.clone(),
},
}
} else {
Expand Down Expand Up @@ -141,7 +142,7 @@ impl ConnectionPool {
LeasedConnection {
inner: OwnedConnectionLease::Reader {
connection: MaybeUninit::new(reader),
pool: self.clone(),
release: readers.release_reader.clone(),
},
}
} else {
Expand Down Expand Up @@ -196,7 +197,9 @@ enum OwnedConnectionLease {
},
Reader {
connection: MaybeUninit<Connection>,
pool: ConnectionPool,
/// Sender cloned at lease creation — avoids navigating back through the pool
/// and eliminates the need for an `unwrap()` on `pool.state.readers` in Drop.
release: Sender<Connection>,
},
}

Expand All @@ -207,18 +210,17 @@ impl Drop for OwnedConnectionLease {
// Send update notifications for writes made on this connection while leased.
let _ = pool.take_update_notifications(connection);
}
OwnedConnectionLease::Reader { connection, pool } => {
OwnedConnectionLease::Reader {
connection,
release,
} => {
let connection = std::mem::replace(connection, MaybeUninit::uninit());
let connection = unsafe {
// safety: Only dropped here
connection.assume_init()
};

pool.state
.readers
.as_ref()
.unwrap()
.release_reader
release
.send_blocking(connection)
.expect("should send connection into pool");
}
Expand Down
4 changes: 2 additions & 2 deletions powersync/src/db/streams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
},
util::SerializedJsonObject,
};
use rusqlite::params;
use rusqlite::{TransactionBehavior, params};
use std::{
cell::Cell,
collections::HashMap,
Expand Down Expand Up @@ -85,7 +85,7 @@ impl<'a> SyncStream<'a> {
let serialized = serde_json::to_string(cmd)?;

let mut writer = self.db.writer().await?;
let writer = writer.transaction()?;
let writer = writer.transaction_with_behavior(TransactionBehavior::Immediate)?;

{
let mut stmt = writer.prepare_cached("SELECT powersync_control(?, ?)")?;
Expand Down
6 changes: 6 additions & 0 deletions powersync/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ impl From<RawPowerSyncError> for PowerSyncError {
}
}

impl From<io::Error> for PowerSyncError {
fn from(value: io::Error) -> Self {
RawPowerSyncError::IO { inner: value }.into()
}
}

impl Display for PowerSyncError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.inner.fmt(f)
Expand Down
9 changes: 5 additions & 4 deletions powersync/src/sync/download/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ pub fn sync_stream(

let stream = stream::once_future(response);

StreamExt::flat_map(stream, |response| {
let items = response_to_lines(response);

stream::once(Ok(DownloadEvent::ConnectionEstablished)).chain(items)
StreamExt::flat_map(stream, |response| match response {
Err(e) => stream::once(Err(e)).boxed(),
Ok(response) => stream::once(Ok(DownloadEvent::ConnectionEstablished))
.chain(response_to_lines(Ok(response)))
.boxed(),
})
}

Expand Down
Loading