Skip to content

Commit 5f9d103

Browse files
authored
Merge pull request #3 from elastacloud/azurecoder/versioning
Azurecoder/versioning
2 parents 4230a02 + c8713cc commit 5f9d103

14 files changed

Lines changed: 538 additions & 255 deletions

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ publish/
197197
# checkin your Azure Web App publish settings, but sensitive information contained
198198
# in these scripts will be unencrypted
199199
PublishScripts/
200-
200+
DotPrompt.Sql.Cli/sample1.yaml
201201
# NuGet Packages
202202
*.nupkg
203203
# NuGet Symbol Packages
@@ -485,4 +485,4 @@ $RECYCLE.BIN/
485485

486486
output/
487487

488-
docs/
488+
docs/

DotPrompt.Sql.Cli/Program.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ public static async Task Main(string[] args)
1212
var config = DatabaseConfigReader.ReadYamlConfig(args[1]);
1313
var connector = new DatabaseConnector();
1414
var connection = await connector.ConnectToDatabase(config);
15-
var loader = new SqlPromptLoader(connection);
16-
await loader.AddSqlPrompt(entity);
15+
IPromptRepository sqlRepository = new SqlPromptRepository(connection);
16+
var loader = new SqlPromptLoader(sqlRepository);
17+
bool upVersioned = await loader.AddSqlPrompt(entity);
18+
Console.WriteLine($"Done: {upVersioned}");
1719

1820
var prompts = loader.Load();
1921
foreach (var prompt in prompts)

DotPrompt.Sql.Cli/prompts/basic.prompt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
model: claude-3-5-sonnet-latest
22
config:
3-
name: basic1
3+
name: basic
44
outputFormat: text
55
temperature: 0.9
66
maxTokens: 500

DotPrompt.Sql.Test/DotPrompt.Sql.Test.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
14+
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.1" />
1415
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
16+
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.11-pre20241216174303" />
17+
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
18+
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
19+
<PackageReference Include="TestContainers.Container.Database.MsSql" Version="1.5.4" />
20+
<PackageReference Include="Testcontainers.MsSql" Version="4.1.0" />
1521
<PackageReference Include="xunit" Version="2.5.3"/>
1622
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
1723
</ItemGroup>
Lines changed: 141 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,172 @@
1-
namespace DotPrompt.Sql.Test;
2-
31
using System;
42
using System.Collections.Generic;
5-
using System.IO;
3+
using System.Data;
4+
using System.Data.SQLite;
5+
using System.Reflection;
6+
using Microsoft.Data.Sqlite;
7+
using System.Threading.Tasks;
8+
using Dapper;
9+
using DotPrompt.Sql;
10+
using Microsoft.Data.SqlClient;
11+
using Testcontainers.MsSql;
612
using Xunit;
713

