using Microsoft.Data.Sqlite;
using OpenNest.Data;
namespace OpenNest.Server;
///
/// The company material list: materials and each one's material_grades, keyed by
/// . Nests, drawings and releases keep their own free-text
/// material and grade; the list only suggests names and fixes their spelling.
///
public sealed partial class NestDatabase
{
// v9 -> v10. The list starts empty: it is filled through POST /api/materials, not from
// the free-text materials already stored. IF NOT EXISTS keeps the step safe on a database
// rewound to v9.
private const string MaterialSchema = """
CREATE TABLE IF NOT EXISTS materials (
key TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
createdAt TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS material_grades (
materialKey TEXT NOT NULL REFERENCES materials(key),
key TEXT NOT NULL,
name TEXT NOT NULL,
createdAt TEXT NOT NULL,
PRIMARY KEY (materialKey, key));
PRAGMA user_version=10;
""";
/// Every listed material with its grades, both in case-insensitive name order.
public IReadOnlyList ListMaterials()
{
lock (_sync)
{
var entries = new List();
var byKey = new Dictionary();
using (var command = _connection.CreateCommand())
{
command.CommandText = "SELECT key, name FROM materials ORDER BY name COLLATE NOCASE, key";
using var reader = command.ExecuteReader();
while (reader.Read())
{
var entry = new MaterialListEntry { Name = reader.GetString(1) };
byKey[reader.GetString(0)] = entry;
entries.Add(entry);
}
}
using (var command = _connection.CreateCommand())
{
command.CommandText = "SELECT materialKey, name FROM material_grades ORDER BY name COLLATE NOCASE, key";
using var reader = command.ExecuteReader();
while (reader.Read())
byKey[reader.GetString(0)].Grades.Add(reader.GetString(1));
}
return entries;
}
}
///
/// Adds a material and, when given, one of its grades; names already listed by key keep
/// their spelling. Returns the material as now listed and whether anything was added.
///
public (MaterialListEntry Entry, bool Added) AddMaterial(MaterialListAddition addition)
{
if (MaterialListRules.GetError(addition) is { } error)
throw new ArgumentException(error, nameof(addition));
var name = MaterialListRules.Spelling(addition.Name);
var key = SharedListNames.Key(name);
var grade = MaterialListRules.Spelling(addition.Grade);
lock (_sync)
{
using var transaction = _connection.BeginTransaction();
var now = DateTime.Now.ToString("o");
var added = InsertMaterial(key, name, now, transaction);
if (grade.Length > 0)
added |= InsertGrade(key, SharedListNames.Key(grade), grade, now, transaction);
transaction.Commit();
return (ListMaterials().Single(entry => SharedListNames.Key(entry.Name) == key), added);
}
}
private bool InsertMaterial(string key, string name, string createdAt, SqliteTransaction transaction)
{
using var command = _connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = "INSERT OR IGNORE INTO materials (key, name, createdAt) VALUES ($key, $name, $createdAt)";
command.Parameters.AddWithValue("$key", key);
command.Parameters.AddWithValue("$name", name);
command.Parameters.AddWithValue("$createdAt", createdAt);
return command.ExecuteNonQuery() > 0;
}
private bool InsertGrade(string materialKey, string key, string name, string createdAt, SqliteTransaction transaction)
{
using var command = _connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = """
INSERT OR IGNORE INTO material_grades (materialKey, key, name, createdAt)
VALUES ($materialKey, $key, $name, $createdAt)
""";
command.Parameters.AddWithValue("$materialKey", materialKey);
command.Parameters.AddWithValue("$key", key);
command.Parameters.AddWithValue("$name", name);
command.Parameters.AddWithValue("$createdAt", createdAt);
return command.ExecuteNonQuery() > 0;
}
}