-
Notifications
You must be signed in to change notification settings - Fork 23
Kafka.DotNet.ksqlDB push queries LINQ provider
This package generates ksql queries from your .NET C# linq queries. You can filter, project, limit etc. your push notifications server side with ksqlDB push queries
Install-Package Kafka.DotNet.ksqlDB
using System;
using Kafka.DotNet.ksqlDB.KSql.Linq;
using Kafka.DotNet.ksqlDB.KSql.Query.Context;
using Kafka.DotNet.ksqlDB.Sample.Model;
var ksqlDbUrl = @"http:\\localhost:8088";
await using var context = new KSqlDBContext(ksqlDbUrl);
using var disposable = context.CreateQueryStream<Tweet>()
.Where(p => p.Message != "Hello world" || p.Id == 1)
.Select(l => new { l.Message, l.Id })
.Take(2)
.Subscribe(tweetMessage =>
{
Console.WriteLine($"{nameof(Tweet)}: {tweetMessage.Id} - {tweetMessage.Message}");
}, error => { Console.WriteLine($"Exception: {error.Message}"); }, () => Console.WriteLine("Completed"));
Console.WriteLine("Press any key to stop the subscription");
Console.ReadKey();
LINQ code written in C# from the sample is equivalent to this ksql query:
SELECT Message, Id FROM Tweets
WHERE Message != 'Hello world' OR Id = 1
EMIT CHANGES
LIMIT 2;
In the above mentioned code snippet everything runs server side except of the IQbservable<TEntity>.Subscribe
method. It subscribes to your ksqlDB stream created in the following manner:
EntityCreationMetadata metadata = new()
{
KafkaTopic = nameof(Tweet),
Partitions = 1,
Replicas = 1
};
var httpClientFactory = new HttpClientFactory(new Uri(@"http:\\localhost:8088"));
var restApiClient = new KSqlDbRestApiClient(httpClientFactory);
var httpResponseMessage = await restApiClient.CreateOrReplaceStreamAsync<Tweet>(metadata);
CreateOrReplaceStreamAsync executes the following statement:
CREATE OR REPLACE STREAM Tweets (
Id INT,
Message VARCHAR
) WITH ( KAFKA_TOPIC='Tweet', VALUE_FORMAT='Json', PARTITIONS='1', REPLICAS='1' );
Run the following insert statements to stream some messages with your ksqldb-cli
docker exec -it $(docker ps -q -f name=ksqldb-cli) ksql http://localhost:8088
INSERT INTO tweets (id, message) VALUES (1, 'Hello world');
INSERT INTO tweets (id, message) VALUES (2, 'ksqlDB rulez!');
or insert a record from C#:
var ksqlDbUrl = @"http:\\localhost:8088";
var httpClientFactory = new HttpClientFactory(new Uri(ksqlDbUrl));
var responseMessage = await new KSqlDbRestApiClient(httpClientFactory)
.InsertIntoAsync(new Tweet { Id = 2, Message = "ksqlDB rulez!" });
Sample project can be found under Samples solution folder in Kafka.DotNet.ksqlDb.sln
External dependencies:
- kafka broker and ksqlDB-server 0.14.0
Clone the repository
git clone https://github.com/tomasfabian/Kafka.DotNet.ksqlDB.git
CD to Samples
CD Samples\Kafka.DotNet.ksqlDB.Sample\
run in command line:
docker compose up -d
Default settings: 'auto.offset.reset' is set to 'earliest' by default. New parameters could be added or existing ones changed in the following manner:
var contextOptions = new KSqlDBContextOptions(@"http:\\localhost:8088");
contextOptions.QueryStreamParameters["auto.offset.reset"] = "latest";
Record class is a base class for rows returned in push queries. It has a 'RowTime' property.
public class Tweet : Kafka.DotNet.ksqlDB.KSql.Query.Record
{
public string Message { get; set; }
}
context.CreateQueryStream<Tweet>()
.Select(c => new { c.RowTime, c.Message });
Stream names are generated based on the generic record types. They are pluralized with Pluralize.NET package
context.CreateQueryStream<Person>();
FROM People
This can be disabled:
var contextOptions = new KSqlDBContextOptions(@"http:\\localhost:8088")
{
ShouldPluralizeStreamName = false
};
new KSqlDBContext(contextOptions).CreateQueryStream<Person>();
FROM Person
In v1.0 was ShouldPluralizeStreamName renamed to ShouldPluralizeFromItemName
var contextOptions = new KSqlDBContextOptions(@"http:\\localhost:8088")
{
ShouldPluralizeFromItemName = false
};
Setting an arbitrary stream name (from_item name):
context.CreateQueryStream<Tweet>("custom_topic_name");
FROM custom_topic_name
Projects each element of a stream into a new form.
context.CreateQueryStream<Tweet>()
.Select(l => new { l.RowTime, l.Message });
Omitting select is equivalent to SELECT *
ksql | c# |
---|---|
VARCHAR | string |
INTEGER | int |
BIGINT | long |
DOUBLE | double |
BOOLEAN | bool |
ARRAY<ElementType> |
C#Type[] |
MAP<KeyType, ValueType> |
IDictionary<C#Type, C#Type> |
STRUCT |
struct |
Array type mapping example (available from v0.3.0): All of the elements in the array must be of the same type. The element type can be any valid SQL type.
ksql: ARRAY<INTEGER>
C# : int[]
Destructuring an array (ksqldb represents the first element of an array as 1):
queryStream
.Select(_ => new { FirstItem = new[] {1, 2, 3}[1] })
Generates the following KSQL:
ARRAY[1, 2, 3][1] AS FirstItem
Array length:
queryStream
.Select(_ => new[] {1, 2, 3}.Length)
Generates the following KSQL:
ARRAY_LENGTH(ARRAY[1, 2, 3])
Struct type mapping example (available from v0.5.0): A struct represents strongly typed structured data. A struct is an ordered collection of named fields that have a specific type. The field types can be any valid SQL type.
struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
queryStream
.Select(c => new Point { X = c.X, Y = 2 });
Generates the following KSQL:
SELECT STRUCT(X := X, Y := 2) FROM StreamName EMIT CHANGES;
Destructure a struct:
queryStream
.Select(c => new Point { X = c.X, Y = 2 }.X);
SELECT STRUCT(X := X, Y := 2)->X FROM StreamName EMIT CHANGES;
Filters a stream of values based on a predicate.
context.CreateQueryStream<Tweet>()
.Where(p => p.Message != "Hello world" || p.Id == 1)
.Where(p => p.RowTime >= 1510923225000);
Multiple Where statements are joined with AND operator.
SELECT * FROM Tweets
WHERE Message != 'Hello world' OR Id = 1 AND RowTime >= 1510923225000
EMIT CHANGES;
Supported operators are:
ksql | meaning | c# |
---|---|---|
= | is equal to | == |
!= or <> | is not equal to | != |
< | is less than | < |
<= | is less than or equal to | <= |
> | is greater than | > |
>= | is greater than or equal to | >= |
AND | logical AND | && |
OR | logical OR | || |
NOT | logical NOT | ! |
Returns a specified number of contiguous elements from the start of a stream. Depends on the 'auto.topic.offset' parameter.
context.CreateQueryStream<Tweet>()
.Take(2);
SELECT * from tweets EMIT CHANGES LIMIT 2;
Providing IObserver<T>
:
using var subscription = new KSqlDBContext(@"http:\\localhost:8088")
.CreateQueryStream<Tweet>()
.Subscribe(new TweetsObserver());
public class TweetsObserver : System.IObserver<Tweet>
{
public void OnNext(Tweet tweetMessage)
{
Console.WriteLine($"{nameof(Tweet)}: {tweetMessage.Id} - {tweetMessage.Message}");
}
public void OnError(Exception error)
{
Console.WriteLine($"{nameof(Tweet)}: {error.Message}");
}
public void OnCompleted()
{
Console.WriteLine($"{nameof(Tweet)}: completed successfully");
}
}
Providing Action<T> onNext, Action<Exception> onError and Action onCompleted
:
using var subscription = new KSqlDBContext(@"http:\\localhost:8088")
.CreateQueryStream<Tweet>()
.Subscribe(
onNext: tweetMessage =>
{
Console.WriteLine($"{nameof(Tweet)}: {tweetMessage.Id} - {tweetMessage.Message}");
},
onError: error => { Console.WriteLine($"Exception: {error.Message}"); },
onCompleted: () => Console.WriteLine("Completed")
);
ToObservable moving to Rx.NET
The following code snippet shows how to observe messages on the desired IScheduler:
using var disposable = context.CreateQueryStream<Tweet>()
.Take(2)
.ToObservable() //client side processing starts here lazily after subscription
.ObserveOn(System.Reactive.Concurrency.DispatcherScheduler.Current)
.Subscribe(new TweetsObserver());
Be cautious regarding to server side and client side processings:
KSql.Linq.IQbservable<Tweet> queryStream = context.CreateQueryStream<Tweet>().Take(2);
System.IObservable<Tweet> observable = queryStream.ToObservable();
//All reactive extension methods are available from this point.
//The not obvious difference is that the processing is done client side, not server side (ksqldb) as in the case of IQbservable:
observable.Throttle(TimeSpan.FromSeconds(3)).Subscribe();
WPF client side batching example:
private static IDisposable ClientSideBatching(KSqlDBContext context)
{
var disposable = context.CreateQueryStream<Tweet>()
//Client side execution
.ToObservable()
.Buffer(TimeSpan.FromMilliseconds(250), 100)
.Where(c => c.Count > 0)
.ObserveOn(System.Reactive.Concurrency.DispatcherScheduler.Current)
.Subscribe(tweets =>
{
foreach (var tweet in tweets)
{
Console.WriteLine(tweet.Message);
}
});
return disposable;
}
ToQueryString is helpful for debugging purposes. It returns the generated ksql query without executing it.
var ksql = context.CreateQueryStream<Tweet>().ToQueryString();
//prints SELECT * FROM tweets EMIT CHANGES;
Console.WriteLine(ksql);
Count the number of rows. When * is specified, the count returned will be the total number of rows.
var ksqlDbUrl = @"http:\\localhost:8088";
var contextOptions = new KSqlDBContextOptions(ksqlDbUrl);
var context = new KSqlDBContext(contextOptions);
context.CreateQueryStream<Tweet>()
.GroupBy(c => c.Id)
.Select(g => new { Id = g.Key, Count = g.Count() })
.Subscribe(count =>
{
Console.WriteLine($"{count.Id} Count: {count.Count}");
Console.WriteLine();
}, error => { Console.WriteLine($"Exception: {error.Message}"); }, () => Console.WriteLine("Completed"));
SELECT Id, COUNT(*) Count FROM Tweets GROUP BY Id EMIT CHANGES;
⚠ There is a known limitation in the early access versions (bellow 1.0). The aggregation functions have to be named/aliased COUNT(*) Count, otherwise the deserialization won't be able to map the unknown column name KSQL_COL_0. The Key should be mapped back to the respective column too Id = g.Key
Or without the new expression:
context.CreateQueryStream<Tweet>()
.GroupBy(c => c.Id)
.Select(g => g.Count());
SELECT COUNT(*) FROM Tweets GROUP BY Id EMIT CHANGES;
context.CreateQueryStream<Tweet>()
.GroupBy(c => c.Id)
//.Select(g => g.Sum(c => c.Amount))
.Select(g => new { Id = g.Key, Agg = g.Sum(c => c.Amount)})
Equivalent to KSql:
SELECT Id, SUM(Amount) Agg FROM Tweets GROUP BY Id EMIT CHANGES;
Creates an async iterator from the query:
var cts = new CancellationTokenSource();
var asyncTweetsEnumerable = context.CreateQueryStream<Tweet>().ToAsyncEnumerable();
await foreach (var tweet in asyncTweetsEnumerable.WithCancellation(cts.Token))
Console.WriteLine(tweet.Message);
Creation of windowed aggregation
var context = new TransactionsDbProvider(ksqlDbUrl);
var windowedQuery = context.CreateQueryStream<Transaction>()
.WindowedBy(new TimeWindows(Duration.OfSeconds(5)).WithGracePeriod(Duration.OfHours(2)))
.GroupBy(c => c.CardNumber)
.Select(g => new { CardNumber = g.Key, Count = g.Count() });
SELECT CardNumber, COUNT(*) Count FROM Transactions
WINDOW TUMBLING (SIZE 5 SECONDS, GRACE PERIOD 2 HOURS)
GROUP BY CardNumber EMIT CHANGES;
var subscription = context.CreateQueryStream<Tweet>()
.GroupBy(c => c.Id)
.WindowedBy(new HoppingWindows(Duration.OfSeconds(5)).WithAdvanceBy(Duration.OfSeconds(4)).WithRetention(Duration.OfDays(7)))
.Select(g => new { g.WindowStart, g.WindowEnd, Id = g.Key, Count = g.Count() })
.Subscribe(c => { Console.WriteLine($"{c.Id}: {c.Count}: {c.WindowStart}: {c.WindowEnd}"); }, exception => {});
SELECT WindowStart, WindowEnd, Id, COUNT(*) Count FROM Tweets
WINDOW HOPPING (SIZE 5 SECONDS, ADVANCE BY 10 SECONDS, RETENTION 7 DAYS)
GROUP BY Id EMIT CHANGES;
Window advancement interval should be more than zero and less than window duration
l => l.Message.ToLower() != "hi";
l => l.Message.ToUpper() != "HI";
LCASE(Latitude) != 'hi'
UCASE(Latitude) != 'HI'
Install-Package Kafka.DotNet.ksqlDB -Version 0.2.0
var query = context.CreateQueryStream<Tweet>()
.GroupBy(c => c.Id)
.Having(c => c.Count() > 2)
.Select(g => new { Id = g.Key, Count = g.Count()});
KSQL:
SELECT Id, COUNT(*) Count FROM Tweets GROUP BY Id HAVING Count(*) > 2 EMIT CHANGES;
A session window aggregates records into a session, which represents a period of activity separated by a specified gap of inactivity, or "idleness".
var query = context.CreateQueryStream<Transaction>()
.GroupBy(c => c.CardNumber)
.WindowedBy(new SessionWindow(Duration.OfSeconds(5)))
.Select(g => new { CardNumber = g.Key, Count = g.Count() });
KSQL:
SELECT CardNumber, COUNT(*) Count FROM Transactions
WINDOW SESSION (5 SECONDS)
GROUP BY CardNumber
EMIT CHANGES;
Time units:
using Kafka.DotNet.ksqlDB.KSql.Query.Windows;
public enum TimeUnits
{
MILLISECONDS, // v2.0.0
SECONDS,
MINUTES,
HOURS,
DAYS
}
Duration duration = Duration.OfHours(2);
Console.WriteLine($"{duration.Value} {duration.TimeUnit}");
How to join table and table
public class Movie : Record
{
public string Title { get; set; }
public int Id { get; set; }
public int Release_Year { get; set; }
}
public class Lead_Actor : Record
{
public string Title { get; set; }
public string Actor_Name { get; set; }
}
using Kafka.DotNet.ksqlDB.KSql.Linq;
var query = context.CreateQueryStream<Movie>()
.Join(
Source.Of<Lead_Actor>(nameof(Lead_Actor)),
movie => movie.Title,
actor => actor.Title,
(movie, actor) => new
{
movie.Id,
Title = movie.Title,
movie.Release_Year,
ActorName = K.Functions.RPad(K.Functions.LPad(actor.Actor_Name.ToUpper(), 15, "*"), 25, "^"),
ActorTitle = actor.Title
}
);
var joinQueryString = query.ToQueryString();
KSQL:
SELECT M.Id Id, M.Title Title, M.Release_Year Release_Year, RPAD(LPAD(UCASE(L.Actor_Name), 15, '*'), 25, '^') ActorName, L.Title ActorTitle
FROM Movies M
INNER JOIN Lead_Actor L
ON M.Title = L.Title
EMIT CHANGES;
⚠ There is a known limitation in the early access versions (bellow 1.0). The Key column, in this case movie.Title, has to be aliased Title = movie.Title, otherwise the deserialization won't be able to map the unknown column name M_TITLE.
AVG(col1)
Return the average value for a given column.
var query = CreateQbservable()
.GroupBy(c => c.RegionCode)
.Select(g => g.Avg(c => c.Citizens));
MIN(col1)
MAX(col1)
Return the minimum/maximum value for a given column and window. Rows that have col1 set to null are ignored.
var queryMin = CreateQbservable()
.GroupBy(c => c.RegionCode)
.Select(g => g.Min(c => c.Citizens));
var queryMax = CreateQbservable()
.GroupBy(c => c.RegionCode)
.Select(g => g.Max(c => c.Citizens));
using Kafka.DotNet.ksqlDB.KSql.Query.Functions;
Expression<Func<Tweet, bool>> likeExpression = c => KSql.Functions.Like(c.Message, "%santa%");
Expression<Func<Tweet, bool>> likeLCaseExpression = c => KSql.Functions.Like(c.Message.ToLower(), "%santa%".ToLower());
KSQL
"LCASE(Message) LIKE LCASE('%santa%')"
The usual arithmetic operators (+,-,/,*,%) may be applied to numeric types, like INT, BIGINT, and DOUBLE:
SELECT USERID, LEN(FIRST_NAME) + LEN(LAST_NAME) AS NAME_LENGTH FROM USERS EMIT CHANGES;
Expression<Func<Person, object>> expression = c => c.FirstName.Length * c.LastName.Length;
Expression<Func<Tweet, int>> lengthExpression = c => c.Message.Length;
KSQL
LEN(Message)
using Kafka.DotNet.ksqlDB.KSql.Query.Functions;
Expression<Func<Tweet, string>> expression1 = c => KSql.Functions.LPad(c.Message, 8, "x");
Expression<Func<Tweet, string>> expression2 = c => KSql.Functions.RPad(c.Message, 8, "x");
Expression<Func<Tweet, string>> expression3 = c => KSql.Functions.Trim(c.Message);
Expression<Func<Tweet, string>> expression4 = c => K.Functions.Substring(c.Message, 2, 3);
KSQL
LPAD(Message, 8, 'x')
RPAD(Message, 8, 'x')
TRIM(Message)
Substring(Message, 2, 3)
Install-Package Kafka.DotNet.ksqlDB -Version 0.3.0
EarliestByOffset, LatestByOffset
Expression<Func<IKSqlGrouping<int, Transaction>, object>> expression1 = l => new { EarliestByOffset = l.EarliestByOffset(c => c.Amount) };
Expression<Func<IKSqlGrouping<int, Transaction>, object>> expression2 = l => new { LatestByOffsetAllowNulls = l.LatestByOffsetAllowNulls(c => c.Amount) };
KSQL
--EARLIEST_BY_OFFSET(col1, [ignoreNulls])
EARLIEST_BY_OFFSET(Amount, True) EarliestByOffset
LATEST_BY_OFFSET(Amount, False) LatestByOffsetAllowNulls
EARLIEST_BY_OFFSET(col1, earliestN, [ignoreNulls])
Return the earliest N values for the specified column as an ARRAY. The earliest values in the partition have the lowest offsets.
await using var context = new KSqlDBContext(@"http:\\localhost:8088");
context.CreateQueryStream<Tweet>()
.GroupBy(c => c.Id)
.Select(g => new { Id = g.Key, EarliestByOffset = g.EarliestByOffset(c => c.Amount, 2) })
.Subscribe(earliest =>
{
Console.WriteLine($"{earliest.Id} array length: {earliest.EarliestByOffset.Length}");
}, error => { Console.WriteLine($"Exception: {error.Message}"); }, () => Console.WriteLine("Completed"));
Generated KSQL:
SELECT Id, EARLIEST_BY_OFFSET(Amount, 2, True) EarliestByOffset
FROM Tweets GROUP BY Id EMIT CHANGES;
Expression<Func<IKSqlGrouping<int, Transaction>, object>> expression1 = l => new { TopK = l.TopK(c => c.Amount, 2) };
Expression<Func<IKSqlGrouping<int, Transaction>, object>> expression2 = l => new { TopKDistinct = l.TopKDistinct(c => c.Amount, 2) };
Expression<Func<IKSqlGrouping<int, Transaction>, object>> expression3 = l => new { Count = l.LongCount(c => c.Amount) };
KSQL
TOPK(Amount, 2) TopKDistinct
TOPKDISTINCT(Amount, 2) TopKDistinct
COUNT(Amount) Count
new KSqlDBContext(@"http:\\localhost:8088").CreateQueryStream<Tweet>()
.GroupBy(c => c.Id)
.Select(g => new { Id = g.Key, TopK = g.TopKDistinct(c => c.Amount, 4) })
.Subscribe(onNext: tweetMessage =>
{
var tops = string.Join(',', tweetMessage.TopK);
Console.WriteLine($"TopKs: {tops}");
Console.WriteLine($"TopKs Array Length: {tops.Length}");
}, onError: error => { Console.WriteLine($"Exception: {error.Message}"); }, onCompleted: () => Console.WriteLine("Completed"));
LEFT OUTER joins will contain leftRecord-NULL records in the result stream, which means that the join contains NULL values for fields selected from the right-hand stream where no match is made.
var query = new KSqlDBContext(@"http:\\localhost:8088").CreateQueryStream<Movie>()
.LeftJoin(
Source.Of<Lead_Actor>(),
movie => movie.Title,
actor => actor.Title,
(movie, actor) => new
{
movie.Id,
ActorTitle = actor.Title
}
);
Generated KSQL:
SELECT M.Id Id, L.Title ActorTitle FROM Movies M
LEFT JOIN Lead_Actors L
ON M.Title = L.Title
EMIT CHANGES;
Example shows how to use Having with Count(column) and Group By compound key:
public class Click
{
public string IP_ADDRESS { get; set; }
public string URL { get; set; }
public string TIMESTAMP { get; set; }
}
var query = context.CreateQueryStream<Click>()
.GroupBy(c => new { c.IP_ADDRESS, c.URL, c.TIMESTAMP })
.WindowedBy(new TimeWindows(Duration.OfMinutes(2)))
.Having(c => c.Count(g => c.Key.IP_ADDRESS) == 1)
.Select(g => new { g.Key.IP_ADDRESS, g.Key.URL, g.Key.TIMESTAMP })
.Take(3);
Generated KSQL:
SELECT IP_ADDRESS, URL, TIMESTAMP FROM Clicks WINDOW TUMBLING (SIZE 2 MINUTES) GROUP BY IP_ADDRESS, URL, TIMESTAMP
HAVING COUNT(IP_ADDRESS) = 1 EMIT CHANGES LIMIT 3;
using var subscription = new KSqlDBContext(@"http:\\localhost:8088")
.CreateQueryStream<Click>()
.Where(c => c.IP_ADDRESS != null || c.IP_ADDRESS == null)
.Select(c => new { c.IP_ADDRESS, c.URL, c.TIMESTAMP });
Generated KSQL:
SELECT IP_ADDRESS, URL, TIMESTAMP
FROM Clicks
WHERE IP_ADDRESS IS NOT NULL OR IP_ADDRESS IS NULL
EMIT CHANGES;
Expression<Func<Tweet, double>> expression1 = c => K.Functions.Abs(c.Amount);
Expression<Func<Tweet, double>> expression2 = c => K.Functions.Ceil(c.Amount);
Expression<Func<Tweet, double>> expression3 = c => K.Functions.Floor(c.Amount);
Expression<Func<Tweet, double>> expression4 = c => K.Functions.Random();
Expression<Func<Tweet, double>> expression5 = c => K.Functions.Sign(c.Amount);
int scale = 3;
Expression<Func<Tweet, double>> expression6 = c => K.Functions.Round(c.Amount, scale);
Generated KSQL:
ABS(Amount)
CEIL(AccountBalance)
FLOOR(AccountBalance)
RANDOM()
SIGN(Amount)
ROUND(Amount, 3)
Some of the ksqldb functions have not been implemented yet. This can be circumvented by calling K.Functions.Dynamic with the appropriate function call and its paramaters. The type of the column value is set with C# as operator.
using Kafka.DotNet.ksqlDB.KSql.Query.Functions;
context.CreateQueryStream<Tweet>()
.Select(c => new { Col = KSql.Functions.Dynamic("IFNULL(Message, 'n/a')") as string, c.Id, c.Amount, c.Message });
The interesting part from the above query is:
K.Functions.Dynamic("IFNULL(Message, 'n/a')") as string
Generated KSQL:
SELECT IFNULL(Message, 'n/a') Col, Id, Amount, Message FROM Tweets EMIT CHANGES;
Result:
+----------------------------+----------------------------+----------------------------+----------------------------+
|COL |ID |AMOUNT |MESSAGE |
+----------------------------+----------------------------+----------------------------+----------------------------+
|Hello world |1 |0.0031 |Hello world |
|n/a |1 |0.1 |null |
Dynamic function call with array result example:
using K = Kafka.DotNet.ksqlDB.KSql.Query.Functions.KSql;
context.CreateQueryStream<Tweet>()
.Select(c => K.F.Dynamic("ARRAY_DISTINCT(ARRAY[1, 1, 2, 3, 1, 2])") as int[])
.Subscribe(
message => Console.WriteLine($"{message[0]} - {message[^1]}"),
error => Console.WriteLine($"Exception: {error.Message}"));
var subscription = context.CreateQueryStream<Tweet>()
.GroupBy(c => c.Id)
.Select(g => new { Id = g.Key, Array = g.CollectSet(c => c.Message) })
//.Select(g => new { Id = g.Key, Array = g.CollectList(c => c.Message) })
.Subscribe(c =>
{
Console.WriteLine($"{c.Id}:");
foreach (var value in c.Array)
{
Console.WriteLine($" {value}");
}
}, exception => { Console.WriteLine(exception.Message); });
Generated KSQL:
SELECT Id, COLLECT_SET(Message) Array
FROM Tweets GROUP BY Id EMIT CHANGES;
SELECT Id, COLLECT_LIST(Message) Array
FROM Tweets GROUP BY Id EMIT CHANGES;
CountDistinct, LongCountDistinct
var subscription = context.CreateQueryStream<Tweet>()
.GroupBy(c => c.Id)
// .Select(g => new { Id = g.Key, Count = g.CountDistinct(c => c.Message) })
.Select(g => new { Id = g.Key, Count = g.LongCountDistinct(c => c.Message) })
.Subscribe(c =>
{
Console.WriteLine($"{c.Id} - {c.Count}");
}, exception => { Console.WriteLine(exception.Message); });
Generated KSQL:
SELECT Id, COUNT_DISTINCT(Message) Count
FROM Tweets GROUP BY Id EMIT CHANGES;
Install-Package Kafka.DotNet.ksqlDB -Version 0.4.0
Maps are an associative data type that map keys of any type to values of any type. The types across all keys must be the same. The same rule holds for values. Destructure maps using bracket syntax ([]).
var dictionary = new Dictionary<string, int>()
{
{ "c", 2 },
{ "d", 4 }
};
MAP('c' := 2, 'd' := 4)
Accessing map elements:
dictionary["c"]
MAP('c' := 2, 'd' := 4)['d']
Deeply nested types:
context.CreateQueryStream<Tweet>()
.Select(c => new
{
Map = new Dictionary<string, int[]>
{
{ "a", new[] { 1, 2 } },
{ "b", new[] { 3, 4 } },
}
});
Generated KSQL:
SELECT MAP('a' := ARRAY[1, 2], 'b' := ARRAY[3, 4]) Map
FROM Tweets EMIT CHANGES;
int epochDays = 18672;
string format = "yyyy-MM-dd";
Expression<Func<Tweet, string>> expression = _ => KSqlFunctions.Instance.DateToString(epochDays, format);
Generated KSQL:
DATETOSTRING(18672, 'yyyy-MM-dd')
new KSqlDBContext(ksqlDbUrl).CreateQueryStream<Movie>()
.Select(c => K.Functions.TimestampToString(c.RowTime, "yyyy-MM-dd''T''HH:mm:ssX"))
Generated KSQL:
SELECT DATETOSTRING(1613503749145, 'yyyy-MM-dd''T''HH:mm:ssX')
FROM tweets EMIT CHANGES;
Install-Package Kafka.DotNet.ksqlDB -Version 0.5.0
Structs are an associative data type that map VARCHAR keys to values of any type. Destructure structs by using arrow syntax (->).
bool sorted = true;
var subscription = new KSqlDBContext(@"http:\\localhost:8088")
.CreateQueryStream<Movie>()
.Select(c => new
{
Entries = KSqlFunctions.Instance.Entries(new Dictionary<string, string>()
{
{"a", "value"}
}, sorted)
})
.Subscribe(c =>
{
foreach (var entry in c.Entries)
{
var key = entry.K;
var value = entry.V;
}
}, error => {});
Generated KSQL:
SELECT ENTRIES(MAP('a' := 'value'), True) Entries
FROM movies_test EMIT CHANGES;
FULL OUTER joins will contain leftRecord-NULL or NULL-rightRecord records in the result stream, which means that the join contains NULL values for fields coming from a stream where no match is made. Define nullable primitive value types in POCOs:
public record Movie
{
public long RowTime { get; set; }
public string Title { get; set; }
public int? Id { get; set; }
public int? Release_Year { get; set; }
}
public class Lead_Actor
{
public string Title { get; set; }
public string Actor_Name { get; set; }
}
var source = new KSqlDBContext(@"http:\\localhost:8088")
.CreateQueryStream<Movie>()
.FullOuterJoin(
Source.Of<Lead_Actor>("Actors"),
movie => movie.Title,
actor => actor.Title,
(movie, actor) => new
{
movie.Id,
Title = movie.Title,
movie.Release_Year,
ActorTitle = actor.Title
}
);
Generated KSQL:
SELECT m.Id Id, m.Title Title, m.Release_Year Release_Year, l.Title ActorTitle FROM movies_test m
FULL OUTER JOIN lead_actor_test l
ON m.Title = l.Title
EMIT CHANGES;
- Select a condition from one or more expressions.
var query = new KSqlDBContext(@"http:\\localhost:8088")
.CreateQueryStream<Tweet>()
.Select(c =>
new
{
case_result =
(c.Amount < 2.0) ? "small" :
(c.Amount < 4.1) ? "medium" : "large"
}
);
SELECT
CASE
WHEN Amount < 2 THEN 'small'
WHEN Amount < 4.1 THEN 'medium'
ELSE 'large'
END AS case_result
FROM Tweets EMIT CHANGES;
NOTE: Switch expressions and if-elseif-else statements are not supported at current versions
public static KSqlDBContextOptions CreateQueryStreamOptions(string ksqlDbUrl)
{
var contextOptions = new KSqlDbContextOptionsBuilder()
.UseKSqlDb(ksqlDbUrl)
.SetupQueryStream(options =>
{
})
.SetupQuery(options =>
{
options.Properties[QueryParameters.AutoOffsetResetPropertyName] = AutoOffsetReset.Latest.ToString().ToLower();
})
.Options;
return contextOptions;
}
netstandard 2.0 does not support Http 2.0. Due to this IKSqlDBContext.CreateQueryStream<TEntity>
is not exposed at the current version.
For these reasons IKSqlDBContext.CreateQuery<TEntity>
was introduced to provide the same functionality via Http 1.1.
Executing pull or push queries
POST /query-stream HTTP/2.0
Accept: application/vnd.ksqlapi.delimited.v1
Content-Type: application/vnd.ksqlapi.delimited.v1
{
"sql": "SELECT * FROM movies EMIT CHANGES;",
"properties": {
"auto.offset.reset": "earliest"
}
}
using System;
using Kafka.DotNet.ksqlDB.KSql.Linq;
using Kafka.DotNet.ksqlDB.KSql.Query.Context;
using Kafka.DotNet.ksqlDB.Sample.Models.Movies;
var ksqlDbUrl = @"http:\\localhost:8088";
var contextOptions = CreateQueryStreamOptions(ksqlDbUrl);
await using var context = new KSqlDBContext(contextOptions);
using var disposable = context.CreateQueryStream<Movie>()
.Subscribe(onNext: movie =>
{
Console.WriteLine($"{nameof(Movie)}: {movie.Id} - {movie.Title} - {movie.RowTime}");
Console.WriteLine();
}, onError: error => { Console.WriteLine($"Exception: {error.Message}"); }, onCompleted: () => Console.WriteLine("Completed"));
POST /query HTTP/1.1
Accept: application/vnd.ksql.v1+json
Content-Type: application/vnd.ksql.v1+json
{
"ksql": "SELECT * FROM movies EMIT CHANGES;",
"streamsProperties": {
"ksql.streams.auto.offset.reset": "earliest"
}
}
using System;
using Kafka.DotNet.ksqlDB.KSql.Linq;
using Kafka.DotNet.ksqlDB.KSql.Query.Context;
using Kafka.DotNet.ksqlDB.Sample.Models.Movies;
var ksqlDbUrl = @"http:\\localhost:8088";
var contextOptions = CreateQueryStreamOptions(ksqlDbUrl);
await using var context = new KSqlDBContext(contextOptions);
using var disposable = context.CreateQuery<Movie>()
.Subscribe(onNext: movie =>
{
Console.WriteLine($"{nameof(Movie)}: {movie.Id} - {movie.Title} - {movie.RowTime}");
Console.WriteLine();
}, onError: error => { Console.WriteLine($"Exception: {error.Message}"); }, onCompleted: () => Console.WriteLine("Completed"));
- scalar collection functions: ArrayIntersect, ArrayJoin
You can use parentheses to change the order of evaluation:
await using var context = new KSqlDBContext(@"http:\\localhost:8088");
var query = context.CreateQueryStream<Location>()
.Select(c => (c.Longitude + c.Longitude) * c.Longitude);
SELECT (Longitude + Longitude) * Longitude FROM Locations EMIT CHANGES;
In Where clauses:
await using var context = new KSqlDBContext(@"http:\\localhost:8088");
var query = context.CreateQueryStream<Location>()
.Where(c => (c.Latitude == "1" || c.Latitude != "2") && c.Latitude == "3");
SELECT * FROM Locations
WHERE ((Latitude = '1') OR (Latitude != '2')) AND (Latitude = '3') EMIT CHANGES;
Redundant brackets are not reduced in the current version
The following examples show how to execute ksql queries from strings:
string ksql = @"SELECT * FROM Movies
WHERE Title != 'E.T.' EMIT CHANGES LIMIT 2;";
QueryParameters queryParameters = new QueryParameters
{
Sql = ksql,
[QueryParameters.AutoOffsetResetPropertyName] = "earliest",
};
await using var context = new KSqlDBContext(@"http:\\localhost:8088");
var moviesSource = context.CreateQuery<Movie>(queryParameters)
.ToObservable();
Query stream:
string ksql = @"SELECT * FROM Movies
WHERE Title != 'E.T.' EMIT CHANGES LIMIT 2;";
QueryStreamParameters queryStreamParameters = new QueryStreamParameters
{
Sql = ksql,
[QueryStreamParameters.AutoOffsetResetPropertyName] = "earliest",
};
await using var context = new KSqlDBContext(@"http:\\localhost:8088");
var source = context.CreateQueryStream<Movie>(queryStreamParameters)
.ToObservable();
Execute a statement - The /ksql resource runs a sequence of SQL statements. All statements, except those starting with SELECT, can be run on this endpoint. To run SELECT statements use the /query endpoint.
using Kafka.DotNet.ksqlDB.KSql.RestApi;
using Kafka.DotNet.ksqlDB.KSql.RestApi.Statements;
public async Task ExecuteStatementAsync()
{
var ksqlDbUrl = @"http:\\localhost:8088";
var httpClientFactory = new HttpClientFactory(new Uri(ksqlDbUrl));
IKSqlDbRestApiClient restApiClient = new KSqlDbRestApiClient(httpClientFactory);
var statement = $@"CREATE OR REPLACE TABLE {nameof(Movies)} (
title VARCHAR PRIMARY KEY,
id INT,
release_year INT
) WITH (
KAFKA_TOPIC='{nameof(Movies)}',
PARTITIONS=1,
VALUE_FORMAT = 'JSON'
);";
KSqlDbStatement ksqlDbStatement = new(statement);
var httpResponseMessage = await restApiClient.ExecuteStatementAsync(ksqlDbStatement);
string responseContent = await httpResponseMessage.Content.ReadAsStringAsync();
}
public record Movies
{
public int Id { get; set; }
public string Title { get; set; }
public int Release_Year { get; set; }
}
KSqlDbStatement allows you to set the statement, content encoding and CommandSequenceNumber.
using Kafka.DotNet.ksqlDB.KSql.RestApi.Statements;
public KSqlDbStatement CreateStatement(string statement)
{
KSqlDbStatement ksqlDbStatement = new(statement) {
ContentEncoding = Encoding.Unicode,
CommandSequenceNumber = 10,
[QueryStreamParameters.AutoOffsetResetPropertyName] = "earliest",
};
return ksqlDbStatement;
}
using Kafka.DotNet.ksqlDB.KSql.RestApi.Extensions;
var httpResponseMessage = await restApiClient.ExecuteStatementAsync(ksqlDbStatement);
var responses = httpResponseMessage.ToStatementResponses();
foreach (var response in responses)
{
Console.WriteLine(response.CommandStatus);
Console.WriteLine(response.CommandId);
}
Statement | Description |
---|---|
EXECUTE STATEMENTS | CreateStatementAsync - execution of general-purpose string statements |
CREATE STREAM | CreateStreamStatement - Create a new materialized stream view, along with the corresponding Kafka topic, and stream the result of the query into the topic. |
CREATE TABLE | CreateOrReplaceStreamStatement - Create or replace a materialized stream view, along with the corresponding Kafka topic, and stream the result of the query into the topic. |
CREATE STREAM AS SELECT | CreateTableStatement - Create a new ksqlDB materialized table view, along with the corresponding Kafka topic, and stream the result of the query as a changelog into the topic. |
CREATE TABLE AS SELECT | CreateOrReplaceTableStatement - Create or replace a ksqlDB materialized table view, along with the corresponding Kafka topic, and stream the result of the query as a changelog into the topic. |
using Kafka.DotNet.ksqlDB.KSql.Linq.Statements;
using Kafka.DotNet.ksqlDB.KSql.Query.Context;
public static async Task Main(string[] args)
{
await using var context = new KSqlDBContext(@"http:\\localhost:8088");
await CreateOrReplaceTableStatement(context);
}
private static async Task CreateOrReplaceTableStatement(IKSqlDBStatementsContext context)
{
var creationMetadata = new CreationMetadata
{
KafkaTopic = "tweetsByTitle",
KeyFormat = SerializationFormats.Json,
ValueFormat = SerializationFormats.Json,
Replicas = 1,
Partitions = 1
};
var httpResponseMessage = await context.CreateOrReplaceTableStatement(tableName: "TweetsByTitle")
.With(creationMetadata)
.As<Movie>()
.Where(c => c.Id < 3)
.Select(c => new {c.Title, ReleaseYear = c.Release_Year})
.PartitionBy(c => c.Title)
.ExecuteStatementAsync();
var statementResponse = httpResponseMessage.ToStatementResponses();
}
Generated KSQL statement:
CREATE OR REPLACE TABLE TweetsByTitle
WITH ( KAFKA_TOPIC='tweetsByTitle', KEY_FORMAT='Json', VALUE_FORMAT='Json', PARTITIONS='1', REPLICAS='1' )
AS SELECT Title, Release_Year AS ReleaseYear FROM Movies
WHERE Id < 3 PARTITION BY Title EMIT CHANGES;
Executes arbitrary statements:
async Task<HttpResponseMessage> ExecuteAsync(string statement)
{
KSqlDbStatement ksqlDbStatement = new(statement);
var httpResponseMessage = await restApiClient.ExecuteStatementAsync(ksqlDbStatement)
.ConfigureAwait(false);
string responseContent = await httpResponseMessage.Content.ReadAsStringAsync();
return httpResponseMessage;
}
Generates ksql statement from Create(OrReplace)[Table|Stream]Statements
await using var context = new KSqlDBContext(@"http:\\localhost:8088");
var statement = context.CreateOrReplaceTableStatement(tableName: "TweetsByTitle")
.As<Movie>()
.Where(c => c.Id < 3)
.Select(c => new {c.Title, ReleaseYear = c.Release_Year})
.PartitionBy(c => c.Title)
.ToStatementString();
Generated KSQL:
CREATE OR REPLACE TABLE TweetsByTitle
AS SELECT Title, Release_Year AS ReleaseYear FROM Movies
WHERE Id < 3 PARTITION BY Title EMIT CHANGES;
Install-Package Kafka.DotNet.ksqlDB -Version 0.10.0-rc.1
A pull query is a form of query issued by a client that retrieves a result as of "now", like a query against a traditional RDBS.
using System.Net.Http;
using System.Threading.Tasks;
using Kafka.DotNet.ksqlDB.KSql.Linq.PullQueries;
using Kafka.DotNet.ksqlDB.KSql.Linq.Statements;
using Kafka.DotNet.ksqlDB.KSql.Query.Context;
using Kafka.DotNet.ksqlDB.KSql.RestApi;
using Kafka.DotNet.ksqlDB.KSql.RestApi.Statements;
using Kafka.DotNet.ksqlDB.KSql.Query.Windows;
IKSqlDbRestApiClient restApiClient;
async Task Main()
{
string url = @"http:\\localhost:8088";
await using var context = new KSqlDBContext(url);
var http = new HttpClientFactory(new Uri(url));
restApiClient = new KSqlDbRestApiClient(http);
await CreateOrReplaceStreamAsync();
var statement = context.CreateTableStatement("avg_sensor_values")
.As<IoTSensor>("sensor_values")
.GroupBy(c => c.SensorId)
.WindowedBy(new TimeWindows(Duration.OfSeconds(5)).WithGracePeriod(Duration.OfHours(2)))
.Select(c => new { SensorId = c.Key, AvgValue = c.Avg(g => g.Value) });
var response = await statement.ExecuteStatementAsync();
response = await InsertAsync(new IoTSensor { SensorId = "sensor-1", Value = 11 });
var result = await context.CreatePullQuery<IoTSensorStats>("avg_sensor_values")
.Where(c => c.SensorId == "sensor-1")
.GetAsync();
Console.WriteLine($"{result?.SensorId} - {result?.AvgValue}");
}
async Task<HttpResponseMessage> CreateOrReplaceStreamAsync()
{
const string createOrReplaceStream =
@"CREATE STREAM sensor_values (
SensorId VARCHAR KEY,
Value INT
) WITH (
kafka_topic = 'sensor_values',
partitions = 2,
value_format = 'json'
);";
return await ExecuteAsync(createOrReplaceStream);
}
async Task<HttpResponseMessage> InsertAsync(IoTSensor sensor)
{
string insert =
$"INSERT INTO sensor_values (SensorId, Value) VALUES ('{sensor.SensorId}', {sensor.Value});";
return await ExecuteAsync(insert);
}
async Task<HttpResponseMessage> ExecuteAsync(string statement)
{
KSqlDbStatement ksqlDbStatement = new(statement);
var httpResponseMessage = await restApiClient.ExecuteStatementAsync(ksqlDbStatement)
.ConfigureAwait(false);
string responseContent = await httpResponseMessage.Content.ReadAsStringAsync();
return httpResponseMessage;
}
public record IoTSensor
{
public string SensorId { get; init; }
public int Value { get; init; }
}
public record IoTSensorStats
{
public string SensorId { get; init; }
public double AvgValue { get; init; }
}
The WHERE clause must contain a value for each primary-key column to retrieve and may optionally include bounds on WINDOWSTART and WINDOWEND if the materialized table is windowed.
using Kafka.DotNet.ksqlDB.KSql.Query.Functions;
string windowStart = "2019-10-03T21:31:16";
string windowEnd = "2025-10-03T21:31:16";
var result = await context.CreatePullQuery<IoTSensorStats>(MaterializedViewName)
.Where(c => c.SensorId == "sensor-1")
.Where(c => Bounds.WindowStart > windowStart && Bounds.WindowEnd <= windowEnd)
.GetAsync();
Generated KSQL:
SELECT * FROM avg_sensor_values
WHERE SensorId = 'sensor-1' AND (WINDOWSTART > '2019-10-03T21:31:16') AND (WINDOWEND <= '2020-10-03T21:31:16');
Execute pull query with plain string query:
string ksql = "SELECT * FROM avg_sensor_values WHERE SensorId = 'sensor-1';";
var result = await context.ExecutePullQuery<IoTSensorStats>(ksql);
Install-Package Kafka.DotNet.ksqlDB -Version 0.11.0-rc.1
- CREATE STREAM - fluent API
EntityCreationMetadata metadata = new()
{
KafkaTopic = nameof(MyMovies),
Partitions = 1,
Replicas = 1
};
string url = @"http:\\localhost:8088";
var http = new HttpClientFactory(new Uri(url));
var restApiClient = new KSqlDbRestApiClient(http);
var httpResponseMessage = await restApiClient.CreateStreamAsync<MyMovies>(metadata, ifNotExists: true);
public record MyMovies
{
[Kafka.DotNet.ksqlDB.KSql.RestApi.Statements.Annotations.Key]
public int Id { get; set; }
public string Title { get; set; }
public int Release_Year { get; set; }
}
CREATE STREAM MyMovies (
Id INT,
Title VARCHAR,
Release_Year INT
) WITH ( KAFKA_TOPIC='MyMovies', VALUE_FORMAT='Json', PARTITIONS='1', REPLICAS='1' );
Create or replace alternative:
var httpResponseMessage = await restApiClient.CreateOrReplaceStreamAsync<MyMovies>(metadata);
- CREATE TABLE - fluent API
EntityCreationMetadata metadata = new()
{
KafkaTopic = nameof(MyMovies),
Partitions = 2,
Replicas = 3
};
string url = @"http:\\localhost:8088";
var http = new HttpClientFactory(new Uri(url));
var restApiClient = new KSqlDbRestApiClient(http);
var httpResponseMessage = await restApiClient.CreateTableAsync<MyMovies>(metadata, ifNotExists: true);
CREATE TABLE MyMovies (
Id INT,
Title VARCHAR,
Release_Year INT
) WITH ( KAFKA_TOPIC='MyMovies', VALUE_FORMAT='Json', PARTITIONS='2', REPLICAS='3' );
class Transaction
{
[Kafka.DotNet.ksqlDB.KSql.RestApi.Statements.Annotations.Decimal(3, 2)]
public decimal Amount { get; set; }
}
Generated KSQL:
Amount DECIMAL(3,2)
Install-Package Kafka.DotNet.ksqlDB -Version 1.0.0
Insert values - Produce a row into an existing stream or table and its underlying topic based on explicitly specified values.
string url = @"http:\\localhost:8088";
var http = new HttpClientFactory(new Uri(url));
var restApiClient = new KSqlDbRestApiClient(http);
var movie = new Movie() { Id = 1, Release_Year = 1988, Title = "Title" };
var response = await restApiClient.InsertIntoAsync(movie);
Properties and fields decorated with the IgnoreByInsertsAttribute are not part of the insert statements:
public class Movie
{
[Kafka.DotNet.ksqlDB.KSql.RestApi.Statements.Annotations.Key]
public int Id { get; set; }
public string Title { get; set; }
public int Release_Year { get; set; }
[Kafka.DotNet.ksqlDB.KSql.RestApi.Statements.Annotations.IgnoreByInserts]
public int IgnoredProperty { get; set; }
}
Generated KSQL:
INSERT INTO Movies (Title, Id, Release_Year) VALUES ('Title', 1, 1988);
var insertProperties = new InsertProperties()
{
FormatDoubleValue = value => value.ToString("E1", CultureInfo.InvariantCulture),
FormatDecimalValue = value => value.ToString(CultureInfo.CreateSpecificCulture("en-GB"))
};
public static readonly Tweet Tweet1 = new()
{
Id = 1,
Amount = 0.00042,
AccountBalance = 533333333421.6332M
};
await restApiProvider.InsertIntoAsync(tweet, insertProperties);
Generated KSQL statement:
INSERT INTO tweetsTest (Id, Amount, AccountBalance) VALUES (1, 4.2E-004, 533333333421.6332);
In order to improve the v1.0.0 release the following methods and properties were renamed:
IKSqlDbRestApiClient interface changes:
| v.0.11.0 | v1.0.0 |
|---------------------------------------------------------------|
| CreateTable<T> | CreateTableAsync<T> |
| CreateStream<T> | CreateStreamAsync<T> |
| CreateOrReplaceTable<T> | CreateOrReplaceTableAsync<T> |
| CreateOrReplaceStream<T> | CreateOrReplaceStreamAsync<T> |
KSQL documentation refers to stream or table name in FROM as from_item
IKSqlDBContext.CreateQuery<TEntity>(string streamName = null)
IKSqlDBContext.CreateQueryStream<TEntity>(string streamName = null)
streamName parameters were renamed to fromItemName:
IKSqlDBContext.CreateQuery<TEntity>(string fromItemName = null)
IKSqlDBContext.CreateQueryStream<TEntity>(string fromItemName = null)
QueryContext.StreamName property was renamed to QueryContext.FromItemName
Source.Of parameter streamName was renamed to fromItemName
KSqlDBContextOptions.ShouldPluralizeStreamName was renamed to ShouldPluralizeFromItemName
Record.RowTime was decorated with IgnoreByInsertsAttribute
⚠ From version 1.0.0 the overriden from item names are pluralized, too. Join items are also affected by this breaking change. This breaking change can cause runtime exceptions for users updating from lower versions. In case that you have never used custom singular from-item names, your code won't be affected, see the example below:
var contextOptions = new KSqlDBContextOptions(@"http:\\localhost:8088")
{
//Default value:
//ShouldPluralizeFromItemName = true
};
var query = new KSqlDBContext(contextOptions)
.CreateQueryStream<Tweet>("Tweet")
.ToQueryString();
KSQL generated since v 1.0
SELECT * FROM Tweets EMIT CHANGES;
KSQL generated before v 1.0
SELECT * FROM Tweet EMIT CHANGES;
Install-Package Kafka.DotNet.ksqlDB -Version 1.1.0-rc.1
Converts any type to its string representation.
var query = context.CreateQueryStream<Movie>()
.GroupBy(c => c.Title)
.Select(c => new { Title = c.Key, Concatenated = K.Functions.Concat(c.Count().ToString(), "_Hello") });
SELECT Title, CONCAT(CAST(COUNT(*) AS VARCHAR), '_Hello') Concatenated FROM Movies GROUP BY Title EMIT CHANGES;
using System;
using Kafka.DotNet.ksqlDB.KSql.Query.Functions;
Expression<Func<Tweet, int>> stringToInt = c => KSQLConvert.ToInt32(c.Message);
Expression<Func<Tweet, long>> stringToLong = c => KSQLConvert.ToInt64(c.Message);
Expression<Func<Tweet, decimal>> stringToDecimal = c => KSQLConvert.ToDecimal(c.Message, 10, 2);
Expression<Func<Tweet, double>> stringToDouble = c => KSQLConvert.ToDouble(c.Message);
CAST(Message AS INT)
CAST(Message AS BIGINT)
CAST(Message AS DECIMAL(10, 2))
CAST(Message AS DOUBLE)
Expression<Func<Tweet, string>> expression = c => K.Functions.Concat(c.Message, "_Value");
https://www.nuget.org/packages/Kafka.DotNet.ksqlDB/
TODO:
- CREATE STREAM AS SELECT - [ WITHIN [(before TIMEUNIT, after TIMEUNIT) | N TIMEUNIT] ]
- CREATE TABLE AS SELECT - EMIT output_refinement
- rest of the ksql query syntax (supported operators etc)
- backpressure support