8-
public class SqlPromptEntityTests : IDisposable
14+
public class SqlPromptRepositoryTests : IAsyncLifetime
915
{
10-
private readonly List<string> _testFiles = new();
16+
private readonly MsSqlContainer _sqlServerContainer;
17+
private IDbConnection _connection;
18+
private SqlPromptRepository _repository;
1119

12-
// Helper method to create and track test YAML files
13-
private void CreateTestYamlFile(string filePath, string content)
20+
public SqlPromptRepositoryTests()
1421
{
15-
File.WriteAllText(filePath, content);
16-
_testFiles.Add(filePath); // Track the file for later cleanup
22+
_sqlServerContainer = new MsSqlBuilder()
23+
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
24+
.WithPassword("YourStrong(!)Password")
25+
.Build();
1726
}
1827

19-
[Fact]
20-
public void FromPromptFile_ValidYaml_ReturnsSqlPromptEntity()
28+
public async Task InitializeAsync()
2129
{
22-
// Arrange
23-
string filePath = "test_prompt.yaml";
24-
string yamlContent = @"
25-
model: gpt-4
26-
config:
27-
name: TestPrompt
28-
outputFormat: json
29-
maxTokens: 200
30-
input:
31-
parameters:
32-
param1: value1
33-
param2: value2
34-
default:
35-
param1: default1
36-
param2: default2
37-
prompts:
38-
system: System message
39-
user: User message";
40-
41-
CreateTestYamlFile(filePath, yamlContent);
30+
await _sqlServerContainer.StartAsync();
4231

43-
// Act
44-
var result = SqlPromptEntity.FromPromptFile(filePath);
32+
_connection = new SqlConnection(_sqlServerContainer.GetConnectionString());
33+
_connection.Open();
4534

46-
// Assert
47-
Assert.NotNull(result);
48-
Assert.Equal("gpt-4", result.Model);
49-
Assert.Equal("TestPrompt", result.PromptName);
50-
Assert.Equal("json", result.OutputFormat);
51-
Assert.Equal(200, result.MaxTokens);
52-
Assert.Equal("System message", result.SystemPrompt);
53-
Assert.Equal("User message", result.UserPrompt);
54-
Assert.Equal("value1", result.Parameters["param1"]);
55-
Assert.Equal("default1", result.Default["param1"]);
35+
_repository = new SqlPromptRepository(_connection);
36+
await InitializeDatabase();
5637
}
5738

58-
[Fact]
59-
public void FromPromptFile_FileDoesNotExist_ThrowsFileNotFoundException()
39+
public Task DisposeAsync()
6040
{
61-
// Arrange
62-
string invalidPath = "non_existent.yaml";
41+
_sqlServerContainer.StopAsync();
42+
_connection?.Dispose();
43+
return Task.CompletedTask;
44+
}
45+
46+
private static string LoadSql(string resourceName)
47+
{
48+
// Get the assembly containing the embedded SQL files
49+
var assembly = Assembly.Load("DotPrompt.Sql"); // Name of the referenced assembly
50+
51+
// Find the full resource name (includes namespace path)
52+
string? fullResourceName = assembly.GetManifestResourceNames()
53+
.FirstOrDefault(name => name.EndsWith(resourceName, StringComparison.OrdinalIgnoreCase));
6354

64-
// Act & Assert
65-
var exception = Assert.Throws<FileNotFoundException>(() => SqlPromptEntity.FromPromptFile(invalidPath));
66-
Assert.Contains("The specified file was not found", exception.Message);
55+
if (fullResourceName == null)
56+
{
57+
throw new FileNotFoundException($"Resource {resourceName} not found in assembly {assembly.FullName}");
58+
}
59+
60+
// Read the embedded resource stream
61+
using var stream = assembly.GetManifestResourceStream(fullResourceName);
62+
using var reader = new StreamReader(stream);
63+
return reader.ReadToEnd();
64+
}
65+
private async Task InitializeDatabase()
66+
{
67+
string tables = LoadSql("CreateDefaultPromptTables.sql");
68+
await _connection.ExecuteAsync(tables);
69+
70+
string procs = LoadSql("AddSqlPrompt.sql");
71+
await _connection.ExecuteAsync(procs);
6772
}
6873

6974
[Fact]
70-
public void FromPromptFile_MissingMandatoryFields_ThrowsException()
75+
public async Task AddSqlPrompt_ValidPrompt_InsertsSuccessfully()
7176
{
7277
// Arrange
73-
string filePath = "test_missing_optional.yaml";
74-
string yamlContent = @"
75-
config:
76-
name: TestPrompt
77-
outputFormat: json
78-
maxTokens: 100
79-
prompts:
80-
system: Default system prompt
81-
user: Default user prompt";
82-
83-
CreateTestYamlFile(filePath, yamlContent);
84-
85-
// Act & Assert
86-
Assert.Throws<ApplicationException>(() => SqlPromptEntity.FromPromptFile(filePath));
78+
var entity = new SqlPromptEntity
79+
{
80+
PromptName = "myprompt",
81+
Model = "gpt4",
82+
OutputFormat = "json",
83+
MaxTokens = 500,
84+
SystemPrompt = "Optimize SQL queries.",
85+
UserPrompt = "Suggest indexing improvements.",
86+
Parameters = new Dictionary<string, string>
87+
{
88+
{ "Temperature", "0.7" },
89+
{ "TopP", "0.9" }
90+
},
91+
Default = new Dictionary<string, object>
92+
{
93+
{ "Temperature", "0.5" }
94+
}
95+
};
96+
97+
// Act
98+
bool result = await _repository.AddSqlPrompt(entity);
99+
100+
// Assert
101+
Assert.True(result, "Expected new prompt version to be inserted.");
87102
}
88103

89104
[Fact]
90-
public void FromPromptFile_InvalidDataType_ThrowsException()
105+
public async Task AddSqlPrompt_SamePromptNoChanges_DoesNotInsertNewVersion()
91106
{
92107
// Arrange
93-
string filePath = "test_invalid_type.yaml";
94-
string yamlContent = @"
95-
model: gpt-4
96-
config:
97-
name: TestPrompt
98-
outputFormat: json
99-
maxTokens: not_a_number
100-
prompts:
101-
system: System prompt
102-
user: User prompt";
103-
104-
CreateTestYamlFile(filePath, yamlContent);
105-
106-
// Act & Assert
107-
Assert.Throws<FormatException>(() => SqlPromptEntity.FromPromptFile(filePath));
108+
var entity = new SqlPromptEntity
109+
{
110+
PromptName = "myprompt",
111+
Model = "gpt4",
112+
OutputFormat = "json",
113+
MaxTokens = 200,
114+
SystemPrompt = "Optimize SQL queries.",
115+
UserPrompt = "Suggest indexing improvements.",
116+
Parameters = new Dictionary<string, string>
117+
{
118+
{ "Temperature", "0.7" },
119+
{ "TopP", "0.9" }
120+
},
121+
Default = new Dictionary<string, object>
122+
{
123+
{ "Temperature", "0.5" }
124+
}
125+
};
126+
127+
await _repository.AddSqlPrompt(entity); // First insert
128+
129+
// Act
130+
bool result = await _repository.AddSqlPrompt(entity); // Try inserting again with no changes
131+
132+
// Assert
133+
Assert.False(result, "No new version should be inserted when nothing has changed.");
108134
}
109135

110-
// Cleanup method called after each test
111-
public void Dispose()
136+
[Fact]
137+
public async Task AddSqlPrompt_WhenMaxTokensChanges_ShouldInsertNewVersion()
112138
{
113-
foreach (var file in _testFiles)
139+
// Arrange
140+
var entity1 = new SqlPromptEntity
114141
{
115-
if (File.Exists(file))
116-
{
117-
File.Delete(file);
118-
}
119-
}
142+
PromptName = "noprompt",
143+
Model = "gpt4",
144+
OutputFormat = "json",
145+
MaxTokens = 500,
146+
SystemPrompt = "Optimize SQL queries.",
147+
UserPrompt = "Suggest indexing improvements.",
148+
Parameters = new Dictionary<string, string> { { "Temperature", "0.7" } },
149+
Default = new Dictionary<string, object> { { "Temperature", "0.5" } }
150+
};
151+
152+
var entity2 = new SqlPromptEntity
153+
{
154+
PromptName = "noprompt", // Same prompt name
155+
Model = "gpt4",
156+
OutputFormat = "json",
157+
MaxTokens = 512, // Changed value
158+
SystemPrompt = "Optimize SQL queries.",
159+
UserPrompt = "Suggest indexing improvements.",
160+
Parameters = new Dictionary<string, string> { { "Temperature", "0.7" } },
161+
Default = new Dictionary<string, object> { { "Temperature", "0.5" } }
162+
};
163+
164+
await _repository.AddSqlPrompt(entity1); // Insert first version
165+
166+
// Act
167+
bool result = await _repository.AddSqlPrompt(entity2);
168+
169+
// Assert
170+
Assert.True(result, "A new version should be inserted when MaxTokens changes.");
120171
}
121-
}
172+
}

