diff --git a/.github/agents/documentation.agent.md b/.github/agents/documentation.agent.md deleted file mode 100644 index 4d54c2e6..00000000 --- a/.github/agents/documentation.agent.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -# Fill in the fields below to create a basic custom agent for your repository. -# The Copilot CLI can be used for local testing: https://gh.io/customagents/cli -# To make this agent available, merge this file into the default repository branch. -# For format details, see: https://gh.io/customagents/config - -name: documentation-agent -description: Make sure that any changes to the source code are reflected in the documentation. This includes updating code snippets, examples, and troubleshooting sections to match the current state of the codebase. If you add new features or change existing ones, be sure to update the relevant sections in this documentation to help users understand how to use the new functionality effectively. Also, the CHANGELOG.md should be updated with a clear description of the changes made, including any new features, bug fixes, or breaking changes. This helps users and contributors keep track of the project's evolution over time. - ---- - -# Documentation Agent - -Make sure that any changes to the source code are reflected in the documentation. -This includes updating code snippets, examples, and troubleshooting sections to match -the current state of the codebase. - -If you add new features or change existing ones, be sure to update the relevant sections -in this documentation to help users understand how to use the new functionality effectively. - -Also, the CHANGELOG.md should be updated with a clear description of the changes made, -including any new features, bug fixes, or breaking changes. This helps users and contributors keep track of the project's evolution over time. - -All md files you create explaining all the changes you did should be added in a folder called -agent_generated. The folder agent_generated is gitignored, so do not create them as a full documentation system. -All md files about documentation for human users should be added in the docs folder; -except for the README.md file in the root folder which should be updated with a slim summary of the -md files in docs with a link to each one. - -The docs folder should never contain md files about specific code changes, but only general documentation for users of the project. -A line in the CHANGELOG.md file should be added for that. - -The agent_generated folder should never contain md files about general documentation for users, but only specific documentation about code changes. -agent_generated is deleted regularly, so it should not contain any documentation that is meant to be permanent. - -Write everything in English, and use markdown format for all the md files you create. Normally you write too many MD files that are -not documentation for users, but documentation about code changes. Those are not very useful, so write less. \ No newline at end of file diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index e3cfb970..40eb3820 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -104,7 +104,7 @@ jobs: keep_files: true - name: SonarQube Scan run: | - mvn -B -f pom.xml package -Dmaven.test.skip=true org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.java.source=24 -Dsonar.java.target=24 -Dsonar.token=$SONAR_TOKEN -Dsonar.host.url=https://sonarcloud.io/ -Dsonar.organization=$SONAR_ORGANIZATION -Dorganization=$SONAR_ORGANIZATION -Dsonar.projectKey=dan323_functional-by-annotations + mvn -B -f pom.xml package -Dmaven.test.skip=true org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.java.source=24 -Dsonar.java.target=24 -Dsonar.token=$SONAR_TOKEN -Dsonar.host.url=https://sonarcloud.io/ -Dsonar.organization=$SONAR_ORGANIZATION -Dorganization=$SONAR_ORGANIZATION -Dsonar.projectKey=functional-by-annotations env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_ORGANIZATION: ${{ secrets.SONAR_ORGANIZATION }} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..4f085a06 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,80 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +```bash +# Build all modules (skip tests) +mvn install -DskipTests + +# Run all tests +mvn test -P test + +# Run tests for a single module +mvn test -P test -pl example-functional +mvn test -P test -pl functional-definitions/functional-compiler + +# Run a single test class +mvn test -P test -pl example-functional -Dtest=FiniteListTest + +# Generate coverage report (JaCoCo) +mvn verify -P jacoco + +# Mutation testing +mvn test pitest:mutationCoverage -pl functional-definitions/functional-compiler + +# SonarQube analysis +mvn sonar:sonar +``` + +The `test` Maven profile is required for running tests — it configures JaCoCo and adds the necessary `--add-exports` JVM arg for the JPMS module system. + +## Module Structure + +Three top-level Maven modules: + +- **`functional-definitions/annotation-definitions`** — Pure API layer. Defines the 9 annotations (`@Functor`, `@Applicative`, `@Monad`, `@Foldable`, `@Traversal`, `@Alternative`, `@Semigroup`, `@Monoid`, `@Ring`), the marker interfaces (`Functional`, `Algebraic`, `ClassAware`), and the structural interfaces (`IFunctor`, `IApplicative`, `IMonad`, `IFoldable`, `ITraversal`, `IAlternative`, `ISemigroup`, `IMonoid`, `IRing`). No dependencies. + +- **`functional-definitions/functional-compiler`** — Compile-time annotation processor. `FunctionalCompiler` (extends `AbstractProcessor`) validates that annotated classes implement the required interface and provide the minimal set of required methods. It does not generate new source files — it validates only. SPI-registered in `module-info.java`. + +- **`example-functional`** — Example implementations of functional data structures: `FiniteList`, `InfiniteList`, `Identity`, `Either`, `Optional`, `Continuation`, `Reader`, `Parser`, `Pair`, `Tree`, `Writer`, `State`, SQL types, and list zipper. The annotation processor is wired in via `annotationProcessorPaths` in its pom.xml. + +- **`jacoco-functional`** — Coverage aggregator only (pom packaging, no source). Activated via `-P jacoco`. + +Dependency direction: `annotation-definitions` ← `functional-compiler` ← `example-functional`. + +## Architecture: How the Annotation Processor Works + +The goal is to let users annotate a class (e.g., `@Monad`) and get a compile-time error if they forgot to implement the required minimal method set (e.g., `pure` + `flatMap`). + +**Processing pipeline in `FunctionalCompiler`:** +1. For each annotated type, collect which `Functional`/`Algebraic` interfaces it directly implements via `CompilerUtils.getDirectFunctionalInterfaces()`. +2. `CompilerFactory.from()` maps each interface to a concrete `Compiler` instance containing a structure-specific signature checker. +3. Each `Compiler.process()` walks the class hierarchy and verifies the required method signatures are present. Errors are emitted via `Messager`. + +**Required methods per structure:** +- `@Functor` / `IFunctor` → `map` +- `@Applicative` / `IApplicative` → `pure` + (`fapply` or `liftA2`) +- `@Monad` / `IMonad` → `pure` + `flatMap` +- `@Foldable` / `IFoldable` → one of: `fold`, `foldMap`, `foldr` +- `@Traversal` / `ITraversal` → `traverse` or `sequenceA` +- `@Alternative` / `IAlternative` → `IApplicative` methods + `empty` + `disjunction` +- `@Semigroup` / `ISemigroup` → `op` +- `@Monoid` / `IMonoid` → `op` + `unit` +- `@Ring` / `IRing` → addition + multiplication operations + +**Signature checking internals** live in `functional-compiler/src/main/java/.../compiler/internal/`: `CompilerFactory`, `Compiler`, `StructureSignatures`, and `NecessaryMethods` (conjunctive/disjunctive/empty states). + +## JPMS (Java Module System) + +All three source modules declare `module-info.java`. When writing tests in `example-functional` that need access to compiler-internal types, the `test` profile adds: +``` +--add-exports=functional.data/com.dan323.mock=functional.compiler +``` + +The `functional.compiler` module uses `provides javax.annotation.processing.Processor with FunctionalCompiler` in its module-info to register the SPI. + +## Java Version + +The project targets Java 17 minimum and is tested on 17, 21, and 24 in CI. Profile-based compiler configuration in the root `pom.xml` handles version-specific settings. Use `--release 17` semantics when writing new code. diff --git a/README.md b/README.md index 7e803bd5..5d870519 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ [![GitHub forks](https://img.shields.io/github/forks/dan323/functional-by-annotations)](https://github.com/dan323/functional-by-annotations/network) [![Last Commit](https://img.shields.io/github/last-commit/dan323/functional-by-annotations)](https://github.com/dan323/functional-by-annotations/commits) -[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=dan323_functional-by-annotations&metric=alert_status)](https://sonarcloud.io/dashboard?id=dan323_functional-by-annotations) -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=dan323_functional-by-annotations&metric=coverage)](https://sonarcloud.io/dashboard?id=dan323_functional-by-annotations) -[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=dan323_functional-by-annotations&metric=bugs)](https://sonarcloud.io/dashboard?id=dan323_functional-by-annotations) -[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=dan323_functional-by-annotations&metric=code_smells)](https://sonarcloud.io/dashboard?id=dan323_functional-by-annotations) +[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=functional-by-annotations&metric=alert_status)](https://sonarcloud.io/dashboard?id=dan323_functional-by-annotations) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=functional-by-annotations&metric=coverage)](https://sonarcloud.io/dashboard?id=dan323_functional-by-annotations) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=functional-by-annotations&metric=bugs)](https://sonarcloud.io/dashboard?id=dan323_functional-by-annotations) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=functional-by-annotations&metric=code_smells)](https://sonarcloud.io/dashboard?id=dan323_functional-by-annotations) > Bring functional programming concepts to Java through annotations and compile-time code generation diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 8d2cc9b5..20cc30f0 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -140,7 +140,7 @@ public class FiniteListFunctional implements Function f) { return list.head().maybe( h -> FiniteList.cons(f.apply(h), map(list.tail(), f)), - FiniteList.nil() + List.nil() ); } @@ -155,7 +155,7 @@ public class FiniteListFunctional implements FiniteList list) { return list.head().maybe( h -> concat(f.apply(h), flatMap(f, list.tail())), - FiniteList.nil() + List.nil() ); } @@ -175,7 +175,7 @@ public class FiniteListFunctional implements IApplicative app, Function f, FiniteList list) { - K empty = ApplicativeUtil.pure(app, FiniteList.nil()); + K empty = ApplicativeUtil.pure(app, List.nil()); return foldr( (x, y) -> ApplicativeUtil.liftA2(app, FiniteList::cons, f.apply(x), y), empty, @@ -185,7 +185,7 @@ public class FiniteListFunctional implements // Alternative public static FiniteList empty() { - return FiniteList.nil(); + return List.nil(); } public static FiniteList disjunction( diff --git a/docs/FUNCTIONAL_STRUCTURES.md b/docs/FUNCTIONAL_STRUCTURES.md index 61b17dc6..7a87c23b 100644 --- a/docs/FUNCTIONAL_STRUCTURES.md +++ b/docs/FUNCTIONAL_STRUCTURES.md @@ -101,7 +101,7 @@ public class ListTraversal implements ITraversal> { IApplicative app, Function f, FiniteList lst) { - K empty = ApplicativeUtil.pure(app, FiniteList.nil()); + K empty = ApplicativeUtil.pure(app, List.nil()); return foldr((x, y) -> ApplicativeUtil.liftA2(app, FiniteList::cons, f.apply(x), y), empty, lst); diff --git a/docs/PARSER.md b/docs/PARSER.md index 52a5bca8..7ba732d0 100644 --- a/docs/PARSER.md +++ b/docs/PARSER.md @@ -71,7 +71,7 @@ var result = numbers.apply("1,2,3"); // Returns: Either.right(Pair(FiniteList.of(1, 2, 3), "")) var empty = numbers.apply("abc"); -// Returns: Either.right(Pair(FiniteList.nil(), "abc")) // zero values is ok +// Returns: Either.right(Pair(List.nil(), "abc")) // zero values is ok ``` ### Disjunction/Alternative diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md index 982786ca..a154cd9b 100644 --- a/docs/PIPELINE.md +++ b/docs/PIPELINE.md @@ -410,14 +410,14 @@ Recommended branch protection rules for `master`: ### Current Targets -| Metric | Target | Current | Status | -|--------|--------|---------|--------| -| Line Coverage | ≥85% | Check [Reports](https://dan323.github.io/functional-by-annotations/) | - | -| Mutation Coverage | ≥85% | Check [Reports](https://dan323.github.io/functional-by-annotations/pit-reports/) | - | -| SonarCloud Quality Gate | Pass | Check [SonarCloud](https://sonarcloud.io/dashboard?id=dan323_functional-by-annotations) | - | -| Bugs | 0 | - | - | -| Vulnerabilities | 0 | - | - | -| Code Smells | A Rating | - | - | +| Metric | Target | Current | Status | +|-------------------------|----------|----------------------------------------------------------------------------------|--------| +| Line Coverage | ≥85% | Check [Reports](https://dan323.github.io/functional-by-annotations/) | - | +| Mutation Coverage | ≥85% | Check [Reports](https://dan323.github.io/functional-by-annotations/pit-reports/) | - | +| SonarCloud Quality Gate | Pass | Check [SonarCloud](https://sonarcloud.io/dashboard?id=functional-by-annotations) | - | +| Bugs | 0 | - | - | +| Vulnerabilities | 0 | - | - | +| Code Smells | A Rating | - | - | ### Historical Trends diff --git a/docs/PLAN.md b/docs/PLAN.md index 1fcede92..e7b4c815 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -1,1286 +1,194 @@ -# v1.3 → v2.0 Production Readiness Plan +# v1.3 → v2.0 Roadmap ## functional-by-annotations -**Current Version:** 1.3 -**Target Version:** 2.0 (Production-Ready) -**Timeline:** 16 weeks (4 months) -**Start Date:** February 18, 2026 -**Target Release Date:** May 31, 2026 - -This document defines a concrete, execution-oriented roadmap to take `functional-by-annotations` from v1.3 to v2.0 as a mature, production-ready library. It is structured as actionable phases with weekly breakdowns, acceptance criteria, and parallel work streams. - ---- - -# Executive Summary: Why v2.0? - -The jump from v1.3 to v2.0 justifies a major version change due to: - -1. **Breaking Changes:** Java 17+ minimum requirement (bump from 11), API refactoring, module reorganization -2. **Reflection Elimination:** Zero-reflection hot paths via MethodHandle caching -3. **Enterprise Features:** Monad transformers, higher-kinded types, advanced parser toolkit -4. **Performance:** <1% overhead vs. native Java (down from <5%) -5. **Security-First:** Vulnerability scanning and compliance policies -6. **Production Readiness:** Full API freeze, comprehensive law verification, enterprise documentation - ---- - -# 1. v2.0 Scope & Stability Target - -## 1.1 Target Environment -- **Java 17+ minimum** (LTS baseline, downgrade from Java 24 in v1.3) -- Maven 3.8+ & Gradle 7.0+ -- Zero mandatory runtime dependencies beyond JDK -- Multi-platform: Linux, macOS, Windows - -## 1.2 v2.0 Exit Criteria -- Stable, frozen public API (no breaking changes until v3.0) -- Documented behavior + edge cases -- Performance benchmarks published (<1% overhead) -- Law verification 100% on all built-in instances -- CI passing across Java 17, 21, 24 -- **Zero reflective calls in hot execution paths** (map/flatMap) -- Security audit completed, CVE scanning enabled -- Modularized architecture (4+ independent modules) -- Migration guide from v1.3 published -- Example projects demonstrating production use - -Acceptance Criteria: -- Public API freeze document created + signed off -- SemVer policy documented -- Breaking changes documented in BREAKING_CHANGES.md -- Java version requirement updated in all documentation - ---- - -# Execution Plan – 16-Week v2.0 Roadmap - -This section breaks down the v2.0 production-readiness plan into a concrete, weekly execution schedule. - -## Phase Overview - -| Phase | Weeks | Focus | Key Deliverables | -|-------|-------|-------|------------------| -| **1: Foundation & API Hardening** | 1-3 | API audit, Java 17+ migration, processor/codegen hardening | API freeze document, breaking changes guide | -| **2: Architecture Modernization** | 4-6 | MethodHandle caching, law verification, benchmarks | Zero-reflection hot paths, LawChecker, perf reports | -| **3: Enterprise Features** | 7-10 | Monad transformers, advanced tooling, optimizations | New algebraic structures, declarative config, <1% overhead | -| **4: Security & Advanced QA** | 11-13 | Security audit, fuzz testing, integration tests | Security report, CVE scanning enabled, comprehensive test matrix | -| **5: Documentation & Release** | 14-16 | API reference polish, migration guides, final QA, v2.0 release | Finalized docs, v2.0-rc1, v2.0 published to Maven Central | - ---- - -## Phase 1: Foundation & API Hardening (Weeks 1-3) - -### Week 1: API Audit & Breaking Changes Analysis - -#### 1.1 Complete Public API Audit (Existing Codebase) - -**Objective:** Inventory all public symbols from v1.3 and categorize stability. - -**Tasks:** -- Audit `functional-definitions/functional-compiler` for all public classes/interfaces -- Categorize each symbol: - - **Frozen:** Core APIs (Functor, Applicative, Monad, Monoid, Semigroup, etc.) - - **Refactored:** APIs with planned changes (configuration, processor options) - - **Deprecated:** Features to remove in v2.0 - - **New:** Features added in v2.0 only -- Create stability matrix: `|Symbol|v1.3 Status|v2.0 Status|Breaking Change?|Migration Path|` -- Document removal rationale for deprecated APIs -- Identify internal APIs to mark @InternalApi - -**Deliverables:** -- `docs/API_AUDIT_v2.0.md` (breaking changes catalog) -- `docs/BREAKING_CHANGES.md` (user-facing guide) -- Stability matrix spreadsheet - -**Acceptance Criteria:** -- 100% of public APIs categorized -- Breaking changes documented with migration examples - -#### 1.2 Java 17+ Migration & Cleanup - -**Objective:** Bump minimum Java version to 17, leverage modern features. - -**Tasks:** -- Update all POMs: `17` -- Update CI matrix: Java 17, 21, 24 -- Review codebase for Java 11-era workarounds (can remove) -- Leverage Java 17+ features: - - Records for immutable data (consider for value types) - - Text blocks for multi-line strings in documentation - - Sealed classes (if appropriate for class hierarchies) -- Update README and installation docs with Java 17+ requirement -- Run tests across all three versions to verify compatibility - -**Deliverables:** -- Updated POMs with Java 17 minimum -- CI pipeline updated -- No compatibility issues with Java 24 - -**Acceptance Criteria:** -- All tests pass on Java 17, 21, 24 -- Zero deprecation warnings in build log -- README reflects Java 17+ requirement +**Current Version:** 1.3-SNAPSHOT +**Target Version:** 2.0 +**Plan Written:** February 18, 2026 +**Plan Revised:** March 23, 2026 +**Target Release:** May 31, 2026 --- -### Week 2: Concurrency & Thread Safety Hardening - -#### 2.1 Thread Safety Sanity Check - -**Objective:** Validate that concurrent use of generated code paths and reflection utilities does not introduce races or blocking behavior in typical usage. +## Status as of March 23, 2026 -**Tasks:** -- Add a lightweight concurrency test that: - - Spawns a moderate number of threads (e.g., 16-32) calling generated static methods in parallel. - - Calls reflection-based utilities in parallel (e.g., `FunctorUtil.map`) using the same inputs. - - Asserts consistent results and no exceptions. -- If any shared caches exist, include them in the test scope. -- Document the thread-safety assumptions (e.g., "no shared mutable state") and the tested scenario. +### Completed +- Java 17+ minimum enforced; CI tests Java 17, 21, 24 +- `ConcurrentTypeClassSanityTest` added for annotation processor thread safety +- `docs/API_AUDIT_v2.0.md` and `docs/BREAKING_CHANGES.md` created +- SQL module scaffolded: `SqlAst`, `Expr`, `Query`, `QueryFunctor`, `RowDecoder`, `Config`, `DbError` +- `ExprCompiler` — converts typed `Expr` AST to SQL strings +- `SqlAstCompiler` — converts `SqlAst` to optimised SQL strings using a `Fragment` intermediate representation (Filter/Project fold; no unnecessary subqueries) +- `RowDecoder` primitive factories: `string`, `integer`, `longCol`, `bool` +- `QueryFunctor`: `pure`, `map`, `empty`, `disjunction`, `liftA2` (CROSS JOIN), `join` (JOIN…ON) +- List overhaul in progress on `feature/overhaul-lists`: `Cycle`, `Merged`, `ZipApplicative` rework, `ListZipper` fixes +- Test suite extended: `SqlAstCompilerTest` (24 tests), `RowDecoderTest` (18 tests), `QueryFunctorTest` (6 tests), `SqlRunnerTest` (9 tests), `RowDecoderMonadTest` (10 tests), `SqlAstMonoidTest` (6 tests) +- Pre-existing compilation bugs fixed: missing `List` import in 6 test/source files +- `SqlRunner` — JDBC execution layer with `run(Config, Query)` and `run(Connection, Query)` overloads +- `RowDecoderMonad` — `@Monad @Alternative` type class instance for `RowDecoder` +- `SqlAstMonoid` — `@Monoid` type class instance for `SqlAst` (UNION monoid) -**Deliverables:** -- `ConcurrentTypeClassSanityTest` in `functional-definitions/functional-compiler` tests with a small number of repeat runs. -- Short note in docs describing what was tested and what is assumed. - -**Acceptance Criteria:** -- No race conditions or flaky failures in 100 consecutive runs. -- No observable contention in basic profiling (if available). +### Not Started (original plan items dropped) +The following items from the original plan are removed as out of scope for this project: +- JMH benchmark module — premature; the processor generates no runtime overhead today +- MethodHandle caching — there is no reflection in hot paths to replace +- YAML-driven type class configuration — over-engineered for this use case +- `ProcessorDebugger` CLI tool — not justified by current usage patterns +- Monad transformers / Bifunctor — valuable but deferred to post-2.0 +- Fuzz testing, 24-hour stability runs — not proportionate +- Security audit with crypto-signed registrations — out of scope +- Community review cycle — deferred; no established user base yet --- -### Week 3: Performance Baseline & Java 17+ Cleanup - -#### 3.1 Establish Performance Baseline with JMH - -**Objective:** Measure current v1.3 performance to establish v2.0 target. - -**Tasks:** -- Create JMH benchmark suite in `benchmarks/` module: - - `MapBenchmark`: Single `map` operation - - `FlatMapBenchmark`: Chained `flatMap` - - `TypeClassResolutionBenchmark`: Type class resolution cost - - `ChainedOperationsBenchmark`: 5+ chained operations -- Baselines to compare against: - - Direct Java (raw Optional/List) - - Manual monad implementation - - Other functional libraries (Vavr, etc.) if applicable -- Run across Java 17, 21, 24 -- Target: <5% overhead for v2.0 (goal: <1% by end of project) -- Generate performance report with graphs: `docs/PERFORMANCE_v2.0.md` - -**Deliverables:** -- JMH benchmark module -- Baseline results (JSON + HTML report) -- Performance report document - -**Acceptance Criteria:** -- Benchmarks compile and run without errors -- Results reproducible (variance < 5% across runs) -- Report shows overhead vs. baseline -- Performance baseline committed for future comparison - -#### 3.2 Code Cleanup & Modernization +## Remaining Work (10 weeks, March 23 – May 31) -**Objective:** Leverage Java 17 features, remove technical debt. +### Phase A: SQL Module Completion (Weeks 1–3, ends April 13) -**Tasks:** -- Replace old `Supplier` patterns with lambda expressions -- Identify opportunities for records (immutable value types) -- Use text blocks for long string literals in tests/docs -- Clean up deprecated Guava usages (if any) -- Update exception handling to modern standards -- Remove workarounds for older Java versions +The SQL module implementation is complete. One documentation task remains. -**Deliverables:** -- Refactored codebase using Java 17 idioms -- No compiler warnings +#### A.1 SQL module documentation -**Acceptance Criteria:** -- All tests pass -- Zero deprecation warnings -- Code cleaner, more idiomatic Java 17 +Update `docs/EXAMPLES.md` with a worked SQL example showing: +- Building a `Query` with `RowDecoder` primitives and combinators +- Applying `SqlAstCompiler.compile` to inspect the generated SQL +- The `QueryFunctor` operations (`map`, `join`, `disjunction`) --- -## Phase 2: Architecture Modernization (Weeks 4-6) - -### Week 4: MethodHandle Caching & Reflection Elimination - -#### 4.1 MethodHandle Cache Implementation - -**Objective:** Replace reflective method invocations with MethodHandle-based caching. - -**Tasks:** -- Create `MethodHandleCache`: - - Caches `MethodHandle` for each type class method - - Lookup by `(Class, String methodName, MethodType)` → `MethodHandle` -- Convert hot paths to use MethodHandle: - - `map` implementation - - `flatMap` implementation - - `bind` operation - - Type class method dispatch -- Ensure thread-safe initialization (e.g., ClassValue-based caching) -- Add monitoring: cache hit/miss metrics via logging -- Benchmark MethodHandle overhead vs. reflection - -**Deliverables:** -- `MethodHandleCache` class (immutable, thread-safe) -- All hot paths refactored to use MethodHandle -- Benchmark: MethodHandle vs. reflection overhead - -**Acceptance Criteria:** -- MethodHandle-based calls < 1% slower than reflection (often faster) -- Zero reflective calls in `map`, `flatMap` during execution -- JFR profiling confirms no `Method.invoke()` in hot paths - -#### 4.2 Explicit Mode & Configuration System - -**Objective:** Allow users to disable automatic discovery for security-conscious deployments. - -**Tasks:** -- Design `FunctionalConfig` API: - ```java - FunctionalConfig config = FunctionalConfig.builder() - .discoveryMode(DiscoveryMode.EXPLICIT) // No classpath scanning - .allowReflection(false) // Fail if reflection needed - .strictTypeChecking(true) // Fail on ambiguous types - .build(); - Functional.initialize(config); - - // Then manually register: - Functional.register(Optional.class, OptionalMonad.instance()); - ``` -- Implement `EXPLICIT` mode: no automatic discovery -- Implement strict type checking mode: ambiguous types → exception -- Update annotation processor to respect explicit mode -- Document configuration options with examples - -**Deliverables:** -- `FunctionalConfig` builder API -- Updated annotation processor -- Configuration examples in docs - -**Acceptance Criteria:** -- All existing tests pass in both automatic and explicit modes -- Zero behavioral difference between modes (same type classes registered) -- Example code demonstrates explicit mode usage - ---- +### Phase B: Annotation Processor — Code Generation (Weeks 3–6, ends May 4) -### Week 5: Law Verification System & Property-Based Testing +The processor currently **validates** that required methods are present but generates nothing. The original v2.0 goal of making annotation-driven code useful requires actually deriving the missing methods. -#### 5.1 LawChecker Framework Enhancement +#### B.1 Code generation for `@Functor` -**Objective:** Build comprehensive law verification system (building on existing v1.0 work). +**What to generate:** `mapConst(F, B): F` derived from `map`. -**Tasks:** -- Enhance/finalize `LawChecker` for v2.0: - - Parameterized verification: `LawChecker.monad(Optional.class)` - - Multiple test generation strategies: - - Exhaustive (for small domains) - - Random sampling (property-based) - - Symbolic execution (if applicable) - - Deterministic failure diagnostics with counter-examples - - Performance: law check < 100ms per instance -- Implement law predicates: - - **Functor:** identity, composition - - **Applicative:** homomorphism, interchange - - **Monad:** left/right identity, associativity - - **Monoid:** associativity, identity - - **Semigroup:** associativity only - - **Foldable:** specific folds - - **Bifunctor:** functor laws for both parameters -- Add verification cache: results stored per type class + version - -**Deliverables:** -- Enhanced `LawChecker` with all algebraic structures -- Property-based testing integration (jqwik) -- Law check performance report - -**Acceptance Criteria:** ```java -LawChecker> checker = LawChecker.monad(Optional.class); -LawCheckResult result = checker.verify(); -assert result.passed() : result.diagnostics(); -// Output: "MonadLeftIdentity[Optional] ✓" +// User writes: +@Functor +class MyFunctor implements IFunctor> { + static My map(My fa, Function f) { … } +} + +// Processor generates (into the same class or a companion): +static My mapConst(My fb, A a) { + return map(fb, ignored -> a); +} ``` -#### 5.2 Built-in Instance Validation & Test Templates - -**Objective:** Verify all built-in instances pass 100% of applicable laws. - -**Tasks:** -- Create reusable law test templates: - - `MonadLawTests` abstract base (inherit + provide generators) - - `FunctorLawTests` for Functor instances - - `MonoidLawTests` for monoid operations - - Example: `OptionalMonadTests extends MonadLawTests { ... }` -- Test built-in instances: - - Optional (Monad, Functor, Applicative) - - List (Monad, Functor, Monoid) - - Stream (Functor, Foldable) - - CompletableFuture (Monad, Applicative) - - Set, Map (Functor variants) - - New in v2.0: Either (Monad, Applicative), Try/Result (Monad) -- Generate law verification matrix: `|Law|Optional|List|Stream|Future|Either|` → ✓/✗ -- Document any law violations found (should be zero; if found, fix or document) - -**Deliverables:** -- Reusable law test template classes -- Law verification test suites for each instance -- Coverage matrix document - -**Acceptance Criteria:** -- All built-in instances pass 100% of applicable laws -- Matrix shows 100% coverage -- No law failures (or documented exceptions) -- Each test suite has >50 generated test cases (property-based) - ---- - -### Week 6: Performance Optimization & Benchmarking - -#### 6.1 Performance Optimization Cycle - -**Objective:** Achieve <2% overhead vs. native Java (aiming for <1%). - -**Tasks:** -- Analyze JMH results from Week 3: - - Identify hotspots: type class dispatch, method invocation - - Profile with JFR to find allocation pressure, GC impact -- Optimization targets: - - Type class dispatch: MethodHandle vs. reflection (Week 4 work) - - Avoid unnecessary boxing/unboxing - - Minimize allocations in hot paths (reuse contexts, lazy evaluation) -- Implement optimizations: - - Inline caching for frequently-used type classes - - Object pool for intermediate values (if GC is bottleneck) - - Lazy initialization of law checkers -- Re-benchmark after each optimization -- Target: <2% overhead on single operations, <1% on chained operations - -**Deliverables:** -- Optimized code (MethodHandle caching from Week 4 is part of this) -- Updated JMH results -- Performance analysis document - -**Acceptance Criteria:** -- MapBenchmark: < 2% overhead vs. raw Optional.map() -- FlatMapBenchmark: < 2% overhead vs. raw flatMap -- TypeClassResolutionBenchmark: < 500ns per lookup (even with synchronization) -- Chained operations: < 1% cumulative overhead - -#### 6.2 Comprehensive Benchmarking Report - -**Objective:** Publish v2.0 performance characteristics. - -**Tasks:** -- Create detailed performance report: `docs/PERFORMANCE_v2.0.md` -- Include: - - Methodology (JMH settings, warm-up, iterations) - - Results across Java 17, 21, 24 - - Comparison with v1.3 baseline - - Comparison with other functional libraries - - Optimization notes (what was done, what was tried) - - Limitations and known issues - - Roadmap for future optimizations -- Generate graphs: overhead % vs. operation complexity -- Document memory footprint: object allocation rates - -**Deliverables:** -- `docs/PERFORMANCE_v2.0.md` -- JMH results (JSON, CSV, HTML) -- Graphs (PNG/SVG) - -**Acceptance Criteria:** -- Report is comprehensive and reproducible -- Methodology is sound (warm-up, iterations, GC consideration) -- Results are within claimed overhead targets -- Recommendations for future optimization are specific - ---- - -## Phase 3: Enterprise Features & Advanced Tooling (Weeks 7-10) - -### Week 7: New Algebraic Structures (Monad Transformers, Bifunctor, etc.) - -#### 7.1 Monad Transformer Framework - -**Objective:** Enable composition of monads for practical enterprise use cases. - -**Tasks:** -- Design monad transformer API: - - `MaybeT` (Maybe monad transformer over M) - - `EitherT` (Either monad transformer) - - `ReaderT` (Reader monad transformer) - - Examples: - ```java - EitherT result = - EitherT.liftOptional(Optional.of("value")); - ``` -- Implement transformer instances with law verification -- Provide practical examples: - - Combining Optional + Either for error handling - - Reader + Future for dependency injection with async - - Maybe + List for optional collections -- Ensure transformers compose (MaybeT over EitherT, etc.) -- Document with extensive examples - -**Deliverables:** -- Monad transformer implementations -- Law test suites for transformers -- Example project using transformers - -**Acceptance Criteria:** -- All transformer laws verified -- Composition works (Transformer) without issues -- Examples demonstrate practical use cases -- <5% performance overhead for transformers - -#### 7.2 Bifunctor & Higher-Order Structures - -**Objective:** Support bifunctors (functors in two arguments) for practical cases like Either. - -**Tasks:** -- Implement `Bifunctor` interface: - - `bimap(f, g)`: apply function to both type parameters - - Left/right functor instances -- Implement for: - - Either (bimap, left, right) - - Pair/Tuple2 (bimap over both elements) - - Function (contravariant in input, covariant in output) -- Add law verification: bifunctor laws -- Document with practical examples - -**Deliverables:** -- Bifunctor interface + implementations -- Law tests -- Usage examples - -**Acceptance Criteria:** -- All bifunctor implementations pass laws -- Examples show practical use (e.g., bimap on Either) - ---- - -### Week 8: Declarative Configuration & Code Generation Introspection - -#### 8.1 Declarative Type Class Configuration - -**Objective:** Allow users to configure type classes via configuration files (YAML/JSON) instead of code. - -**Tasks:** -- Design configuration file format (YAML): - ```yaml - typeclasses: - java.util.Optional: - instances: - - Functor - - Applicative - - Monad - methods: - map: java.util.Optional::map - flatMap: java.util.Optional::flatMap - java.util.List: - instances: - - Functor - - Monad - methods: - map: java.util.stream.Collectors::toList - ``` -- Implement configuration loader: - - Parse YAML → internal resolution configuration - - Validate configuration (all methods exist, correct signatures) - - Merge with automatic discovery results - - Support environment variables for dynamic configuration -- Add CLI tool: `functional-config-validate config.yaml` -- Document configuration schema - -**Deliverables:** -- Configuration file format spec -- ConfigurationLoader implementation -- CLI validation tool -- Example configuration files - -**Acceptance Criteria:** -- Configuration files parse correctly -- Validation tool provides clear error messages -- Configuration can override automatic discovery -- Example projects use configuration files - -#### 8.2 Code Generation Introspection & Debugging Tools - -**Objective:** Provide tools to inspect and debug annotation processor output. - -**Tasks:** -- Create debugging API: - ```java - ProcessorDebugger debugger = ProcessorDebugger.enable(); - // Process annotations... - debugger.dumpGeneratedCode("output/generated"); // Write .java files - debugger.reportStatistics(); // Number of instances, methods, etc. - ``` -- Implement: - - Generated code dumper (write source files for inspection) - - Statistics reporter (how many instances generated, for which classes, etc.) - - Conflict detector (ambiguous type class resolutions) - - Suggestion engine (if type class not found, suggest alternatives) -- Add Maven plugin goal: `mvn functional:debug` -- Include in documentation: troubleshooting guide using debugger - -**Deliverables:** -- ProcessorDebugger API -- Maven/Gradle plugin integration -- Troubleshooting guide - -**Acceptance Criteria:** -- Generated code can be inspected -- Statistics provide useful feedback -- Conflicts are clearly reported -- Plugin goals work correctly - ---- - -### Week 9: Advanced Parser Toolkit & DSL - -#### 9.1 Enhanced Parser Combinator Library - -**Objective:** Build production-grade parser combinator toolkit (if applicable to project scope). - -**Tasks:** -- Enhance parser combinators: - - `seq`: sequence parsers - - `alt`: alternative parsers - - `many`, `sepBy`, `sepEndBy`: repetition - - `attempt`: lookahead without consumption - - Error recovery strategies - - Position tracking for error messages -- Add parser primitive types: - - `satisfy(predicate)`: match character/token - - `literal(String)`: match exact string - - `regex(Pattern)`: match regex -- Implement practical examples: - - JSON parser - - Configuration file parser - - Simple arithmetic expression parser -- Performance: parse 1MB file in < 100ms -- Error messages: show position + context for failures - -**Deliverables:** -- Enhanced parser library -- Example parsers (JSON, config, expressions) -- Performance benchmarks +This is the simplest possible derivation and serves as the proof-of-concept for code generation. -**Acceptance Criteria:** -- All parsers compile and run -- Error recovery works and improves user experience -- Performance is acceptable (< 100ms for 1MB) -- Examples demonstrate practical use +**Implementation notes:** +- Use `javax.annotation.processing.Filer` to write a source file (or inject into the class via byte-code — source file is simpler and more debuggable) +- Generate only if `mapConst` is not already present +- Follow existing `FunctionalCompiler` → `CompilerFactory` → `Compiler` structure; add a `CodeGenerator` step after validation passes -#### 9.2 DSL for Type Class Declaration +#### B.2 Code generation for `@Applicative` -**Objective:** Allow users to declare type classes via fluent API or annotations. +Derive `keepLeft` and `keepRight` from `liftA2`. -**Tasks:** -- Fluent API for type class declaration: - ```java - Functional.typeclass("MyMonad") - .withMethod("map", "myType.map(Object -> Object)") - .withMethod("flatMap", "myType.flatMap(Object -> MyMonad)") - .withLawCheck(MonadLaws.class) - .register(); - ``` -- Annotation-based DSL: - ```java - @TypeClass( - name = "MyMonad", - methods = { - @InstanceMethod(name = "map", signature = "..."), - @InstanceMethod(name = "flatMap", signature = "...") - } - ) - public class MyMonadInstance { ... } - ``` -- Document both approaches with examples -- Ensure declarative approach is type-safe (compile-time checks where possible) - -**Deliverables:** -- Fluent API for type class declaration -- Annotation-based alternative -- Examples for both approaches - -**Acceptance Criteria:** -- Both approaches compile correctly -- Generated type classes behave identically -- Type safety enforced at compile-time - ---- - -### Week 10: Enterprise Features QA & Stabilization - -#### 10.1 Integration Testing of New Features - -**Objective:** Verify monad transformers, bifunctors, and advanced features work together. - -**Tasks:** -- Create integration test suite: - - Monad transformers composed together - - Bifunctors combined with transformers - - Configuration-driven type classes working with transformers - - Parser combinators using monad abstractions -- Real-world scenario tests: - - Error handling with Either + Optional transformers - - Dependency injection with Reader monad - - Streaming data with List + Stream transformers -- Load testing: 10,000+ type class registrations + lookups -- Stress testing: 100+ concurrent transformer operations - -**Deliverables:** -- Comprehensive integration test suite -- Real-world scenario tests -- Load/stress test reports - -**Acceptance Criteria:** -- All integration tests pass -- No unexpected interactions between features -- Performance acceptable under load -- No memory leaks (verified with JProfiler or similar) - -#### 10.2 Documentation & Examples for Enterprise Features - -**Objective:** Document all new features with practical examples. - -**Tasks:** -- Create `docs/ENTERPRISE_FEATURES.md`: - - Monad transformers explanation + examples - - Bifunctors use cases - - Configuration system walkthrough - - Parser combinators tutorial - - DSL for type classes -- Add example project: `example-functional-enterprise/` - - Real-world use cases: web framework integration, error handling, etc. - - Demonstrates all new v2.0 features - - Includes tests using law verification -- Add migration guide: how to use new features from v1.3 code -- Video tutorial or interactive guide (optional) - -**Deliverables:** -- Enterprise features documentation -- Example project -- Migration guide - -**Acceptance Criteria:** -- All new features documented with examples -- Example project runs successfully -- Novice user can understand features from documentation alone - ---- - -## Phase 4: Security Hardening & Advanced QA (Weeks 11-13) - -### Week 11: Security Audit & Vulnerability Scanning - -#### 11.1 Security Code Review - -**Objective:** Identify and mitigate security vulnerabilities. - -**Tasks:** -- Conduct security-focused code review: - - Reflection injection: can attackers manipulate type class resolution? - - Deserialization attacks: can type classes be deserialized unsafely? - - Classpath poisoning: can malicious JAR override type classes? -- Implement security controls: - - Type class validation: verify method signatures match expectations - - Signed registrations: optional crypto signing of type class registrations - - Audit logging: log type class resolution + invocation operations -- Document security guarantees: - - What threats are mitigated - - What threats are not addressed (out of scope) - - Recommended deployment practices (no dynamic classpath changes, locked-down environments, etc.) - -**Deliverables:** -- Security audit report (`docs/SECURITY_AUDIT.md`) -- Security control implementations -- Security best practices guide - -**Acceptance Criteria:** -- Audit report identifies zero critical vulnerabilities -- Mitigation strategies implemented for all identified risks -- Security guide provides actionable deployment advice - -#### 11.2 Automated CVE Scanning & Dependency Management - -**Objective:** Set up automated vulnerability scanning for dependencies. - -**Tasks:** -- Integrate dependency checking tools: - - Maven: `maven-dependency-check-plugin` - - OWASP Dependency-Check for CVE scanning - - Enable on CI/CD (block PRs if critical CVEs found) -- Establish dependency update policy: - - Auto-update patch versions (e.g., 1.2.3 → 1.2.4) - - Manual review for minor/major updates - - Quarterly dependency audit -- Create SECURITY.md: - - Reporting process for security issues - - Responsible disclosure policy - - Contact information for security team - - CVE/security advisory announcement process - -**Deliverables:** -- CVE scanning integrated into CI/CD -- Dependency update policy document -- SECURITY.md file - -**Acceptance Criteria:** -- CI/CD blocks on critical CVEs -- Zero critical CVEs at release time -- SECURITY.md published in repository - ---- - -### Week 12: Fuzz Testing & Robustness - -#### 12.1 Fuzz Testing for Parser Combinators - -**Objective:** Use fuzz testing to find edge cases and crashes. - -**Tasks:** -- Set up fuzzing infrastructure: - - JQF (Java QuickCheck Fuzzer) or similar - - Fuzz targets: parser combinators, configuration loader, type class resolver -- Implement fuzz targets: - ```java - @Fuzz - public void fuzzParser(byte[] input) { - try { - Parser.parse(new String(input)); - } catch (ParseException e) { - // Expected - } - // Should not throw unhandled exceptions or crash - } - ``` -- Run fuzz for extended period (24+ hours on CI) -- Analyze results: any crashes or hangs → fix -- Document found/fixed issues - -**Deliverables:** -- Fuzz testing setup -- Fuzz targets implementation -- Bug reports + fixes for any issues found -- Fuzzing results summary - -**Acceptance Criteria:** -- Fuzz tests run for 24+ hours without crashes -- Any crashes found are fixed -- No unhandled exceptions during fuzzing - -#### 12.2 Regression Testing & Edge Cases - -**Objective:** Ensure v2.0 handles edge cases and doesn't regress. - -**Tasks:** -- Create comprehensive edge case test suite: - - Null values in type classes - - Cyclic type class dependencies - - Very large type class hierarchies (100+ types) - - Unicode identifiers (if supported) - - Deeply nested generics (Optional>>) - - Type class registration order effects -- Add regression tests: ensure no regressions from v1.3 - - All v1.3 tests pass on v2.0 (with migration adjustments) - - Performance doesn't degrade - - Behavior matches documented contracts -- Document known limitations: - - Cases where behavior may differ from expectations - - Recommended workarounds - -**Deliverables:** -- Edge case test suite -- Regression test suite -- Known limitations documentation - -**Acceptance Criteria:** -- All edge cases handled gracefully -- Zero regressions from v1.3 (documented migrations only) -- Known limitations are explicitly documented - ---- - -### Week 13: Final Integration Testing & QA Sign-Off - -#### 13.1 Comprehensive Integration Testing - -**Objective:** Verify entire system works end-to-end across all modules. - -**Tasks:** -- Integration test matrix: - - All modules together (core + laws + instances + enterprise) - - All Java versions (17, 21, 24) - - All discovery modes (automatic + explicit) - - All configuration methods (code + YAML) - - All use cases (simple + complex + real-world) -- Automated test matrix execution (matrix build in CI/CD) -- Real-world scenario tests: - - Web framework integration (if applicable) - - Concurrent application under load - - 24-hour stability test (long-running process) -- Generate compatibility matrix: `|Feature|Java 17|Java 21|Java 24|Explicit|Auto| → ✓/✗` - -**Deliverables:** -- Integration test suite -- Compatibility matrix -- Stability test results (24-hour run) - -**Acceptance Criteria:** -- All integration tests pass across all configurations -- No regressions or unexpected behavior -- Stability test completes without errors or hangs - -#### 13.2 QA Sign-Off & Release Readiness - -**Objective:** Formally certify v2.0 is ready for release. - -**Tasks:** -- Create release checklist (comprehensive): - ``` - ✓ API Freeze: Documented, no breaking changes since Week 1 - ✓ Performance: <2% overhead on all benchmarks - ✓ Laws: 100% pass on all built-in instances - ✓ Security: Audit complete, zero critical CVEs - ✓ Tests: >90% coverage, all pass - ✓ CI/CD: Green across all platforms/versions - ✓ Documentation: Complete, reviewed, examples work - ✓ Modules: Clean dependency graph, all publishable - ✓ Backwards Compatibility: v1.3 migration path clear - ✓ Release Notes: Comprehensive CHANGELOG + breaking changes - ``` -- Have external reviewers (community members) sign off -- Create RELEASE_NOTES.md for v2.0 -- Tag `v2.0-rc1` (release candidate 1) in git -- Plan any final fixes based on RC feedback - -**Deliverables:** -- Signed-off release checklist -- v2.0-rc1 tag -- Release notes - -**Acceptance Criteria:** -- Checklist 100% complete with evidence -- External sign-off received -- v2.0-rc1 ready for final testing/feedback - ---- - -## Phase 5: Documentation Polish & v2.0 Release (Weeks 14-16) - -### Week 14: API Reference & Migration Guide - -#### 14.1 Comprehensive API Reference - -**Objective:** Complete, polished API documentation for all public symbols. - -**Tasks:** -- Generate Javadoc for all public APIs: - - Clear descriptions for every class/method - - Type parameter documentation - - Usage examples for complex APIs - - Cross-links between related APIs - - Tags: @since("2.0"), @deprecated (if applicable), @see, etc. -- Create structured API reference: - - Organized by module (core, laws, instances, enterprise) - - Organized by typeclass (Functor, Applicative, Monad, etc.) - - Organized by use case (error handling, collections, async, etc.) -- Add interactive examples (if feasible): - - Copy-paste ready code snippets - - Runnable examples (maybe via web playground) -- Generate PDF version of API docs -- Host on GitHub Pages with versioning - -**Deliverables:** -- Complete Javadoc -- Structured API reference documents -- PDF API guide -- GitHub Pages hosted documentation - -**Acceptance Criteria:** -- Every public symbol has documented Javadoc -- Examples are copy-paste ready and correct -- Documentation builds without warnings -- GitHub Pages reflects latest docs - -#### 14.2 Migration Guide: v1.3 → v2.0 - -**Objective:** Help users upgrade from v1.3 to v2.0. - -**Tasks:** -- Create `docs/MIGRATION_v1.3_to_v2.0.md`: - - Breaking changes summary - - Before/after code for each breaking change - - New features and how to use them - - Performance improvements (what to expect) - - Recommended practices for v2.0 - - Troubleshooting common migration issues -- Provide migration checklist: - - Update Java version to 17+ - - Update dependencies - - Rename/refactor deprecated APIs - - Update configuration (if using new features) -- Create automated migration tool (if complex): - - Script to help with Java AST transformations - - Lint rules to flag deprecated API usage -- Organize guide by migration difficulty (easy → advanced) -- Add FAQ section - -**Deliverables:** -- Comprehensive migration guide -- Migration checklist -- Migration tool (optional) -- FAQ - -**Acceptance Criteria:** -- v1.3 users can upgrade to v2.0 with clear steps -- Before/after examples for all breaking changes -- FAQ covers common questions -- Tool (if provided) handles most migrations automatically - ---- - -### Week 15: Documentation Finalization & Community Review - -#### 15.1 User Guide & Troubleshooting Updates - -**Objective:** Finalize all user-facing documentation. - -**Tasks:** -- Update `docs/GETTING_STARTED.md`: - - Newest features from v2.0 - - Updated examples with Java 17+ syntax - - Links to advanced guides - - Common next steps after quickstart -- Update `docs/EXAMPLES.md`: - - Real-world examples for v2.0 - - Monad transformer examples - - Error handling patterns - - Async patterns with CompletableFuture - - Domain-specific examples (if applicable) -- Comprehensive `docs/TROUBLESHOOTING.md`: - - FAQ (30+ entries) - - Common errors and solutions - - Performance tuning tips - - Debugging techniques using ProcessorDebugger - - Community resources -- Update README.md: - - Badges (build status, coverage, Maven Central, license) - - Feature summary for v2.0 - - Quick example - - Links to documentation - - Contribution guidelines updated -- Create `docs/CONTRIBUTION_GUIDE.md`: - - Development environment setup - - Building + testing - - Code style + conventions - - Pull request process - - Reporting bugs/security issues - -**Deliverables:** -- Updated user guides -- Comprehensive troubleshooting guide -- Updated README -- Contribution guide - -**Acceptance Criteria:** -- All guides reflect v2.0 features -- Examples are up-to-date and runnable -- FAQ covers majority of common issues -- Contribution guide makes onboarding clear - -#### 15.2 Community Review & Feedback Cycle - -**Objective:** Get community feedback on v2.0 before final release. - -**Tasks:** -- Announce v2.0-rc1 to community: - - GitHub discussions/issues - - Java community forums - - Reddit r/java - - Twitter/social media -- Solicit feedback: - - API usability - - Documentation clarity - - Performance expectations - - Missing features - - Breaking change concerns -- Create feedback issue template -- Track feedback in GitHub issues with label `v2.0-feedback` -- Categorize feedback: - - Critical (must fix before release) - - Important (nice to fix, may delay release slightly) - - Nice-to-have (defer to v2.1) -- Implement fixes based on critical feedback -- Tag `v2.0-rc2` if significant changes made - -**Deliverables:** -- Community feedback summary -- Issues created for feedback items -- v2.0-rc2 (if needed) -- Feedback response documentation - -**Acceptance Criteria:** -- Community provided feedback (minimum 10+ substantive responses) -- Critical issues addressed -- Release notes include community contributors - ---- - -### Week 16: Final Release & Launch - -#### 16.1 Final QA & v2.0 Release - -**Objective:** Release v2.0.0 to Maven Central. - -**Tasks:** -- Final checks before release: - - All tests pass (100% run on all platforms) - - No critical issues in bug tracker - - Documentation is final (no more corrections) - - Version numbers updated (POMs, README, docs) -- Create final release tag: `v2.0.0` -- Build + sign artifacts: - - JAR files for all modules - - Source JAR - - Javadoc JAR - - GPG signatures -- Deploy to Maven Central: - - Upload to Sonatype Nexus - - Release from staging repository - - Verify artifacts appear in Central (can take 10 minutes) -- Verify downloads work: - - Test with fresh Maven/Gradle project - - Verify all dependencies resolve - - Run example projects -- Create GitHub Release: - - Tag: `v2.0.0` - - Release title: "v2.0.0: Production-Ready Functional Programming" - - Release notes: link to CHANGELOG, highlights, thank you to contributors - - Attach artifacts (JARs, sources, docs) -- Announce release: - - GitHub announcement/discussion - - Update website/landing page - - Social media: Twitter, Reddit, etc. - - Java community newsletters - - Email to known stakeholders - -**Deliverables:** -- v2.0.0 released to Maven Central -- GitHub Release published -- Announcement distributed -- Example projects running against v2.0.0 - -**Acceptance Criteria:** -- Artifact downloadable from Maven Central -- All modules available -- GitHub Release published with comprehensive notes -- No critical issues reported post-release (first 48 hours) - -#### 16.2 Post-Release Monitoring & v2.0.1 Hotfix Plan +```java +static F keepLeft(F fa, F fb) { return liftA2((a,b) -> a, fa, fb); } +static F keepRight(F fa, F fb) { return liftA2((a,b) -> b, fa, fb); } +``` -**Objective:** Monitor v2.0 for issues, plan rapid hotfixes if needed. +#### B.3 Code generation for `@Monad` -**Tasks:** -- Establish post-release support: - - Monitor GitHub issues daily for 1 week - - Respond to user questions in discussions - - Track bug reports separately from feature requests - - Create v2.0.1 milestone for critical fixes -- Hotfix criteria: - - Critical security vulnerability → hotfix immediately - - Crash/regression from v1.3 → hotfix within 48 hours - - Data loss or correctness bug → hotfix within 1 week - - Nice-to-have improvements → v2.1 (not hotfix) -- Plan v2.0.1 release: - - Only critical fixes included - - Fast-track CI/CD (no RC phase) - - Released within 1-2 weeks if needed -- Create 2-week support window for v2.0: - - Focus on stability - - Address critical feedback - - Plan v2.1 based on community input -- Transition to v2.1 planning: - - Gather feature requests for v2.1 - - Create roadmap for future enhancements +Derive `join` from `flatMap`, and `map`/`fapply` if not provided. -**Deliverables:** -- Post-release monitoring process -- v2.0.1 hotfix plan + criteria -- v2.1 feature backlog -- Release notes template for v2.0.1 +```java +static F join(F> ffa) { return flatMap(Function.identity(), ffa); } +``` -**Acceptance Criteria:** -- v2.0 released successfully -- Community issues responded to promptly -- Hotfix plan in place for any critical issues -- v2.1 roadmap sketched based on feedback +**Acceptance criteria for Phase B:** +- Generated methods are syntactically correct Java +- Generated code compiles when the processor runs on `example-functional` +- Existing validation tests still pass +- At least one test in `example-functional` verifies a generated method is callable --- -# Risk Mitigation & Contingency Planning - -## Key Risks +### Phase C: Test Coverage & Law Verification (Weeks 6–9, ends May 18) -| Risk | Impact | Probability | Mitigation | -|------|--------|-------------|-----------| -| **Scope Creep** | Timeline slip by 4+ weeks | High | Weekly scope freeze; defer non-essential features to v2.1 | -| **Performance Target Miss** | Release with <5% vs. <2% overhead | Medium | Dedicated optimization Week 6; use JFR profiling early | -| **Reflection Elimination Challenges** | MethodHandle refactor takes longer | Medium | Start Week 4; have fallback plan (keep reflection for v2.0) | -| **Security Vulnerabilities Found Late** | Delay release by 2-3 weeks | Low | Conduct security review Week 11 (planned); fix earlier issues | -| **Community Feedback Conflicts** | Can't satisfy all users | High | Clear breaking changes policy; strong rationale for changes | -| **Dependency CVEs** | Must update critical dependency, may introduce incompatibilities | Medium | Dependency automation + regular updates; block CVEs in CI | -| **Java Version Incompatibility** | Discovered bug on Java 24 late | Low | Test on Java 17/21/24 at every phase; use continuous testing | -| **Module Split Introduces Bugs** | Integration issues between modules | Medium | Phase 1 planning + strong integration tests (Phase 4) | +#### C.1 Property-based law tests -## Contingency Plans +Add [jqwik](https://jqwik.net/) to `example-functional` test scope. Write parameterised law checks for the existing built-in instances: -### Timeline Overrun -- **If Phase 1 slips by 1 week:** Extend entire timeline by 1 week (target June 30 instead of May 31) -- **If Phase 2-3 slip by 2+ weeks:** Defer non-critical enterprise features (bifunctors, transformers partially) to v2.1; focus on core API freeze + performance for v2.0 -- **If Phase 4-5 slip:** Extend RC phase; consider v2.0-rc2 cycle if feedback requires significant changes +| Law | Instances to verify | +|------------------------------------------|---------------------------------| +| Functor identity/composition | `FiniteList`, `Maybe`, `Either` | +| Monad left/right identity, associativity | `Maybe`, `Either`, `FiniteList` | +| Monoid associativity + identity | `FiniteList` (as monoid) | +| Semigroup associativity | integer/string semigroups | -### Performance Targets Miss -- **If Week 6 benchmarks show 3-4% overhead:** Accept as v2.0 baseline; plan optimization for v2.0.1 or v2.1 -- **If MethodHandle approach proves slower:** Revert to reflection with better caching; document rationale +Format: abstract base `FunctorLawTest` that sub-tests inherit; sub-test provides a generator and the type class instance. -### Module Split Issues -- **If circular dependencies found:** Refactor to break cycles; worst case: merge modules back together (may delay 1-2 weeks) -- **If integration tests fail across modules:** Add additional integration phase (1 week); ensure modules work together seamlessly +#### C.2 Compiler test coverage -### Security Issues Found Late -- **If critical vulnerability in Week 12-13:** Fix + re-test (2-3 days); push v2.0-rc2 with fix -- **If critical CVE in dependency:** Update dependency + verify compatibility; may require code changes +`functional-compiler` tests currently only cover happy paths via `ConcurrentTypeClassSanityTest` and the annotation processing sanity checks. Add: -## Buffer & Parallel Execution - -- **Built-in Buffer:** 2-3 days per week for unexpected issues -- **Parallel Streams:** Phases can be parallelized (Phase 2B testing runs during Phase 3 engineering) -- **Critical Path:** Weeks 1-3 (API freeze) and Weeks 4-6 (architecture) are critical; delays here cascade -- **Flexible Phases:** Weeks 8-10 (enterprise features) can be trimmed or deferred if needed +- Missing required method → correct error message emitted +- Wrong method signature → correct error message +- Class annotated but interface not implemented → correct error message +- Multiple annotations on the same class (e.g., `@Functor @Monad`) → both validated --- -# Parallel Work Streams - -Due to open-source nature, recommend 3-4 concurrent streams with sync points: +### Phase D: Documentation & Release Prep (Weeks 9–12, ends May 25) -### Stream A: Architecture & Core (Weeks 1-6, 11-13) -- **Owners:** Core maintainers -- **Focus:** API audit, processor/codegen hardening, MethodHandle caching, security -- **Deliverables:** Stable core, zero-reflection hot paths -- **Sync Points:** Week 3 (API freeze), Week 6 (performance baseline) +#### D.1 Javadoc pass -### Stream B: Testing & Validation (Weeks 5-13) -- **Owners:** QA specialists, test engineers -- **Focus:** Law verification, benchmarking, fuzz testing, integration tests -- **Deliverables:** >90% test coverage, comprehensive law validation -- **Dependencies:** Follows Stream A (needs stable architecture) -- **Sync Points:** Week 6 (performance report), Week 13 (QA sign-off) +Every public class and method in `annotation-definitions` needs a Javadoc comment explaining: +- What the structure/annotation requires +- Which methods are required vs. generated +- A one-line example -### Stream C: Enterprise Features (Weeks 7-10) -- **Owners:** Advanced feature contributors -- **Focus:** Monad transformers, bifunctors, configuration, tooling -- **Deliverables:** New algebraic structures, advanced features -- **Dependencies:** Follows Stream A (needs stable architecture) -- **Sync Points:** Week 10 (integration testing) +#### D.2 Update existing docs -### Stream D: Documentation & Release (Weeks 8-16) -- **Owners:** Technical writers, release manager -- **Focus:** User guides, migration guide, API reference, Maven Central publishing -- **Deliverables:** Complete documentation, successful release -- **Dependencies:** Follows all other streams (waits for stabilization) -- **Sync Points:** Week 14 (community review), Week 16 (release) - -### Stream Sync Points - -``` -Week 1-3 ────────────────────────────────────────────── API Freeze ────── - ├─ Stream A: API audit, Java 17+ migration - ├─ Stream B: Setup testing infrastructure - ├─ Stream C: Planning enterprise features - └─ Stream D: Outline documentation +- `docs/FUNCTIONAL_STRUCTURES.md` — add `@Alternative` specification; update `@Applicative` to mention `keepLeft`/`keepRight` generation +- `docs/EXAMPLES.md` — add SQL example (from A.2) +- `docs/PARSER.md` — review against current `ParserApplicative` implementation; fix any drift +- `CHANGELOG.md` — update with all changes on `feature/overhaul-lists` -Week 4-6 ────────────────────────────────────────────── Performance Baseline ────── - ├─ Stream A: MethodHandle caching - ├─ Stream B: Law verification, benchmarking - ├─ Stream C: Design monad transformers - └─ Stream D: Planning migration guide +#### D.3 API freeze -Week 7-10 ───────────────────────────────────────────── Integration Testing ────── - ├─ Stream A: Explicit mode, configuration - ├─ Stream B: Fuzz testing, integration tests - ├─ Stream C: Implement enterprise features - └─ Stream D: Write user guides - -Week 11-13 ──────────────────────────────────────────── QA Sign-Off ────── - ├─ Stream A: Security audit, finalization - ├─ Stream B: Final regression/load testing - ├─ Stream C: Feature stabilization - └─ Stream D: Migration guide, API reference complete - -Week 14-16 ──────────────────────────────────────────── Release ────── - ├─ All Streams: Final coordination - ├─ Stream D: Lead release process - └─ All: Post-release monitoring -``` +- Tag the public API surface: add `@SuppressWarnings("unused")` where needed; mark internal types with a comment +- Write a short `docs/API_STABILITY.md` that lists which packages are stable API vs. internal --- -# Success Metrics for v2.0 +### Phase E: Release (Weeks 12–16, ends May 31) -By end of Week 16, the project achieves production-readiness via: +#### E.1 Merge and integration -| Metric | Target | Status | -|--------|--------|--------| -| **API Stability** | Frozen API, no breaking changes until v3.0 | Critical | -| **Performance** | <2% overhead on single ops, <1% on chained | Target: <1% | -| **Test Coverage** | >90% overall, >95% on public APIs | Critical | -| **Law Verification** | 100% pass on all built-in + enterprise instances | Critical | -| **Thread Safety** | Zero race conditions under 1000+ concurrent ops | Critical | -| **Reflection Elimination** | 0 reflective calls in hot execution paths | Target | -| **Security** | Security audit complete, zero critical CVEs | Critical | -| **Documentation** | 100% of public APIs documented + examples | Critical | -| **Modularization** | 4+ modules, clean dependency graph | Target | -| **Release** | v2.0 on Maven Central, production-ready | Critical | +- Merge `feature/overhaul-lists` into `master` +- Verify CI is green on Java 17, 21, 24 +- Bump version to `2.0` in all POMs ---- +#### E.2 Maven Central publication -# Post-v2.0 Roadmap (v2.1+) +- Build + GPG-sign all artifacts: `mvn deploy -Pgpg` +- Promote from Sonatype staging +- Verify installation with a fresh empty project -### v2.1 (Weeks 17-20, ~June-July 2026) -- Bifunctor enhancements (Profunctor, Strong, Choice) -- Compile-time code generation module -- Plugin system for third-party extensions -- Performance micro-optimizations (target <0.5% overhead) +#### E.3 GitHub Release -### v2.2+ (Future) -- Integration with Kotlin coroutines -- Integration with reactive libraries (RxJava, Project Reactor) -- Advanced type system features -- Community-contributed extensions +- Tag `v2.0.0` +- Write release notes summarising the v1.3 → v2.0 changes +- Link to `docs/BREAKING_CHANGES.md` for migration --- -# Definitions - -## What Makes v2.0 "Production-Ready"? - -✓ **API is stable** — Public API frozen; breaking changes only in future majors -✓ **Performance is documented** — Benchmarks published; overhead < 2% -✓ **Laws verified** — All instances pass 100% of algebraic laws -✓ **Thread safety guaranteed** — Concurrent ops safe; documented guarantees -✓ **Reflection contained** — Zero reflection in hot execution paths -✓ **Security audited** — Known vulnerabilities fixed; CVE scanning enabled -✓ **CI/tooling automated** — Multi-version testing, static analysis, gates green -✓ **Documentation complete** — Users can adopt without external help -✓ **Modularization clean** — Core publishable independently -✓ **Released to Maven Central** — Users can depend on v2.0 confidently +## Revised Exit Criteria for v2.0 -When all 10 items are ✓, v2.0 is production-ready. +| Criterion | Status | +|------------------------------------------------------------|-----------| +| Java 17+ minimum, CI on 17/21/24 | ✅ Done | +| `SqlRunner` complete | ✅ Done | +| At least one annotation generates derived methods | ⬜ Pending | +| All built-in instances verified against functor/monad laws | ⬜ Pending | +| Compiler error messages tested | ⬜ Pending | +| Javadoc on all public API symbols | ⬜ Pending | +| CHANGELOG up to date | ⬜ Pending | +| CI green on master after merge | ⬜ Pending | +| Published to Maven Central | ⬜ Pending | diff --git a/example-functional/src/main/java/com/dan323/functional/data/continuation/ContinuationMonad.java b/example-functional/src/main/java/com/dan323/functional/data/continuation/ContinuationMonad.java index 4a90219f..9cd06ffb 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/continuation/ContinuationMonad.java +++ b/example-functional/src/main/java/com/dan323/functional/data/continuation/ContinuationMonad.java @@ -11,8 +11,10 @@ public final class ContinuationMonad implements IMonad> { private ContinuationMonad() { } + private static final ContinuationMonad CONTINUATION_MONAD = new ContinuationMonad<>(); + public static ContinuationMonad getInstance() { - return new ContinuationMonad<>(); + return (ContinuationMonad) CONTINUATION_MONAD; } public Continuation map(Continuation base, Function mapping) { diff --git a/example-functional/src/main/java/com/dan323/functional/data/function/FunctionFrom.java b/example-functional/src/main/java/com/dan323/functional/data/function/Reader.java similarity index 80% rename from example-functional/src/main/java/com/dan323/functional/data/function/FunctionFrom.java rename to example-functional/src/main/java/com/dan323/functional/data/function/Reader.java index b2f850a0..6844f67b 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/function/FunctionFrom.java +++ b/example-functional/src/main/java/com/dan323/functional/data/function/Reader.java @@ -7,16 +7,16 @@ import java.util.function.Function; @Monad -public final class FunctionFrom implements IMonad> { +public final class Reader implements IMonad> { - private FunctionFrom() { + private Reader() { } - public static FunctionFrom getInstance() { - return (FunctionFrom) FUNCTION_FROM; + public static Reader getInstance() { + return (Reader) READER; } - private static final FunctionFrom FUNCTION_FROM = new FunctionFrom<>(); + private static final Reader READER = new Reader<>(); public Function map(Function base, Function mapping) { return mapping.compose(base); diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/Cycle.java b/example-functional/src/main/java/com/dan323/functional/data/list/Cycle.java index 2dedeead..1b081824 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/Cycle.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/Cycle.java @@ -7,7 +7,7 @@ public final class Cycle extends InfiniteList { private final FiniteList cycled; Cycle(FiniteList cycle) { - if (cycle == null || cycle.length() == 0) { + if (cycle == null || cycle.length() < 1) { throw new IllegalArgumentException("The list to be cycled must not be null or empty."); } this.cycled = cycle; @@ -21,7 +21,7 @@ public A getHead() { @Override public InfiniteList tail() { var init = cycled.tail(); - return ListUtils.concat(init, this); + return (InfiniteList) ListUtils.concat(init, this); } @Override diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/FiniteList.java b/example-functional/src/main/java/com/dan323/functional/data/list/FiniteList.java index 7bdfac9e..e8e2a4e6 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/FiniteList.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/FiniteList.java @@ -13,10 +13,6 @@ static FiniteList cons(A a, FiniteList tail) { return new FinCons<>(a, tail); } - static FiniteList nil() { - return (FiniteList) Nil.NIL; - } - /** * This default implementation only works finite lists, since otherwise the program does not finish * @@ -29,7 +25,7 @@ static FiniteList nil() { */ @Override default FiniteList map(Function mapping) { - return head().maybe(h -> FiniteList.cons(mapping.apply(h), tail().map(mapping)), nil()); + return head().maybe(h -> FiniteList.cons(mapping.apply(h), tail().map(mapping)), List.nil()); } FiniteList tail(); @@ -37,7 +33,7 @@ default FiniteList map(Function mapping) { @SafeVarargs static FiniteList of(A... a) { if (a.length == 0) { - return nil(); + return List.nil(); } else { return FiniteList.of(0, a); } @@ -51,7 +47,7 @@ default FiniteList cons(A head) { @SafeVarargs private static FiniteList of(int n, A... a) { if (n >= a.length) { - return nil(); + return List.nil(); } else { return cons(a[n], of(n + 1, a)); } diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/FiniteListFunctional.java b/example-functional/src/main/java/com/dan323/functional/data/list/FiniteListFunctional.java index 281d582f..fa64f8ec 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/FiniteListFunctional.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/FiniteListFunctional.java @@ -24,7 +24,7 @@ private FiniteListFunctional() { } public static K traverse(IApplicative applicative, Function fun, FiniteList lst) { - var empty = ApplicativeUtil.pure(applicative, FiniteList.nil()); + var empty = ApplicativeUtil.pure(applicative, List.nil()); return foldr((x, y) -> ApplicativeUtil.liftA2(applicative, (BiFunction, FiniteList>) FiniteList::cons, fun.apply(x), y), empty, lst); } @@ -51,7 +51,7 @@ public static FiniteList disjunction(FiniteList op1, FiniteList op2 } public static FiniteList empty() { - return FiniteList.nil(); + return List.nil(); } public static FiniteList pure(A a) { @@ -59,7 +59,7 @@ public static FiniteList pure(A a) { } public static FiniteList flatMap(Function> f, FiniteList base) { - return base.head().maybe(h -> concat(f.apply(h), flatMap(f, base.tail())), FiniteList.nil()); + return base.head().maybe(h -> concat(f.apply(h), flatMap(f, base.tail())), List.nil()); } @Override diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/InfiniteList.java b/example-functional/src/main/java/com/dan323/functional/data/list/InfiniteList.java index a6e723ab..07a13c35 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/InfiniteList.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/InfiniteList.java @@ -9,7 +9,7 @@ * * @param type of elements in the list */ -public abstract sealed class InfiniteList implements List permits Cons, Cycle, Generating, Generating.GeneratingMapped, Repeat, Zipped { +public sealed abstract class InfiniteList implements List permits Cons, Cycle, Generating, Generating.GeneratingMapped, Merged, Repeat, Zipped { @Override public abstract InfiniteList tail(); diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/List.java b/example-functional/src/main/java/com/dan323/functional/data/list/List.java index de720f7c..7167f474 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/List.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/List.java @@ -20,18 +20,30 @@ static InfiniteList generate(A first, UnaryOperator generator){ } default FiniteList limit(int k){ - return head().maybe(h -> limitWithHead(h, k), FiniteList.nil()); + return head().maybe(h -> limitWithHead(h, k), List.nil()); } private FiniteList limitWithHead(A h, int k){ if (k == 0){ - return FiniteList.nil(); + return List.nil(); } else { return FiniteList.cons(h, tail().limit(k-1)); } } - static List cycle(FiniteList lst){ + static InfiniteList interleave(InfiniteList fa, InfiniteList fb) { + return new Merged<>(fa, fb); + } + + static List cycle(List lst){ + if (lst instanceof FiniteList fl) { + return cycle(fl); + } else { + return lst; + } + } + + private static List cycle(FiniteList lst){ if (lst.length() == 0) { return nil(); } else if (lst.length() == 1) { diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/ListUtils.java b/example-functional/src/main/java/com/dan323/functional/data/list/ListUtils.java index 3709f14b..09dc866f 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/ListUtils.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/ListUtils.java @@ -3,14 +3,22 @@ public final class ListUtils { public static FiniteList reverse(FiniteList lst) { - return lst.head().maybe(h -> concat(reverse(lst.tail()), FiniteList.cons(h, FiniteList.nil())), FiniteList.nil()); + return lst.head().maybe(h -> concat(reverse(lst.tail()), FiniteList.cons(h, List.nil())), List.nil()); } - public static FiniteList concat(FiniteList a, FiniteList b){ - return a.head().maybe(h -> concat(a.tail(), b).cons(h), b); + public static FiniteList concat(FiniteList a, FiniteList b) { + return a.head().maybe(h -> FiniteList.cons(h, concat(a.tail(), b)), b); } - public static InfiniteList concat(FiniteList a, InfiniteList b){ + private static List concat(FiniteList a, List b){ return a.head().maybe(h -> concat(a.tail(), b).cons(h), b); } + + public static List concat(List a, List b){ + if (a instanceof FiniteList fa) { + return concat(fa, b); + } else { + return a; + } + } } diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/Merged.java b/example-functional/src/main/java/com/dan323/functional/data/list/Merged.java new file mode 100644 index 00000000..d2fb8bac --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/list/Merged.java @@ -0,0 +1,37 @@ +package com.dan323.functional.data.list; + +import java.util.function.BiFunction; +import java.util.function.Function; + +public final class Merged extends InfiniteList{ + + private final InfiniteList listLeft; + private final InfiniteList listRight; + + Merged(InfiniteList listLeft, InfiniteList listRight){ + if (listLeft == null || listRight == null){ + throw new IllegalArgumentException("Inputs must not be null"); + } + this.listLeft = listLeft; + this.listRight = listRight; + } + + @Override + public A getHead() { + return listLeft.getHead(); + } + + @Override + public InfiniteList tail() { + return new Merged<>(listRight, listLeft.tail()); + } + + @Override + public InfiniteList map(Function mapping) { + return new Merged<>(listLeft.map(mapping), listRight.map(mapping)); + } + + public InfiniteList zipBy(BiFunction mapper, Merged other) { + return new Merged<>((InfiniteList)ZipApplicative.liftA2(mapper,listLeft, other.listLeft), (InfiniteList) ZipApplicative.liftA2(mapper,listRight, other.listRight)); + } +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/ZipApplicative.java b/example-functional/src/main/java/com/dan323/functional/data/list/ZipApplicative.java index 688c6317..3124997c 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/ZipApplicative.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/ZipApplicative.java @@ -30,10 +30,8 @@ public static List pure(A a) { * @param type of elements in the resulting list */ public static List liftA2(BiFunction fun, List lstA, List lstB) { - if (lstA instanceof FiniteList) { - return lstA.head().maybe(a -> lstB.head().maybe(b -> FiniteList.cons(fun.apply(a, b), (FiniteList) liftA2(fun, lstA.tail(), lstB.tail())), FiniteList.nil()), FiniteList.nil()); - } else if (lstB instanceof FiniteList) { - return lstB.head().maybe(a -> lstA.head().maybe(b -> FiniteList.cons(fun.apply(b, a), (FiniteList) liftA2(fun, lstA.tail(), lstB.tail())), FiniteList.nil()), FiniteList.nil()); + if (lstA instanceof FiniteList || lstB instanceof FiniteList) { + return lstA.head().maybe(a -> lstB.head().maybe(b -> FiniteList.cons(fun.apply(a, b), (FiniteList) liftA2(fun, lstA.tail(), lstB.tail())), List.nil()), List.nil()); } else if (lstA instanceof Repeat ra) { Function auxFun = b -> fun.apply(ra.getHead(), b); return lstB.map(auxFun); @@ -44,6 +42,8 @@ public static List liftA2(BiFunction fun, List lstA, Li return ca.zipBy(fun, (InfiniteList) lstB); } else if (lstB instanceof Cons cb) { return cb.zipBy((a,b) -> fun.apply(b,a), (InfiniteList) lstA); + } else if (lstA instanceof Merged ma && lstB instanceof Merged mb) { + return ma.zipBy(fun, mb); } else { return new Zipped<>((InfiniteList) lstA, fun, (InfiniteList) lstB); } diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/zipper/ListZipper.java b/example-functional/src/main/java/com/dan323/functional/data/list/zipper/ListZipper.java index a00d7cad..2ecc799f 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/zipper/ListZipper.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/zipper/ListZipper.java @@ -41,7 +41,7 @@ public int index(){ if (finiteList == null) { throw new IllegalArgumentException("No input can be null"); } - this.left = FiniteList.nil(); + this.left = List.nil(); this.right = finiteList; } diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/zipper/ListZipperFunctor.java b/example-functional/src/main/java/com/dan323/functional/data/list/zipper/ListZipperFunctor.java index 38f38761..47fd5f00 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/zipper/ListZipperFunctor.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/zipper/ListZipperFunctor.java @@ -4,6 +4,7 @@ import com.dan323.functional.annotation.funcs.IFunctor; import com.dan323.functional.data.list.FiniteList; import com.dan323.functional.data.list.FiniteListFunctional; +import com.dan323.functional.data.list.List; import com.dan323.functional.data.optional.Maybe; import com.dan323.functional.data.optional.MaybeMonad; @@ -16,7 +17,7 @@ public final class ListZipperFunctor implements IFunctor> { public static ListZipper map(ListZipper base, Function mapping) { Maybe ma = MaybeMonad.map(base.get(), mapping); - FiniteList bList = ma.maybe(p -> FiniteList.cons(p, FiniteListFunctional.map(base.getRight(), mapping)), FiniteList.nil()); + FiniteList bList = ma.maybe(p -> FiniteList.cons(p, FiniteListFunctional.map(base.getRight(), mapping)), List.nil()); return new ListZipper<>(FiniteListFunctional.map(base.getLeft(), mapping), bList); } diff --git a/example-functional/src/main/java/com/dan323/functional/data/parser/ParserApplicative.java b/example-functional/src/main/java/com/dan323/functional/data/parser/ParserApplicative.java index e77c6e56..1fe2e66f 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/parser/ParserApplicative.java +++ b/example-functional/src/main/java/com/dan323/functional/data/parser/ParserApplicative.java @@ -6,6 +6,7 @@ import com.dan323.functional.annotation.funcs.IApplicative; import com.dan323.functional.data.either.Either; import com.dan323.functional.data.list.FiniteList; +import com.dan323.functional.data.list.List; import com.dan323.functional.data.state.StateMonad; import java.util.function.Function; @@ -52,7 +53,7 @@ public static Parser disjunction(Parser first, Parser second) { } public static Parser> many(Parser parser) { - return disjunction(some(parser), pure(FiniteList.nil())); + return disjunction(some(parser), pure(List.nil())); } public static Parser whenFailureWhenSuccess(Parser parser, Supplier> errorParser, Supplier> successParser) { diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/Config.java b/example-functional/src/main/java/com/dan323/functional/data/sql/Config.java new file mode 100644 index 00000000..af2b5eb2 --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/Config.java @@ -0,0 +1,14 @@ +package com.dan323.functional.data.sql; + +import com.dan323.functional.data.function.Reader; + +import java.util.function.Function; + +public record Config(String host, int port, String db) { + + public static final Function getPath = Reader.getInstance().liftA2( + (url, db) -> url + "/" + db, + Reader.getInstance() + .liftA2((host, port) -> "jdbc:postgresql://" + host + ":" + port, Config::host, Config::port), + Config::db); +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/DbError.java b/example-functional/src/main/java/com/dan323/functional/data/sql/DbError.java new file mode 100644 index 00000000..29447112 --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/DbError.java @@ -0,0 +1,8 @@ +package com.dan323.functional.data.sql; + +public sealed interface DbError permits DbError.SqlError, DbError.DecodeError { + + record SqlError(String message) implements DbError {} + + record DecodeError(String message) implements DbError {} +} \ No newline at end of file diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/Expr.java b/example-functional/src/main/java/com/dan323/functional/data/sql/Expr.java new file mode 100644 index 00000000..8bfd2b37 --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/Expr.java @@ -0,0 +1,94 @@ +package com.dan323.functional.data.sql; + +public sealed interface Expr + permits Expr.Column, + Expr.Literal, + Expr.Add, + Expr.Sub, + Expr.Mul, + Expr.Div, + Expr.Eq, + Expr.Neq, + Expr.Gt, + Expr.Lt, + Expr.Gte, + Expr.Lte, + Expr.And, + Expr.Or, + Expr.Not { + + record Column( + String tableAlias, + String name, + Class type + ) implements Expr {} + + record Literal( + A value, + Class type + ) implements Expr {} + + record Add( + Expr left, + Expr right + ) implements Expr {} + + record Sub( + Expr left, + Expr right + ) implements Expr {} + + record Mul( + Expr left, + Expr right + ) implements Expr {} + + record Div( + Expr left, + Expr right + ) implements Expr {} + + record Eq( + Expr left, + Expr right + ) implements Expr {} + + record Neq( + Expr left, + Expr right + ) implements Expr {} + + record Gt( + Expr left, + Expr right + ) implements Expr {} + + record Lt( + Expr left, + Expr right + ) implements Expr {} + + record Gte( + Expr left, + Expr right + ) implements Expr {} + + record Lte( + Expr left, + Expr right + ) implements Expr {} + + record And( + Expr left, + Expr right + ) implements Expr {} + + record Or( + Expr left, + Expr right + ) implements Expr {} + + record Not( + Expr value + ) implements Expr {} +} \ No newline at end of file diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/ExprCompiler.java b/example-functional/src/main/java/com/dan323/functional/data/sql/ExprCompiler.java new file mode 100644 index 00000000..63efe881 --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/ExprCompiler.java @@ -0,0 +1,57 @@ +package com.dan323.functional.data.sql; + +public final class ExprCompiler { + + public static String compile(Expr expr) { + + if (expr instanceof Expr.Column c) { + return c.tableAlias() + "." + c.name(); + } else if (expr instanceof Expr.Literal l) { + if (l.value() instanceof String s) { + String escaped = s.replace("'", "''"); + return "'" + escaped + "'"; + } + return l.value().toString(); + } else if (expr instanceof Expr.Add a) { + return "(" + compile(a.left()) + + " + " + compile(a.right()) + ")"; + } else if (expr instanceof Expr.Gt g) { + return "(" + compile(g.left()) + + " > " + compile(g.right()) + ")"; + } else if (expr instanceof Expr.And a) { + return "(" + compile(a.left()) + + " AND " + compile(a.right()) + ")"; + } else if (expr instanceof Expr.Or a) { + return "(" + compile(a.left()) + + " OR " + compile(a.right()) + ")"; + } else if (expr instanceof Expr.Not n) { + return "(NOT " + compile(n.value()) + ")"; + } else if (expr instanceof Expr.Sub a) { + return "(" + compile(a.left()) + + " - " + compile(a.right()) + ")"; + } else if (expr instanceof Expr.Lt l) { + return "(" + compile(l.left()) + + " < " + compile(l.right()) + ")"; + } else if (expr instanceof Expr.Mul m) { + return "(" + compile(m.left()) + + " * " + compile(m.right()) + ")"; + } else if (expr instanceof Expr.Lte g) { + return "(" + compile(g.left()) + + " <= " + compile(g.right()) + ")"; + } else if (expr instanceof Expr.Gte g) { + return "(" + compile(g.left()) + + " >= " + compile(g.right()) + ")"; + } else if (expr instanceof Expr.Eq g) { + return "(" + compile(g.left()) + + " = " + compile(g.right()) + ")"; + } else if (expr instanceof Expr.Neq g) { + return "(" + compile(g.left()) + + " <> " + compile(g.right()) + ")"; + } else if (expr instanceof Expr.Div m) { + return "(" + compile(m.left()) + + " / " + compile(m.right()) + ")"; + } else { + throw new IllegalStateException("Unknown Expr: " + expr); + } + } +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/Query.java b/example-functional/src/main/java/com/dan323/functional/data/sql/Query.java new file mode 100644 index 00000000..c8989f79 --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/Query.java @@ -0,0 +1,4 @@ +package com.dan323.functional.data.sql; + +public record Query(SqlAst sql, RowDecoder decoder){ +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/QueryFunctor.java b/example-functional/src/main/java/com/dan323/functional/data/sql/QueryFunctor.java new file mode 100644 index 00000000..1cfb78e5 --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/QueryFunctor.java @@ -0,0 +1,43 @@ +package com.dan323.functional.data.sql; + +import com.dan323.functional.annotation.Alternative; +import com.dan323.functional.annotation.funcs.IAlternative; + +import com.dan323.functional.data.either.Either; + +import java.util.function.BiFunction; +import java.util.function.Function; + +@Alternative +public class QueryFunctor implements IAlternative> { + + @Override + public Class> getClassAtRuntime() { + return (Class>) (Class) Query.class; + } + + public static Query map(Query base, Function mapping) { + return new Query<>(base.sql(), RowDecoder.map(base.decoder(), mapping)); + } + + public static Query pure(A value) { + return new Query<>(new SqlAst.Pure(), rs -> Either.right(value)); + } + + public static Query empty() { + return new Query<>(new SqlAst.Empty(), RowDecoder.empty()); + } + + public static Query disjunction(Query first, Query snd) { + return new Query<>(new SqlAst.Union(first.sql(), snd.sql()), first.decoder()); + } + + public static Query liftA2(BiFunction mapping, Query qa, Query qb) { + return new Query<>(new SqlAst.Product(qa.sql(), qb.sql()), RowDecoder.liftA2(mapping, qa.decoder(), qb.decoder())); + } + + public static Query join(BiFunction mapping, Query qa, Query qb, Expr on) { + return new Query<>(new SqlAst.Join(qa.sql(), qb.sql(), on), RowDecoder.liftA2(mapping, qa.decoder(), qb.decoder())); + } + +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/RowDecoder.java b/example-functional/src/main/java/com/dan323/functional/data/sql/RowDecoder.java new file mode 100644 index 00000000..3e5d6349 --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/RowDecoder.java @@ -0,0 +1,86 @@ +package com.dan323.functional.data.sql; + +import com.dan323.functional.data.either.Either; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.function.BiFunction; +import java.util.function.Function; + +@FunctionalInterface +public interface RowDecoder { + Either decode(ResultSet rs); + + static RowDecoder map(RowDecoder dec, Function mapping){ + return rs -> dec.decode(rs).either( + err -> Either.left(err), + a -> Either.right(mapping.apply(a)) + ); + } + + static RowDecoder liftA2(BiFunction mapping, RowDecoder decA, RowDecoder decB){ + return rs -> decA.decode(rs).either( + err -> Either.left(err), + a -> decB.decode(rs).either( + err -> Either.left(err), + b -> Either.right(mapping.apply(a,b)) + ) + ); + } + + static RowDecoder empty() { + return rs -> Either.left(new DbError.DecodeError("Not implemented")); + } + + static RowDecoder string(String column) { + return rs -> { + try { + String value = rs.getString(column); + return value != null + ? Either.right(value) + : Either.left(new DbError.DecodeError("null value in column: " + column)); + } catch (SQLException e) { + return Either.left(new DbError.DecodeError(e.getMessage())); + } + }; + } + + static RowDecoder integer(String column) { + return rs -> { + try { + int value = rs.getInt(column); + return rs.wasNull() + ? Either.left(new DbError.DecodeError("null value in column: " + column)) + : Either.right(value); + } catch (SQLException e) { + return Either.left(new DbError.DecodeError(e.getMessage())); + } + }; + } + + static RowDecoder longCol(String column) { + return rs -> { + try { + long value = rs.getLong(column); + return rs.wasNull() + ? Either.left(new DbError.DecodeError("null value in column: " + column)) + : Either.right(value); + } catch (SQLException e) { + return Either.left(new DbError.DecodeError(e.getMessage())); + } + }; + } + + static RowDecoder bool(String column) { + return rs -> { + try { + boolean value = rs.getBoolean(column); + return rs.wasNull() + ? Either.left(new DbError.DecodeError("null value in column: " + column)) + : Either.right(value); + } catch (SQLException e) { + return Either.left(new DbError.DecodeError(e.getMessage())); + } + }; + } +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/RowDecoderMonad.java b/example-functional/src/main/java/com/dan323/functional/data/sql/RowDecoderMonad.java new file mode 100644 index 00000000..0ace0952 --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/RowDecoderMonad.java @@ -0,0 +1,59 @@ +package com.dan323.functional.data.sql; + +import com.dan323.functional.annotation.Alternative; +import com.dan323.functional.annotation.Monad; +import com.dan323.functional.annotation.funcs.IAlternative; +import com.dan323.functional.annotation.funcs.IMonad; +import com.dan323.functional.data.either.Either; + +import java.util.function.BiFunction; +import java.util.function.Function; + +@Monad +@Alternative +public final class RowDecoderMonad implements IMonad>, IAlternative> { + + private RowDecoderMonad() {} + + private static final RowDecoderMonad INSTANCE = new RowDecoderMonad(); + + public static RowDecoderMonad getInstance() { + return INSTANCE; + } + + public static RowDecoder pure(A value) { + return rs -> Either.right(value); + } + + public static RowDecoder map(RowDecoder dec, Function f) { + return RowDecoder.map(dec, f); + } + + public static RowDecoder fapply(RowDecoder> decF, RowDecoder decA) { + return flatMap(f -> map(decA, f), decF); + } + + public static RowDecoder liftA2(BiFunction f, RowDecoder da, RowDecoder db) { + return RowDecoder.liftA2(f, da, db); + } + + /** Chains two decoders: decodes A first, then uses the result to choose and run the next decoder. */ + public static RowDecoder flatMap(Function> f, RowDecoder dec) { + return rs -> dec.decode(rs).either(Either::left, a -> f.apply(a).decode(rs)); + } + + /** A decoder that always fails — the identity element for {@code disjunction}. */ + public static RowDecoder empty() { + return RowDecoder.empty(); + } + + /** Tries {@code first}; falls back to {@code second} if the first decoder fails. */ + public static RowDecoder disjunction(RowDecoder first, RowDecoder second) { + return rs -> first.decode(rs).either(err -> second.decode(rs), Either::right); + } + + @Override + public Class> getClassAtRuntime() { + return (Class>) (Class) RowDecoder.class; + } +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/SqlAst.java b/example-functional/src/main/java/com/dan323/functional/data/sql/SqlAst.java new file mode 100644 index 00000000..9ec5b87a --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/SqlAst.java @@ -0,0 +1,25 @@ +package com.dan323.functional.data.sql; + +import com.dan323.functional.data.list.List; + +public sealed interface SqlAst permits SqlAst.Empty, SqlAst.Table, SqlAst.Product, SqlAst.Join, + SqlAst.Filter, SqlAst.Union, SqlAst.Pure, SqlAst.Project { + + record Table(String name, String alias) implements SqlAst { + public Table(String name) { this(name, null); } + } + + record Product(SqlAst left, SqlAst right) implements SqlAst {} + + record Join(SqlAst left, SqlAst right, Expr on) implements SqlAst {} + + record Filter(SqlAst source, Expr condition) implements SqlAst {} + + record Union(SqlAst left, SqlAst right) implements SqlAst {} + + record Empty() implements SqlAst {} + + record Pure() implements SqlAst {} + + record Project(List> exprList, SqlAst source) implements SqlAst {} +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/SqlAstCompiler.java b/example-functional/src/main/java/com/dan323/functional/data/sql/SqlAstCompiler.java new file mode 100644 index 00000000..eaeee5d7 --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/SqlAstCompiler.java @@ -0,0 +1,88 @@ +package com.dan323.functional.data.sql; + +import com.dan323.functional.data.list.List; + +public final class SqlAstCompiler { + + private SqlAstCompiler() {} + + private sealed interface Fragment permits Fragment.Query, Fragment.Union { + record Query(String select, String from, String where) implements Fragment {} + record Union(Fragment left, Fragment right) implements Fragment {} + } + + public static String compile(SqlAst ast) { + return render(toFragment(ast)); + } + + private static String render(Fragment fragment) { + if (fragment instanceof Fragment.Query q) { + StringBuilder sb = new StringBuilder("SELECT ").append(q.select()); + if (q.from() != null) sb.append(" FROM ").append(q.from()); + if (q.where() != null) sb.append(" WHERE ").append(q.where()); + return sb.toString(); + } else if (fragment instanceof Fragment.Union u) { + return "(" + render(u.left()) + ") UNION (" + render(u.right()) + ")"; + } + throw new IllegalStateException("Unknown Fragment: " + fragment); + } + + private static Fragment toFragment(SqlAst ast) { + if (ast instanceof SqlAst.Empty) { + return new Fragment.Query("1", null, "FALSE"); + } else if (ast instanceof SqlAst.Pure) { + return new Fragment.Query("1", null, null); + } else if (ast instanceof SqlAst.Table t) { + String from = t.alias() != null ? t.name() + " AS " + t.alias() : t.name(); + return new Fragment.Query("*", from, null); + } else if (ast instanceof SqlAst.Filter f) { + Fragment src = toFragment(f.source()); + String newCond = ExprCompiler.compile(f.condition()); + if (src instanceof Fragment.Query q) { + String combined = q.where() == null ? newCond : "(" + q.where() + ") AND (" + newCond + ")"; + return new Fragment.Query(q.select(), q.from(), combined); + } + return new Fragment.Query("*", "(" + render(src) + ")", newCond); + } else if (ast instanceof SqlAst.Project p) { + Fragment src = toFragment(p.source()); + String cols = joinExprs(p.exprList()); + if (src instanceof Fragment.Query q && "*".equals(q.select())) { + return new Fragment.Query(cols, q.from(), q.where()); + } + return new Fragment.Query(cols, "(" + render(src) + ")", null); + } else if (ast instanceof SqlAst.Join j) { + String leftSrc = asJoinSource(toFragment(j.left())); + String rightSrc = asJoinSource(toFragment(j.right())); + String from = j.on() == null + ? leftSrc + " CROSS JOIN " + rightSrc + : leftSrc + " JOIN " + rightSrc + " ON " + ExprCompiler.compile(j.on()); + return new Fragment.Query("*", from, null); + } else if (ast instanceof SqlAst.Product p) { + String leftSrc = asJoinSource(toFragment(p.left())); + String rightSrc = asJoinSource(toFragment(p.right())); + return new Fragment.Query("*", leftSrc + " CROSS JOIN " + rightSrc, null); + } else if (ast instanceof SqlAst.Union u) { + return new Fragment.Union(toFragment(u.left()), toFragment(u.right())); + } + throw new IllegalStateException("Unknown SqlAst: " + ast); + } + + // A fragment can be used directly as a JOIN source only when it has no WHERE + // and no custom projection — otherwise it must be wrapped as a subquery. + private static String asJoinSource(Fragment fragment) { + if (fragment instanceof Fragment.Query q && q.where() == null && "*".equals(q.select())) { + return q.from(); + } + return "(" + render(fragment) + ")"; + } + + private static String joinExprs(List> exprs) { + return exprs.head().maybe( + h -> { + String tail = joinExprs(exprs.tail()); + String compiled = ExprCompiler.compile(h); + return tail.isEmpty() ? compiled : compiled + ", " + tail; + }, + ""); + } +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/SqlAstMonoid.java b/example-functional/src/main/java/com/dan323/functional/data/sql/SqlAstMonoid.java new file mode 100644 index 00000000..9000948d --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/SqlAstMonoid.java @@ -0,0 +1,35 @@ +package com.dan323.functional.data.sql; + +import com.dan323.functional.annotation.Monoid; +import com.dan323.functional.annotation.algs.IMonoid; + +/** + * Monoid instance for {@link SqlAst}. + * + *
    + *
  • {@code unit()} is the empty query — the identity element.
  • + *
  • {@code op(a, b)} is {@code UNION} — combines two queries into one.
  • + *
