metastrings is a great database if you want a dynamic schema, and you don't want to pay for it. With no infrastructure at all, and just four database commands, add dynamic database power to your projects today.
Introduction
I've been working on this database project for a few years now. There was a lot of ambition, to make it a server database, built on top of MySQL, CSV, full-text...
At this point, it's just a really easy to use file-based dynamic database. If you want to add database technology to your application without writing a lot of CREATE
statements or involving a server, metastrings
is for you.
Follow this carsdb
example to see metastrings
in action:
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.IO;
namespace metastrings
{
class Program
{
static async Task Main()
{
using (var ctxt = new Context("cars.db"))
{
Console.WriteLine("Adding cars...");
await AddCarAsync(ctxt, 1982, "Chrysler", "LeBaron");
await AddCarAsync(ctxt, 1983, "Toyota", "Tercel");
await AddCarAsync(ctxt, 1998, "Toyota", "Tacoma");
await AddCarAsync(ctxt, 2001, "Nissan", "Xterra");
await AddCarAsync(ctxt, 1987, "Nissan", "Pathfinder");
Console.WriteLine("Getting old cars...");
var oldCarGuids = new List<string>();
Select select =
Sql.Parse
(
"SELECT value, year, make, model " +
"FROM cars " +
"WHERE year < @year " +
"ORDER BY year ASC"
);
select.AddParam("@year", 1990);
using (var reader = await ctxt.ExecSelectAsync(select))
{
while (reader.Read())
{
oldCarGuids.Add(reader.GetString(0));
Console.WriteLine
(
reader.GetDouble(1) + ": " +
reader.GetString(2) + " - " +
reader.GetString(3)
);
}
}
Console.WriteLine("Deleting old cars...");
await ctxt.Cmd.DeleteAsync("cars", oldCarGuids);
Console.WriteLine("Cleaning up...");
await ctxt.Cmd.DropAsync("cars");
Console.WriteLine("All done.");
}
}
static async Task AddCarAsync(Context ctxt, int year, string make, string model)
{
Define define = new Define("cars", Guid.NewGuid().ToString());
define.Set("year", year);
define.Set("make", make);
define.Set("model", model);
await ctxt.Cmd.DefineAsync(define);
}
}
}
Using the Code
You can get the code on github or nuget, or use the attached snapshot.
Use the metastrings.sln file to load and build all the projects:
carsdb
- the example above, ready to run metaq
- query evaluator demo program mslib
- the metastrings
library, this is the project you'd include in your solution tests
- unit tests that cover the bases
Get the tests to pass, and you're well on your way.
How It Works
metastrings
breaks up your schema into its own schema:
Tables
have the table name and whether the primary key is numeric. Names
are the columns, and have the column name and whether the column is numeric. Values
are all the unique values for column data or primary keys in the entire database. Values
can be numbers or string
s. Items
are the rows, and have the primary key value. ItemNameValues
are the cells in the table, gluing together Items
, Names
and Values
.
So that's the secret sauce. Completely dynamic, but it cannot possibly be fast.
Tables, a Simple Class
Tables
is much like the other class-per-table types (Names
, Values
, etc.), and it is the simplest:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace metastrings
{
public class TableObj
{
public int id;
public string name;
public bool isNumeric;
}
public static class Tables
{
internal static string[] CreateSql
{
get
{
return new[]
{
"CREATE TABLE tables\n(\n" +
"id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE,\n" +
"name TEXT NOT NULL UNIQUE,\n" +
"isNumeric BOOLEAN NOT NULL\n" +
")"
};
}
}
public static void Reset(Context ctxt)
{
sm_cache.Clear();
sm_cacheBack.Clear();
ctxt.Db.ExecuteSql("DELETE FROM tables");
}
public static async Task<int> GetIdAsync(Context ctxt, string name,
bool isNumeric = false, bool noCreate = false, bool noException = false)
{
int id;
if (sm_cache.TryGetValue(name, out id))
return id;
if (!Utils.IsWord(name))
throw new MetaStringsException($"Types.GetId name is not valid: {name}");
if (Utils.IsNameReserved(name))
throw new MetaStringsException($"Types.GetId name is reserved: {name}");
Exception lastExp = null;
bool isExpFinal = false;
for (int tryCount = 1; tryCount <= 4; ++tryCount)
{
try
{
Dictionary<string, object> cmdParams = new Dictionary<string, object>();
cmdParams.Add("@name", name);
string selectSql = "SELECT id FROM tables WHERE name = @name";
object idObj = await ctxt.Db.ExecuteScalarAsync
(selectSql, cmdParams).ConfigureAwait(false);
id = Utils.ConvertDbInt32(idObj);
if (id >= 0)
{
sm_cache[name] = id;
return id;
}
if (noCreate)
{
if (noException)
return -1;
isExpFinal = true;
throw new MetaStringsException
($"Tables.GetId cannot create new table: {name}", lastExp);
}
cmdParams.Add("@isNumeric", isNumeric);
string insertSql = "INSERT INTO tables (name, isNumeric)
VALUES (@name, @isNumeric)";
id = (int)await ctxt.Db.ExecuteInsertAsync
(insertSql, cmdParams).ConfigureAwait(false);
sm_cache[name] = id;
return id;
}
catch (Exception exp)
{
if (isExpFinal)
throw exp;
lastExp = exp;
}
}
throw new MetaStringsException("Tables.GetId fails after a few tries", lastExp);
}
public static async Task<TableObj> GetTableAsync(Context ctxt, int id)
{
if (id < 0)
return null;
TableObj obj;
if (sm_cacheBack.TryGetValue(id, out obj))
return obj;
string sql = $"SELECT name, isNumeric FROM tables WHERE id = {id}";
using (var reader = await ctxt.Db.ExecuteReaderAsync(sql).ConfigureAwait(false))
{
if (!await reader.ReadAsync().ConfigureAwait(false))
throw new MetaStringsException
($"Tables.GetTable fails to find record: {id}");
obj =
new TableObj()
{
id = id,
name = reader.GetString(0),
isNumeric = reader.GetBoolean(1)
};
sm_cacheBack[id] = obj;
return obj;
}
}
internal static void ClearCaches()
{
sm_cache.Clear();
sm_cacheBack.Clear();
}
private static ConcurrentDictionary<string, int> sm_cache =
new ConcurrentDictionary<string, int>();
private static ConcurrentDictionary<int, TableObj> sm_cacheBack =
new ConcurrentDictionary<int, TableObj>();
}
}
Sql, Where the Magic Happens
The Sql
class is where SQL queries are parsed into a dynamic SQL data structure, and where parsed dynamic SQL is converted into real SQLite SQL against the above schema.
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text;
using System.Linq;
namespace metastrings
{
public static class Sql
{
private enum SqlState
{
SELECT,
FROM,
WHERE,
ORDER,
LIMIT
}
public static Select Parse(string sql)
{
string[] tokens = Utils.Tokenize(sql);
if (tokens.Length == 0 ||
(tokens.Length == 1 && string.IsNullOrWhiteSpace(tokens[0])))
throw new SqlException("No tokens", sql);
Select select = new Select();
SqlState state = SqlState.SELECT;
int idx = 0;
while (idx < tokens.Length)
{
string currentToken = tokens[idx];
if (state == SqlState.SELECT)
{
if (!currentToken.Equals("SELECT", StringComparison.OrdinalIgnoreCase))
throw new SqlException("No SELECT", sql);
select.select = new List<string>();
while (true)
{
++idx;
if (idx >= tokens.Length)
throw new SqlException("No SELECT columns", sql);
currentToken = tokens[idx];
bool lastColumn = !currentToken.EndsWith
(",", StringComparison.Ordinal);
if (!lastColumn)
currentToken = currentToken.TrimEnd(',');
Utils.ValidateColumnName(currentToken, sql);
select.select.Add(currentToken);
if (lastColumn)
break;
}
++idx;
state = SqlState.FROM;
continue;
}
if (state == SqlState.FROM)
{
if (!currentToken.Equals("FROM", StringComparison.OrdinalIgnoreCase))
throw new SqlException("No FROM", sql);
++idx;
if (idx >= tokens.Length)
throw new SqlException("No FROM table", sql);
currentToken = tokens[idx];
Utils.ValidateTableName(currentToken, sql);
select.from = currentToken;
++idx;
state = SqlState.WHERE;
continue;
}
if (state == SqlState.WHERE)
{
if (!currentToken.Equals("WHERE", StringComparison.OrdinalIgnoreCase))
{
state = SqlState.ORDER;
continue;
}
CriteriaSet criteriaSet = new CriteriaSet();
select.where = new List<CriteriaSet> { criteriaSet };
++idx;
while ((idx + 3) <= tokens.Length)
{
var criteria =
new Criteria()
{
name = tokens[idx++],
op = tokens[idx++],
paramName = tokens[idx++]
};
Utils.ValidateColumnName(criteria.name, sql);
Utils.ValidateOperator(criteria.op, sql);
Utils.ValidateParameterName(criteria.paramName, sql);
criteriaSet.AddCriteria(criteria);
if
(
(idx + 3) <= tokens.Length
&&
tokens[idx].Equals("AND", StringComparison.OrdinalIgnoreCase)
)
{
++idx;
continue;
}
else
{
break;
}
}
if (criteriaSet.criteria.Count == 0)
throw new SqlException("No WHERE criteria", sql);
state = SqlState.ORDER;
continue;
}
if (state == SqlState.ORDER)
{
string nextToken = (idx + 1) < tokens.Length ? tokens[idx + 1] : "";
if
(
(idx + 3) > tokens.Length
||
!currentToken.Equals("ORDER", StringComparison.OrdinalIgnoreCase)
||
!nextToken.Equals("BY", StringComparison.OrdinalIgnoreCase)
)
{
state = SqlState.LIMIT;
continue;
}
idx += 2;
var orders = new List<Order>();
select.orderBy = orders;
while (idx < tokens.Length)
{
currentToken = tokens[idx];
bool currentEnds = idx == tokens.Length - 1 ||
currentToken.EndsWith(",", StringComparison.Ordinal);
nextToken = "ASC";
if (!currentEnds)
{
if ((idx + 1) < tokens.Length)
nextToken = tokens[++idx];
}
bool nextEnds = nextToken.EndsWith(",", StringComparison.Ordinal);
bool isLimit = nextToken.Equals
("LIMIT", StringComparison.OrdinalIgnoreCase);
bool lastColumn = isLimit || !(currentEnds || nextEnds);
currentToken = currentToken.TrimEnd(',');
nextToken = nextToken.TrimEnd(',');
bool isDescending;
{
if (nextToken.Equals
("ASC", StringComparison.OrdinalIgnoreCase))
isDescending = false;
else if (nextToken.Equals
("DESC", StringComparison.OrdinalIgnoreCase))
isDescending = true;
else if (isLimit)
isDescending = false;
else
throw new SqlException("Invalid ORDER BY", sql);
}
Utils.ValidateColumnName(currentToken, sql);
var orderObj = new Order()
{ field = currentToken, descending = isDescending };
orders.Add(orderObj);
if (!isLimit)
++idx;
if (lastColumn)
break;
}
state = SqlState.LIMIT;
continue;
}
if (state == SqlState.LIMIT)
{
if (currentToken.Equals("LIMIT", StringComparison.OrdinalIgnoreCase))
{
++idx;
if (idx >= tokens.Length)
throw new SqlException("No LIMIT value", sql);
currentToken = tokens[idx];
int limitVal;
if (!int.TryParse(currentToken, out limitVal))
throw new SqlException("Invalid LIMIT value", sql);
select.limit = limitVal;
++idx;
break;
}
else
{
throw new SqlException("Invalid final statement", sql);
}
}
throw new SqlException($"Invalid SQL parser state: {state}", sql);
}
if (idx < tokens.Length - 1)
throw new SqlException("Not all parsed", sql);
if (select.select.Count == 0)
throw new SqlException("No SELECT columns", sql);
if (string.IsNullOrWhiteSpace(select.from))
throw new SqlException("No FROM", sql);
return select;
}
public static async Task<string> GenerateSqlAsync(Context ctxt, Select query)
{
if (string.IsNullOrWhiteSpace(query.from))
throw new MetaStringsException("Invalid query, FROM is missing");
if (query.select == null || query.select.Count == 0)
throw new MetaStringsException("Invalid query, SELECT is empty");
if (query.orderBy != null)
{
foreach (var order in query.orderBy)
{
string orderField = order.field.Trim();
if (!query.select.Contains(orderField))
{
throw
new MetaStringsException
(
"Invalid query, ORDER BY columns must be present in
SELECT column list: " +
$"{order.field.Trim()}"
);
}
}
}
if (query.where != null)
{
foreach (var criteriaSet in query.where)
{
foreach (var criteria in criteriaSet.criteria)
{
Utils.ValidateColumnName(criteria.name, "WHERE");
Utils.ValidateOperator(criteria.op, "WHERE");
Utils.ValidateParameterName(criteria.paramName, "WHERE");
}
}
}
int tableId = await Tables.GetIdAsync(ctxt, query.from,
noCreate: true, noException: true).ConfigureAwait(false);
TableObj tableObj =
await Tables.GetTableAsync(ctxt, tableId).ConfigureAwait(false);
var names = new List<string>();
names.AddRange(query.select);
if (query.orderBy != null)
names.AddRange(query.orderBy.Select(o => o.field));
if (query.where != null)
{
foreach (var criteriaSet in query.where)
names.AddRange(criteriaSet.criteria.Select(w => w.name));
}
names = names.Select(n => n.Trim()).Where
(n => !string.IsNullOrEmpty(n)).Distinct().ToList();
var nameObjs = new Dictionary<string, NameObj>(names.Count);
foreach (var name in names)
{
if (Utils.IsNameReserved(name))
{
nameObjs.Add(name, null);
}
else
{
NameObj nameObj;
{
int nameId = await Names.GetIdAsync(ctxt, tableId, name,
noCreate: true, noException: true).ConfigureAwait(false);
if (nameId < 0)
nameObj = null;
else
nameObj = await Names.GetNameAsync(ctxt, nameId);
}
nameObjs.Add(name, nameObj);
}
}
string selectPart = "";
foreach (var name in query.select.Select(n => n.Trim()).Where
(n => !string.IsNullOrWhiteSpace(n)))
{
var cleanName = Utils.CleanName(name);
if (selectPart.Length > 0)
selectPart += ",\r\n";
if (name == "value")
{
if (tableObj == null)
selectPart += "NULL";
else if (tableObj.isNumeric)
selectPart += "bv.numberValue";
else
selectPart += "bv.stringValue";
}
else if (name == "id")
selectPart += "i.id";
else if (name == "created")
selectPart += "i.created";
else if (name == "lastmodified")
selectPart += "i.lastmodified";
else if (name == "count")
selectPart += "COUNT(*)";
else if (nameObjs[name] == null)
selectPart += "NULL";
else if (nameObjs[name].isNumeric)
selectPart += $"iv{cleanName}.numberValue";
else
selectPart += $"iv{cleanName}.stringValue";
selectPart += $" AS {cleanName}";
}
selectPart = "SELECT\r\n" + selectPart;
string fromPart = "FROM\r\nitems AS i";
if (nameObjs.ContainsKey("value"))
fromPart += "\r\nJOIN bvalues bv ON bv.id = i.valueid";
foreach (var name in names.Select(n => n.Trim()).Where
(n => !string.IsNullOrWhiteSpace(n)))
{
if (!Utils.IsNameReserved(name) && nameObjs.ContainsKey(name) &&
nameObjs[name] != null)
{
var cleanName = Utils.CleanName(name);
fromPart +=
$"\r\nLEFT OUTER JOIN itemvalues AS iv{cleanName}
ON iv{cleanName}.itemid = i.id" +
$" AND iv{cleanName}.nameid = {nameObjs[name].id}";
}
}
string wherePart = $"i.tableid = {tableId}";
if (query.where != null)
{
foreach (var criteriaSet in query.where)
{
if (criteriaSet.criteria.Count == 0)
continue;
wherePart += "\r\nAND\r\n";
wherePart += "(";
bool addedOneYet = false;
foreach (var where in criteriaSet.criteria)
{
string name = where.name.Trim();
if (string.IsNullOrWhiteSpace(name))
continue;
if (!addedOneYet)
addedOneYet = true;
else
wherePart += $" {Enum.GetName
(criteriaSet.combine.GetType(), criteriaSet.combine)} ";
var nameObj = nameObjs[name];
var cleanName = Utils.CleanName(name);
if (cleanName == "id")
{
wherePart += $"i.id {where.op} {where.paramName}";
}
else if (cleanName == "value")
{
if (tableObj == null)
wherePart += "1 = 0";
else if (tableObj.isNumeric)
wherePart += $"bv.numberValue {where.op} {where.paramName}";
else
wherePart += $"bv.stringValue {where.op} {where.paramName}";
}
else if (cleanName == "created" || cleanName == "lastmodified")
{
wherePart += $"{cleanName} {where.op} {where.paramName}";
}
else if (nameObj == null)
{
wherePart += "1 = 0";
}
else if (nameObj.isNumeric)
{
wherePart += $"iv{cleanName}.numberValue
{where.op} {where.paramName}";
}
else
{
wherePart += $"iv{cleanName}.stringValue
{where.op} {where.paramName}";
}
}
wherePart += ")";
}
}
wherePart = "WHERE " + wherePart;
string orderBy = "";
if (query.orderBy != null)
{
foreach (var order in query.orderBy)
{
if (string.IsNullOrWhiteSpace(order.field))
continue;
if (orderBy.Length > 0)
orderBy += ",\r\n";
string orderColumn = order.field;
if (!Utils.IsNameReserved(order.field))
Utils.CleanName(order.field);
orderBy += orderColumn + (order.descending ? " DESC" : " ASC");
}
if (orderBy.Length > 0)
orderBy = "ORDER BY\r\n" + orderBy;
}
string limitPart = "";
if (query.limit > 0)
limitPart = $"LIMIT\r\n{query.limit}";
StringBuilder sb = new StringBuilder();
sb.Append($"{selectPart.Trim()}");
sb.Append($"\r\n\r\n{fromPart}");
if (!string.IsNullOrWhiteSpace(wherePart))
{
sb.Append($"\r\n\r\n{wherePart}");
}
if (!string.IsNullOrWhiteSpace(orderBy))
{
sb.Append($"\r\n\r\n{orderBy}");
}
if (!string.IsNullOrWhiteSpace(limitPart))
{
sb.Append($"\r\n\r\n{limitPart}");
}
string sql = sb.ToString();
return sql;
}
}
}
Context, Connection String Management and SQL Helper Functions
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Data;
using System.Data.Common;
using System.Data.SQLite;
using System.IO;
namespace metastrings
{
public class Context : IDisposable
{
public Context(string dbConnStr)
{
string actualDbConnStr;
if (!sm_dbConnStrs.TryGetValue(dbConnStr, out actualDbConnStr))
{
lock (sm_dbBuildLock)
{
if (!sm_dbConnStrs.TryGetValue(dbConnStr, out actualDbConnStr))
{
actualDbConnStr = dbConnStr;
if (!IsDbServer(actualDbConnStr))
{
string dbFilePath = DbConnStrToFilePath(actualDbConnStr);
actualDbConnStr = "Data Source=" + dbFilePath;
if (!(File.Exists(dbFilePath) &&
new FileInfo(dbFilePath).Length > 0))
{
SQLiteConnection.CreateFile(dbFilePath);
using (var db = new SqlLiteDb(actualDbConnStr))
{
RunSql(db, Tables.CreateSql);
RunSql(db, Names.CreateSql);
RunSql(db, Values.CreateSql);
RunSql(db, Items.CreateSql);
RunSql(db, LongStrings.CreateSql);
}
}
using (var db = new SqlLiteDb(actualDbConnStr))
RunSql(db, new[] { "PRAGMA journal_mode = WAL",
"PRAGMA synchronous = NORMAL" });
}
sm_dbConnStrs[dbConnStr] = actualDbConnStr;
}
}
}
IsServerDb = IsDbServer(actualDbConnStr);
if (IsServerDb)
Db = new MySqlDb(actualDbConnStr);
else
Db = new SqlLiteDb(actualDbConnStr);
}
public void Dispose()
{
if (Db != null)
{
Db.Dispose();
Db = null;
}
if (m_postItemOps != null && m_postItemOps.Count > 0)
throw new MetaStringsException
("Post ops remain; call ProcessPostOpsAsync
before disposing the metastrings context");
}
public IDb Db { get; private set; }
public bool IsServerDb { get; private set; }
public Command Cmd => new Command(this);
public MsTrans BeginTrans()
{
return Db.BeginTrans();
}
public async Task<DbDataReader> ExecSelectAsync(Select select)
{
var cmdParams = select.cmdParams;
var sql = await Sql.GenerateSqlAsync(this, select).ConfigureAwait(false);
return await Db.ExecuteReaderAsync(sql, cmdParams).ConfigureAwait(false);
}
public async Task<object> ExecScalarAsync(Select select)
{
var cmdParams = select.cmdParams;
var sql = await Sql.GenerateSqlAsync(this, select).ConfigureAwait(false);
return await Db.ExecuteScalarAsync(sql, cmdParams).ConfigureAwait(false);
}
public async Task<long> ExecScalar64Async(Select select)
{
object result = await ExecScalarAsync(select).ConfigureAwait(false);
long val = Utils.ConvertDbInt64(result);
return val;
}
public async Task<List<T>> ExecListAsync<T>(Select select)
{
var values = new List<T>();
using (var reader = await ExecSelectAsync(select).ConfigureAwait(false))
{
while (await reader.ReadAsync().ConfigureAwait(false))
values.Add((T)reader.GetValue(0));
}
return values;
}
public async Task<ListDictionary<K, V>> ExecDictAsync<K, V>(Select select)
{
var values = new ListDictionary<K, V>();
using (var reader = await ExecSelectAsync(select).ConfigureAwait(false))
{
while (await reader.ReadAsync().ConfigureAwait(false))
values.Add((K)reader.GetValue(0), (V)reader.GetValue(1));
}
return values;
}
public async Task<long> GetRowIdAsync(string tableName, object key)
{
Utils.ValidateTableName(tableName, "GetRowId");
Select select = Sql.Parse($"SELECT id FROM {tableName} WHERE value = @value");
select.AddParam("@value", key);
long id = await ExecScalar64Async(select).ConfigureAwait(false);
return id;
}
public async Task<object> GetRowValueAsync(string table, long id)
{
Utils.ValidateTableName(table, "GetRowValueAsync");
Select select = Sql.Parse($"SELECT value FROM {table} WHERE id = @id");
select.AddParam("@id", id);
object val = await ExecScalarAsync(select).ConfigureAwait(false);
return val;
}
public async Task ProcessPostOpsAsync()
{
if (m_postItemOps == null || m_postItemOps.Count == 0)
return;
var totalTimer = ScopeTiming.StartTiming();
try
{
using (var msTrans = BeginTrans())
{
foreach (string sql in m_postItemOps)
await Db.ExecuteSqlAsync(sql).ConfigureAwait(false);
msTrans.Commit();
}
}
finally
{
m_postItemOps.Clear();
ScopeTiming.RecordScope("ProcessItemPostOps", totalTimer);
}
}
internal void AddPostOp(string sql)
{
if (m_postItemOps == null)
m_postItemOps = new List<string>();
m_postItemOps.Add(sql);
}
internal void ClearPostOps()
{
if (m_postItemOps != null)
m_postItemOps.Clear();
}
private List<string> m_postItemOps;
private static string DbConnStrToFilePath(string connStr)
{
if (IsDbServer(connStr))
throw new MetaStringsException("Connection string is not for file-based DB");
string filePath = connStr;
int equals = filePath.IndexOf('=');
if (equals > 0)
filePath = filePath.Substring(equals + 1);
filePath = filePath.Replace("[UserRoaming]",
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
filePath = filePath.Replace("[MyDocuments]",
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
string directoryPath = Path.GetDirectoryName(filePath);
if (!Directory.Exists(directoryPath))
Directory.CreateDirectory(directoryPath);
return filePath;
}
private static bool IsDbServer(string connStr)
{
bool isServer = connStr.IndexOf
("Server=", 0, StringComparison.OrdinalIgnoreCase) >= 0;
return isServer;
}
private static void RunSql(IDb db, string[] sqlQueries)
{
foreach (string sql in sqlQueries)
db.ExecuteSql(sql);
}
private static object sm_dbBuildLock = new object();
private static ConcurrentDictionary<string, string> sm_dbConnStrs =
new ConcurrentDictionary<string, string>();
}
}
Command, Where the Action Is
The Command
class implements the Define UPSERT
functionality, as well as the DELETE
and DROP
commands.
Note that there is more functionality here than is covered in the cars
DB demo.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace metastrings
{
public class Command
{
public Command(Context ctxt)
{
Ctxt = ctxt;
}
public async Task DefineAsync(Define define)
{
var totalTimer = ScopeTiming.StartTiming();
try
{
var localTimer = ScopeTiming.StartTiming();
bool isKeyNumeric = !(define.key is string);
int tableId = await Tables.GetIdAsync
(Ctxt, define.table, isKeyNumeric).ConfigureAwait(false);
long valueId = await Values.GetIdAsync
(Ctxt, define.key).ConfigureAwait(false);
long itemId = await Items.GetIdAsync
(Ctxt, tableId, valueId).ConfigureAwait(false);
ScopeTiming.RecordScope("Define.Setup", localTimer);
if (define.metadata != null)
{
var nameValueIds = new Dictionary<int, long>();
foreach (var kvp in define.metadata)
{
bool isMetadataNumeric = !(kvp.Value is string);
int nameId = await Names.GetIdAsync
(Ctxt, tableId, kvp.Key, isMetadataNumeric).ConfigureAwait(false);
if (kvp.Value == null)
{
nameValueIds[nameId] = -1;
continue;
}
bool isNameNumeric = await Names.GetNameIsNumericAsync
(Ctxt, nameId).ConfigureAwait(false);
bool isValueNumeric = !(kvp.Value is string);
if (isValueNumeric != isNameNumeric)
{
throw
new MetaStringsException
(
$"Data numeric does not match name: {kvp.Key}" +
$"\n - value is numeric: {isValueNumeric} - {kvp.Value}" +
$"\n - name is numeric: {isNameNumeric}"
);
}
nameValueIds[nameId] =
await Values.GetIdAsync(Ctxt, kvp.Value).ConfigureAwait(false);
}
ScopeTiming.RecordScope("Define.NameIds", localTimer);
Items.SetItemData(Ctxt, itemId, nameValueIds);
ScopeTiming.RecordScope("Define.ItemsCommit", localTimer);
}
await Ctxt.ProcessPostOpsAsync().ConfigureAwait(false);
ScopeTiming.RecordScope("Define.PostOps", localTimer);
}
#if !DEBUG
catch
{
Ctxt.ClearPostOps();
throw;
}
#endif
finally
{
ScopeTiming.RecordScope("Define", totalTimer);
}
}
public async Task<string> GenerateSqlAsync(Select query)
{
var totalTimer = ScopeTiming.StartTiming();
try
{
string sql = await Sql.GenerateSqlAsync(Ctxt, query).ConfigureAwait(false);
return sql;
}
finally
{
ScopeTiming.RecordScope("Cmd.GenerateSql", totalTimer);
}
}
public async Task<GetResponse> GetAsync(GetRequest request)
{
var totalTimer = ScopeTiming.StartTiming();
try
{
var responses = new List<Dictionary<string, object>>(request.values.Count);
int tableId = await Tables.GetIdAsync
(Ctxt, request.table, noCreate: true).ConfigureAwait(false);
foreach (var value in request.values)
{
long valueId =
await Values.GetIdAsync(Ctxt, value).ConfigureAwait(false);
long itemId = await Items.GetIdAsync
(Ctxt, tableId, valueId, noCreate: true).ConfigureAwait(false);
if (itemId < 0)
{
responses.Add(null);
continue;
}
var metaIds = await Items.GetItemDataAsync
(Ctxt, itemId).ConfigureAwait(false);
var metaStrings = await NameValues.GetMetadataValuesAsync
(Ctxt, metaIds).ConfigureAwait(false);
responses.Add(metaStrings);
}
GetResponse response = new GetResponse() { metadata = responses };
return response;
}
finally
{
ScopeTiming.RecordScope("Cmd.Get", totalTimer);
}
}
public async Task<GetResponse> QueryGetAsync(QueryGetRequest request)
{
var totalTimer = ScopeTiming.StartTiming();
try
{
var itemValues = new Dictionary<long, object>();
{
Select select = new Select();
select.select = new List<string> { "id", "value" };
select.from = request.from;
select.where = request.where;
select.orderBy = request.orderBy;
select.limit = request.limit;
select.cmdParams = request.cmdParams;
using (var reader =
await Ctxt.ExecSelectAsync(select).ConfigureAwait(false))
{
while (await reader.ReadAsync().ConfigureAwait(false))
itemValues.Add(reader.GetInt64(0), reader.GetValue(1));
}
}
var responses = new List<Dictionary<string, object>>(itemValues.Count);
foreach (var itemId in itemValues.Keys)
{
var metaIds = await Items.GetItemDataAsync
(Ctxt, itemId).ConfigureAwait(false);
var metaStrings = await NameValues.GetMetadataValuesAsync
(Ctxt, metaIds).ConfigureAwait(false);
metaStrings["id"] = (double)itemId;
metaStrings["value"] = itemValues[itemId];
responses.Add(metaStrings);
}
GetResponse response = new GetResponse() { metadata = responses };
return response;
}
finally
{
ScopeTiming.RecordScope("Cmd.QueryGet", totalTimer);
}
}
public async Task DeleteAsync(string table, object value)
{
await DeleteAsync(new Delete(table, value)).ConfigureAwait(false);
}
public async Task DeleteAsync(string table, IEnumerable<object> values)
{
await DeleteAsync(new Delete(table, values)).ConfigureAwait(false);
}
public async Task DeleteAsync(Delete toDelete)
{
var totalTimer = ScopeTiming.StartTiming();
try
{
int tableId = await Tables.GetIdAsync(Ctxt, toDelete.table,
noCreate: true, noException: true).ConfigureAwait(false);
if (tableId < 0)
return;
foreach (var val in toDelete.values)
{
long valueId = await Values.GetIdAsync(Ctxt, val).ConfigureAwait(false);
string sql = $"DELETE FROM items
WHERE valueid = {valueId} AND tableid = {tableId}";
Ctxt.AddPostOp(sql);
}
await Ctxt.ProcessPostOpsAsync().ConfigureAwait(false);
}
finally
{
ScopeTiming.RecordScope("Cmd.Delete", totalTimer);
}
}
public async Task DropAsync(string table)
{
var totalTimer = ScopeTiming.StartTiming();
try
{
NameValues.ClearCaches();
int tableId = await Tables.GetIdAsync
(Ctxt, table, noCreate: true, noException: true).ConfigureAwait(false);
if (tableId < 0)
return;
await Ctxt.Db.ExecuteSqlAsync($"DELETE FROM itemnamevalues
WHERE nameid IN (SELECT id FROM names
WHERE tableid = {tableId})").ConfigureAwait(false);
await Ctxt.Db.ExecuteSqlAsync($"DELETE FROM names
WHERE tableid = {tableId}").ConfigureAwait(false);
await Ctxt.Db.ExecuteSqlAsync($"DELETE FROM items
WHERE tableid = {tableId}").ConfigureAwait(false);
await Ctxt.Db.ExecuteSqlAsync($"DELETE FROM tables
WHERE id = {tableId}").ConfigureAwait(false);
NameValues.ClearCaches();
}
finally
{
ScopeTiming.RecordScope("Cmd.Drop", totalTimer);
}
}
public void Reset(bool includeNameValues = false)
{
if (includeNameValues)
NameValues.Reset(Ctxt);
else
Items.Reset(Ctxt);
NameValues.ClearCaches();
}
public async Task<SchemaResponse> GetSchemaAsync(string table)
{
string sql =
"SELECT t.name AS tablename, n.name AS colname " +
"FROM tables t JOIN names n ON n.tableid = t.id";
string requestedTable = table;
bool haveRequestedTableName = !string.IsNullOrWhiteSpace(requestedTable);
if (haveRequestedTableName)
sql += " WHERE t.name = @name";
sql += " ORDER BY tablename, colname";
Dictionary<string, object> cmdParams = new Dictionary<string, object>();
if (haveRequestedTableName)
cmdParams.Add("@name", requestedTable);
var responseDict = new ListDictionary<string, List<string>>();
using (var reader = await Ctxt.Db.ExecuteReaderAsync
(sql, cmdParams).ConfigureAwait(false))
{
while (await reader.ReadAsync().ConfigureAwait(false))
{
string curTable = reader.GetString(0);
string colname = reader.GetString(1);
if (!responseDict.ContainsKey(curTable))
responseDict.Add(curTable, new List<string>());
responseDict[curTable].Add(colname);
}
}
SchemaResponse response = new SchemaResponse() { tables = responseDict };
return response;
}
public async Task CreateTableAsync(string name, bool isNumeric)
{
await Tables.GetIdAsync(Ctxt, name, isNumeric).ConfigureAwait(false);
}
public async Task PutLongStringAsync(LongStringPut put)
{
await LongStrings.StoreStringAsync
(Ctxt, put.itemId, put.fieldName, put.longString).ConfigureAwait(false);
}
public async Task<string> GetLongStringAsync(LongStringOp get)
{
string longString = await LongStrings.GetStringAsync
(Ctxt, get.itemId, get.fieldName).ConfigureAwait(false);
return longString;
}
public async Task DeleteLongStringAsync(LongStringOp del)
{
await LongStrings.DeleteStringAsync
(Ctxt, del.itemId, del.fieldName).ConfigureAwait(false);
}
private Context Ctxt;
}
}
Conclusion and Points of Interest
As you can see, metastrings
will serve you well as a file-based dynamic database... and now you know how it works, too.
Enjoy!
History
- 9th August, 2021: Initial version