Skip to content

Commit c50d788

Browse files
authored
Merge pull request #276 from RayforceDB/dev
v2.1.3
2 parents d7a53fa + 1aaf331 commit c50d788

33 files changed

Lines changed: 1031 additions & 110 deletions

.github/workflows/release.yml

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,17 @@ jobs:
101101
strategy:
102102
fail-fast: false
103103
matrix:
104+
# Distributables must be PORTABLE, not -march=native: a native binary
105+
# built on a runner can SIGILL on a different/older CPU. x86-64-v3 =
106+
# AVX2 baseline (~2013+), which keeps the engine's vectorized reductions
107+
# fast. The macOS runner is the oldest Apple-Silicon class, so native
108+
# there is effectively a safe floor (arm64 has no x86-style optional-ISA
109+
# traps, and baselining to armv8-a would only lose M1 tuning).
104110
include:
105111
- os: ubuntu-latest
112+
march: x86-64-v3
106113
- os: macos-latest
114+
march: native
107115
# Windows is not build-ready yet (IOCP stub, unguarded POSIX in
108116
# main.c/heap.c, no Makefile path). Add once the port lands:
109117
# - os: windows-latest
@@ -114,8 +122,8 @@ jobs:
114122
with:
115123
ref: ${{ needs.prepare.outputs.sha }}
116124

117-
- name: Build release artifact
118-
run: make dist RAY_VERSION=${{ needs.prepare.outputs.version }}
125+
- name: Build release artifact (portable)
126+
run: make dist RAY_VERSION=${{ needs.prepare.outputs.version }} RAY_MARCH=${{ matrix.march }}
119127

