From 9d1dd48dec19938a8871c1e007a684a36af1d903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Wed, 20 May 2026 18:03:48 +0200 Subject: [PATCH 1/2] feat: add maxJoins quota to Quota case class and enable Community Federation Add maxJoins: Option[Int] field to Quota with tier defaults (Community=1, Pro=5, Enterprise=unlimited). Grant Feature.Federation to Community license with maxClusters=Some(1) enabling same-cluster cross-index JOINs for free-tier users. Closed Issue #88 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../softnetwork/elastic/licensing/package.scala | 15 ++++++++++----- .../elastic/licensing/LicenseManagerSpec.scala | 4 ++-- .../elastic/licensing/QuotaSpec.scala | 17 +++++++++++++++-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/licensing/src/main/scala/app/softnetwork/elastic/licensing/package.scala b/licensing/src/main/scala/app/softnetwork/elastic/licensing/package.scala index 9b0813fb..ccb23f6a 100644 --- a/licensing/src/main/scala/app/softnetwork/elastic/licensing/package.scala +++ b/licensing/src/main/scala/app/softnetwork/elastic/licensing/package.scala @@ -132,7 +132,8 @@ package object licensing { val Community: LicenseKey = LicenseKey( id = "community", licenseType = LicenseType.Community, - features = Set(Feature.MaterializedViews, Feature.JdbcDriver, Feature.FlightSql), + features = + Set(Feature.MaterializedViews, Feature.JdbcDriver, Feature.FlightSql, Feature.Federation), expiresAt = None, quota = Some(Quota.Community) ) @@ -142,7 +143,8 @@ package object licensing { maxMaterializedViews: Option[Int], // None = unlimited maxQueryResults: Option[Int], // None = unlimited maxConcurrentQueries: Option[Int], - maxClusters: Option[Int] = Some(0) // None = unlimited + maxClusters: Option[Int] = Some(0), // None = unlimited + maxJoins: Option[Int] = Some(0) // None = unlimited ) object Quota { @@ -150,21 +152,24 @@ package object licensing { maxMaterializedViews = Some(3), maxQueryResults = Some(10000), maxConcurrentQueries = Some(5), - maxClusters = Some(0) + maxClusters = Some(1), + maxJoins = Some(1) ) val Pro: Quota = Quota( maxMaterializedViews = Some(50), maxQueryResults = Some(1000000), maxConcurrentQueries = Some(50), - maxClusters = Some(5) + maxClusters = Some(5), + maxJoins = Some(5) ) val Enterprise: Quota = Quota( maxMaterializedViews = None, // Unlimited maxQueryResults = None, maxConcurrentQueries = None, - maxClusters = None + maxClusters = None, + maxJoins = None ) } diff --git a/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseManagerSpec.scala b/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseManagerSpec.scala index 87f4f92c..edfde6ff 100644 --- a/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseManagerSpec.scala +++ b/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseManagerSpec.scala @@ -35,8 +35,8 @@ class LicenseManagerSpec extends AnyFlatSpec with Matchers { manager.hasFeature(Feature.FlightSql) shouldBe true } - it should "not include Federation" in { - manager.hasFeature(Feature.Federation) shouldBe false + it should "include Federation" in { + manager.hasFeature(Feature.Federation) shouldBe true } it should "not include OdbcDriver" in { diff --git a/licensing/src/test/scala/app/softnetwork/elastic/licensing/QuotaSpec.scala b/licensing/src/test/scala/app/softnetwork/elastic/licensing/QuotaSpec.scala index 38bf5066..45f6ac5c 100644 --- a/licensing/src/test/scala/app/softnetwork/elastic/licensing/QuotaSpec.scala +++ b/licensing/src/test/scala/app/softnetwork/elastic/licensing/QuotaSpec.scala @@ -21,8 +21,12 @@ import org.scalatest.matchers.should.Matchers class QuotaSpec extends AnyFlatSpec with Matchers { - "Quota.Community" should "have maxClusters = Some(0)" in { - Quota.Community.maxClusters shouldBe Some(0) + "Quota.Community" should "have maxClusters = Some(1)" in { + Quota.Community.maxClusters shouldBe Some(1) + } + + it should "have maxJoins = Some(1)" in { + Quota.Community.maxJoins shouldBe Some(1) } it should "have maxMaterializedViews = Some(3)" in { @@ -41,6 +45,10 @@ class QuotaSpec extends AnyFlatSpec with Matchers { Quota.Pro.maxClusters shouldBe Some(5) } + it should "have maxJoins = Some(5)" in { + Quota.Pro.maxJoins shouldBe Some(5) + } + it should "have maxMaterializedViews = Some(50)" in { Quota.Pro.maxMaterializedViews shouldBe Some(50) } @@ -57,6 +65,10 @@ class QuotaSpec extends AnyFlatSpec with Matchers { Quota.Enterprise.maxClusters shouldBe None } + it should "have maxJoins = None (unlimited)" in { + Quota.Enterprise.maxJoins shouldBe None + } + it should "have maxMaterializedViews = None (unlimited)" in { Quota.Enterprise.maxMaterializedViews shouldBe None } @@ -76,5 +88,6 @@ class QuotaSpec extends AnyFlatSpec with Matchers { maxConcurrentQueries = Some(1) ) quota.maxClusters shouldBe Some(0) + quota.maxJoins shouldBe Some(0) } } From 9830febc471cf40a2beb47e3947561d072161c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Thu, 21 May 2026 08:59:55 +0200 Subject: [PATCH 2/2] feat: add max_joins quota to GatewayApi and update LicenseExecutorSpec --- .../scala/app/softnetwork/elastic/client/GatewayApi.scala | 1 + .../app/softnetwork/elastic/client/LicenseExecutorSpec.scala | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/app/softnetwork/elastic/client/GatewayApi.scala b/core/src/main/scala/app/softnetwork/elastic/client/GatewayApi.scala index 13157fb7..12bf61b4 100644 --- a/core/src/main/scala/app/softnetwork/elastic/client/GatewayApi.scala +++ b/core/src/main/scala/app/softnetwork/elastic/client/GatewayApi.scala @@ -1632,6 +1632,7 @@ class LicenseExecutor( "max_clusters" -> formatQuota(mgr.quotas.maxClusters), "max_result_rows" -> formatQuota(mgr.quotas.maxQueryResults), "max_concurrent_queries" -> formatQuota(mgr.quotas.maxConcurrentQueries), + "max_joins" -> formatQuota(mgr.quotas.maxJoins), "expires_at" -> formatExpiry(key.expiresAt), "days_remaining" -> key.daysRemaining.getOrElse(-1L), "status" -> graceStatus diff --git a/core/src/test/scala/app/softnetwork/elastic/client/LicenseExecutorSpec.scala b/core/src/test/scala/app/softnetwork/elastic/client/LicenseExecutorSpec.scala index b0b66aaa..bb53e4a6 100644 --- a/core/src/test/scala/app/softnetwork/elastic/client/LicenseExecutorSpec.scala +++ b/core/src/test/scala/app/softnetwork/elastic/client/LicenseExecutorSpec.scala @@ -79,7 +79,7 @@ class LicenseExecutorSpec extends AnyFlatSpec with Matchers { row should contain key "max_materialized_views" row("max_materialized_views") shouldBe "3" row should contain key "max_clusters" - row("max_clusters") shouldBe "0" + row("max_clusters") shouldBe "1" row should contain key "max_result_rows" row("max_result_rows") shouldBe "10000" row should contain key "max_concurrent_queries" @@ -89,6 +89,8 @@ class LicenseExecutorSpec extends AnyFlatSpec with Matchers { row("days_remaining") shouldBe -1L row should contain key "status" row("status") shouldBe "Active" + row should contain key "max_joins" + row("max_joins") shouldBe "1" } it should "return strategy license info when strategy is configured" in {