DotPrompt.Sql/DatabaseConnector.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public async Task<IDbConnection> ConnectToDatabase(DatabaseConfig config)
2525
var connection = new SqlConnection(connectionString);
2626
await connection.OpenAsync();
2727
await CreatePromptTables(connection);
28+
await CreateStoredProcs(connection);
2829
Console.WriteLine("Connected to the database successfully!");
2930
return connection;
3031
}
@@ -42,6 +43,14 @@ private async Task CreatePromptTables(SqlConnection connection)
4243
await command.ExecuteNonQueryAsync();
4344
}
4445

46+
private async Task CreateStoredProcs(SqlConnection connection)
47+
{
48+
// 1. Does the prompt table exist already
49+
string? sqlCreate = DatabaseConfigReader.LoadQuery("AddSqlPrompt.sql");
50+
await using SqlCommand command = new SqlCommand(sqlCreate, connection);
51+
await command.ExecuteNonQueryAsync();
52+
}
53+
4554
private static string BuildConnectionString(DatabaseConfig config)
4655
{
4756
if (config.AadAuthentication)

DotPrompt.Sql/DotPrompt.Sql.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
<AssemblyName>DotPrompt.Sql</AssemblyName>
99
<Description>A SQL store for DotPrompt allowing you to store prompt files in a relational form</Description>
1010
<Authors>Richard Conway</Authors>
11-
<Version>0.1.1</Version>
12-
<PackageVersion>0.1.1</PackageVersion>
11+
<Version>0.2.1</Version>
12+
<PackageVersion>0.2.1</PackageVersion>
1313
<Company>Elastacloud</Company>
1414
<PackageProjectUrl>https://elastacloud.com</PackageProjectUrl>
1515
<RepositoryUrl>https://github.com/elastacloud/DotPrompt.Sql</RepositoryUrl>
@@ -43,6 +43,8 @@
4343
<EmbeddedResource Include="Resources\SqlQueries\InsertPromptDefaults.sql" />
4444
<None Remove="Resources\SqlQueries\CreateDefaultPromptTables.sql" />
4545
<EmbeddedResource Include="Resources\SqlQueries\CreateDefaultPromptTables.sql" />
46+
<None Remove="Resources\SqlQueries\AddSqlPrompt.sql" />
47+
<EmbeddedResource Include="Resources\SqlQueries\AddSqlPrompt.sql" />
4648
</ItemGroup>
4749

4850
</Project>

DotPrompt.Sql/IPromptRepository.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace DotPrompt.Sql;
2+
3+
/// <summary>
4+
/// Defines a prompt repository which will be injected into a loader
5+
/// </summary>
6+
public interface IPromptRepository
7+
{
8+
/// <summary>
9+
/// Adds a SQL prompt and upversions the prompt if it's changed
10+
/// </summary>
11+
/// <param name="entity">The prompt entity that is being added or upversioned</param>
12+
/// <returns>A boolean to denote whether it added the prompt or not</returns>
13+
Task<bool> AddSqlPrompt(SqlPromptEntity entity);
14+
/// <summary>
15+
/// Loads all instances of the prompt from the catalog but only the latest versions
16+
/// </summary>
17+
/// <returns>An enumeration of prompts with different names</returns>
18+
IEnumerable<SqlPromptEntity> Load();
19+
}

0 commit comments

Comments
 (0)