120128
- name: Upload artifacts to release
121129
env:
@@ -126,6 +134,28 @@ jobs:
126134
gh release upload "v${{ needs.prepare.outputs.version }}" \
127135
dist/*.tar.gz dist/*.sha256 --clobber
128136
137+
# Package the just-built (already-portable) Linux binary as a .deb —
138+
# `make dist` left ./rayforce in place, so no rebuild is needed.
139+
- name: Build .deb (Linux)
140+
if: matrix.os == 'ubuntu-latest'
141+
env:
142+
RAY_VERSION: ${{ needs.prepare.outputs.version }}
143+
run: |
144+
set -euo pipefail
145+
ver="$(curl -fsSL https://api.github.com/repos/goreleaser/nfpm/releases/latest | grep -oP '"tag_name":\s*"\K[^"]+')"
146+
curl -fsSL "https://github.com/goreleaser/nfpm/releases/download/${ver}/nfpm_${ver#v}_amd64.deb" -o /tmp/nfpm.deb
147+
sudo dpkg -i /tmp/nfpm.deb
148+
mkdir -p dist
149+
nfpm pkg --packager deb --config packaging/nfpm.yaml --target dist/
150+
ls -l dist/*.deb
151+
152+
- name: Upload .deb to release (Linux)
153+
if: matrix.os == 'ubuntu-latest'
154+
env:
155+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
156+
GH_REPO: ${{ github.repository }}
157+
run: gh release upload "v${{ needs.prepare.outputs.version }}" dist/*.deb --clobber
158+
129159
publish:
130160
needs: [prepare, build]
131161
runs-on: ubuntu-latest
@@ -169,3 +199,40 @@ jobs:
169199
--data-urlencode "content=${content}")"
170200
echo "Zulip response: $resp"
171201
echo "$resp" | grep -q '"result": *"success"'
202+
203+
# Bump the Homebrew tap (RayforceDB/homebrew-tap → Formula/rayforce.rb) to this
204+
# release. Best-effort: skipped when HOMEBREW_TAP_TOKEN isn't configured, and
205+
# continue-on-error so a tap hiccup never reds an already-shipped release.
206+
homebrew:
207+
needs: [prepare, publish]
208+
runs-on: ubuntu-latest
209+
env:
210+
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
211+
steps:
212+
- uses: actions/checkout@v5
213+
with:
214+
ref: ${{ needs.prepare.outputs.sha }}
215+
- name: Update tap formula
216+
if: ${{ env.HOMEBREW_TAP_TOKEN != '' }}
217+
continue-on-error: true
218+
env:
219+
VERSION: ${{ needs.prepare.outputs.version }}
220+
GH_REPO: ${{ github.repository }}
221+
TAP_REPO: RayforceDB/homebrew-tap
222+
run: |
223+
set -euo pipefail
224+
# Build-from-source formula: point at the GitHub source tarball for the
225+
# tag and pin its sha256.
226+
url="https://github.com/${GH_REPO}/archive/refs/tags/v${VERSION}.tar.gz"
227+
sha="$(curl -fsSL "$url" | sha256sum | cut -d' ' -f1)"
228+
tmp="$(mktemp -d)"
229+
git clone --depth 1 "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/${TAP_REPO}.git" "$tmp"
230+
mkdir -p "$tmp/Formula"
231+
sed -e "s|__URL__|${url}|" -e "s|__SHA256__|${sha}|" \
232+
packaging/homebrew-formula.rb.tmpl > "$tmp/Formula/rayforce.rb"
233+
cd "$tmp"
234+
git config user.name "rayforce-release"
235+
git config user.email "release@rayforcedb.com"
236+
git add Formula/rayforce.rb
237+
git commit -m "rayforce ${VERSION}" || { echo "formula unchanged — nothing to push"; exit 0; }
238+
git push origin HEAD

Makefile

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,16 @@ DEPFLAGS = -MMD -MP
3131

3232
UNAME_S := $(shell uname -s)
3333

34-
DEBUG_CFLAGS = -fPIC $(WARNS) -std=$(STD) -g -O0 -march=native -DDEBUG \
34+
# Target microarchitecture. Default `native` = build for THIS machine (fastest;
35+
# right for local builds and the per-machine release tarballs). Override for
36+
# REDISTRIBUTABLE packages (.deb) that must run on any CPU, e.g.
37+
# `make release RAY_MARCH=x86-64-v3` (AVX2 baseline, ~2013+) — a -march=native
38+
# binary handed to a different/older CPU dies with SIGILL.
39+
RAY_MARCH ?= native
40+
41+
DEBUG_CFLAGS = -fPIC $(WARNS) -std=$(STD) -g -O0 -march=$(RAY_MARCH) -DDEBUG \
3542
-fsanitize=address,undefined -fno-omit-frame-pointer
36-
RELEASE_CFLAGS = -fPIC $(WARNS) -std=$(STD) -O3 -march=native \
43+
RELEASE_CFLAGS = -fPIC $(WARNS) -std=$(STD) -O3 -march=$(RAY_MARCH) \
3744
-funroll-loops -fomit-frame-pointer -fno-math-errno \
3845
-fassociative-math -ffp-contract=fast -fno-signed-zeros -fno-trapping-math
3946
# -fassociative-math: license to reorder FP additions/multiplications.
@@ -52,7 +59,7 @@ RELEASE_CFLAGS = -fPIC $(WARNS) -std=$(STD) -O3 -march=native \
5259
# with the profile runtime, so we drop them; -O0 keeps line numbers
5360
# and avoids dead-code regions getting marked uncovered for the
5461
# wrong reason. See `make coverage` below.
55-
COVERAGE_CFLAGS = -fPIC $(WARNS) -std=$(STD) -g -O0 -march=native -DDEBUG \
62+
COVERAGE_CFLAGS = -fPIC $(WARNS) -std=$(STD) -g -O0 -march=$(RAY_MARCH) -DDEBUG \
5663
-fno-omit-frame-pointer -fprofile-instr-generate -fcoverage-mapping
5764
COVERAGE_LDFLAGS = -fprofile-instr-generate -fcoverage-mapping
5865

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ analytics and graph traversals share a single operation DAG, pass through a
2727
multi-pass optimizer, and execute as morsel-driven bytecode compiled at
2828
execution time. No malloc.
2929

30+
## Install
31+
32+
**Homebrew** (macOS & Linux):
33+
34+
```bash
35+
brew install rayforcedb/tap/rayforce
36+
```
37+
38+
**Debian / Ubuntu** (`.deb`, x86-64) — grab the `.deb` from the
39+
[latest release](https://github.com/RayforceDB/rayforce/releases/latest):
40+
41+
```bash
42+
curl -LO https://github.com/RayforceDB/rayforce/releases/download/vX.Y.Z/rayforce_X.Y.Z_amd64.deb
43+
sudo dpkg -i rayforce_X.Y.Z_amd64.deb
44+
```
45+
46+
**Prebuilt tarball** (Linux x86-64 / macOS arm64) is also attached to each
47+
release. Or build [from source](#quick-start) below — zero dependencies, just
48+
`make`.
49+
3050
## Quick Start
3151

3252
```bash

RELEASE.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,34 @@ make dist RAY_VERSION=2.1.3
9696
is skipped, and a Zulip error never fails the release (`continue-on-error`).
9797
If Zulip rejects the post, the bot may need to be a *Generic bot* (not just an
9898
incoming-webhook) and subscribed to the Announcements channel.
99+
- **Optional — Homebrew tap**: create an empty repo **`RayforceDB/homebrew-tap`**
100+
and add a repository secret **`HOMEBREW_TAP_TOKEN`** (a PAT — fine-grained or
101+
classic — with **contents: write** on `homebrew-tap`; the default
102+
`GITHUB_TOKEN` can't push to a *different* repo). The `homebrew` job then
103+
rewrites `Formula/rayforce.rb` in the tap on every release. Without the secret
104+
the job is skipped. Users install with `brew install rayforcedb/tap/rayforce`.
105+
106+
## Packaging / install channels
107+
108+
Each release publishes, in addition to the source:
109+
110+
- **Tarballs**`rayforce-X.Y.Z-linux-x86_64.tar.gz` and
111+
`…-darwin-arm64.tar.gz` (+ `.sha256`). The Linux one is **portable**
112+
(`RAY_MARCH=x86-64-v3`, AVX2/~2013+) so it can't SIGILL on a different/older
113+
CPU. macOS stays `-march=native`: the runner is the oldest Apple-Silicon
114+
class, so it's already a safe floor (arm64 has no x86-style optional-ISA
115+
traps, and baselining to `armv8-a` would only cost M1 tuning).
116+
- **`.deb`** (`rayforce_X.Y.Z_amd64.deb`) — the same portable Linux binary,
117+
packaged with nfpm ([`packaging/nfpm.yaml`](packaging/nfpm.yaml)). No setup
118+
(uses `GITHUB_TOKEN`).
119+
- **Homebrew** (`rayforcedb/tap`) — a **build-from-source** formula
120+
([`packaging/homebrew-formula.rb.tmpl`](packaging/homebrew-formula.rb.tmpl)),
121+
auto-bumped by the `homebrew` job. Compiling on the user's machine means all
122+
Mac arches work and there's no redistribution-portability footgun.
123+
124+
> Want maximum per-CPU performance instead of a portable binary? Build from
125+
> source — `make` and Homebrew both default to `-march=native`. Only the
126+
> *distributed* x86-64 artifacts use the `x86-64-v3` baseline.
99127
100128
## Platform support
101129

docs/javascripts/main.js

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ function rfUpdateBannerOffset() {
180180

181181
const REPO = 'RayforceDB/rayforce';
182182
const CACHE_KEY = 'gh-release:' + REPO;
183-
const CACHE_TTL = 60 * 60 * 1000; /* 1 hour */
183+
const REVALIDATE_AFTER = 5 * 60 * 1000; /* refetch the latest release at most this often */
184184

