Skip to content

Commit 61f311b

Browse files
feat: add FluxRecord.row with response data stored in Array (#57)
1 parent e694dc0 commit 61f311b

File tree

9 files changed

+260
-12
lines changed

9 files changed

+260
-12
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## 1.5.0 [unreleased]
22

3+
### Features
4+
1. [#57](https://github.com/influxdata/influxdb-client-swift/pull/57): Added `FluxRecord.row` which stores response data in a array
5+
36
## 1.4.0 [2022-09-30]
47

58
### CI

Examples/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@
1616
- [AsyncAwait](AsyncAwait#asyncawait) - How to use `async/await` with the InfluxDB client.
1717
- [InfluxDBStatus](InfluxDBStatus#influxdbstatus) - This is an example how to check status of InfluxDB
1818
- [InvokableScripts](InvokableScripts#invokablescripts) - How to use Invokable scripts Cloud API to create custom endpoints that query data
19-
19+
- [RecordRow](RecordRow#recordrow) - How to use `FluxRecord.row` instead of `FluxRecord.values`,
20+
in case of duplicity column names

Examples/RecordRow/.swiftlint.yml

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
opt_in_rules:
2+
- array_init
3+
- collection_alignment
4+
- contains_over_first_not_nil
5+
- closure_end_indentation
6+
- closure_spacing
7+
- conditional_returns_on_newline
8+
- empty_count
9+
- empty_string
10+
- explicit_init
11+
- explicit_self
12+
- fatal_error_message
13+
- first_where
14+
- implicit_return
15+
- missing_docs
16+
- modifier_order
17+
- multiline_arguments
18+
- multiline_literal_brackets
19+
- multiline_parameters
20+
- operator_usage_whitespace
21+
- pattern_matching_keywords
22+
- redundant_nil_coalescing
23+
- redundant_type_annotation
24+
- sorted_first_last
25+
- sorted_imports
26+
- trailing_closure
27+
- unneeded_parentheses_in_closure_argument
28+
- unused_import
29+
- unused_declaration
30+
- vertical_parameter_alignment_on_call
31+
- vertical_whitespace_closing_braces
32+
- vertical_whitespace_opening_braces
33+
34+
excluded:
35+
- Package.swift
36+
- .build/

Examples/RecordRow/Package.swift

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// swift-tools-version:5.5
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "RecordRow",
7+
platforms: [
8+
.macOS(.v10_15)
9+
],
10+
products: [
11+
.executable(name: "record-row", targets: ["RecordRow"])
12+
],
13+
dependencies: [
14+
.package(name: "influxdb-client-swift", path: "../../"),
15+
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.2")
16+
],
17+
targets: [
18+
.executableTarget(name: "RecordRow", dependencies: [
19+
.product(name: "InfluxDBSwiftApis", package: "influxdb-client-swift"),
20+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
21+
])
22+
]
23+
)

Examples/RecordRow/README.md

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# RecordRow
2+
3+
Using 'FluxRecord.row' in case of duplicated column names in response.
4+
5+
## Prerequisites:
6+
- Docker
7+
- Cloned examples:
8+
```bash
9+
git clone [email protected]:influxdata/influxdb-client-swift.git
10+
cd Examples/RecordRow
11+
```
12+
13+
## Sources:
14+
- [Package.swift](/Examples/RecordRow/Package.swift)
15+
- [RecordRow.swift](/Examples/RecordRow/Sources/RecordRow/RecordRow.swift)
16+
17+
## How to test:
18+
1. Start InfluxDB:
19+
```bash
20+
docker run --rm \
21+
--name influxdb_v2 \
22+
--detach \
23+
--publish 8086:8086 \
24+
influxdb:latest
25+
```
26+
2. Configure your username, password, organization, bucket and token:
27+
```bash
28+
docker run --rm \
29+
--link influxdb_v2 \
30+
curlimages/curl -s -i -X POST http://influxdb_v2:8086/api/v2/setup \
31+
-H 'accept: application/json' \
32+
-d '{"username": "my-user", "password": "my-password", "org": "my-org", "bucket": "my-bucket", "token": "my-token"}'
33+
```
34+
3. Start SwiftCLI by:
35+
```bash
36+
docker run --rm \
37+
--link influxdb_v2 \
38+
--privileged \
39+
--interactive \
40+
--tty \
41+
--volume $PWD/../..:/client \
42+
--workdir /client/Examples/RecordRow \
43+
swift:5.7 /bin/bash
44+
```
45+
4. Execute by:
46+
```bash
47+
swift run record-row --org my-org --bucket my-bucket --token my-token --url http://influxdb_v2:8086
48+
```
49+
50+
## Expected output
51+
52+
```bash
53+
The response contains columns with duplicated names: result, table
54+
You should use the 'FluxRecord.row' to access your data instead of 'FluxRecord.values' dictionary.
55+
------------------------------------------ FluxRecord.values ------------------------------------------
56+
_measurement: point, _start: 2022-10-13 07:56:08 +0000, _stop: 2022-10-13 07:57:08 +0000, _time: 2022-10-13 07:57:08 +0000, result: 1.0, table: my-table
57+
_measurement: point, _start: 2022-10-13 07:56:08 +0000, _stop: 2022-10-13 07:57:08 +0000, _time: 2022-10-13 07:57:08 +0000, result: 2.0, table: my-table
58+
_measurement: point, _start: 2022-10-13 07:56:08 +0000, _stop: 2022-10-13 07:57:08 +0000, _time: 2022-10-13 07:57:08 +0000, result: 3.0, table: my-table
59+
_measurement: point, _start: 2022-10-13 07:56:08 +0000, _stop: 2022-10-13 07:57:08 +0000, _time: 2022-10-13 07:57:08 +0000, result: 4.0, table: my-table
60+
_measurement: point, _start: 2022-10-13 07:56:08 +0000, _stop: 2022-10-13 07:57:08 +0000, _time: 2022-10-13 07:57:08 +0000, result: 5.0, table: my-table
61+
-------------------------------------------- FluxRecord.row -------------------------------------------
62+
_result, 0, 2022-10-13 07:56:08 +0000, 2022-10-13 07:57:08 +0000, 2022-10-13 07:57:08 +0000, point, 1.0, my-table
63+
_result, 0, 2022-10-13 07:56:08 +0000, 2022-10-13 07:57:08 +0000, 2022-10-13 07:57:08 +0000, point, 2.0, my-table
64+
_result, 0, 2022-10-13 07:56:08 +0000, 2022-10-13 07:57:08 +0000, 2022-10-13 07:57:08 +0000, point, 3.0, my-table
65+
_result, 0, 2022-10-13 07:56:08 +0000, 2022-10-13 07:57:08 +0000, 2022-10-13 07:57:08 +0000, point, 4.0, my-table
66+
_result, 0, 2022-10-13 07:56:08 +0000, 2022-10-13 07:57:08 +0000, 2022-10-13 07:57:08 +0000, point, 5.0, my-table
67+
68+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import ArgumentParser
2+
import Foundation
3+
import InfluxDBSwift
4+
import InfluxDBSwiftApis
5+
6+
@main
7+
struct RecordRow: AsyncParsableCommand {
8+
@Option(name: .shortAndLong, help: "The name or id of the bucket destination.")
9+
private var bucket: String
10+
11+
@Option(name: .shortAndLong, help: "The name or id of the organization destination.")
12+
private var org: String
13+
14+
@Option(name: .shortAndLong, help: "Authentication token.")
15+
private var token: String
16+
17+
@Option(name: .shortAndLong, help: "HTTP address of InfluxDB.")
18+
private var url: String
19+
}
20+
21+
extension RecordRow {
22+
mutating func run() async throws {
23+
//
24+
// Creating client
25+
//
26+
let client = InfluxDBClient(
27+
url: url,
28+
token: token,
29+
options: InfluxDBClient.InfluxDBOptions(bucket: bucket, org: org))
30+
31+
//
32+
// Write test data into InfluxDB
33+
//
34+
for i in 1...5 {
35+
let point = InfluxDBClient
36+
.Point("point")
37+
.addField(key: "table", value: .string("my-table"))
38+
.addField(key: "result", value: .double(Double(i)))
39+
try await client.makeWriteAPI().write(point: point)
40+
}
41+
42+
//
43+
// Query data with pivot
44+
//
45+
let query = """
46+
from(bucket: "\(self.bucket)")
47+
|> range(start: -1m)
48+
|> filter(fn: (r) => (r["_measurement"] == "point"))
49+
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
50+
"""
51+
52+
let records = try await client.queryAPI.query(query: query)
53+
54+
//
55+
// Write data to output
56+
//
57+
var values: Array<String> = Array()
58+
var row: Array<String> = Array()
59+
60+
try records.forEach { record in
61+
values.append(record.values.sorted(by: { $0.0 < $1.0 }).map {
62+
val in
63+
"\(val.key): \(val.value)"
64+
}
65+
.joined(separator: ", "))
66+
row.append(record.row.compactMap { val in
67+
"\(val)"
68+
}
69+
.joined(separator: ", "))
70+
}
71+
72+
print("------------------------------------------ FluxRecord.values ------------------------------------------")
73+
print(values.joined(separator: "\n"))
74+
print("-------------------------------------------- FluxRecord.row -------------------------------------------")
75+
print(row.joined(separator: "\n"))
76+
77+
client.close()
78+
}
79+
}

Sources/InfluxDBSwift/FluxCSVParser.swift

+17-5
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,12 @@ internal class FluxCSVParser {
6262
default:
6363
if startNewTable {
6464
if responseMode == .onlyNames && table?.columns.isEmpty ?? true {
65-
groups = row.map { _ in "false" }
66-
try addDatatype(row: row.map { _ in "string" })
65+
groups = row.map { _ in
66+
"false"
67+
}
68+
try addDatatype(row: row.map { _ in
69+
"string"
70+
})
6771
}
6872
try addGroups()
6973
try addNames(row: row)
@@ -123,13 +127,21 @@ internal class FluxCSVParser {
123127
let column: QueryAPI.FluxColumn = table.columns[$0.offset]
124128
column.name = $0.element
125129
}
130+
131+
let duplicates = Dictionary(grouping: table.columns.compactMap { $0.name }) { $0 }.filter { $1.count > 1 }.keys
132+
if !duplicates.isEmpty {
133+
print("""
134+
The response contains columns with duplicated names: \(duplicates.joined(separator: ", "))
135+
You should use the 'FluxRecord.row' to access your data instead of 'FluxRecord.values' dictionary.
136+
""")
137+
}
126138
}
127139

128140
private func parseRow(row: [String]) throws -> QueryAPI.FluxRecord {
129141
guard let table = table else {
130142
throw InfluxDBClient.InfluxDBError.generic(Self.errorMessage)
131143
}
132-
144+
var recordRow: [Any] = Array()
133145
let values: [String: Decodable] = table.columns.reduce(into: [String: Decodable]()) { result, column in
134146
var value: String = row[column.index + 1]
135147
if value.isEmpty {
@@ -155,9 +167,9 @@ internal class FluxCSVParser {
155167
default:
156168
result[column.name] = value
157169
}
170+
recordRow.append(result[column.name] ?? "" as Any)
158171
}
159-
160-
return QueryAPI.FluxRecord(values: values)
172+
return QueryAPI.FluxRecord(values: values, row: recordRow)
161173
}
162174
}
163175

Sources/InfluxDBSwift/QueryAPI.swift

+11-4
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public class QueryAPI {
131131
org: String? = nil,
132132
params: [String: String]? = nil,
133133
responseQueue: DispatchQueue = .main)
134-
-> AnyPublisher<FluxRecordCursor, InfluxDBClient.InfluxDBError> {
134+
-> AnyPublisher<FluxRecordCursor, InfluxDBClient.InfluxDBError> {
135135
Future<FluxRecordCursor, InfluxDBClient.InfluxDBError> { promise in
136136
self.query(query: query, org: org, params: params, responseQueue: responseQueue) { result -> Void in
137137
switch result {
@@ -141,7 +141,8 @@ public class QueryAPI {
141141
promise(.failure(error))
142142
}
143143
}
144-
}.eraseToAnyPublisher()
144+
}
145+
.eraseToAnyPublisher()
145146
}
146147
#endif
147148

@@ -262,7 +263,8 @@ public class QueryAPI {
262263
promise(.failure(error))
263264
}
264265
}
265-
}.eraseToAnyPublisher()
266+
}
267+
.eraseToAnyPublisher()
266268
}
267269
#endif
268270

@@ -330,11 +332,16 @@ extension QueryAPI {
330332
/// The list of values in Record
331333
public let values: [String: Decodable]
332334

335+
/// The array of record's columns
336+
public let row: [Any]
337+
333338
/// Initialize records with values.
334339
///
335340
/// - Parameter values: record values
336-
public init(values: [String: Decodable]) {
341+
/// row: record's columns
342+
public init(values: [String: Decodable], row: [Any]) {
337343
self.values = values
344+
self.row = row
338345
}
339346

340347
public static func == (lhs: FluxRecord, rhs: FluxRecord) -> Bool {

Tests/InfluxDBSwiftTests/FluxCSVParserTests.swift

+21-2
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,9 @@ final class FluxCSVParserTests: XCTestCase {
237237

238238
XCTAssertEqual(10, tuples[0].table.columns.count)
239239
XCTAssertEqual(2, tuples[0].table.columns.filter {
240-
$0.group
241-
}.count)
240+
$0.group
241+
}
242+
.count)
242243
}
243244

244245
func testUnknownTypeAsString() throws {
@@ -473,6 +474,24 @@ final class FluxCSVParserTests: XCTestCase {
473474
XCTAssertEqual("west", records[0].values["region"] as? String)
474475
}
475476

477+
func testParseDuplicateColumnNames() throws {
478+
let data = """
479+
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,string,double,string
480+
#group,false,false,true,true,false,true,false,false
481+
#default,_result,,,,,,,
482+
,result,table,_start,_stop,_time,_measurement,result,table
483+
,,0,2022-10-12T08:36:55.358106426Z,2022-10-12T08:37:55.358106426Z,2022-10-12T08:37:55.316995385Z,point,25.3,my-table
484+
,,0,2022-10-12T08:36:55.358106426Z,2022-10-12T08:37:55.358106426Z,2022-10-12T08:37:55.329323051Z,point,25.3,my-table
485+
,,0,2022-10-12T08:36:55.358106426Z,2022-10-12T08:37:55.358106426Z,2022-10-12T08:37:55.336790801Z,point,25.3,my-table
486+
487+
"""
488+
let records = try parse_to_records(data: data, responseMode: .full)
489+
XCTAssertEqual(3, records.count)
490+
XCTAssertEqual(6, records[0].values.count)
491+
XCTAssertEqual(8, records[0].row.count)
492+
XCTAssertEqual(25.3, records[0].row[6] as? Double)
493+
}
494+
476495
// swiftlint:enable line_length trailing_whitespace
477496

478497
func parse_to_records(data: String, responseMode: FluxCSVParser.ResponseMode = .full)

0 commit comments

Comments
 (0)