Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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)
)
Expand All @@ -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"
}

Expand All @@ -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)
)
Expand All @@ -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"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,43 +83,35 @@ 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
def values: Seq[Feature] = Seq(
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"
}
}

Expand Down Expand Up @@ -180,32 +172,28 @@ 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)
)

val Enterprise: Quota = Quota(
maxMaterializedViews = None, // Unlimited
maxQueryResults = None,
maxConcurrentQueries = None,
maxClusters = None,
maxJoins = None
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
}

Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading