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
2 changes: 1 addition & 1 deletion .github/workflows/testlinux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
container: swift:5.9-jammy
container: swift:6.2

steps:
- name: Check out Source
Expand Down
64 changes: 34 additions & 30 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,43 +42,50 @@ swift package generate-xcodeproj

## Architecture Overview

### Core Data Models
- **DTOv1/DTOv2**: Main data transfer objects with versioning
- `DTOv1`: Legacy models (InsightGroup, LexiconPayloadKey, OrganizationJoinRequest)
- `DTOv2`: Current models (Organization, User, App, Insight, Badge, etc.)
- **Models.swift**: Additional DTOs for API requests, authentication, and UI state

### Query System
### Query System (`Query/`)
- **CustomQuery**: Main query builder for Apache Druid integration
- Supports multiple query types: timeseries, groupBy, topN, scan, timeBoundary, funnel, experiment
- Query types: `timeseries`, `groupBy`, `topN`, `scan`, `timeBoundary`, `funnel`, `experiment`, `retention`
- Handles filters, aggregations, post-aggregations, and time intervals
- **Query Components**:
- `Aggregator`: Define aggregation functions (sum, count, etc.)
- `Aggregator`: Aggregation functions (sum, count, etc.)
- `Filter`: Query filtering logic
- `DimensionSpec`: Dimension specifications for grouping
- `QueryGranularity`: Time granularity (day, week, month)
- `VirtualColumn`: Computed columns