185185
/* Only Linux x86_64 and macOS (arm64) ship today; Windows/mobile/unknown fall
186186
* through to the releases page. navigator.platform can't tell Apple Silicon
@@ -236,34 +236,46 @@ function rfUpdateBannerOffset() {
236236
function readCache() {
237237
try {
238238
const v = JSON.parse(localStorage.getItem(CACHE_KEY));
239-
if (!v || (Date.now() - v.t) > CACHE_TTL) return null;
240-
return v.rel;
239+
return (v && v.rel && v.rel.tag) ? v : null;
241240
} catch (e) { return null; }
242241
}
243242
function writeCache(rel) {
244243
try { localStorage.setItem(CACHE_KEY, JSON.stringify({ t: Date.now(), rel: rel })); } catch (e) {}
245244
}
245+
/* Persist always, but repaint only when the latest tag actually changed — so
246+
a fresh release shows up without re-flipping the banner on every load. */
247+
function update(rel) {
248+
const changed = !cached || cached.rel.tag !== rel.tag;
249+
writeCache(rel);
250+
if (changed) apply(rel);
251+
}
246252

253+
/* Stale-while-revalidate: paint the cached release instantly (may be slightly
254+
stale), then revalidate in the background and repaint only if it changed.
255+
Without this, a cached value froze the banner on the old version until its
256+
TTL — a new release wouldn't show for up to an hour. */
247257
const cached = readCache();
248-
if (cached) { apply(cached); return; }
258+
if (cached) apply(cached.rel);
259+
if (cached && (Date.now() - cached.t) < REVALIDATE_AFTER) return; /* fresh enough; skip the network */
249260

