Skip to content
Open
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
10 changes: 10 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,13 @@ npm_link_package(
src = "@valdi//src/valdi_modules/src/valdi/valdi_core:valdi_core_dts",
visibility = ["//visibility:public"],
)

filegroup(
name = "clientsql_generator_test_data",
srcs = [
"MODULE.bazel",
"fossa-deps.yml",
],
testonly = True,
visibility = ["//compiler/clientsql:__pkg__"],
)
18 changes: 18 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,24 @@ http_archive(
url = "https://github.com/fmtlib/fmt/releases/download/7.1.3/fmt-7.1.3.zip",
)

# MODULE.bazel and WORKSPACE repository setup cannot share repository-rule macros. Keep this
# declaration in sync with bzl/dependencies.bzl; ClientSQL's dependency-shape test guards it.
http_archive(
name = "valdi_clientsql_sqlite",
build_file = "@valdi//third-party/sqlite:sqlite.BUILD",
sha256 = "0e9483900e92cd5de8fd48d16bf9200145a61f7fd5be542a5ac81d8a9516eb9c",
strip_prefix = "sqlite-autoconf-3530400",
url = "https://www.sqlite.org/2026/sqlite-autoconf-3530400.tar.gz",
)

http_archive(
name = "valdi_clientsql_sqlite_316",
build_file = "@valdi//third-party/sqlite:sqlite_316.BUILD",
sha256 = "3b5dfb65807e2b17e6463357df848e322badba01dc9a4a1de8fdbb72d448e3b0",
strip_prefix = "sqlite-amalgamation-3160000",
url = "https://www.sqlite.org/2017/sqlite-amalgamation-3160000.zip",
)

