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 6f2059b2..9c7928af 100644 --- a/core/src/main/scala/app/softnetwork/elastic/client/GatewayApi.scala +++ b/core/src/main/scala/app/softnetwork/elastic/client/GatewayApi.scala @@ -1636,7 +1636,6 @@ class LicenseExecutor( "max_materialized_views" -> formatQuota(mgr.quotas.maxMaterializedViews), "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), 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 3eac3a44..d24851af 100644 --- a/core/src/test/scala/app/softnetwork/elastic/client/LicenseExecutorSpec.scala +++ b/core/src/test/scala/app/softnetwork/elastic/client/LicenseExecutorSpec.scala @@ -79,20 +79,18 @@ class LicenseExecutorSpec extends AnyFlatSpec with Matchers { row should contain key "platform" row("platform") shouldBe "PRODUCTION" row should contain key "max_materialized_views" - row("max_materialized_views") shouldBe "3" + row("max_materialized_views") shouldBe "1" row should contain key "max_clusters" 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" - row("max_concurrent_queries") shouldBe "5" row should contain key "expires_at" row("expires_at") shouldBe "never" 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" + row("max_joins") shouldBe "2" } it should "return strategy license info when strategy is configured" in { @@ -117,7 +115,6 @@ class LicenseExecutorSpec extends AnyFlatSpec with Matchers { row("max_materialized_views") shouldBe "50" row("max_clusters") shouldBe "5" row("max_result_rows") shouldBe "1000000" - row("max_concurrent_queries") shouldBe "50" row("platform") shouldBe "PRODUCTION" row("expires_at") shouldBe "2026-12-31T23:59:59Z" row("days_remaining").asInstanceOf[Long] should be > 0L @@ -202,7 +199,6 @@ class LicenseExecutorSpec extends AnyFlatSpec with Matchers { override def quotas: Quota = Quota( maxMaterializedViews = Some(25), maxQueryResults = Some(500000), - maxConcurrentQueries = Some(25), maxClusters = Some(2), maxJoins = Some(2) ) @@ -223,7 +219,6 @@ class LicenseExecutorSpec extends AnyFlatSpec with Matchers { row("max_materialized_views") shouldBe "25" row("max_clusters") shouldBe "2" row("max_result_rows") shouldBe "500000" - row("max_concurrent_queries") shouldBe "25" row("max_joins") shouldBe "2" } @@ -235,7 +230,6 @@ class LicenseExecutorSpec extends AnyFlatSpec with Matchers { override def quotas: Quota = Quota( maxMaterializedViews = Some(5), maxQueryResults = Some(10000), - maxConcurrentQueries = Some(5), maxClusters = Some(1), maxJoins = Some(1) ) @@ -255,7 +249,6 @@ class LicenseExecutorSpec extends AnyFlatSpec with Matchers { row("max_materialized_views") shouldBe "5" row("max_clusters") shouldBe "1" row("max_result_rows") shouldBe "10000" - row("max_concurrent_queries") shouldBe "5" row("max_joins") shouldBe "1" } 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 ae41088e..b28d2fcd 100644 --- a/licensing/src/main/scala/app/softnetwork/elastic/licensing/package.scala +++ b/licensing/src/main/scala/app/softnetwork/elastic/licensing/package.scala @@ -83,8 +83,6 @@ package object licensing { case object MaterializedViews extends Feature case object JdbcDriver extends ProductType case object AdbcDriver extends ProductType // Story 15.2 (A11) -- replaces OdbcDriver - case object UnlimitedResults extends Feature - case object AdvancedAggregations extends Feature case object FlightSql extends ProductType case object Federation extends ProductType case object Repl extends ProductType // Story 15.2 (A11) -- NEW @@ -92,34 +90,28 @@ package object licensing { MaterializedViews, JdbcDriver, AdbcDriver, - UnlimitedResults, - AdvancedAggregations, FlightSql, Federation, Repl ) def fromString(s: String): Option[Feature] = s.trim.toLowerCase match { - case "materialized_views" => Some(MaterializedViews) - case "jdbc_driver" => Some(JdbcDriver) - case "adbc_driver" => Some(AdbcDriver) - case "unlimited_results" => Some(UnlimitedResults) - case "advanced_aggregations" => Some(AdvancedAggregations) - case "flight_sql" => Some(FlightSql) - case "federation" => Some(Federation) - case "repl" => Some(Repl) - case _ => None + case "materialized_views" => Some(MaterializedViews) + case "jdbc_driver" => Some(JdbcDriver) + case "adbc_driver" => Some(AdbcDriver) + case "flight_sql" => Some(FlightSql) + case "federation" => Some(Federation) + case "repl" => Some(Repl) + case _ => None } def toSnakeCase(f: Feature): String = f match { - case MaterializedViews => "materialized_views" - case JdbcDriver => "jdbc_driver" - case AdbcDriver => "adbc_driver" - case UnlimitedResults => "unlimited_results" - case AdvancedAggregations => "advanced_aggregations" - case FlightSql => "flight_sql" - case Federation => "federation" - case Repl => "repl" + case MaterializedViews => "materialized_views" + case JdbcDriver => "jdbc_driver" + case AdbcDriver => "adbc_driver" + case FlightSql => "flight_sql" + case Federation => "federation" + case Repl => "repl" } } @@ -180,24 +172,21 @@ package object licensing { case class Quota( maxMaterializedViews: Option[Int], // None = unlimited maxQueryResults: Option[Int], // None = unlimited - maxConcurrentQueries: Option[Int], maxClusters: Option[Int] = Some(0), // None = unlimited maxJoins: Option[Int] = Some(0) // None = unlimited ) object Quota { val Community: Quota = Quota( - maxMaterializedViews = Some(3), + maxMaterializedViews = Some(1), // ADR D6: 3 -> 1 (operationalization signal) maxQueryResults = Some(10000), - maxConcurrentQueries = Some(5), maxClusters = Some(1), - maxJoins = Some(1) + maxJoins = Some(2) // ADR D5: 1 -> 2 (3-table JOIN free-tier aha) ) val Pro: Quota = Quota( maxMaterializedViews = Some(50), maxQueryResults = Some(1000000), - maxConcurrentQueries = Some(50), maxClusters = Some(5), maxJoins = Some(5) ) @@ -205,7 +194,6 @@ package object licensing { val Enterprise: Quota = Quota( maxMaterializedViews = None, // Unlimited maxQueryResults = None, - maxConcurrentQueries = None, maxClusters = None, maxJoins = None ) diff --git a/licensing/src/test/scala/app/softnetwork/elastic/licensing/FeatureFromStringSpec.scala b/licensing/src/test/scala/app/softnetwork/elastic/licensing/FeatureFromStringSpec.scala index d54d86a4..394debb7 100644 --- a/licensing/src/test/scala/app/softnetwork/elastic/licensing/FeatureFromStringSpec.scala +++ b/licensing/src/test/scala/app/softnetwork/elastic/licensing/FeatureFromStringSpec.scala @@ -33,14 +33,6 @@ class FeatureFromStringSpec extends AnyFlatSpec with Matchers { Feature.fromString("adbc_driver") shouldBe Some(Feature.AdbcDriver) } - it should "map unlimited_results" in { - Feature.fromString("unlimited_results") shouldBe Some(Feature.UnlimitedResults) - } - - it should "map advanced_aggregations" in { - Feature.fromString("advanced_aggregations") shouldBe Some(Feature.AdvancedAggregations) - } - it should "map flight_sql" in { Feature.fromString("flight_sql") shouldBe Some(Feature.FlightSql) } @@ -57,6 +49,11 @@ class FeatureFromStringSpec extends AnyFlatSpec with Matchers { Feature.fromString("warp_drive") shouldBe None } + it should "return None for retired flag strings" in { + Feature.fromString("unlimited_results") shouldBe None + Feature.fromString("advanced_aggregations") shouldBe None + } + it should "be case-insensitive" in { Feature.fromString("MATERIALIZED_VIEWS") shouldBe Some(Feature.MaterializedViews) Feature.fromString("Flight_Sql") shouldBe Some(Feature.FlightSql) diff --git a/licensing/src/test/scala/app/softnetwork/elastic/licensing/FeatureSpec.scala b/licensing/src/test/scala/app/softnetwork/elastic/licensing/FeatureSpec.scala index 68d1d897..f49afa38 100644 --- a/licensing/src/test/scala/app/softnetwork/elastic/licensing/FeatureSpec.scala +++ b/licensing/src/test/scala/app/softnetwork/elastic/licensing/FeatureSpec.scala @@ -29,8 +29,8 @@ class FeatureSpec extends AnyFlatSpec with Matchers { Feature.Federation shouldBe a[Feature] } - "Feature.values" should "contain all 8 features" in { - Feature.values should have size 8 + "Feature.values" should "contain all 6 features" in { + Feature.values should have size 6 } it should "contain FlightSql and Federation" in { @@ -43,8 +43,6 @@ class FeatureSpec extends AnyFlatSpec with Matchers { Feature.MaterializedViews, Feature.JdbcDriver, Feature.AdbcDriver, - Feature.UnlimitedResults, - Feature.AdvancedAggregations, Feature.FlightSql, Feature.Federation, Feature.Repl diff --git a/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseKeySpec.scala b/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseKeySpec.scala index 1aa5c4b5..a2ac0a92 100644 --- a/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseKeySpec.scala +++ b/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseKeySpec.scala @@ -50,12 +50,10 @@ class LicenseKeySpec extends AnyFlatSpec with Matchers { features = Feature.values.toSet, expiresAt = None ) - key.features should have size 8 + key.features should have size 6 key.features should contain(Feature.MaterializedViews) key.features should contain(Feature.JdbcDriver) key.features should contain(Feature.AdbcDriver) - key.features should contain(Feature.UnlimitedResults) - key.features should contain(Feature.AdvancedAggregations) key.features should contain(Feature.FlightSql) key.features should contain(Feature.Federation) key.features should contain(Feature.Repl) 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 376cdc0a..97b7f139 100644 --- a/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseManagerSpec.scala +++ b/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseManagerSpec.scala @@ -47,14 +47,6 @@ class LicenseManagerSpec extends AnyFlatSpec with Matchers { manager.hasFeature(Feature.Repl) shouldBe true } - it should "not include UnlimitedResults" in { - manager.hasFeature(Feature.UnlimitedResults) shouldBe false - } - - it should "not include AdvancedAggregations" in { - manager.hasFeature(Feature.AdvancedAggregations) shouldBe false - } - it should "return Community quotas" in { manager.quotas shouldBe Quota.Community } diff --git a/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseUsageSpec.scala b/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseUsageSpec.scala index 722699ad..9d87b31c 100644 --- a/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseUsageSpec.scala +++ b/licensing/src/test/scala/app/softnetwork/elastic/licensing/LicenseUsageSpec.scala @@ -48,19 +48,19 @@ class LicenseUsageSpec extends AnyFlatSpec with Matchers { } "checkQuota for MaterializedViews" should "return None when within quota" in { - val usage = LicenseUsage(totalMvsActive = 2) + val usage = LicenseUsage(totalMvsActive = 0) usage.checkQuota(Feature.MaterializedViews, Quota.Community) shouldBe None } it should "return None at exact quota boundary" in { - val usage = LicenseUsage(totalMvsActive = 3) + val usage = LicenseUsage(totalMvsActive = 1) usage.checkQuota(Feature.MaterializedViews, Quota.Community) shouldBe None } it should "detect exceeded maxMaterializedViews" in { - val usage = LicenseUsage(totalMvsActive = 4) + val usage = LicenseUsage(totalMvsActive = 2) usage.checkQuota(Feature.MaterializedViews, Quota.Community) shouldBe Some( - QuotaExceeded("maxMaterializedViews", 4, 3) + QuotaExceeded("maxMaterializedViews", 2, 1) ) } @@ -100,6 +100,5 @@ class LicenseUsageSpec extends AnyFlatSpec with Matchers { val usage = LicenseUsage(totalMvsActive = 100, totalFederatedClusters = 100) usage.checkQuota(Feature.JdbcDriver, Quota.Community) shouldBe None usage.checkQuota(Feature.FlightSql, Quota.Community) shouldBe None - usage.checkQuota(Feature.UnlimitedResults, Quota.Community) shouldBe None } } 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 45f6ac5c..0db639bd 100644 --- a/licensing/src/test/scala/app/softnetwork/elastic/licensing/QuotaSpec.scala +++ b/licensing/src/test/scala/app/softnetwork/elastic/licensing/QuotaSpec.scala @@ -25,22 +25,18 @@ class QuotaSpec extends AnyFlatSpec with Matchers { Quota.Community.maxClusters shouldBe Some(1) } - it should "have maxJoins = Some(1)" in { - Quota.Community.maxJoins shouldBe Some(1) + it should "have maxJoins = Some(2)" in { + Quota.Community.maxJoins shouldBe Some(2) } - it should "have maxMaterializedViews = Some(3)" in { - Quota.Community.maxMaterializedViews shouldBe Some(3) + it should "have maxMaterializedViews = Some(1)" in { + Quota.Community.maxMaterializedViews shouldBe Some(1) } it should "have maxQueryResults = Some(10000)" in { Quota.Community.maxQueryResults shouldBe Some(10000) } - it should "have maxConcurrentQueries = Some(5)" in { - Quota.Community.maxConcurrentQueries shouldBe Some(5) - } - "Quota.Pro" should "have maxClusters = Some(5)" in { Quota.Pro.maxClusters shouldBe Some(5) } @@ -57,10 +53,6 @@ class QuotaSpec extends AnyFlatSpec with Matchers { Quota.Pro.maxQueryResults shouldBe Some(1000000) } - it should "have maxConcurrentQueries = Some(50)" in { - Quota.Pro.maxConcurrentQueries shouldBe Some(50) - } - "Quota.Enterprise" should "have maxClusters = None (unlimited)" in { Quota.Enterprise.maxClusters shouldBe None } @@ -77,15 +69,10 @@ class QuotaSpec extends AnyFlatSpec with Matchers { Quota.Enterprise.maxQueryResults shouldBe None } - it should "have maxConcurrentQueries = None (unlimited)" in { - Quota.Enterprise.maxConcurrentQueries shouldBe None - } - "Quota default constructor" should "use maxClusters = Some(0)" in { val quota = Quota( maxMaterializedViews = Some(10), - maxQueryResults = Some(100), - maxConcurrentQueries = Some(1) + maxQueryResults = Some(100) ) quota.maxClusters shouldBe Some(0) quota.maxJoins shouldBe Some(0) diff --git a/licensing/src/test/scala/app/softnetwork/elastic/licensing/ResultLimitInvariantSpec.scala b/licensing/src/test/scala/app/softnetwork/elastic/licensing/ResultLimitInvariantSpec.scala new file mode 100644 index 00000000..9ae5acaa --- /dev/null +++ b/licensing/src/test/scala/app/softnetwork/elastic/licensing/ResultLimitInvariantSpec.scala @@ -0,0 +1,58 @@ +/* + * Copyright 2025 SOFTNETWORK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.softnetwork.elastic.licensing + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +/** ADR D3 invariant: "unlimited results" has ONE source of truth — `Quota.maxQueryResults` — not a + * removed `UnlimitedResults` feature flag. Community = Some(10000), Pro = Some(1000000), + * Enterprise = None (unlimited). + */ +class ResultLimitInvariantSpec extends AnyFlatSpec with Matchers { + + "Result-limit policy" should "be driven solely by Quota.maxQueryResults per tier" in { + Quota.Community.maxQueryResults shouldBe Some(10000) + Quota.Pro.maxQueryResults shouldBe Some(1000000) + Quota.Enterprise.maxQueryResults shouldBe None // unlimited + } + + it should "treat Enterprise (maxQueryResults = None) as the unlimited case" in { + Quota.Enterprise.maxQueryResults.isEmpty shouldBe true + } + + "The Feature enum" should "carry no result-limit flag" in { + // The retired UnlimitedResults / AdvancedAggregations flags must not exist + // under any (case-insensitive) spelling, and the catalog must stay at 6. + Feature.values should have size 6 + Feature.values.map(Feature.toSnakeCase) should contain noneOf ( + "unlimited_results", + "advanced_aggregations" + ) + Feature.fromString("unlimited_results") shouldBe None + Feature.fromString("advanced_aggregations") shouldBe None + } + + "Community manager" should "express the result cap via quota, not a feature" in { + val mgr = new CommunityLicenseManager + mgr.quotas.maxQueryResults shouldBe Some(10000) + // No flag encodes the limit: + Feature.values.foreach(f => + mgr.hasFeature(f) shouldBe LicenseKey.Community.features.contains(f) + ) + } +} diff --git a/testkit/src/main/scala/app/softnetwork/elastic/client/repl/ReplGatewayIntegrationSpec.scala b/testkit/src/main/scala/app/softnetwork/elastic/client/repl/ReplGatewayIntegrationSpec.scala index 008794fc..3e1da5ec 100644 --- a/testkit/src/main/scala/app/softnetwork/elastic/client/repl/ReplGatewayIntegrationSpec.scala +++ b/testkit/src/main/scala/app/softnetwork/elastic/client/repl/ReplGatewayIntegrationSpec.scala @@ -1440,7 +1440,6 @@ trait ReplGatewayIntegrationSpec extends ReplIntegrationTestKit { row("license_type") shouldBe "Community" row should contain key "max_materialized_views" row should contain key "max_result_rows" - row should contain key "max_concurrent_queries" row should contain key "max_clusters" row should contain key "expires_at" row("expires_at") shouldBe "never"