250-
/* Primary: ungh.cc. Shape: { release: { tag, assets: [{ downloadUrl }] } }. */
251-
fetch('https://ungh.cc/repos/' + REPO + '/releases/latest')
252-
.then(r => r.ok ? r.json() : Promise.reject(new Error('ungh ' + r.status)))
261+
/* GitHub's API is authoritative and fresh; ungh.cc (CDN-cached, can lag
262+
GitHub by ~a day) is the fallback when the direct API is unreachable. */
263+
fetch('https://api.github.com/repos/' + REPO + '/releases/latest', { headers: { Accept: 'application/vnd.github+json' } })
264+
.then(r => r.ok ? r.json() : Promise.reject(new Error('gh ' + r.status)))
253265
.then(d => {
254-
const r = d.release || d;
255-
const rel = { tag: r.tag, urls: (r.assets || []).map(a => a.downloadUrl).filter(Boolean) };
256-
if (!rel.tag) throw new Error('ungh shape');
257-
writeCache(rel); apply(rel);
266+
const rel = { tag: d.tag_name, urls: (d.assets || []).map(a => a.browser_download_url).filter(Boolean) };
267+
if (!rel.tag) throw new Error('gh shape');
268+
update(rel);
258269
})
259270
.catch(() => {
260-
/* Fallback: GitHub API. Shape: { tag_name, assets: [{ browser_download_url }] }. */
261-
return fetch('https://api.github.com/repos/' + REPO + '/releases/latest', { headers: { Accept: 'application/vnd.github+json' } })
262-
.then(r => r.ok ? r.json() : Promise.reject(new Error('gh ' + r.status)))
271+
/* Fallback: ungh.cc. Shape: { release: { tag, assets: [{ downloadUrl }] } }. */
272+
return fetch('https://ungh.cc/repos/' + REPO + '/releases/latest')
273+
.then(r => r.ok ? r.json() : Promise.reject(new Error('ungh ' + r.status)))
263274
.then(d => {
264-
const rel = { tag: d.tag_name, urls: (d.assets || []).map(a => a.browser_download_url).filter(Boolean) };
265-
if (!rel.tag) throw new Error('gh shape');
266-
writeCache(rel); apply(rel);
275+
const r = d.release || d;
276+
const rel = { tag: r.tag, urls: (r.assets || []).map(a => a.downloadUrl).filter(Boolean) };
277+
if (!rel.tag) throw new Error('ungh shape');
278+
update(rel);
267279
});
268280
})
269281
.catch(() => { /* CTAs stay on /releases/latest; banner keeps its default text. */ });

packaging/homebrew-formula.rb.tmpl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Homebrew formula TEMPLATE for the rayforcedb/tap tap.
2+
#
3+
# release.yml substitutes __URL__ and __SHA256__ (the GitHub source tarball for
4+
# the tag and its sha256) and pushes the result to RayforceDB/homebrew-tap as
5+
# Formula/rayforce.rb on every release.
6+
#
7+
# Build-from-source on purpose: it compiles for the user's own CPU (so it works
8+
# on every Mac arch and never SIGILLs the way a redistributed -march=native
9+
# binary can), and a zero-dependency `make` build needs no `depends_on`.
10+
class Rayforce < Formula
11+
desc "Embeddable columnar analytics and graph traversal engine in pure C"
12+
homepage "https://rayforcedb.com/"
13+
url "__URL__"
14+
sha256 "__SHA256__"
15+
license "MIT"
16+
head "https://github.com/RayforceDB/rayforce.git", branch: "master"
17+
18+
def install
19+
# Inject the release version (no .git in a source tarball, so git-describe
20+
# can't), build optimized for the user's machine, install binary + header.
21+
system "make", "release", "RAY_VERSION=#{version}"
22+
bin.install "rayforce"
23+
include.install "include/rayforce.h"
24+
end
25+
26+
test do
27+
assert_match "usage", shell_output("#{bin}/rayforce --help")
28+
assert_match version.to_s, pipe_output("#{bin}/rayforce", "(.sys.build)\n")
29+
end
30+
end

