Skip to content

Commit 8af0a5f

Browse files
Veeran Puthumkaraclaude
andcommitted
release: 2.0.0 — CosmoSQLClient.CosmoKv driver
Adds CosmoSQLClient.CosmoKv, a new driver that runs a T-SQL subset directly on top of an embedded CosmoKv LSM-tree store — same ISqlDatabase surface as MsSql / Postgres / MySql / Sqlite. Capabilities delivered across 8 phases (181 tests green): • Hand-rolled T-SQL lexer + recursive-descent parser • DDL: CREATE TABLE (BIT/TINYINT/.../BIGINT/REAL/FLOAT/DECIMAL(p,s)/ NVARCHAR(n|MAX)/DATETIME2/UNIQUEIDENTIFIER/VARBINARY/...), CREATE INDEX (single + multi-col), DROP TABLE/INDEX, IDENTITY, PRIMARY KEY, NOT NULL, DEFAULT, UNIQUE • DML: INSERT VALUES (multi-row), SELECT (* | col list | aliases), WHERE (=, <>, <, <=, >, >=, AND, OR, NOT, parens, LIKE, IN, IS [NOT] NULL — full three-valued logic), ORDER BY (multi-col, ASC/DESC, alias-aware), TOP n, OFFSET n ROWS FETCH NEXT m ROWS ONLY, UPDATE SET WHERE, DELETE WHERE • Functions: GETUTCDATE, GETDATE, DATEADD, DATEDIFF, DATEPART, LEN, SUBSTRING, CHARINDEX, REPLACE, UPPER, LOWER, LTRIM, RTRIM, ISNULL, COALESCE, NULLIF, FORMAT, JSON_VALUE, CAST, CONVERT • Aggregation: COUNT(*) / COUNT(col), SUM, AVG, MIN, MAX, GROUP BY, HAVING — including hour-bucket histogram queries • Query planner: cost-based index selection (equality > range > order-only), single-column equality + range scans, ORDER BY pushdown when index satisfies order • Transactions: BEGIN/COMMIT/ROLLBACK [TRAN[SACTION]] mapped to CosmoKv MVCC; implicit per-statement txn wraps every DML so multi-key writes (row + IDENTITY counter + N index entries) are atomic even without explicit BEGIN; SqlTransactionConflictException on commit-time conflict • UniqueConstraintViolationException on duplicate UNIQUE columns Consumer audit: CosmoS3.S3Repository's full SQL surface is supported as-is (see Phase6S3RepositoryShapeTests). MurshisoftZatca.Data DbServices's single-table T-SQL slice is supported; its JOIN-bearing and stored-procedure paths are out of scope by design. LocalAppLogStore needs SQLite → T-SQL rewrites in the consumer code (translation table in Phase7ConsumerShapeTests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1e42c7e commit 8af0a5f

43 files changed

Lines changed: 8304 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/nuget.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ jobs:
7878
-p:Version=${{ steps.version.outputs.VERSION }} \
7979
-o ./nupkgs
8080
81+
dotnet pack src/CosmoSQLClient.CosmoKv/CosmoSQLClient.CosmoKv.csproj \
82+
-c Release \
83+
-p:Version=${{ steps.version.outputs.VERSION }} \
84+
-o ./nupkgs
85+
8186
ls -la ./nupkgs
8287
8388
- name: Push to NuGet

CosmoSQLClient-Dotnet.sln

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CosmoSQLClient.MySql.Entity
3535
EndProject
3636
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CosmoSQLClient.Sqlite.EntityFrameworkCore", "src\CosmoSQLClient.Sqlite.EntityFrameworkCore\CosmoSQLClient.Sqlite.EntityFrameworkCore.csproj", "{0571EEBB-64D4-4E6E-B6B4-A74D3127854A}"
3737
EndProject
38+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CosmoSQLClient.CosmoKv", "src\CosmoSQLClient.CosmoKv\CosmoSQLClient.CosmoKv.csproj", "{63463845-4079-4171-ABE3-525606D36AB6}"
39+
EndProject
40+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CosmoSQLClient.CosmoKv.Tests", "tests\CosmoSQLClient.CosmoKv.Tests\CosmoSQLClient.CosmoKv.Tests.csproj", "{65E8881F-CEEC-4085-84DC-7BBFDE746E43}"
41+
EndProject
3842
Global
3943
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4044
Debug|Any CPU = Debug|Any CPU
@@ -213,6 +217,30 @@ Global
213217
{0571EEBB-64D4-4E6E-B6B4-A74D3127854A}.Release|x64.Build.0 = Release|Any CPU
214218
{0571EEBB-64D4-4E6E-B6B4-A74D3127854A}.Release|x86.ActiveCfg = Release|Any CPU
215219
{0571EEBB-64D4-4E6E-B6B4-A74D3127854A}.Release|x86.Build.0 = Release|Any CPU
220+
{63463845-4079-4171-ABE3-525606D36AB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
221+
{63463845-4079-4171-ABE3-525606D36AB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
222+
{63463845-4079-4171-ABE3-525606D36AB6}.Debug|x64.ActiveCfg = Debug|Any CPU
223+
{63463845-4079-4171-ABE3-525606D36AB6}.Debug|x64.Build.0 = Debug|Any CPU
224+
{63463845-4079-4171-ABE3-525606D36AB6}.Debug|x86.ActiveCfg = Debug|Any CPU
225+
{63463845-4079-4171-ABE3-525606D36AB6}.Debug|x86.Build.0 = Debug|Any CPU
226+
{63463845-4079-4171-ABE3-525606D36AB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
227+
{63463845-4079-4171-ABE3-525606D36AB6}.Release|Any CPU.Build.0 = Release|Any CPU
228+
{63463845-4079-4171-ABE3-525606D36AB6}.Release|x64.ActiveCfg = Release|Any CPU
229+
{63463845-4079-4171-ABE3-525606D36AB6}.Release|x64.Build.0 = Release|Any CPU
230+
{63463845-4079-4171-ABE3-525606D36AB6}.Release|x86.ActiveCfg = Release|Any CPU
231+
{63463845-4079-4171-ABE3-525606D36AB6}.Release|x86.Build.0 = Release|Any CPU
232+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
233+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Debug|Any CPU.Build.0 = Debug|Any CPU
234+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Debug|x64.ActiveCfg = Debug|Any CPU
235+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Debug|x64.Build.0 = Debug|Any CPU
236+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Debug|x86.ActiveCfg = Debug|Any CPU
237+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Debug|x86.Build.0 = Debug|Any CPU
238+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Release|Any CPU.ActiveCfg = Release|Any CPU
239+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Release|Any CPU.Build.0 = Release|Any CPU
240+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Release|x64.ActiveCfg = Release|Any CPU
241+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Release|x64.Build.0 = Release|Any CPU
242+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Release|x86.ActiveCfg = Release|Any CPU
243+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43}.Release|x86.Build.0 = Release|Any CPU
216244
EndGlobalSection
217245
GlobalSection(SolutionProperties) = preSolution
218246
HideSolutionNode = FALSE
@@ -232,5 +260,7 @@ Global
232260
{FAB775B8-AE25-47BD-979E-FA4BFC309A05} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
233261
{D1DF69A2-70CD-4274-BB29-4AD4A7ED7367} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
234262
{0571EEBB-64D4-4E6E-B6B4-A74D3127854A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
263+
{63463845-4079-4171-ABE3-525606D36AB6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
264+
{65E8881F-CEEC-4085-84DC-7BBFDE746E43} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
235265
EndGlobalSection
236266
EndGlobal

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
<Authors>vkuttyp</Authors>
88
<PackageLicenseExpression>MIT</PackageLicenseExpression>
99
<RepositoryUrl>https://github.com/vkuttyp/CosmoSQLClient-Dotnet</RepositoryUrl>
10-
<Version>1.9.54</Version>
10+
<Version>2.0.0</Version>
1111
</PropertyGroup>
1212
</Project>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using CosmoSQLClient.Core;
2+
using CosmoSQLClient.CosmoKv.Schema;
3+
4+
namespace CosmoSQLClient.CosmoKv.Ast;
5+
6+
/// <summary>Root of the AST hierarchy. One concrete subtype per statement form.</summary>
7+
internal abstract record Statement;
8+
9+
// ── DDL ──────────────────────────────────────────────────────────────────────
10+
11+
internal sealed record CreateTableStatement(
12+
string TableName,
13+
IReadOnlyList<ColumnSchema> Columns,
14+
int? PrimaryKeyColumnIndex,
15+
int? IdentityColumnIndex,
16+
bool IfNotExists) : Statement;
17+
18+
internal sealed record CreateIndexStatement(
19+
string IndexName,
20+
string TableName,
21+
IReadOnlyList<string> Columns,
22+
bool IsUnique) : Statement;
23+
24+
internal sealed record DropTableStatement(string TableName, bool IfExists) : Statement;
25+
internal sealed record DropIndexStatement(string IndexName, string? TableName, bool IfExists) : Statement;
26+
27+
// ── DML ──────────────────────────────────────────────────────────────────────
28+
29+
internal sealed record InsertStatement(
30+
string TableName,
31+
IReadOnlyList<string>? Columns, // null = all columns in declaration order
32+
IReadOnlyList<IReadOnlyList<Expression>> Rows) : Statement;
33+
34+
internal sealed record SelectStatement(
35+
IReadOnlyList<SelectItem> Items,
36+
string TableName,
37+
Expression? Where,
38+
IReadOnlyList<Expression>? GroupBy,
39+
Expression? Having,
40+
IReadOnlyList<OrderByItem>? OrderBy,
41+
long? Top,
42+
long? Offset,
43+
long? Fetch) : Statement;
44+
45+
internal sealed record SelectItem(Expression Expr, string? Alias);
46+
47+
internal sealed record OrderByItem(Expression Expr, bool Descending);
48+
49+
internal sealed record UpdateStatement(
50+
string TableName,
51+
IReadOnlyList<UpdateAssignment> Assignments,
52+
Expression? Where) : Statement;
53+
54+
internal sealed record UpdateAssignment(string ColumnName, Expression Value);
55+
56+
internal sealed record DeleteStatement(
57+
string TableName,
58+
Expression? Where) : Statement;
59+
60+
// ── Transaction control ──────────────────────────────────────────────────────
61+
62+
/// <summary><c>BEGIN TRAN[SACTION] [name]</c>. Name is accepted but not honoured.</summary>
63+
internal sealed record BeginTransactionStatement(string? Name) : Statement;
64+
65+
/// <summary><c>COMMIT [TRAN[SACTION]] [name]</c>.</summary>
66+
internal sealed record CommitTransactionStatement(string? Name) : Statement;
67+
68+
/// <summary><c>ROLLBACK [TRAN[SACTION]] [name]</c>. Phase 5: nested savepoints not supported.</summary>
69+
internal sealed record RollbackTransactionStatement(string? Name) : Statement;
70+
71+
// ── Expressions ──────────────────────────────────────────────────────────────
72+
73+
internal abstract record Expression;
74+
75+
/// <summary>Literal — number, string, boolean, NULL, hex/binary.</summary>
76+
internal sealed record LiteralExpression(SqlValue Value) : Expression;
77+
78+
/// <summary>Parameter reference like <c>@name</c>; bound from the caller's parameter list.</summary>
79+
internal sealed record ParameterExpression(string Name) : Expression;
80+
81+
/// <summary>Bare column reference. Phase 1 only — no qualifier resolution.</summary>
82+
internal sealed record ColumnExpression(string Name) : Expression;
83+
84+
/// <summary><c>SELECT *</c>'s sentinel. Resolved against the source table at execution time.</summary>
85+
internal sealed record StarExpression : Expression;
86+
87+
/// <summary>Binary operator: arithmetic, comparison, logical AND/OR.</summary>
88+
internal sealed record BinaryExpression(BinaryOperator Op, Expression Left, Expression Right) : Expression;
89+
90+
/// <summary>Unary operator: NOT (logical), - (numeric negation).</summary>
91+
internal sealed record UnaryExpression(UnaryOperator Op, Expression Operand) : Expression;
92+
93+
/// <summary><c>expr IS [NOT] NULL</c>.</summary>
94+
internal sealed record IsNullExpression(Expression Operand, bool Negated) : Expression;
95+
96+
/// <summary><c>expr [NOT] LIKE pattern</c>. <c>%</c> = any sequence, <c>_</c> = single char. Phase 2: no ESCAPE clause.</summary>
97+
internal sealed record LikeExpression(Expression Source, Expression Pattern, bool Negated) : Expression;
98+
99+
/// <summary><c>expr [NOT] IN (v1, v2, …)</c>.</summary>
100+
internal sealed record InExpression(Expression Source, IReadOnlyList<Expression> Values, bool Negated) : Expression;
101+
102+
/// <summary>
103+
/// A function call: <c>NAME(arg1, arg2, …)</c>. Aggregate vs scalar is
104+
/// resolved at execution time from the function registry — keeping that
105+
/// decision out of the parser lets new functions land without grammar churn.
106+
/// </summary>
107+
internal sealed record FunctionCallExpression(string Name, IReadOnlyList<Expression> Args) : Expression;
108+
109+
/// <summary><c>COUNT(*)</c>. Counts rows including those with NULLs in every column.</summary>
110+
internal sealed record CountStarExpression : Expression;
111+
112+
/// <summary>
113+
/// <c>CAST(expr AS type)</c> and <c>CONVERT(type, expr)</c>. Style argument
114+
/// (T-SQL CONVERT's third) is ignored by Phase 3; the cast follows the
115+
/// column-coercion rules in <see cref="CosmoSQLClient.CosmoKv.Execution.TypeCoercer"/>.
116+
/// </summary>
117+
internal sealed record CastExpression(Expression Operand, Schema.SqlColumnType TargetType) : Expression;
118+
119+
internal enum BinaryOperator
120+
{
121+
And, Or,
122+
Eq, Ne, Lt, Le, Gt, Ge,
123+
Add, Sub, Mul, Div, Mod,
124+
}
125+
126+
internal enum UnaryOperator
127+
{
128+
Not, Negate,
129+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
namespace CosmoSQLClient.CosmoKv;
2+
3+
/// <summary>
4+
/// Connection settings for <see cref="CosmoKvConnection"/>.
5+
/// <para>
6+
/// <see cref="DataSource"/> is a filesystem directory that holds the CosmoKv
7+
/// store (WAL, SSTables, value log, manifest). The directory is created on
8+
/// open when <see cref="CreateIfMissing"/> is <c>true</c>.
9+
/// </para>
10+
/// </summary>
11+
public sealed record CosmoKvConfiguration
12+
{
13+
/// <summary>
14+
/// Filesystem directory that backs the store. Required. Use an absolute
15+
/// path in production; relative paths resolve against the process CWD.
16+
/// </summary>
17+
public required string DataSource { get; init; }
18+
19+
/// <summary>
20+
/// Create <see cref="DataSource"/> if it does not yet exist. Default <c>true</c>.
21+
/// </summary>
22+
public bool CreateIfMissing { get; init; } = true;
23+
24+
public string ConnectionString => $"Data Source={DataSource};CreateIfMissing={CreateIfMissing}";
25+
26+
/// <summary>
27+
/// Parse a connection string of the form
28+
/// <c>Data Source=/path/to/dir;CreateIfMissing=true</c>. Keys are matched
29+
/// case-insensitively; whitespace around <c>=</c> and <c>;</c> is tolerated.
30+
/// </summary>
31+
public static CosmoKvConfiguration Parse(string connectionString)
32+
{
33+
if (string.IsNullOrWhiteSpace(connectionString))
34+
throw new ArgumentException("Connection string is empty.", nameof(connectionString));
35+
36+
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
37+
foreach (var part in connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
38+
{
39+
int eq = part.IndexOf('=');
40+
if (eq < 0) continue;
41+
dict[part[..eq].Trim()] = part[(eq + 1)..].Trim();
42+
}
43+
44+
string? Get(params string[] keys)
45+
{
46+
foreach (var k in keys)
47+
if (dict.TryGetValue(k, out var v)) return v;
48+
return null;
49+
}
50+
51+
var dataSource = Get("Data Source", "DataSource", "Path")
52+
?? throw new ArgumentException("Connection string missing 'Data Source'.", nameof(connectionString));
53+
54+
bool create = true;
55+
if (Get("CreateIfMissing", "Create") is string s && bool.TryParse(s, out var b))
56+
create = b;
57+
58+
return new CosmoKvConfiguration
59+
{
60+
DataSource = dataSource,
61+
CreateIfMissing = create,
62+
};
63+
}
64+
}

0 commit comments

Comments
 (0)