bazel_dep(name = "android_macros")
local_path_override(
module_name = "android_macros",
Expand Down
12 changes: 12 additions & 0 deletions apps/ledger_sql_demo/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
load("//bzl/valdi:valdi_application.bzl", "valdi_application")

valdi_application(
name = "ledger_sql_demo",
desktop_window_height = 820,
desktop_window_width = 900,
ios_bundle_id = "com.snap.valdi.ledgersqldemo",
root_component_path = "App@ledger_sql_demo/src/LedgerSqlDemoApp",
title = "Ledger SQL Demo",
version = "1.0.0",
deps = ["//apps/ledger_sql_demo/src/valdi/ledger_sql_demo"],
)
41 changes: 41 additions & 0 deletions apps/ledger_sql_demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Ledger SQL Demo

Valdi app that exercises ClientSQL generated bindings with a small double-entry ledger.

The app opens `LedgerDb`, seeds a handful of accounts, and subscribes to reactive aggregate queries for balances, recent ledger entries, and the transaction log. Transfers run inside `LedgerDb.transaction()`: each transfer inserts the debit entry, credit entry, and transaction log row as one committed unit. The generated binding calls the native ClientSQL transaction API, so writer work is serialized by the SQLite runtime and watched aggregate queries refresh after the native commit completes.

The Valdi debugger Data section includes a ClientSQL browser that can inspect the live database while this demo is running. For a live target it shows table rows plus ClientSQL runtime state such as active transaction, deferred writer work, handle count, reader pool readiness, watcher count, and a bounded transaction history with commit/rollback durations.

Expected local workflow:

```bash
valdi install macos --application //apps/ledger_sql_demo:ledger_sql_demo_macos
valdi hotreload --target //apps/ledger_sql_demo:ledger_sql_demo_hotreload
```

Hot reload can update the Valdi TypeScript UI, but changes to the native ClientSQL runtime require rebuilding and relaunching the macOS app.

iOS build validation:

```bash
cd apps/ledger_sql_demo
npm run ios:build
```

Android build and install workflow:

```bash
valdi install android \
--application //apps/ledger_sql_demo:ledger_sql_demo_android
```

To run the same flow as an Android smoke check, with a UI assertion and screenshot capture:

```bash
cd apps/ledger_sql_demo
npm run android:smoke:build
```

The smoke script waits for a connected emulator/device, installs the APK, launches the demo, taps `Run stress batch`, waits for the transaction-complete status text, and writes `/tmp/ledger-sql-android-smoke.png`. Use `ADB=/path/to/adb` or `--device <serial>` when your local `adb` selection needs to be explicit.

The demo should seed on first launch, and the `Run stress batch` button should append four transfer rows in one transaction, causing the reactive totals to refresh once after commit.
13 changes: 13 additions & 0 deletions apps/ledger_sql_demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "ledger_sql_demo",
"private": true,
"scripts": {
"macos": "valdi install macos --application //apps/ledger_sql_demo:ledger_sql_demo_macos",
"hotreload": "valdi hotreload --target //apps/ledger_sql_demo:ledger_sql_demo_hotreload",
"ios:build": "valdi build ios --application //apps/ledger_sql_demo:ledger_sql_demo_ios",
"android:build": "valdi build android --application //apps/ledger_sql_demo:ledger_sql_demo_android",
"android:install": "valdi install android --application //apps/ledger_sql_demo:ledger_sql_demo_android",
"android:smoke": "python3 scripts/android_smoke.py",
"android:smoke:build": "python3 scripts/android_smoke.py --build"
}
}
199 changes: 199 additions & 0 deletions apps/ledger_sql_demo/scripts/android_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""Build/install/launch smoke check for the Ledger ClientSQL Android demo."""

from __future__ import annotations

import argparse
import os
import re
import shutil
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Sequence


ROOT = Path(__file__).resolve().parents[3]
DEFAULT_APK = ROOT / "bazel-bin/apps/ledger_sql_demo/ledger_sql_demo_android.apk"
DEFAULT_SCREENSHOT = Path("/tmp/ledger-sql-android-smoke.png")
PACKAGE = "com.snap.valdi.ledger_sql_demo"
ACTIVITY = f"{PACKAGE}/.StartActivity"
ANDROID_BUILD_COMMAND = [
"valdi",
"build",
"android",
"--application",
"//apps/ledger_sql_demo:ledger_sql_demo_android",
]
BOUNDS_RE = re.compile(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]")


def run(args: Sequence[str], *, cwd: Path = ROOT, capture: bool = False) -> subprocess.CompletedProcess:
print("$ " + " ".join(str(arg) for arg in args), flush=True)
return subprocess.run(
[str(arg) for arg in args],
cwd=cwd,
capture_output=capture,
check=True,
)


def default_adb_path() -> str:
if os.environ.get("ADB"):
return os.environ["ADB"]
if shutil.which("adb"):
return "adb"

candidates = []
for variable in ("ANDROID_HOME", "ANDROID_SDK_ROOT"):
if os.environ.get(variable):
candidates.append(Path(os.environ[variable]) / "platform-tools/adb")
candidates.append(Path.home() / "Library/Android/sdk/platform-tools/adb")

for candidate in candidates:
if candidate.exists():
return str(candidate)
return "adb"


def adb(args: Sequence[str], *, device: str | None, capture: bool = False) -> subprocess.CompletedProcess:
adb_path = default_adb_path()
command = [adb_path]
if device:
command.extend(["-s", device])
command.extend(args)
return run(command, capture=capture)


def wait_for_boot(device: str | None, timeout_seconds: float) -> None:
adb(["wait-for-device"], device=device)
deadline = time.monotonic() + timeout_seconds
while time.monotonic() < deadline:
result = adb(["shell", "getprop", "sys.boot_completed"], device=device, capture=True)
if result.stdout.decode(errors="replace").strip() == "1":
return
time.sleep(1)
raise TimeoutError("Android device did not finish booting before the smoke timeout")


def dump_ui(device: str | None) -> str:
adb(["shell", "uiautomator", "dump", "/sdcard/ledger_sql_demo_window.xml"], device=device)
result = adb(["exec-out", "cat", "/sdcard/ledger_sql_demo_window.xml"], device=device, capture=True)
return result.stdout.decode(errors="replace")


def node_text(node: ET.Element) -> str:
return node.attrib.get("text", "")


def parse_ui_xml(xml_text: str) -> ET.Element | None:
try:
return ET.fromstring(xml_text)
except ET.ParseError:
return None


def find_bounds(xml_text: str, text: str) -> tuple[int, int, int, int] | None:
root = parse_ui_xml(xml_text)
if root is None:
return None
for node in root.iter("node"):
candidate = node_text(node)
if candidate == text or text in candidate:
bounds = node.attrib.get("bounds", "")
match = BOUNDS_RE.fullmatch(bounds)
if match:
return (
int(match.group(1)),
int(match.group(2)),
int(match.group(3)),
int(match.group(4)),
)
return None


def contains_text(xml_text: str, text: str) -> bool:
root = parse_ui_xml(xml_text)
if root is None:
return False
return any(text in node_text(node) for node in root.iter("node"))


def tap_bounds(device: str | None, bounds: tuple[int, int, int, int]) -> None:
left, top, right, bottom = bounds
adb(["shell", "input", "tap", str((left + right) // 2), str((top + bottom) // 2)], device=device)


def tap_text(device: str | None, text: str) -> bool:
bounds = find_bounds(dump_ui(device), text)
if bounds is None:
return False
tap_bounds(device, bounds)
return True


def wait_for_text(device: str | None, text: str, timeout_seconds: float) -> None:
deadline = time.monotonic() + timeout_seconds
last_xml = ""
while time.monotonic() < deadline:
last_xml = dump_ui(device)
if contains_text(last_xml, text):
return
time.sleep(1)
raise AssertionError(f"Did not find Android UI text {text!r}. Last dump:\n{last_xml[:2000]}")


def screenshot(device: str | None, path: Path) -> None:
result = adb(["exec-out", "screencap", "-p"], device=device, capture=True)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(result.stdout)
print(f"Wrote screenshot: {path}", flush=True)


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--build", action="store_true", help="Build the Android APK before installing it.")
parser.add_argument("--apk", type=Path, default=DEFAULT_APK, help="APK to install.")
parser.add_argument("--device", help="adb device serial. Defaults to adb's selected device.")
parser.add_argument("--timeout", type=float, default=60, help="Seconds to wait for boot and UI text.")
parser.add_argument("--reset-seed", action="store_true", help="Tap Reset seed data before the stress batch.")
parser.add_argument("--screenshot", type=Path, default=DEFAULT_SCREENSHOT, help="Screenshot output path.")
args = parser.parse_args()

if args.build:
run(ANDROID_BUILD_COMMAND)

if not args.apk.exists():
raise FileNotFoundError(f"APK not found: {args.apk}. Pass --build or build it first.")

wait_for_boot(args.device, args.timeout)
adb(["install", "-r", str(args.apk)], device=args.device)
adb(["shell", "am", "start", "-n", ACTIVITY], device=args.device)

wait_for_text(args.device, "Ledger ClientSQL Demo", args.timeout)
wait_for_text(args.device, "Ledger actions ready.", args.timeout)
if args.reset_seed:
if not tap_text(args.device, "Reset seed data"):
raise AssertionError("Could not find Reset seed data button in Android UI dump")
wait_for_text(args.device, "Reset and reseeded ledger tables.", args.timeout)

if not tap_text(args.device, "Run stress batch"):
raise AssertionError("Could not find Run stress batch button in Android UI dump")

wait_for_text(args.device, "Committed four transfers in one transaction", args.timeout)
screenshot(args.device, args.screenshot)
print("Ledger ClientSQL Android smoke passed.", flush=True)
return 0


if __name__ == "__main__":
try:
raise SystemExit(main())
except subprocess.CalledProcessError as error:
if error.stdout:
sys.stdout.buffer.write(error.stdout)
if error.stderr:
sys.stderr.buffer.write(error.stderr)
raise
27 changes: 27 additions & 0 deletions apps/ledger_sql_demo/src/valdi/ledger_sql_demo/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
load("//bzl/valdi:valdi_module.bzl", "valdi_module")

valdi_module(
name = "ledger_sql_demo",
srcs = glob([
"src/**/*.ts",
"src/**/*.tsx",
]) + [
"tsconfig.json",
],
android_output_target = "release",
ios_module_name = "SCCLedgerSQLDemo",
ios_output_target = "release",
sql_db_names = ["LedgerDb"],
sql_srcs = glob([
"sql/**/*.sq",
"sql/**/*.sqm",
"sql/sql_types.yaml",
"sql/sql_manifest.yaml",
]),
visibility = ["//visibility:public"],
deps = [
"//src/valdi_modules/src/valdi/client_sql",
"//src/valdi_modules/src/valdi/valdi_core",
"//src/valdi_modules/src/valdi/valdi_tsx",
],
)
10 changes: 10 additions & 0 deletions apps/ledger_sql_demo/src/valdi/ledger_sql_demo/module.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: ledger_sql_demo
ios:
module_name: SCCLedgerSQLDemo
output: release
android:
output: release
dependencies:
- client_sql
- valdi_core
- valdi_tsx
Loading
Loading