diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index cd5372a..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "attribution": { - "commit": "", - "pr": "" - }, - "hooks": { - "PostToolUse": [ - { - "matcher": "Write|Edit", - "hooks": [ - { - "type": "command", - "command": "jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; case \"$f\" in *.rb) bin/stree write \"$f\" ;; esac; } 2>/dev/null || true" - } - ] - } - ] - } -} diff --git a/.envrc b/.envrc deleted file mode 100644 index b50a72f..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -export MUTANT_SINCE=HEAD diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100755 index 3a43294..0000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -stash_ref="" -cleanup() { - if [ -n "$stash_ref" ]; then - git stash pop --quiet "$stash_ref" || true - fi -} -trap cleanup EXIT - -if ! git diff --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then - git stash push --keep-index --include-untracked --quiet --message "pre-commit: $(date +%s)" - stash_ref="stash@{0}" -fi - -bin/rake diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ae00924..f629ca3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,17 +15,29 @@ jobs: runs-on: ubuntu-latest env: - MUTANT_SINCE: ${{ github.event.pull_request.base.sha || 'HEAD^' }} + MUTANT_SINCE: ${{ github.event.pull_request.base.sha || github.event.before }} steps: - uses: actions/checkout@v6 with: persist-credentials: false fetch-depth: 0 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 + - name: Install Nix + uses: cachix/install-nix-action@v31 with: - ruby-version: '4.0.2' - bundler-cache: true - - name: Run the default task - run: bin/rake + extra_nix_config: | + accept-flake-config = true + - name: Set up cachix + uses: cachix/cachix-action@v16 + with: + name: devenv + - name: Install devenv + run: nix profile install nixpkgs#devenv + - name: Cache gems + uses: actions/cache@v4 + with: + path: .devenv/state/.bundle + key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile.lock') }} + restore-keys: ${{ runner.os }}-gems- + - name: Run the tests + run: devenv tasks run test diff --git a/.gitignore b/.gitignore index eeca105..ca4d092 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,12 @@ +/.claude/settings.json +/.mcp.json /.bundle/ /.mutant/ /pkg/ /tmp/ /test/pg_regress/results/ +.devenv* +devenv.local.nix +devenv.local.yaml +.pre-commit-config.yaml +/.githooks/ diff --git a/AGENTS.md b/AGENTS.md index 6495684..4cc278e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,5 @@ # Agent rules - Prefer inlining variables when they are not reused. -- After editing files, run `bin/rake format`. +- After editing files, run `treefmt`. - When adding and building a feature, follow the TDD process. diff --git a/Gemfile.lock b/Gemfile.lock index 9c1ddd0..abbd85f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -313,7 +313,6 @@ CHECKSUMS benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f - bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785 concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d @@ -396,4 +395,4 @@ CHECKSUMS zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 BUNDLED WITH - 4.0.11 + 2.7.2 diff --git a/Rakefile b/Rakefile index 153bf89..6a897fa 100644 --- a/Rakefile +++ b/Rakefile @@ -1,79 +1,4 @@ # frozen_string_literal: true require "bundler/gem_tasks" -require "fileutils" -require "minitest/test_task" -require "uri" require_relative "lib/en57/tasks" - -Minitest::TestTask.create - -def pg_regress_bin - candidates = [ - ENV["PG_REGRESS"], - `command -v pg_regress`.strip, - "/opt/homebrew/lib/postgresql@18/pgxs/src/test/regress/pg_regress", - ] - - candidates.find do |candidate| - candidate && !candidate.empty? && File.executable?(candidate) - end -end - -task :format do - system("stree write **/*.rb") - system("pg_format -i db/**/*.sql") -end - -task :mutate do - system("bin/mutant run") -end - -task :mutate_since do - system("bin/mutant run --since #{ENV.fetch("MUTANT_SINCE")}") -end - -desc "Run pg_regress tests against a pg-ephemeral instance" -task :pg_regress do - require "pg_ephemeral" - - pg_regress = pg_regress_bin - unless pg_regress - abort("pg_regress not found. Set PG_REGRESS=/path/to/pg_regress") - end - - server = PgEphemeral.start - uri = URI(server.url) - - host = uri.host - port = uri.port - user = uri.user - dbname = uri.path.delete_prefix("/") - bindir = ENV["PG_BINDIR"] || `pg_config --bindir`.strip - schedule = ENV["PG_REGRESS_SCHEDULE"] || "test/pg_regress/schedule_existing" - - FileUtils.rm_rf("test/pg_regress/results") - FileUtils.mkdir_p("test/pg_regress/results") - - success = - system( - { "PGPASSWORD" => uri.password }, - pg_regress, - "--use-existing", - "--host=#{host}", - "--port=#{port}", - "--user=#{user}", - "--dbname=#{dbname}", - "--inputdir=test/pg_regress", - "--outputdir=test/pg_regress/results", - "--expecteddir=test/pg_regress", - "--bindir=#{bindir}", - "--schedule=#{schedule}", - ) - - abort("pg_regress failed") unless success -ensure - server&.shutdown -end - -task default: %i[test mutate_since] diff --git a/bin/setup b/bin/setup deleted file mode 100755 index dce67d8..0000000 --- a/bin/setup +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' -set -vx - -bundle install - -# Do any other automated setup that you need to do here diff --git a/db/schema/0.1.0.sql b/db/schema/0.1.0.sql index 4e2ad57..1f947a0 100644 --- a/db/schema/0.1.0.sql +++ b/db/schema/0.1.0.sql @@ -23,7 +23,7 @@ CREATE TYPE en57.event AS ( type text, data jsonb, meta jsonb, - tags text[] + tags text [] ); CREATE TYPE en57.append_result AS ( @@ -31,11 +31,13 @@ CREATE TYPE en57.append_result AS ( conflicting_events jsonb ); -CREATE FUNCTION en57.append_events (new_events en57.event[], append_condition jsonb DEFAULT '{}'::jsonb) - RETURNS en57.append_result - LANGUAGE plpgsql - SET enable_seqscan = OFF - AS $$ +CREATE FUNCTION en57.append_events( + new_events en57.event [], append_condition jsonb DEFAULT '{}'::jsonb +) +RETURNS en57.append_result +LANGUAGE plpgsql +SET enable_seqscan = OFF +AS $$ DECLARE criteria jsonb[] := ARRAY ( SELECT @@ -150,16 +152,21 @@ INSERT INTO en57.events (id, type, data, meta) END; $$; -CREATE FUNCTION en57.read_events (criteria jsonb[], batch_size int DEFAULT NULL, after_position bigint DEFAULT NULL) - RETURNS TABLE ( - "position" bigint, - id uuid, - type text, - data jsonb, - meta jsonb, - tags text[]) - LANGUAGE SQL - AS $$ +CREATE FUNCTION en57.read_events( + criteria jsonb [], + batch_size int DEFAULT NULL, + after_position bigint DEFAULT NULL +) +RETURNS TABLE ( + "position" bigint, + id uuid, + type text, + data jsonb, + meta jsonb, + tags text [] +) +LANGUAGE sql +AS $$ WITH parsed_criteria AS ( SELECT c, @@ -226,4 +233,3 @@ ORDER BY e.position LIMIT batch_size; $$; - diff --git a/db/seeds/concurrent_append_non_conflicting_tags_seeded.sql b/db/seeds/concurrent_append_non_conflicting_tags_seeded.sql index da205ff..bc73bbc 100644 --- a/db/seeds/concurrent_append_non_conflicting_tags_seeded.sql +++ b/db/seeds/concurrent_append_non_conflicting_tags_seeded.sql @@ -4,7 +4,7 @@ -- of the same shape the benchmark queries. INSERT INTO en57.events (id, type) SELECT - gen_random_uuid (), + gen_random_uuid(), 'event_benchmarked' FROM generate_series(1, 1000000); @@ -15,4 +15,3 @@ SELECT 'writer:' || substr(md5(random()::text), 1, 8) FROM en57.events; - diff --git a/db/seeds/res.sql b/db/seeds/res.sql index 6902cb6..5ac9466 100644 --- a/db/seeds/res.sql +++ b/db/seeds/res.sql @@ -10,184 +10,206 @@ SET client_encoding = 'UTF8'; SET standard_conforming_strings = ON; -SELECT - pg_catalog.set_config('search_path', '', FALSE); +SELECT pg_catalog.set_config('search_path', '', FALSE); SET check_function_bodies = FALSE; -SET xmloption = content; +SET xmloption = CONTENT; -SET client_min_messages = warning; +SET client_min_messages = WARNING; SET row_security = OFF; SET default_tablespace = ''; -SET default_table_access_method = heap; +SET default_table_access_method = HEAP; -- -- Name: ar_internal_metadata; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.ar_internal_metadata ( - key character varying NOT NULL, - value character varying, - created_at timestamp(6) without time zone NOT NULL, - updated_at timestamp(6) without time zone NOT NULL +CREATE TABLE PUBLIC.AR_INTERNAL_METADATA ( + KEY character varying NOT NULL, + VALUE character varying, + CREATED_AT timestamp(6) without time zone NOT NULL, + UPDATED_AT timestamp(6) without time zone NOT NULL ); -- -- Name: event_store_events; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.event_store_events ( - id bigint NOT NULL, - event_id uuid NOT NULL, - event_type character varying NOT NULL, - metadata jsonb, - data jsonb NOT NULL, - created_at timestamp(6) without time zone NOT NULL, - valid_at timestamp(6) without time zone +CREATE TABLE PUBLIC.EVENT_STORE_EVENTS ( + ID bigint NOT NULL, + EVENT_ID uuid NOT NULL, + EVENT_TYPE character varying NOT NULL, + METADATA jsonb, + DATA jsonb NOT NULL, + CREATED_AT timestamp(6) without time zone NOT NULL, + VALID_AT timestamp(6) without time zone ); -- -- Name: event_store_events_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- -CREATE SEQUENCE public.event_store_events_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; +CREATE SEQUENCE PUBLIC.EVENT_STORE_EVENTS_ID_SEQ +START WITH 1 +INCREMENT BY 1 +NO MINVALUE +NO MAXVALUE +CACHE 1; -- -- Name: event_store_events_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- -ALTER SEQUENCE public.event_store_events_id_seq OWNED BY public.event_store_events.id; +ALTER SEQUENCE PUBLIC.EVENT_STORE_EVENTS_ID_SEQ OWNED BY PUBLIC.EVENT_STORE_EVENTS.ID; -- -- Name: event_store_events_in_streams; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.event_store_events_in_streams ( - id bigint NOT NULL, - stream character varying NOT NULL, +CREATE TABLE PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS ( + ID bigint NOT NULL, + STREAM character varying NOT NULL, "position" integer, - event_id uuid NOT NULL, - created_at timestamp(6) without time zone NOT NULL + EVENT_ID uuid NOT NULL, + CREATED_AT timestamp(6) without time zone NOT NULL ); -- -- Name: event_store_events_in_streams_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- -CREATE SEQUENCE public.event_store_events_in_streams_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; +CREATE SEQUENCE PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS_ID_SEQ +START WITH 1 +INCREMENT BY 1 +NO MINVALUE +NO MAXVALUE +CACHE 1; -- -- Name: event_store_events_in_streams_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- -ALTER SEQUENCE public.event_store_events_in_streams_id_seq OWNED BY public.event_store_events_in_streams.id; +ALTER SEQUENCE PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS_ID_SEQ OWNED BY PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS.ID; -- -- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.schema_migrations ( - version character varying NOT NULL +CREATE TABLE PUBLIC.SCHEMA_MIGRATIONS ( + VERSION character varying NOT NULL ); -- -- Name: event_store_events id; Type: DEFAULT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.event_store_events - ALTER COLUMN id SET DEFAULT nextval('public.event_store_events_id_seq'::regclass); +ALTER TABLE ONLY PUBLIC.EVENT_STORE_EVENTS +ALTER COLUMN ID SET DEFAULT nextval( + 'public.event_store_events_id_seq'::regclass +); -- -- Name: event_store_events_in_streams id; Type: DEFAULT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.event_store_events_in_streams - ALTER COLUMN id SET DEFAULT nextval('public.event_store_events_in_streams_id_seq'::regclass); +ALTER TABLE ONLY PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS +ALTER COLUMN ID SET DEFAULT nextval( + 'public.event_store_events_in_streams_id_seq'::regclass +); -- -- Name: ar_internal_metadata ar_internal_metadata_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.ar_internal_metadata - ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key); +ALTER TABLE ONLY PUBLIC.AR_INTERNAL_METADATA +ADD CONSTRAINT AR_INTERNAL_METADATA_PKEY PRIMARY KEY (KEY); -- -- Name: event_store_events_in_streams event_store_events_in_streams_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.event_store_events_in_streams - ADD CONSTRAINT event_store_events_in_streams_pkey PRIMARY KEY (id); +ALTER TABLE ONLY PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS +ADD CONSTRAINT EVENT_STORE_EVENTS_IN_STREAMS_PKEY PRIMARY KEY (ID); -- -- Name: event_store_events event_store_events_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.event_store_events - ADD CONSTRAINT event_store_events_pkey PRIMARY KEY (id); +ALTER TABLE ONLY PUBLIC.EVENT_STORE_EVENTS +ADD CONSTRAINT EVENT_STORE_EVENTS_PKEY PRIMARY KEY (ID); -- -- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.schema_migrations - ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); +ALTER TABLE ONLY PUBLIC.SCHEMA_MIGRATIONS +ADD CONSTRAINT SCHEMA_MIGRATIONS_PKEY PRIMARY KEY (VERSION); -- -- Name: index_event_store_events_in_streams_on_created_at; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_in_streams_on_created_at ON public.event_store_events_in_streams USING btree (created_at); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_IN_STREAMS_ON_CREATED_AT ON PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS USING BTREE ( + CREATED_AT +); -- -- Name: index_event_store_events_in_streams_on_event_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_in_streams_on_event_id ON public.event_store_events_in_streams USING btree (event_id); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_IN_STREAMS_ON_EVENT_ID ON PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS USING BTREE ( + EVENT_ID +); -- -- Name: index_event_store_events_in_streams_on_stream_and_event_id; Type: INDEX; Schema: public; Owner: - -- -CREATE UNIQUE INDEX index_event_store_events_in_streams_on_stream_and_event_id ON public.event_store_events_in_streams USING btree (stream, event_id); +CREATE UNIQUE INDEX INDEX_EVENT_STORE_EVENTS_IN_STREAMS_ON_STREAM_AND_EVENT_ID ON PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS USING BTREE ( + STREAM, EVENT_ID +); -- -- Name: index_event_store_events_in_streams_on_stream_and_position; Type: INDEX; Schema: public; Owner: - -- -CREATE UNIQUE INDEX index_event_store_events_in_streams_on_stream_and_position ON public.event_store_events_in_streams USING btree (stream, "position"); +CREATE UNIQUE INDEX INDEX_EVENT_STORE_EVENTS_IN_STREAMS_ON_STREAM_AND_POSITION ON PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS USING BTREE ( + STREAM, "position" +); -- -- Name: index_event_store_events_on_as_of; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_on_as_of ON public.event_store_events USING btree (COALESCE(valid_at, created_at)); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_ON_AS_OF ON PUBLIC.EVENT_STORE_EVENTS USING BTREE ( + coalesce(VALID_AT, CREATED_AT) +); -- -- Name: index_event_store_events_on_created_at; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_on_created_at ON public.event_store_events USING btree (created_at); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_ON_CREATED_AT ON PUBLIC.EVENT_STORE_EVENTS USING BTREE ( + CREATED_AT +); -- -- Name: index_event_store_events_on_event_id; Type: INDEX; Schema: public; Owner: - -- -CREATE UNIQUE INDEX index_event_store_events_on_event_id ON public.event_store_events USING btree (event_id); +CREATE UNIQUE INDEX INDEX_EVENT_STORE_EVENTS_ON_EVENT_ID ON PUBLIC.EVENT_STORE_EVENTS USING BTREE ( + EVENT_ID +); -- -- Name: index_event_store_events_on_event_type; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_on_event_type ON public.event_store_events USING btree (event_type); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_ON_EVENT_TYPE ON PUBLIC.EVENT_STORE_EVENTS USING BTREE ( + EVENT_TYPE +); -- -- Name: index_event_store_events_on_valid_at; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_event_store_events_on_valid_at ON public.event_store_events USING btree (valid_at); +CREATE INDEX INDEX_EVENT_STORE_EVENTS_ON_VALID_AT ON PUBLIC.EVENT_STORE_EVENTS USING BTREE ( + VALID_AT +); -- -- Name: event_store_events_in_streams fk_rails_c8d52b5857; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.event_store_events_in_streams - ADD CONSTRAINT fk_rails_c8d52b5857 FOREIGN KEY (event_id) REFERENCES public.event_store_events (event_id); +ALTER TABLE ONLY PUBLIC.EVENT_STORE_EVENTS_IN_STREAMS +ADD CONSTRAINT FK_RAILS_C8D52B5857 FOREIGN KEY ( + EVENT_ID +) REFERENCES PUBLIC.EVENT_STORE_EVENTS (EVENT_ID); -- -- PostgreSQL database dump complete -- -SET search_path TO "$user", public; - -INSERT INTO "schema_migrations" (version) - VALUES ('20260521134825'); +SET search_path TO "$user", PUBLIC; +INSERT INTO "schema_migrations" (VERSION) +VALUES ('20260521134825'); diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 0000000..66f8095 --- /dev/null +++ b/devenv.lock @@ -0,0 +1,218 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1782231411, + "narHash": "sha256-W/Yhek5L3nc0gaGgMHPrzO6DbLGq7Yb9DKw/kvLwij0=", + "owner": "cachix", + "repo": "devenv", + "rev": "a4fd3348220d96f7602385adff0ace482e720586", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781733627, + "narHash": "sha256-U3yTuGBnmXvXoQI3qkpfEDsn9RovQPAjN7ndRco+3u0=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "3bbec39bc90eadfa031e6f3b77272f3f60803e39", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "inputs": { + "nixpkgs-src": "nixpkgs-src" + }, + "locked": { + "lastModified": 1782132010, + "narHash": "sha256-ZnAVHdVrotp80iIMm5CSR1fdxPlw7Uwmwxb+O/wsgZ8=", + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "12866ae2dddbc0ab8b329915f8072bb9c75bde89", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-ruby": { + "inputs": { + "flake-compat": "flake-compat_2", + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1782291298, + "narHash": "sha256-s5qd1ZWKoc8m6dnTvcHTMWerE8xXOSG82Vgp7eIPesk=", + "owner": "bobvanderlinden", + "repo": "nixpkgs-ruby", + "rev": "5a59e45e96465645c00422fedf5e62e337f6cfea", + "type": "github" + }, + "original": { + "owner": "bobvanderlinden", + "repo": "nixpkgs-ruby", + "type": "github" + } + }, + "nixpkgs-src": { + "flake": false, + "locked": { + "lastModified": 1781607440, + "narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3e41b24abd260e8f71dbe2f5737d24122f972158", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "nixpkgs-ruby": "nixpkgs-ruby", + "treefmt-nix": "treefmt-nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1780220602, + "narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "db947814a175b7ca6ded66e21383d938df01c227", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} \ No newline at end of file diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 0000000..c7217c1 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,124 @@ +{ pkgs, lib, ... }: + +{ + languages.ruby.enable = true; + languages.ruby.version = "4.0.5"; + + packages = [ + pkgs.postgresql_18 + pkgs.postgresql_18.pg_config + pkgs.sqlfluff + pkgs.jq + pkgs.libyaml + ]; + + enterShell = '' + export MUTANT_SINCE="''${MUTANT_SINCE:-HEAD}" + ''; + + tasks = { + "dev:setup".exec = "bundle install --quiet"; + "devenv:enterShell".after = [ "dev:setup" ]; + "dev:format" = { + exec = "treefmt"; + after = [ "dev:setup" ]; + }; + "test:unit" = { + exec = "bin/m test"; + after = [ "dev:setup" ]; + }; + "test:mutate" = { + exec = ''bin/mutant run --since "''${MUTANT_SINCE:-HEAD}"''; + after = [ "dev:setup" ]; + }; + "test:pg".exec = "pg-regress"; + }; + + treefmt.enable = true; + treefmt.config = { + programs = { + nixfmt.enable = true; + }; + settings.formatter = { + ruby = { + command = "stree"; + options = [ "write" ]; + includes = [ "*.rb" ]; + }; + sql = { + command = "sqlfluff"; + options = [ + "format" + "--dialect" + "postgres" + "--exclude-rules" + "LT05" + ]; + includes = [ "db/**/*.sql" ]; + }; + }; + }; + + scripts.pg-regress.exec = '' + set -euo pipefail + cd "$DEVENV_ROOT" + + pg_regress=${pkgs.postgresql_18.dev}/lib/pgxs/src/test/regress/pg_regress + bindir=${pkgs.postgresql_18}/bin + workdir=$(mktemp -d) + datadir=$workdir/data + socketdir=$workdir/socket + dbname=en57_regress + mkdir -p "$socketdir" + + cleanup() { + "$bindir/pg_ctl" -D "$datadir" -m immediate stop >/dev/null 2>&1 || true + rm -rf "$workdir" + } + trap cleanup EXIT + + "$bindir/initdb" -D "$datadir" -U postgres --auth=trust >/dev/null + "$bindir/pg_ctl" -D "$datadir" -w \ + -o "-k $socketdir -c listen_addresses='''" start >/dev/null + "$bindir/createdb" -h "$socketdir" -U postgres "$dbname" + "$bindir/psql" -h "$socketdir" -U postgres -d "$dbname" \ + -v ON_ERROR_STOP=1 -q -f db/schema/0.1.0.sql + + rm -rf test/pg_regress/results + mkdir -p test/pg_regress/results + + "$pg_regress" \ + --use-existing \ + --host="$socketdir" \ + --user=postgres \ + --dbname="$dbname" \ + --inputdir=test/pg_regress \ + --outputdir=test/pg_regress/results \ + --expecteddir=test/pg_regress \ + --bindir="$bindir" \ + --schedule=test/pg_regress/schedule_existing + ''; + + claude.code.enable = true; + claude.code.hooks = { + format = { + name = "Format edited files with treefmt"; + hookType = "PostToolUse"; + matcher = "Write|Edit"; + command = '' + jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; treefmt "$f"; } 2>/dev/null || true + ''; + }; + test = { + name = "Run the test namespace on stop"; + hookType = "Stop"; + command = '' + input=$(cat) + cd "''${DEVENV_ROOT:-.}" || exit 0 + devenv tasks run test 1>&2 && exit 0 + [ "$(printf '%s' "$input" | jq -r '.stop_hook_active // false')" = "true" ] && exit 0 + exit 2 + ''; + }; + }; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 0000000..fa85261 --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,18 @@ +inputs: + git-hooks: + url: github:cachix/git-hooks.nix + inputs: + nixpkgs: + follows: nixpkgs + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling + nixpkgs-ruby: + url: github:bobvanderlinden/nixpkgs-ruby + inputs: + nixpkgs: + follows: nixpkgs + treefmt-nix: + url: github:numtide/treefmt-nix + inputs: + nixpkgs: + follows: nixpkgs