packaging/nfpm.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# nfpm config — produces the Rayforce .deb.
2+
#
3+
# Driven by .github/workflows/release.yml on the Linux build, which first
4+
# rebuilds a PORTABLE binary (`make release RAY_MARCH=x86-64-v3`, NOT the
5+
# default -march=native) so the package runs on any modern x86-64 CPU, then
6+
# runs `nfpm pkg --packager deb`. $RAY_VERSION is expanded from the env.
7+
#
8+
# To also emit .rpm / .apk later, just run nfpm with --packager rpm / apk
9+
# (same config) — no changes needed here.
10+
name: "rayforce"
11+
arch: "amd64"
12+
platform: "linux"
13+
version: "${RAY_VERSION}"
14+
maintainer: "Anton Kundenko <anton.kundenko@gmail.com>"
15+
description: |
16+
Embeddable columnar analytics and graph traversal engine in pure C.
17+
Built-in Rayfall query language, IPC, datalog, and vector search. Zero deps.
18+
vendor: "RayforceDB"
19+
homepage: "https://rayforcedb.com/"
20+
license: "MIT"
21+
section: "database"
22+
priority: "optional"
23+
24+
contents:
25+
- src: ./rayforce
26+
dst: /usr/bin/rayforce
27+
- src: ./include/rayforce.h
28+
dst: /usr/include/rayforce.h
29+
- src: ./LICENSE
30+
dst: /usr/share/doc/rayforce/copyright
31+
- src: ./README.md
32+
dst: /usr/share/doc/rayforce/README.md

src/lang/eval.c

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,19 @@ ray_t* atomic_map_binary_op(ray_binary_fn fn, uint16_t dag_opcode, ray_t* left,
449449

450450
int64_t len;
451451
if (left_coll && right_coll) {
452-
len = ray_len(left) < ray_len(right) ? ray_len(left) : ray_len(right);
452+
/* Both operands are vectors: lengths MUST match. Silently
453+
* truncating to the shorter (the prior behaviour) hid a real
454+
* error and diverged from the compiled/VM path, which already
455+
* rejects the mismatch (src/ops/expr.c). A 1-element vector
456+
* does NOT broadcast (only a true atom does — see the
457+
* !left_coll/!right_coll branch). */
458+
if (ray_len(left) != ray_len(right)) {
459+
const char* opn = ray_opcode_name(dag_opcode);
460+
return ray_error("length", "%s: operand lengths must match, got %lld and %lld",
461+
opn ? opn : "binary op",
462+
(long long)ray_len(left), (long long)ray_len(right));
463+
}
464+
len = ray_len(left);
453465
} else {
454466
len = left_coll ? ray_len(left) : ray_len(right);
455467
}
@@ -2964,6 +2976,7 @@ static void ray_register_builtins(void) {
29642976
register_unary("dl-eval", RAY_FN_NONE, ray_dl_eval_fn);
29652977
register_binary("dl-query", RAY_FN_NONE, ray_dl_query_fn);
29662978
register_binary("dl-provenance", RAY_FN_NONE, ray_dl_provenance_fn);
2979+
register_unary("dl-free", RAY_FN_NONE, ray_dl_free_fn);
29672980

29682981
/* Vector similarity / embeddings / HNSW */
29692982
register_binary("cos-dist", RAY_FN_NONE, ray_cos_dist_fn);

src/lang/internal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ ray_t* ray_dl_stratify_fn(ray_t* x);
516516
ray_t* ray_dl_eval_fn(ray_t* x);
517517
ray_t* ray_dl_query_fn(ray_t* prog_obj, ray_t* pred_obj);
518518
ray_t* ray_dl_provenance_fn(ray_t* prog_obj, ray_t* pred_obj);
519+
ray_t* ray_dl_free_fn(ray_t* x);
519520
void ray_dl_reset_rules(void);
520521

521522
/* System builtins (formerly static in eval.c, now in system.c) */

0 commit comments

Comments
 (0)