+ * + * This lets you fold a collection of queries into a single query: + * {@code queries.stream().reduce(SqlAstMonoid.unit(), SqlAstMonoid::op)}. + */ +@Monoid +public final class SqlAstMonoid implements IMonoid { + + private SqlAstMonoid() {} + + private static final SqlAstMonoid INSTANCE = new SqlAstMonoid(); + + public static SqlAstMonoid getInstance() { + return INSTANCE; + } + + public static SqlAst op(SqlAst left, SqlAst right) { + return new SqlAst.Union(left, right); + } + + public static SqlAst unit() { + return new SqlAst.Empty(); + } +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/sql/SqlRunner.java b/example-functional/src/main/java/com/dan323/functional/data/sql/SqlRunner.java new file mode 100644 index 00000000..ccf41338 --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/sql/SqlRunner.java @@ -0,0 +1,43 @@ +package com.dan323.functional.data.sql; + +import com.dan323.functional.data.either.Either; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public final class SqlRunner { + + private SqlRunner() {} + + public static
Either> run(Config cfg, Query query) { + try (var conn = DriverManager.getConnection(Config.getPath.apply(cfg))) { + return run(conn, query); + } catch (SQLException e) { + return Either.left(new DbError.SqlError(e.getMessage())); + } + } + + public static Either> run(Connection conn, Query query) { + var sql = SqlAstCompiler.compile(query.sql()); + try (var stmt = conn.createStatement(); + var rs = stmt.executeQuery(sql)) { + var results = new ArrayList(); + while (rs.next()) { + DbError error = query.decoder().decode(rs).either(Function.identity(), val -> { + results.add(val); + return null; + }); + if (error != null) { + return Either.left(error); + } + } + return Either.right(results); + } catch (SQLException e) { + return Either.left(new DbError.SqlError(e.getMessage())); + } + } +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/state/StackActions.java b/example-functional/src/main/java/com/dan323/functional/data/state/StackActions.java index 9b68cd32..5d9d9163 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/state/StackActions.java +++ b/example-functional/src/main/java/com/dan323/functional/data/state/StackActions.java @@ -1,7 +1,7 @@ package com.dan323.functional.data.state; import com.dan323.functional.data.either.Either; -import com.dan323.functional.data.function.FunctionFrom; +import com.dan323.functional.data.function.Reader; import com.dan323.functional.data.list.FiniteList; import com.dan323.functional.data.list.List; import com.dan323.functional.data.optional.Maybe; @@ -85,7 +85,7 @@ static StackActions pop() { * @return ... -> | */ static StackActions reset() { - return s -> Either.right(new Pair<>(Maybe.of(), FiniteList.nil())); + return s -> Either.right(new Pair<>(Maybe.of(), List.nil())); } /** @@ -182,7 +182,7 @@ default StackActions thenByPopped(Function, StackActions> fun) { * @see #thenByPopped(Function) */ default StackActions then(StackActions st) { - return this.thenByPopped(FunctionFrom.>getInstance().pure(st)); + return this.thenByPopped(Reader.>getInstance().pure(st)); } /** @@ -194,7 +194,7 @@ default StackActions then(StackActions st) { * @see com.dan323.functional.data.either.Right#toString() */ default String print() { - return apply(FiniteList.nil()).toString(); + return apply(List.nil()).toString(); } } diff --git a/example-functional/src/main/java/module-info.java b/example-functional/src/main/java/module-info.java index 6ebbcfe2..04a830e4 100644 --- a/example-functional/src/main/java/module-info.java +++ b/example-functional/src/main/java/module-info.java @@ -1,6 +1,7 @@ module functional.data { requires functional.annotations; requires functional.compiler; + requires java.sql; exports com.dan323.functional.data.continuation; exports com.dan323.functional.data.either; @@ -19,4 +20,5 @@ exports com.dan323.functional.data.parser; exports com.dan323.functional.data; exports com.dan323.functional.data.writer; + exports com.dan323.functional.data.sql; } \ No newline at end of file diff --git a/example-functional/src/test/java/com/dan323/algebraic/FiniteListMonoidTest.java b/example-functional/src/test/java/com/dan323/algebraic/FiniteListMonoidTest.java index b05e34c9..df40efb0 100644 --- a/example-functional/src/test/java/com/dan323/algebraic/FiniteListMonoidTest.java +++ b/example-functional/src/test/java/com/dan323/algebraic/FiniteListMonoidTest.java @@ -2,6 +2,7 @@ import com.dan323.functional.data.list.FiniteList; import com.dan323.functional.data.list.FiniteListFunctional; +import com.dan323.functional.data.list.List; import com.dan323.functional.data.optional.Maybe; import org.junit.jupiter.api.Test; @@ -17,6 +18,6 @@ public void operationTest() { @Test public void unitTest() { - assertEquals(FiniteList.nil(), FiniteListFunctional.getAlternativeMonoid().unit()); + assertEquals(List.nil(), FiniteListFunctional.getAlternativeMonoid().unit()); } } diff --git a/example-functional/src/test/java/com/dan323/functional/FiniteListTest.java b/example-functional/src/test/java/com/dan323/functional/FiniteListTest.java index 64a3b8fc..498fa050 100644 --- a/example-functional/src/test/java/com/dan323/functional/FiniteListTest.java +++ b/example-functional/src/test/java/com/dan323/functional/FiniteListTest.java @@ -2,6 +2,7 @@ import com.dan323.functional.data.list.FiniteList; import com.dan323.functional.data.list.FiniteListFunctional; +import com.dan323.functional.data.list.List; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -10,8 +11,8 @@ public class FiniteListTest { @Test public void emptyListMap() { - var sol = FiniteListFunctional.map(FiniteList.nil(), Object::toString); - assertEquals(FiniteList.nil(), sol); + var sol = FiniteListFunctional.map(List.nil(), Object::toString); + assertEquals(List.nil(), sol); } @Test diff --git a/example-functional/src/test/java/com/dan323/functional/ListTest.java b/example-functional/src/test/java/com/dan323/functional/ListTest.java index 7a25aca9..2930fc35 100644 --- a/example-functional/src/test/java/com/dan323/functional/ListTest.java +++ b/example-functional/src/test/java/com/dan323/functional/ListTest.java @@ -12,7 +12,7 @@ public class ListTest { public void nilTail() { assertEquals(List.nil(), List.nil().tail()); assertEquals(List.nil(), FiniteList.of()); - assertEquals(0, FiniteList.nil().length()); + assertEquals(0, List.nil().length()); } @Test diff --git a/example-functional/src/test/java/com/dan323/functional/ListZipperTest.java b/example-functional/src/test/java/com/dan323/functional/ListZipperTest.java index f9bbb9fb..d7feef9f 100644 --- a/example-functional/src/test/java/com/dan323/functional/ListZipperTest.java +++ b/example-functional/src/test/java/com/dan323/functional/ListZipperTest.java @@ -1,6 +1,7 @@ package com.dan323.functional; import com.dan323.functional.data.list.FiniteList; +import com.dan323.functional.data.list.List; import com.dan323.functional.data.list.zipper.ListZipper; import com.dan323.functional.data.optional.Maybe; import org.junit.jupiter.api.Test; @@ -73,7 +74,7 @@ public void testIndex() { @Test public void testEmptyListZipper() { - var zipper = ListZipper.zipFrom(FiniteList.nil()); + var zipper = ListZipper.zipFrom(List.nil()); assertEquals(Maybe.of(), zipper.get()); assertEquals(Maybe.of(), zipper.moveRight()); assertEquals(Maybe.of(), zipper.moveLeft()); diff --git a/example-functional/src/test/java/com/dan323/functional/ParserTest.java b/example-functional/src/test/java/com/dan323/functional/ParserTest.java index f815adda..f0530c4c 100644 --- a/example-functional/src/test/java/com/dan323/functional/ParserTest.java +++ b/example-functional/src/test/java/com/dan323/functional/ParserTest.java @@ -2,6 +2,7 @@ import com.dan323.functional.data.either.Either; import com.dan323.functional.data.list.FiniteList; +import com.dan323.functional.data.list.List; import com.dan323.functional.data.optional.Maybe; import com.dan323.functional.data.pair.Pair; import com.dan323.functional.data.parser.Parser; @@ -66,7 +67,7 @@ void sepByWithMultipleElements() { void sepByEmpty() { var sepByInt = Parser.sepBy(Parser.intParser(), Parser.stringParser(",")); var result = sepByInt.apply("abc"); - assertEquals(Either.right(new Pair<>(FiniteList.nil(), "abc")), result); + assertEquals(Either.right(new Pair<>(List.nil(), "abc")), result); } @Test diff --git a/example-functional/src/test/java/com/dan323/functional/QueryFunctorTest.java b/example-functional/src/test/java/com/dan323/functional/QueryFunctorTest.java new file mode 100644 index 00000000..faa6ef9f --- /dev/null +++ b/example-functional/src/test/java/com/dan323/functional/QueryFunctorTest.java @@ -0,0 +1,70 @@ +package com.dan323.functional; + +import com.dan323.functional.data.either.Either; +import com.dan323.functional.data.sql.Expr; +import com.dan323.functional.data.sql.Query; +import com.dan323.functional.data.sql.QueryFunctor; +import com.dan323.functional.data.sql.RowDecoder; +import com.dan323.functional.data.sql.SqlAst; +import com.dan323.functional.data.sql.SqlAstCompiler; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class QueryFunctorTest { + + @Test + public void pureProducesSelectOneAndIgnoresResultSet() { + var query = QueryFunctor.pure("hello"); + assertEquals("SELECT 1", SqlAstCompiler.compile(query.sql())); + assertEquals(Either.right("hello"), query.decoder().decode(null)); + } + + @Test + public void emptyProducesSelectOneWhereFalse() { + assertEquals("SELECT 1 WHERE FALSE", SqlAstCompiler.compile(QueryFunctor.empty().sql())); + } + + @Test + public void mapPreservesSqlAndTransformsDecoder() { + var base = QueryFunctor.pure(5); + var mapped = QueryFunctor.map(base, x -> x * 2); + assertEquals(SqlAstCompiler.compile(base.sql()), SqlAstCompiler.compile(mapped.sql())); + assertEquals(Either.right(10), mapped.decoder().decode(null)); + } + + @Test + public void disjunctionProducesUnionSql() { + var qa = new Query<>(new SqlAst.Table("archived"), RowDecoder.string("name")); + var qb = new Query<>(new SqlAst.Table("active"), RowDecoder.string("name")); + assertEquals( + "(SELECT * FROM archived) UNION (SELECT * FROM active)", + SqlAstCompiler.compile(QueryFunctor.disjunction(qa, qb).sql())); + } + + // liftA2 must use CROSS JOIN (Product), not an unconditional Join with null. + @Test + public void liftA2UsesCrossJoin() { + var qa = QueryFunctor.pure(1); + var qb = QueryFunctor.pure(2); + var qc = QueryFunctor.liftA2(Integer::sum, qa, qb); + assertEquals( + "SELECT * FROM (SELECT 1) CROSS JOIN (SELECT 1)", + SqlAstCompiler.compile(qc.sql())); + assertEquals(Either.right(3), qc.decoder().decode(null)); + } + + // join must produce a JOIN … ON … in the SQL. + @Test + public void joinUsesJoinWithCondition() { + var qa = new Query<>(new SqlAst.Table("orders"), RowDecoder.string("id")); + var qb = new Query<>(new SqlAst.Table("customers"), RowDecoder.string("name")); + var on = new Expr.Eq<>( + new Expr.Column<>("o", "id", Integer.class), + new Expr.Column<>("c", "id", Integer.class)); + var qc = QueryFunctor.join((id, name) -> id + "-" + name, qa, qb, on); + assertEquals( + "SELECT * FROM orders JOIN customers ON (o.id = c.id)", + SqlAstCompiler.compile(qc.sql())); + } +} diff --git a/example-functional/src/test/java/com/dan323/functional/FunctionFromTest.java b/example-functional/src/test/java/com/dan323/functional/ReaderTest.java similarity index 53% rename from example-functional/src/test/java/com/dan323/functional/FunctionFromTest.java rename to example-functional/src/test/java/com/dan323/functional/ReaderTest.java index 168b74a0..900e13d3 100644 --- a/example-functional/src/test/java/com/dan323/functional/FunctionFromTest.java +++ b/example-functional/src/test/java/com/dan323/functional/ReaderTest.java @@ -1,50 +1,50 @@ package com.dan323.functional; -import com.dan323.functional.data.function.FunctionFrom; +import com.dan323.functional.data.function.Reader; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -public class FunctionFromTest { +public class ReaderTest { @Test public void functionFromFunctor() { - var fun = FunctionFrom.getInstance().map(Integer::parseInt, String::valueOf); + var fun = Reader.getInstance().map(Integer::parseInt, String::valueOf); assertEquals("9", fun.apply("9")); } @Test public void functionFromPure() { - var fun = FunctionFrom.getInstance().pure(9); + var fun = Reader.getInstance().pure(9); assertEquals(9, fun.apply(8)); } @Test public void functionFromFapply() { - var fun = FunctionFrom.getInstance().fapply(x -> (y -> y - x), u -> u * 2); + var fun = Reader.getInstance().fapply(x -> (y -> y - x), u -> u * 2); assertEquals(8, fun.apply(8)); } @Test public void functionFromLiftA2() { - var fun = FunctionFrom.getInstance().liftA2((a, b) -> a * b, u -> u * 3, v -> v * 2); + var fun = Reader.getInstance().liftA2((a, b) -> a * b, u -> u * 3, v -> v * 2); assertEquals(24, fun.apply(2)); } @Test public void functionFromJoin() { - var fun = FunctionFrom.getInstance().join(x -> (y -> x + y * 7)); + var fun = Reader.getInstance().join(x -> (y -> x + y * 7)); assertEquals(56, fun.apply(7)); } @Test public void functionFromFlatMap(){ - var fun = FunctionFrom.getInstance().flatMap(x -> (y-> Math.max(x,y)), y -> y*2-1); + var fun = Reader.getInstance().flatMap(x -> (y-> Math.max(x,y)), y -> y*2-1); assertEquals(5, fun.apply(3)); } diff --git a/example-functional/src/test/java/com/dan323/functional/RowDecoderMonadTest.java b/example-functional/src/test/java/com/dan323/functional/RowDecoderMonadTest.java new file mode 100644 index 00000000..2f6a67c3 --- /dev/null +++ b/example-functional/src/test/java/com/dan323/functional/RowDecoderMonadTest.java @@ -0,0 +1,152 @@ +package com.dan323.functional; + +import com.dan323.functional.data.either.Either; +import com.dan323.functional.data.sql.DbError; +import com.dan323.functional.data.sql.RowDecoder; +import com.dan323.functional.data.sql.RowDecoderMonad; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Proxy; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RowDecoderMonadTest { + + // ------------------------------------------------------------------------- + // pure + // ------------------------------------------------------------------------- + + @Test + public void pureIgnoresResultSetAndReturnsRight() { + assertEquals(Either.right(42), RowDecoderMonad.pure(42).decode(null)); + } + + // ------------------------------------------------------------------------- + // flatMap — the key operation + // ------------------------------------------------------------------------- + + @Test + public void flatMapChainsDecodersUsingFirstResult() { + // Decode a name, then build a greeting that also reads a title column. + var decoder = RowDecoderMonad.flatMap( + name -> RowDecoderMonad.map(RowDecoder.string("title"), title -> title + " " + name), + RowDecoder.string("name")); + var result = decoder.decode(stub(Map.of("name", "Smith", "title", "Dr."))); + assertEquals(Either.right("Dr. Smith"), result); + } + + @Test + public void flatMapShortCircuitsWhenFirstDecoderFails() { + var decoder = RowDecoderMonad.flatMap( + name -> RowDecoder.string("title"), + RowDecoder.string("name")); + // "name" absent → first decoder fails; second is never invoked + var result = decoder.decode(stub(Map.of("title", "Dr."))); + assertEquals(Either.left(new DbError.DecodeError("null value in column: name")), result); + } + + @Test + public void flatMapPropagatesSecondDecoderFailure() { + var decoder = RowDecoderMonad.flatMap( + name -> RowDecoder.string("title"), + RowDecoder.string("name")); + // "name" present, "title" absent → second decoder fails + var result = decoder.decode(stub(Map.of("name", "Smith"))); + assertEquals(Either.left(new DbError.DecodeError("null value in column: title")), result); + } + + // ------------------------------------------------------------------------- + // disjunction — fallback decoders + // ------------------------------------------------------------------------- + + @Test + public void disjunctionReturnsFirstWhenItSucceeds() { + var decoder = RowDecoderMonad.disjunction( + RowDecoder.string("preferred_name"), + RowDecoder.string("name")); + var result = decoder.decode(stub(Map.of("preferred_name", "Bob", "name", "Robert"))); + assertEquals(Either.right("Bob"), result); + } + + @Test + public void disjunctionFallsBackToSecondWhenFirstFails() { + var decoder = RowDecoderMonad.disjunction( + RowDecoder.string("preferred_name"), + RowDecoder.string("name")); + // "preferred_name" absent → falls back to "name" + var result = decoder.decode(stub(Map.of("name", "Robert"))); + assertEquals(Either.right("Robert"), result); + } + + @Test + public void disjunctionReturnsSecondErrorWhenBothFail() { + var decoder = RowDecoderMonad.disjunction( + RowDecoder.string("preferred_name"), + RowDecoder.string("name")); + var result = decoder.decode(stub(Map.of())); + assertEquals(Either.left(new DbError.DecodeError("null value in column: name")), result); + } + + // ------------------------------------------------------------------------- + // empty + // ------------------------------------------------------------------------- + + @Test + public void emptyAlwaysFails() { + assertEquals( + Either.left(new DbError.DecodeError("Not implemented")), + RowDecoderMonad.empty().decode(null)); + } + + // ------------------------------------------------------------------------- + // fapply + // ------------------------------------------------------------------------- + + @Test + public void fapplyAppliesFunctionDecoder() { + java.util.function.Function toUpper = String::toUpperCase; + var decF = RowDecoderMonad.pure(toUpper); + var decA = RowDecoder.string("name"); + var result = RowDecoderMonad.fapply(decF, decA).decode(stub(Map.of("name", "alice"))); + assertEquals(Either.right("ALICE"), result); + } + + @Test + public void fapplyShortCircuitsWhenFunctionDecoderFails() { + // Explicit lambda avoids functional-interface-within-functional-interface inference ambiguity. + RowDecoder> decF = + rs -> Either.left(new DbError.DecodeError("no function")); + var decA = RowDecoder.string("name"); + var result = RowDecoderMonad.fapply(decF, decA).decode(stub(Map.of("name", "alice"))); + assertEquals(Either.left(new DbError.DecodeError("no function")), result); + } + + // ------------------------------------------------------------------------- + // Stub helper + // ------------------------------------------------------------------------- + + private static ResultSet stub(Map values) { + boolean[] lastWasNull = {false}; + return (ResultSet) Proxy.newProxyInstance( + ResultSet.class.getClassLoader(), + new Class[]{ResultSet.class}, + (proxy, method, args) -> switch (method.getName()) { + case "getString" -> { + Object v = values.get(args[0]); + lastWasNull[0] = (v == null); + yield v; + } + case "getInt" -> { + Object v = values.get(args[0]); + lastWasNull[0] = (v == null); + yield v == null ? 0 : ((Number) v).intValue(); + } + case "wasNull" -> lastWasNull[0]; + case "close" -> null; + default -> throw new UnsupportedOperationException(method.getName()); + }); + } +} diff --git a/example-functional/src/test/java/com/dan323/functional/RowDecoderTest.java b/example-functional/src/test/java/com/dan323/functional/RowDecoderTest.java new file mode 100644 index 00000000..5c8d29b4 --- /dev/null +++ b/example-functional/src/test/java/com/dan323/functional/RowDecoderTest.java @@ -0,0 +1,202 @@ +package com.dan323.functional; + +import com.dan323.functional.data.either.Either; +import com.dan323.functional.data.sql.DbError; +import com.dan323.functional.data.sql.RowDecoder; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Proxy; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RowDecoderTest { + + // ------------------------------------------------------------------------- + // string + // ------------------------------------------------------------------------- + + @Test + public void stringDecodesColumn() { + assertEquals(Either.right("hello"), RowDecoder.string("name").decode(stub(Map.of("name", "hello")))); + } + + @Test + public void stringReturnsDecodeErrorOnNull() { + var result = RowDecoder.string("name").decode(stub(Map.of())); + assertEquals(Either.left(new DbError.DecodeError("null value in column: name")), result); + } + + @Test + public void stringReturnsDecodeErrorOnSqlException() { + var result = RowDecoder.string("name").decode(throwingStub("db error")); + assertEquals(Either.left(new DbError.DecodeError("db error")), result); + } + + // ------------------------------------------------------------------------- + // integer + // ------------------------------------------------------------------------- + + @Test + public void integerDecodesColumn() { + assertEquals(Either.right(42), RowDecoder.integer("amount").decode(stub(Map.of("amount", 42)))); + } + + @Test + public void integerReturnsDecodeErrorOnNull() { + var result = RowDecoder.integer("amount").decode(stub(Map.of())); + assertEquals(Either.left(new DbError.DecodeError("null value in column: amount")), result); + } + + @Test + public void integerReturnsDecodeErrorOnSqlException() { + var result = RowDecoder.integer("amount").decode(throwingStub("db error")); + assertEquals(Either.left(new DbError.DecodeError("db error")), result); + } + + // ------------------------------------------------------------------------- + // longCol + // ------------------------------------------------------------------------- + + @Test + public void longColDecodesColumn() { + assertEquals(Either.right(123L), RowDecoder.longCol("id").decode(stub(Map.of("id", 123L)))); + } + + @Test + public void longColReturnsDecodeErrorOnNull() { + var result = RowDecoder.longCol("id").decode(stub(Map.of())); + assertEquals(Either.left(new DbError.DecodeError("null value in column: id")), result); + } + + @Test + public void longColReturnsDecodeErrorOnSqlException() { + var result = RowDecoder.longCol("id").decode(throwingStub("db error")); + assertEquals(Either.left(new DbError.DecodeError("db error")), result); + } + + // ------------------------------------------------------------------------- + // bool + // ------------------------------------------------------------------------- + + @Test + public void boolDecodesColumn() { + assertEquals(Either.right(true), RowDecoder.bool("active").decode(stub(Map.of("active", true)))); + } + + @Test + public void boolReturnsDecodeErrorOnNull() { + var result = RowDecoder.bool("active").decode(stub(Map.of())); + assertEquals(Either.left(new DbError.DecodeError("null value in column: active")), result); + } + + @Test + public void boolReturnsDecodeErrorOnSqlException() { + var result = RowDecoder.bool("active").decode(throwingStub("db error")); + assertEquals(Either.left(new DbError.DecodeError("db error")), result); + } + + // ------------------------------------------------------------------------- + // Combinators + // ------------------------------------------------------------------------- + + @Test + public void mapTransformsRightValue() { + var decoder = RowDecoder.map(RowDecoder.integer("n"), x -> x * 2); + assertEquals(Either.right(10), decoder.decode(stub(Map.of("n", 5)))); + } + + @Test + public void mapPassesThroughLeft() { + var decoder = RowDecoder.map(RowDecoder.integer("n"), x -> x * 2); + var result = decoder.decode(stub(Map.of())); + assertEquals(Either.left(new DbError.DecodeError("null value in column: n")), result); + } + + @Test + public void liftA2CombinesTwoDecoders() { + var decoder = RowDecoder.liftA2( + (a, b) -> a + " " + b, + RowDecoder.string("first"), + RowDecoder.string("last")); + assertEquals(Either.right("John Doe"), decoder.decode(stub(Map.of("first", "John", "last", "Doe")))); + } + + @Test + public void liftA2ShortCircuitsOnFirstError() { + var decoder = RowDecoder.liftA2( + (a, b) -> a + b, + RowDecoder.string("first"), + RowDecoder.string("last")); + // "first" column is absent → left, "last" is present but never reached + var result = decoder.decode(stub(Map.of("last", "Doe"))); + assertEquals(Either.left(new DbError.DecodeError("null value in column: first")), result); + } + + @Test + public void liftA2ReturnsSecondErrorWhenFirstSucceeds() { + var decoder = RowDecoder.liftA2( + (a, b) -> a + b, + RowDecoder.string("first"), + RowDecoder.string("last")); + var result = decoder.decode(stub(Map.of("first", "John"))); + assertEquals(Either.left(new DbError.DecodeError("null value in column: last")), result); + } + + @Test + public void emptyAlwaysReturnsLeft() { + assertEquals( + Either.left(new DbError.DecodeError("Not implemented")), + RowDecoder.empty().decode(stub(Map.of()))); + } + + // ------------------------------------------------------------------------- + // Stub helpers + // ------------------------------------------------------------------------- + + /** Stubs a ResultSet whose columns are backed by the given map. Missing keys are SQL NULL. */ + private static ResultSet stub(Map values) { + boolean[] lastWasNull = {false}; + return (ResultSet) Proxy.newProxyInstance( + ResultSet.class.getClassLoader(), + new Class[]{ResultSet.class}, + (proxy, method, args) -> switch (method.getName()) { + case "getString" -> { + Object v = values.get(args[0]); + lastWasNull[0] = (v == null); + yield v; + } + case "getInt" -> { + Object v = values.get(args[0]); + lastWasNull[0] = (v == null); + yield v == null ? 0 : ((Number) v).intValue(); + } + case "getLong" -> { + Object v = values.get(args[0]); + lastWasNull[0] = (v == null); + yield v == null ? 0L : ((Number) v).longValue(); + } + case "getBoolean" -> { + Object v = values.get(args[0]); + lastWasNull[0] = (v == null); + yield v == null ? Boolean.FALSE : v; + } + case "wasNull" -> lastWasNull[0]; + case "close" -> null; + default -> throw new UnsupportedOperationException(method.getName()); + }); + } + + /** Stubs a ResultSet that throws SQLException with the given message on any getter. */ + private static ResultSet throwingStub(String message) { + return (ResultSet) Proxy.newProxyInstance( + ResultSet.class.getClassLoader(), + new Class[]{ResultSet.class}, + (proxy, method, args) -> { + if (method.getName().startsWith("get")) throw new SQLException(message); + return null; + }); + } +} diff --git a/example-functional/src/test/java/com/dan323/functional/SqlAstCompilerTest.java b/example-functional/src/test/java/com/dan323/functional/SqlAstCompilerTest.java new file mode 100644 index 00000000..a91194cd --- /dev/null +++ b/example-functional/src/test/java/com/dan323/functional/SqlAstCompilerTest.java @@ -0,0 +1,324 @@ +package com.dan323.functional; + +import com.dan323.functional.data.list.FiniteList; +import com.dan323.functional.data.list.List; +import com.dan323.functional.data.sql.Expr; +import com.dan323.functional.data.sql.SqlAst; +import com.dan323.functional.data.sql.SqlAstCompiler; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SqlAstCompilerTest { + + // ------------------------------------------------------------------------- + // Leaf nodes + // ------------------------------------------------------------------------- + + @Test + public void empty() { + assertEquals("SELECT 1 WHERE FALSE", SqlAstCompiler.compile(new SqlAst.Empty())); + } + + @Test + public void pure() { + assertEquals("SELECT 1", SqlAstCompiler.compile(new SqlAst.Pure())); + } + + @Test + public void table() { + assertEquals("SELECT * FROM orders", SqlAstCompiler.compile(new SqlAst.Table("orders"))); + } + + // ------------------------------------------------------------------------- + // Filter — folding paths + // ------------------------------------------------------------------------- + + // Filter on a plain table: WHERE is set for the first time. + @Test + public void filterOnTable() { + var ast = new SqlAst.Filter( + new SqlAst.Table("orders", "o"), + new Expr.Gt(col("o", "amount"), lit(100))); + assertEquals("SELECT * FROM orders AS o WHERE (o.amount > 100)", SqlAstCompiler.compile(ast)); + } + + // Two consecutive filters are folded into a single WHERE … AND …, no subquery. + @Test + public void twoFiltersAreFlattenedWithAnd() { + var ast = new SqlAst.Filter( + new SqlAst.Filter( + new SqlAst.Table("orders", "o"), + new Expr.Gt(col("o", "amount"), lit(50))), + new Expr.Lt(col("o", "amount"), lit(200))); + assertEquals( + "SELECT * FROM orders AS o WHERE ((o.amount > 50)) AND ((o.amount < 200))", + SqlAstCompiler.compile(ast)); + } + + // Three consecutive filters fold into a single WHERE clause. + @Test + public void threeFiltersAreFlattenedIntoOneWhere() { + var ast = new SqlAst.Filter( + new SqlAst.Filter( + new SqlAst.Filter( + new SqlAst.Table("orders", "o"), + new Expr.Gt(col("o", "id"), lit(0))), + new Expr.Gt(col("o", "amount"), lit(0))), + new Expr.Lt(col("o", "amount"), lit(100))); + assertEquals( + "SELECT * FROM orders AS o WHERE (((o.id > 0)) AND ((o.amount > 0))) AND ((o.amount < 100))", + SqlAstCompiler.compile(ast)); + } + + // Filter applied to a Union must wrap — the WHERE cannot fold into either branch. + @Test + public void filterOnUnionWrapsSubquery() { + var union = new SqlAst.Union(new SqlAst.Table("a"), new SqlAst.Table("b")); + var ast = new SqlAst.Filter( + union, + new Expr.Eq<>(colInt("t", "x"), litInt(1))); + assertEquals( + "SELECT * FROM ((SELECT * FROM a) UNION (SELECT * FROM b)) WHERE (t.x = 1)", + SqlAstCompiler.compile(ast)); + } + + // ------------------------------------------------------------------------- + // Project — folding paths + // ------------------------------------------------------------------------- + + // Project on a plain table: SELECT columns replace the default *. + @Test + public void project() { + var ast = new SqlAst.Project(twoColumns(), new SqlAst.Table("orders", "o")); + assertEquals("SELECT o.id, o.name FROM orders AS o", SqlAstCompiler.compile(ast)); + } + + // Project on Filter: both fold into one query (the motivating optimisation). + @Test + public void projectOnFilterIsFlattened() { + var ast = new SqlAst.Project( + twoColumns(), + new SqlAst.Filter( + new SqlAst.Table("orders", "o"), + new Expr.Lt(col("o", "amount"), lit(4)))); + assertEquals( + "SELECT o.id, o.name FROM orders AS o WHERE (o.amount < 4)", + SqlAstCompiler.compile(ast)); + } + + // Filter on Project: both fold into one query (the motivating optimisation). + @Test + public void filterOnProjectIsFlattened() { + var ast = new SqlAst.Filter( + new SqlAst.Project(twoColumns(), new SqlAst.Table("orders", "o")), + new Expr.Lt(col("o", "amount"), lit(4))); + assertEquals( + "SELECT o.id, o.name FROM orders AS o WHERE (o.amount < 4)", + SqlAstCompiler.compile(ast)); + } + + // Filter + Project + Filter all collapse into a single SELECT … FROM … WHERE. + @Test + public void filterProjectFilterAllFoldIntoOneQuery() { + var ast = new SqlAst.Filter( + new SqlAst.Project( + twoColumns(), + new SqlAst.Filter( + new SqlAst.Table("orders", "o"), + new Expr.Gt(col("o", "amount"), lit(0)))), + new Expr.Lt(col("o", "amount"), lit(4))); + assertEquals( + "SELECT o.id, o.name FROM orders AS o WHERE ((o.amount > 0)) AND ((o.amount < 4))", + SqlAstCompiler.compile(ast)); + } + + // Project on an already-projected query cannot fold (select ≠ *) — wraps subquery. + @Test + public void projectOnProjectWrapsSubquery() { + var ast = new SqlAst.Project( + oneColumn(), + new SqlAst.Project(twoColumns(), new SqlAst.Table("orders", "o"))); + assertEquals( + "SELECT o.id FROM (SELECT o.id, o.name FROM orders AS o)", + SqlAstCompiler.compile(ast)); + } + + // Project on a Union must wrap — the Union fragment is not a Query. + @Test + public void projectOnUnionWrapsSubquery() { + var ast = new SqlAst.Project( + oneColumn(), + new SqlAst.Union(new SqlAst.Table("a"), new SqlAst.Table("b"))); + assertEquals( + "SELECT o.id FROM ((SELECT * FROM a) UNION (SELECT * FROM b))", + SqlAstCompiler.compile(ast)); + } + + // After wrapping via Project-on-Union, a subsequent Filter still folds in. + @Test + public void filterOnProjectOnUnionFoldsIntoProjectedQuery() { + var ast = new SqlAst.Filter( + new SqlAst.Project( + oneColumn(), + new SqlAst.Union(new SqlAst.Table("a"), new SqlAst.Table("b"))), + new Expr.Eq<>(colInt("t", "x"), litInt(1))); + assertEquals( + "SELECT o.id FROM ((SELECT * FROM a) UNION (SELECT * FROM b)) WHERE (t.x = 1)", + SqlAstCompiler.compile(ast)); + } + + // ------------------------------------------------------------------------- + // Join + // ------------------------------------------------------------------------- + + // Both sides are plain tables: no subqueries in the FROM clause. + @Test + public void joinTwoTables() { + var ast = new SqlAst.Join( + new SqlAst.Table("orders", "o"), + new SqlAst.Table("customers", "c"), + new Expr.Eq<>(colInt("o", "customer_id"), colInt("c", "id"))); + assertEquals( + "SELECT * FROM orders AS o JOIN customers AS c ON (o.customer_id = c.id)", + SqlAstCompiler.compile(ast)); + } + + @Test + public void joinWithNullConditionIsCrossJoin() { + assertEquals( + "SELECT * FROM a CROSS JOIN b", + SqlAstCompiler.compile(new SqlAst.Join(new SqlAst.Table("a"), new SqlAst.Table("b"), null))); + } + + // One filtered side is wrapped as a subquery; the other bare table is not. + @Test + public void joinWithOneFilteredSideWrapsSubquery() { + var ast = new SqlAst.Join( + new SqlAst.Filter( + new SqlAst.Table("orders", "o"), + new Expr.Gt(col("o", "amount"), lit(0))), + new SqlAst.Table("customers", "c"), + new Expr.Eq<>(colInt("o", "customer_id"), colInt("c", "id"))); + assertEquals( + "SELECT * FROM (SELECT * FROM orders AS o WHERE (o.amount > 0)) JOIN customers AS c ON (o.customer_id = c.id)", + SqlAstCompiler.compile(ast)); + } + + // Both filtered sides are independently wrapped. + @Test + public void joinWithBothSidesFilteredWrapsBothSubqueries() { + var ast = new SqlAst.Join( + new SqlAst.Filter(new SqlAst.Table("orders", "o"), new Expr.Gt(col("o", "amount"), lit(0))), + new SqlAst.Filter(new SqlAst.Table("customers", "c"), new Expr.Gt(col("c", "id"), lit(0))), + new Expr.Eq<>(colInt("o", "customer_id"), colInt("c", "id"))); + assertEquals( + "SELECT * FROM (SELECT * FROM orders AS o WHERE (o.amount > 0))" + + " JOIN (SELECT * FROM customers AS c WHERE (c.id > 0))" + + " ON (o.customer_id = c.id)", + SqlAstCompiler.compile(ast)); + } + + // Filter on a Join result folds into the join — no wrapping needed. + @Test + public void filterOnJoinFoldsIntoJoin() { + var ast = new SqlAst.Filter( + new SqlAst.Join( + new SqlAst.Table("orders", "o"), + new SqlAst.Table("customers", "c"), + new Expr.Eq<>(colInt("o", "customer_id"), colInt("c", "id"))), + new Expr.Gt(col("o", "amount"), lit(0))); + assertEquals( + "SELECT * FROM orders AS o JOIN customers AS c ON (o.customer_id = c.id) WHERE (o.amount > 0)", + SqlAstCompiler.compile(ast)); + } + + // Project on a Join result folds in — replaces SELECT *. + @Test + public void projectOnJoinFoldsIntoJoin() { + var ast = new SqlAst.Project( + twoColumns(), + new SqlAst.Join( + new SqlAst.Table("orders", "o"), + new SqlAst.Table("customers", "c"), + new Expr.Eq<>(colInt("o", "customer_id"), colInt("c", "id")))); + assertEquals( + "SELECT o.id, o.name FROM orders AS o JOIN customers AS c ON (o.customer_id = c.id)", + SqlAstCompiler.compile(ast)); + } + + // ------------------------------------------------------------------------- + // Product + // ------------------------------------------------------------------------- + + @Test + public void productOfTwoTables() { + assertEquals( + "SELECT * FROM a CROSS JOIN b", + SqlAstCompiler.compile(new SqlAst.Product(new SqlAst.Table("a"), new SqlAst.Table("b")))); + } + + // A filtered table used in a Product is wrapped as a subquery. + @Test + public void productWithFilteredSideWrapsSubquery() { + var ast = new SqlAst.Product( + new SqlAst.Filter(new SqlAst.Table("orders", "o"), new Expr.Gt(col("o", "amount"), lit(0))), + new SqlAst.Table("customers")); + assertEquals( + "SELECT * FROM (SELECT * FROM orders AS o WHERE (o.amount > 0)) CROSS JOIN customers", + SqlAstCompiler.compile(ast)); + } + + // ------------------------------------------------------------------------- + // Union + // ------------------------------------------------------------------------- + + @Test + public void unionOfTwoTables() { + var ast = new SqlAst.Union(new SqlAst.Table("archived_orders"), new SqlAst.Table("orders")); + assertEquals( + "(SELECT * FROM archived_orders) UNION (SELECT * FROM orders)", + SqlAstCompiler.compile(ast)); + } + + // Union whose branches are themselves non-trivial queries. + @Test + public void unionOfFilteredTables() { + var ast = new SqlAst.Union( + new SqlAst.Filter(new SqlAst.Table("archived_orders", "o"), new Expr.Gt(col("o", "amount"), lit(0))), + new SqlAst.Filter(new SqlAst.Table("orders", "o"), new Expr.Gt(col("o", "amount"), lit(0)))); + assertEquals( + "(SELECT * FROM archived_orders AS o WHERE (o.amount > 0)) UNION (SELECT * FROM orders AS o WHERE (o.amount > 0))", + SqlAstCompiler.compile(ast)); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private static Expr.Column col(String alias, String name) { + return new Expr.Column<>(alias, name, Number.class); + } + + private static Expr.Column colInt(String alias, String name) { + return new Expr.Column<>(alias, name, Integer.class); + } + + private static Expr.Literal lit(int value) { + return new Expr.Literal<>(value, Number.class); + } + + private static Expr.Literal litInt(int value) { + return new Expr.Literal<>(value, Integer.class); + } + + private static FiniteList> oneColumn() { + return FiniteList.cons(new Expr.Column<>("o", "id", Integer.class), List.nil()); + } + + private static FiniteList> twoColumns() { + return FiniteList.cons( + new Expr.Column<>("o", "id", Integer.class), + FiniteList.cons(new Expr.Column<>("o", "name", String.class), List.nil())); + } +} diff --git a/example-functional/src/test/java/com/dan323/functional/SqlAstMonoidTest.java b/example-functional/src/test/java/com/dan323/functional/SqlAstMonoidTest.java new file mode 100644 index 00000000..4d136cd4 --- /dev/null +++ b/example-functional/src/test/java/com/dan323/functional/SqlAstMonoidTest.java @@ -0,0 +1,80 @@ +package com.dan323.functional; + +import com.dan323.functional.data.sql.SqlAst; +import com.dan323.functional.data.sql.SqlAstCompiler; +import com.dan323.functional.data.sql.SqlAstMonoid; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +public class SqlAstMonoidTest { + + @Test + public void unitIsEmpty() { + assertInstanceOf(SqlAst.Empty.class, SqlAstMonoid.unit()); + } + + @Test + public void opProducesUnion() { + var a = new SqlAst.Table("a"); + var b = new SqlAst.Table("b"); + var result = SqlAstMonoid.op(a, b); + assertInstanceOf(SqlAst.Union.class, result); + assertEquals("(SELECT * FROM a) UNION (SELECT * FROM b)", SqlAstCompiler.compile(result)); + } + + // Left identity: op(unit(), q) is semantically equivalent to q (UNION with empty set). + @Test + public void leftIdentityHoldsSemantically() { + var q = new SqlAst.Table("orders"); + var combined = SqlAstMonoid.op(SqlAstMonoid.unit(), q); + assertEquals( + "(SELECT 1 WHERE FALSE) UNION (SELECT * FROM orders)", + SqlAstCompiler.compile(combined)); + } + + // Right identity: op(q, unit()) is semantically equivalent to q. + @Test + public void rightIdentityHoldsSemantically() { + var q = new SqlAst.Table("orders"); + var combined = SqlAstMonoid.op(q, SqlAstMonoid.unit()); + assertEquals( + "(SELECT * FROM orders) UNION (SELECT 1 WHERE FALSE)", + SqlAstCompiler.compile(combined)); + } + + // Associativity: op(op(a,b),c) and op(a,op(b,c)) produce the same structure. + @Test + public void opIsAssociative() { + var a = new SqlAst.Table("a"); + var b = new SqlAst.Table("b"); + var c = new SqlAst.Table("c"); + var leftAssoc = SqlAstMonoid.op(SqlAstMonoid.op(a, b), c); + var rightAssoc = SqlAstMonoid.op(a, SqlAstMonoid.op(b, c)); + // Both are valid; left-assoc wraps differently but each is a valid UNION tree. + assertEquals( + "((SELECT * FROM a) UNION (SELECT * FROM b)) UNION (SELECT * FROM c)", + SqlAstCompiler.compile(leftAssoc)); + assertEquals( + "(SELECT * FROM a) UNION ((SELECT * FROM b) UNION (SELECT * FROM c))", + SqlAstCompiler.compile(rightAssoc)); + } + + // Folding a list of queries with op/unit mirrors Stream.reduce. + @Test + public void foldingListOfQueriesProducesUnionChain() { + var tables = List.of("orders", "archived_orders", "deleted_orders"); + var result = tables.stream() + .map(SqlAst.Table::new) + .map(t -> (SqlAst) t) + .reduce(SqlAstMonoid.unit(), SqlAstMonoid::op); + assertEquals( + "(((SELECT 1 WHERE FALSE) UNION (SELECT * FROM orders))" + + " UNION (SELECT * FROM archived_orders))" + + " UNION (SELECT * FROM deleted_orders)", + SqlAstCompiler.compile(result)); + } +} diff --git a/example-functional/src/test/java/com/dan323/functional/SqlRunnerTest.java b/example-functional/src/test/java/com/dan323/functional/SqlRunnerTest.java new file mode 100644 index 00000000..7ce0d60f --- /dev/null +++ b/example-functional/src/test/java/com/dan323/functional/SqlRunnerTest.java @@ -0,0 +1,232 @@ +package com.dan323.functional; + +import com.dan323.functional.data.either.Either; +import com.dan323.functional.data.sql.DbError; +import com.dan323.functional.data.sql.Query; +import com.dan323.functional.data.sql.RowDecoder; +import com.dan323.functional.data.sql.SqlAst; +import com.dan323.functional.data.sql.SqlRunner; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SqlRunnerTest { + + // ------------------------------------------------------------------------- + // Happy paths + // ------------------------------------------------------------------------- + + @Test + public void runDecodesAllRows() { + var conn = connectionOver(rows(Map.of("name", "Alice"), Map.of("name", "Bob"))); + var query = new Query<>(new SqlAst.Table("users"), RowDecoder.string("name")); + assertEquals(Either.right(List.of("Alice", "Bob")), SqlRunner.run(conn, query)); + } + + @Test + public void runReturnsEmptyListWhenNoRows() { + var conn = connectionOver(rows()); + var query = new Query<>(new SqlAst.Table("users"), RowDecoder.string("name")); + assertEquals(Either.right(List.of()), SqlRunner.run(conn, query)); + } + + @Test + public void runDecodesIntegerColumn() { + var conn = connectionOver(rows(Map.of("amount", 10), Map.of("amount", 20))); + var query = new Query<>(new SqlAst.Table("orders"), RowDecoder.integer("amount")); + assertEquals(Either.right(List.of(10, 20)), SqlRunner.run(conn, query)); + } + + // ------------------------------------------------------------------------- + // Decode errors + // ------------------------------------------------------------------------- + + @Test + public void runShortCircuitsOnFirstDecodeError() { + // Second row has "name" missing → decode error; third row is never reached. + var conn = connectionOver(rows( + Map.of("name", "Alice"), + Map.of(), + Map.of("name", "Charlie"))); + var query = new Query<>(new SqlAst.Table("users"), RowDecoder.string("name")); + assertEquals( + Either.left(new DbError.DecodeError("null value in column: name")), + SqlRunner.run(conn, query)); + } + + @Test + public void runShortCircuitsOnFirstRowDecodeError() { + var conn = connectionOver(rows(Map.of())); // first row already fails + var query = new Query<>(new SqlAst.Table("users"), RowDecoder.string("name")); + assertEquals( + Either.left(new DbError.DecodeError("null value in column: name")), + SqlRunner.run(conn, query)); + } + + // ------------------------------------------------------------------------- + // SQL exceptions + // ------------------------------------------------------------------------- + + @Test + public void runReturnsDbErrorWhenExecuteQueryThrows() { + var conn = throwingConnection("table not found"); + var query = new Query<>(new SqlAst.Table("users"), RowDecoder.string("name")); + assertEquals( + Either.left(new DbError.SqlError("table not found")), + SqlRunner.run(conn, query)); + } + + // ------------------------------------------------------------------------- + // SQL compilation + // ------------------------------------------------------------------------- + + @Test + public void runPassesCompiledSqlToStatement() { + String[] captured = {null}; + var conn = connectionCapturing(captured); + var query = new Query<>(new SqlAst.Table("orders"), RowDecoder.string("id")); + SqlRunner.run(conn, query); + assertEquals("SELECT * FROM orders", captured[0]); + } + + @Test + public void runCompilesSqlAstBeforeExecution() { + String[] captured = {null}; + var conn = connectionCapturing(captured); + var query = new Query<>( + new SqlAst.Filter( + new SqlAst.Table("orders"), + new com.dan323.functional.data.sql.Expr.Gt( + new com.dan323.functional.data.sql.Expr.Column<>("o", "amount", Number.class), + new com.dan323.functional.data.sql.Expr.Literal<>(0, Number.class))), + RowDecoder.string("id")); + SqlRunner.run(conn, query); + assertEquals("SELECT * FROM orders WHERE (o.amount > 0)", captured[0]); + } + + // ------------------------------------------------------------------------- + // Public API: connection failure + // ------------------------------------------------------------------------- + + @Test + public void runPublicApiReturnsDbErrorWhenConnectionFails() { + // No PostgreSQL instance is running at this port — connection must fail. + var cfg = new com.dan323.functional.data.sql.Config("localhost", 1, "test"); + var query = new Query<>(new SqlAst.Table("t"), RowDecoder.string("x")); + var result = SqlRunner.run(cfg, query); + boolean isSqlError = result.either(err -> err instanceof DbError.SqlError, val -> false); + assertTrue(isSqlError); + } + + // ------------------------------------------------------------------------- + // Stub helpers + // ------------------------------------------------------------------------- + + @SafeVarargs + private static ResultSet rows(Map... rowData) { + int[] index = {-1}; + boolean[] lastWasNull = {false}; + return (ResultSet) Proxy.newProxyInstance( + ResultSet.class.getClassLoader(), + new Class[]{ResultSet.class}, + (proxy, method, args) -> switch (method.getName()) { + case "next" -> { + index[0]++; + yield index[0] < rowData.length; + } + case "getString" -> { + Object v = rowData[index[0]].get(args[0]); + lastWasNull[0] = (v == null); + yield v; + } + case "getInt" -> { + Object v = rowData[index[0]].get(args[0]); + lastWasNull[0] = (v == null); + yield v == null ? 0 : ((Number) v).intValue(); + } + case "getLong" -> { + Object v = rowData[index[0]].get(args[0]); + lastWasNull[0] = (v == null); + yield v == null ? 0L : ((Number) v).longValue(); + } + case "getBoolean" -> { + Object v = rowData[index[0]].get(args[0]); + lastWasNull[0] = (v == null); + yield v == null ? Boolean.FALSE : v; + } + case "wasNull" -> lastWasNull[0]; + case "close" -> null; + default -> throw new UnsupportedOperationException(method.getName()); + }); + } + + private static Connection connectionOver(ResultSet rs) { + var stmt = (Statement) Proxy.newProxyInstance( + Statement.class.getClassLoader(), + new Class[]{Statement.class}, + (proxy, method, args) -> switch (method.getName()) { + case "executeQuery" -> rs; + case "close" -> null; + default -> throw new UnsupportedOperationException(method.getName()); + }); + return (Connection) Proxy.newProxyInstance( + Connection.class.getClassLoader(), + new Class[]{Connection.class}, + (proxy, method, args) -> switch (method.getName()) { + case "createStatement" -> stmt; + case "close" -> null; + default -> throw new UnsupportedOperationException(method.getName()); + }); + } + + /** Connection whose Statement throws SQLException on executeQuery. */ + private static Connection throwingConnection(String message) { + var stmt = (Statement) Proxy.newProxyInstance( + Statement.class.getClassLoader(), + new Class[]{Statement.class}, + (proxy, method, args) -> { + if ("executeQuery".equals(method.getName())) throw new SQLException(message); + return null; + }); + return (Connection) Proxy.newProxyInstance( + Connection.class.getClassLoader(), + new Class[]{Connection.class}, + (proxy, method, args) -> switch (method.getName()) { + case "createStatement" -> stmt; + case "close" -> null; + default -> throw new UnsupportedOperationException(method.getName()); + }); + } + + /** Connection that captures the SQL string passed to executeQuery. */ + private static Connection connectionCapturing(String[] target) { + var stmt = (Statement) Proxy.newProxyInstance( + Statement.class.getClassLoader(), + new Class[]{Statement.class}, + (proxy, method, args) -> switch (method.getName()) { + case "executeQuery" -> { + target[0] = (String) args[0]; + yield rows(); + } + case "close" -> null; + default -> throw new UnsupportedOperationException(method.getName()); + }); + return (Connection) Proxy.newProxyInstance( + Connection.class.getClassLoader(), + new Class[]{Connection.class}, + (proxy, method, args) -> switch (method.getName()) { + case "createStatement" -> stmt; + case "close" -> null; + default -> throw new UnsupportedOperationException(method.getName()); + }); + } +} diff --git a/example-functional/src/test/java/com/dan323/functional/StackActionsTest.java b/example-functional/src/test/java/com/dan323/functional/StackActionsTest.java index 6873af02..f42fa9ea 100644 --- a/example-functional/src/test/java/com/dan323/functional/StackActionsTest.java +++ b/example-functional/src/test/java/com/dan323/functional/StackActionsTest.java @@ -2,6 +2,7 @@ import com.dan323.functional.data.either.Either; import com.dan323.functional.data.list.FiniteList; +import com.dan323.functional.data.list.List; import com.dan323.functional.data.optional.Maybe; import com.dan323.functional.data.state.StackActions; import org.junit.jupiter.api.Test; @@ -15,24 +16,24 @@ public class StackActionsTest { @Test public void pushTest() { StackActions stackActions = push(4).then(push(5)).then(push(7)); - assertEquals(Either.right(FiniteList.of(7, 5, 4)), stackActions.execute(FiniteList.nil())); + assertEquals(Either.right(FiniteList.of(7, 5, 4)), stackActions.execute(List.nil())); } @Test public void dupTest() { StackActions stackActions = push(4).then(dup()); - assertEquals(Either.right(FiniteList.of(4, 4)), stackActions.execute(FiniteList.nil())); + assertEquals(Either.right(FiniteList.of(4, 4)), stackActions.execute(List.nil())); } @Test public void popTest() { StackActions stackActions = push(4).then(pop()); - assertEquals(Either.right(Maybe.of(4)), stackActions.evaluate(FiniteList.nil())); + assertEquals(Either.right(Maybe.of(4)), stackActions.evaluate(List.nil())); assertEquals(Either.right(Maybe.of(4)), stackActions.evaluate(FiniteList.of(1, 23, 13))); stackActions = pop(); - assertEquals(Either.left(poppingEmpty()), stackActions.evaluate(FiniteList.nil())); - assertEquals(Either.left(poppingEmpty()), stackActions.execute(FiniteList.nil())); + assertEquals(Either.left(poppingEmpty()), stackActions.evaluate(List.nil())); + assertEquals(Either.left(poppingEmpty()), stackActions.execute(List.nil())); assertEquals(Either.right(Maybe.of(1)), stackActions.evaluate(FiniteList.of(1, 23, 13))); assertEquals(Either.right(FiniteList.of(23, 13)), stackActions.execute(FiniteList.of(1, 23, 13))); } @@ -40,36 +41,36 @@ public void popTest() { @Test public void thenByPoppedTest() { StackActions stackActions = push(4).then(push(6)).thenByPopped(x -> x.maybe(y -> y == 4 ? push(7) : push(9), push(8))); - assertEquals(Either.right(FiniteList.of(8, 6, 4)), stackActions.execute(FiniteList.nil())); + assertEquals(Either.right(FiniteList.of(8, 6, 4)), stackActions.execute(List.nil())); StackActions stackActions2 = push(4).then(pop()).thenByPopped(x -> x.maybe(y -> y == 4 ? push(7) : push(9), push(8))); - assertEquals(Either.right(FiniteList.of(7)), stackActions2.execute(FiniteList.nil())); + assertEquals(Either.right(FiniteList.of(7)), stackActions2.execute(List.nil())); } @Test public void doNothingTest() { StackActions stackActions = push(4).then(push(6)).then(doNothing()); - assertEquals(Either.right(FiniteList.of(6, 4)), stackActions.execute(FiniteList.nil())); - assertEquals(Either.right(Maybe.of()), stackActions.evaluate(FiniteList.nil())); + assertEquals(Either.right(FiniteList.of(6, 4)), stackActions.execute(List.nil())); + assertEquals(Either.right(Maybe.of()), stackActions.evaluate(List.nil())); StackActions stackActions2 = push(4).then(drop()); - assertEquals(Either.right(FiniteList.of()), stackActions2.execute(FiniteList.nil())); - assertEquals(Either.right(Maybe.of()), stackActions2.evaluate(FiniteList.nil())); + assertEquals(Either.right(FiniteList.of()), stackActions2.execute(List.nil())); + assertEquals(Either.right(Maybe.of()), stackActions2.evaluate(List.nil())); } @Test public void overTest() { StackActions stackActions = push(4).then(push(6)).then(over()); - assertEquals(Either.right(FiniteList.of(4, 6, 4)), stackActions.execute(FiniteList.nil())); - assertEquals(Either.right(Maybe.of()), stackActions.evaluate(FiniteList.nil())); + assertEquals(Either.right(FiniteList.of(4, 6, 4)), stackActions.execute(List.nil())); + assertEquals(Either.right(Maybe.of()), stackActions.evaluate(List.nil())); stackActions = push(4).then(over()); - assertEquals(Either.left(poppingEmpty()), stackActions.execute(FiniteList.nil())); - assertEquals(Either.left(poppingEmpty()), stackActions.evaluate(FiniteList.nil())); + assertEquals(Either.left(poppingEmpty()), stackActions.execute(List.nil())); + assertEquals(Either.left(poppingEmpty()), stackActions.evaluate(List.nil())); stackActions = over(); - assertEquals(Either.left(poppingEmpty()), stackActions.execute(FiniteList.nil())); - assertEquals(Either.left(poppingEmpty()), stackActions.evaluate(FiniteList.nil())); + assertEquals(Either.left(poppingEmpty()), stackActions.execute(List.nil())); + assertEquals(Either.left(poppingEmpty()), stackActions.evaluate(List.nil())); } diff --git a/functional-definitions/functional-compiler/src/test/java/com/dan323/functional/util/monad/MonadJoin.java b/functional-definitions/functional-compiler/src/test/java/com/dan323/functional/util/monad/MonadJoin.java index 78cb8f2d..b228a8ff 100644 --- a/functional-definitions/functional-compiler/src/test/java/com/dan323/functional/util/monad/MonadJoin.java +++ b/functional-definitions/functional-compiler/src/test/java/com/dan323/functional/util/monad/MonadJoin.java @@ -3,7 +3,6 @@ import com.dan323.functional.annotation.Monad; import com.dan323.functional.annotation.funcs.IMonad; -import java.util.List; import java.util.Optional; import java.util.function.Function; diff --git a/sonar-project.properties b/sonar-project.properties index 5548ad0a..b810324f 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -12,11 +12,11 @@ # ============================================================================ # Project Identification # ============================================================================ -sonar.projectKey=dan323_functional-by-annotations +sonar.projectKey=functional-by-annotations sonar.projectName=Functional Java by Annotations sonar.projectVersion=1.2-SNAPSHOT sonar.projectDescription=Functional programming structures for Java via annotations and compile-time code generation. -sonar.organization=dan323-github +sonar.organization=dan323 # ============================================================================ # Source Code Configuration