### Druid Integration
- **Druid/**: Complete Apache Druid configuration DTOs
- `configuration/`: Tuning configs, compaction configs
- `data/input/`: Input formats, sources, and dimension specs
- `indexing/`: Parallel indexing, batch processing
- `ingestion/`: Native batch ingestion specs
- `segment/`: Data schema and transformation specs
- `Supervisor/`: Kafka streaming supervision

### Chart Configuration
- **ChartConfiguration**: Display settings for analytics charts
- **ChartDefinitionDTO**: Chart metadata and configuration
- **InsightDisplayMode**: Chart types (lineChart, barChart, pieChart, etc.)

### Query Results
- **QueryResult**: Polymorphic result handling for different query types
- `PostAggregator`: Post-aggregation calculations
- `Datasource`: Data source configuration

### Query Generation (`QueryGeneration/`)
- **CustomQuery+Funnel**: Funnel analysis query generation
- **CustomQuery+Experiment**: A/B experiment queries
- **CustomQuery+Retention**: Retention analysis queries
- **Precompilable**: Query precompilation protocol
- **SQLQueryConversion**: SQL conversion utilities

### Query Results (`QueryResult/`)
- **QueryResult**: Polymorphic enum for different result types
- **TimeSeriesQueryResult**: Time-based query results
- **TopNQueryResult**: Top-N dimension results
- **GroupByQueryResult**: Grouped aggregation results
- **ScanQueryResult**: Raw data scanning results
- **TimeBoundaryResult**: Time boundary query results
- Helper types: `StringWrapper`, `DoubleWrapper`, `DoublePlusInfinity`

### Druid Configuration (`Druid/`)
- `configuration/`: TuningConfig, AutoCompactionConfig
- `data/input/`: Input formats and dimension specs
- `indexer/`: Granularity specs
- `indexing/`: Kinesis streaming, parallel batch indexing
- `ingestion/`: Task specs, native batch, ingestion specs
- `segment/`: Data schema and transform specs

### Supervisor (`Supervisor/`)
- Kafka/Kinesis streaming supervision DTOs

### Chart Configuration (`Chart Configuration/`)
- **ChartConfiguration**: Display settings for analytics charts
- **ChartAggregationConfiguration**: Aggregation configuration
- **ChartConfigurationOptions**: Chart options

## Key Dependencies

Expand All @@ -87,9 +94,6 @@ swift package generate-xcodeproj

## Development Notes

### DTO Versioning
The library uses a versioning strategy with `DTOv1` and `DTOv2` namespaces. `DTOv2.Insight` is deprecated in favor of V3InsightsController patterns.

### Query Hashing
CustomQuery implements stable hashing using SHA256 for caching and query deduplication. The `stableHashValue` property provides consistent query identification.

Expand All @@ -98,7 +102,7 @@ Tests are organized by functionality:
- **DataTransferObjectsTests**: Basic DTO serialization/deserialization
- **QueryTests**: Query building and validation
- **QueryResultTests**: Result parsing and handling
- **QueryGenerationTests**: Advanced query generation (funnels, experiments)
- **QueryGenerationTests**: Advanced query generation (funnels, experiments, retention)
- **SupervisorTests**: Druid supervisor configuration
- **DataSchemaTests**: Data ingestion schema validation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ public extension CustomQuery {
return filter
case .range:
return filter
case .equals:
return filter
case .null:
return filter
case .and(let filterExpression):
return Filter.and(.init(fields: filterExpression.fields.map { compileRelativeFilterInterval(filter: $0) }))
case .or(let filterExpression):
Expand Down
111 changes: 108 additions & 3 deletions Sources/DataTransferObjects/Query/Filter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,97 @@ public struct FilterNot: Codable, Hashable, Equatable, Sendable {
public let field: Filter
}

/// The equality filter matches rows where a column value equals a specific value.
public struct FilterEquals: Codable, Hashable, Equatable, Sendable {
public init(column: String, matchValueType: MatchValueType, matchValue: MatchValue) {
self.column = column
self.matchValueType = matchValueType
self.matchValue = matchValue
}

public enum MatchValueType: String, Codable, Hashable, Equatable, Sendable {
case string = "STRING"
case long = "LONG"
case double = "DOUBLE"
case float = "FLOAT"
case arrayString = "ARRAY<STRING>"
case arrayLong = "ARRAY<LONG>"
case arrayDouble = "ARRAY<DOUBLE>"
case arrayFloat = "ARRAY<FLOAT>"
}

public enum MatchValue: Hashable, Equatable, Sendable {
case string(String)
case int(Int)
case double(Double)
case arrayString([String])
case arrayInt([Int])
case arrayDouble([Double])
}

public let column: String
public let matchValueType: MatchValueType
public let matchValue: MatchValue
}

extension FilterEquals.MatchValue: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

if let arrayString = try? container.decode([String].self) {
self = .arrayString(arrayString)
} else if let arrayInt = try? container.decode([Int].self) {
self = .arrayInt(arrayInt)
} else if let arrayDouble = try? container.decode([Double].self) {
self = .arrayDouble(arrayDouble)
} else if let string = try? container.decode(String.self) {
self = .string(string)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let double = try? container.decode(Double.self) {
self = .double(double)
} else {
throw DecodingError.typeMismatch(
FilterEquals.MatchValue.self,
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Expected String, Int, Double, or array types"
)
)
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()

switch self {
case .string(let value):
try container.encode(value)
case .int(let value):
try container.encode(value)
case .double(let value):
try container.encode(value)
case .arrayString(let value):
try container.encode(value)
case .arrayInt(let value):
try container.encode(value)
case .arrayDouble(let value):
try container.encode(value)
}
}
}

/// The null filter matches rows where a column value is null.
public struct FilterNull: Codable, Hashable, Equatable, Sendable {
public init(column: String) {
self.column = column
}

public let column: String
}

/// A filter is a JSON object indicating which rows of data should be included in the computation
/// for a query. Its essentially the equivalent of the WHERE clause in SQL.
/// for a query. It's essentially the equivalent of the WHERE clause in SQL.
public indirect enum Filter: Codable, Hashable, Equatable, Sendable {
/// The selector filter will match a specific dimension with a specific value.
/// Selector filters can be used as the base filters for more complex Boolean
Expand All @@ -157,6 +246,12 @@ public indirect enum Filter: Codable, Hashable, Equatable, Sendable {
// to, less than or equal to, and "between"
case range(FilterRange)

/// The equality filter matches rows where a column value equals a specific value.
case equals(FilterEquals)

/// The null filter matches rows where a column value is null.
case null(FilterNull)

// logical expression filters
case and(FilterExpression)
case or(FilterExpression)
Expand All @@ -179,14 +274,18 @@ public indirect enum Filter: Codable, Hashable, Equatable, Sendable {
self = try .interval(FilterInterval(from: decoder))
case "regex":
self = try .regex(FilterRegex(from: decoder))
case "range":
self = try .range(FilterRange(from: decoder))
case "equals":
self = try .equals(FilterEquals(from: decoder))
case "null":
self = try .null(FilterNull(from: decoder))
case "and":
self = try .and(FilterExpression(from: decoder))
case "or":
self = try .or(FilterExpression(from: decoder))
case "not":
self = try .not(FilterNot(from: decoder))
case "range":
self = try .range(FilterRange(from: decoder))
default:
throw EncodingError.invalidValue("Invalid type", .init(codingPath: [CodingKeys.type], debugDescription: "Invalid Type", underlyingError: nil))
}
Expand Down Expand Up @@ -216,6 +315,12 @@ public indirect enum Filter: Codable, Hashable, Equatable, Sendable {
case let .range(range):
try container.encode("range", forKey: .type)
try range.encode(to: encoder)
case let .equals(equals):
try container.encode("equals", forKey: .type)
try equals.encode(to: encoder)
case let .null(null):
try container.encode("null", forKey: .type)
try null.encode(to: encoder)
case let .and(and):
try container.encode("and", forKey: .type)
try and.encode(to: encoder)
Expand Down
Loading
Loading