diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.Const.cs b/Runtime/GuruCore/Runtime/Analytics/Analytics.Const.cs index f1d9dcf..32b8ad0 100644 --- a/Runtime/GuruCore/Runtime/Analytics/Analytics.Const.cs +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.Const.cs @@ -114,6 +114,7 @@ namespace Guru // 经济相关 public static readonly string ParameterBalance = "balance"; // 用于余额 public static readonly string ParameterSku = "sku"; // sku + public static readonly string ParameterScene = "scene"; // sku public static readonly string ParameterVirtualCurrencyName = "virtual_currency_name"; // 虚拟货币名称 } } \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.Economic.cs b/Runtime/GuruCore/Runtime/Analytics/Analytics.Economic.cs index a650af8..80e1e71 100644 --- a/Runtime/GuruCore/Runtime/Analytics/Analytics.Economic.cs +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.Economic.cs @@ -72,65 +72,59 @@ namespace Guru /// - /// 获取虚拟货币 + /// 获取道具/货币 /// - /// - /// - /// + /// /// - /// - /// - /// - /// - public static void EarnVirtualCurrency(string currencyName, int value, int balance, - string method = "", - string levelName = "", - bool isIap = false, - string sku = "", - string scene = "") + /// + /// + /// + public static void EarnVirtualCurrency(string virtualCurrencyName, string method, int balance, int value, + string methodDetails = "") { - if (isIap) method = "iap_buy"; var data = new Dictionary() { - { ParameterVirtualCurrencyName, currencyName }, + { ParameterVirtualCurrencyName, virtualCurrencyName }, + { ParameterItemCategory, method }, + { ParameterItemName, methodDetails }, { ParameterValue, value }, { ParameterBalance, balance }, - { ParameterLevelName, levelName }, - { ParameterItemCategory, method }, }; - - if (!string.IsNullOrEmpty(scene)) data[ParameterItemName] = scene; // 获取的虚拟货币或者道具的场景 - if (!string.IsNullOrEmpty(sku)) data[ParameterSku] = sku; // 商品的 sku - LogEvent(EventEarnVirtualCurrency, data, new EventSetting() { EnableFirebaseAnalytics = true }); // FB 上报收入点 - FB.LogAppEvent(EventEarnVirtualCurrency, value, data); + // FB.LogAppEvent(EventEarnVirtualCurrency, value, data); } - - public static void SpendVirtualCurrency(string currencyName, int value, int balance, - string method = "", - string levelName = "", - string scene = "") + /// + /// 消耗道具/货币 + /// + /// + /// + /// + /// + /// + /// + public static void SpendVirtualCurrency(string contentId, string contentType, int price, + string virtualCurrencyName, int balance, string scene = "") { var data = new Dictionary() { - { ParameterVirtualCurrencyName, currencyName }, - { ParameterValue, value }, + { ParameterVirtualCurrencyName, virtualCurrencyName }, + { ParameterValue, price }, { ParameterBalance, balance }, - { ParameterLevelName, levelName }, - { ParameterItemCategory, method }, + { ParameterItemName, contentId }, + { ParameterItemCategory, contentType }, }; - - if (!string.IsNullOrEmpty(scene)) data[ParameterItemName] = scene; // 获取的虚拟货币或者道具的场景 + if (!string.IsNullOrEmpty(scene)) data[ParameterScene] = scene; // 获取的虚拟货币或者道具的场景 LogEvent(EventSpendVirtualCurrency, data, new EventSetting() { EnableFirebaseAnalytics = true }); // FB 上报消费点 - FB.LogAppEvent(EventSpendVirtualCurrency, value, data); + // FB.LogAppEvent(EventSpendVirtualCurrency, value, data); + // FB 上报消耗事件买量点 - FBSpentCredits(value, scene, method); // 点位信息有变化 + FBSpentCredits(contentId, contentType, price); // 点位信息有变化 } @@ -140,7 +134,7 @@ namespace Guru /// /// /// - private static void FBSpentCredits(int amount, string contentId, string contentType) + private static void FBSpentCredits(string contentId, string contentType, float amount) { FB.LogAppEvent(AppEventName.SpentCredits, amount, new Dictionary() diff --git a/Runtime/GuruInventory.meta b/Runtime/GuruInventory.meta new file mode 100644 index 0000000..ac8a012 --- /dev/null +++ b/Runtime/GuruInventory.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 93bb1ade31b44cc9bb3dbad3051cbe78 +timeCreated: 1706922645 \ No newline at end of file diff --git a/Runtime/GuruInventory/README.md b/Runtime/GuruInventory/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Runtime/GuruInventory/README.md.meta b/Runtime/GuruInventory/README.md.meta new file mode 100644 index 0000000..2475cd2 --- /dev/null +++ b/Runtime/GuruInventory/README.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1f4820a46a3a4b02ac85a48a4e25014c +timeCreated: 1706922699 \ No newline at end of file diff --git a/Runtime/GuruInventory/Runtime.meta b/Runtime/GuruInventory/Runtime.meta new file mode 100644 index 0000000..303bb17 --- /dev/null +++ b/Runtime/GuruInventory/Runtime.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e921121b06404396b164c6538e68c6aa +timeCreated: 1706922657 \ No newline at end of file diff --git a/Runtime/GuruInventory/Runtime/InventoryData.cs b/Runtime/GuruInventory/Runtime/InventoryData.cs new file mode 100644 index 0000000..98ae1f9 --- /dev/null +++ b/Runtime/GuruInventory/Runtime/InventoryData.cs @@ -0,0 +1,131 @@ + +namespace Guru +{ + using System; + using System.Collections.Generic; + using Newtonsoft.Json; + + [Serializable] + public class InventoryData: IJsonData + { + [JsonProperty("v")] + public List valid; + [JsonProperty("e")] + public List expired; + + public static InventoryData FromJson(string json) + { + if (JsonDataHelper.Parse(json, out var d)) + { + return d; + } + return new InventoryData(); + } + + public InventoryData() + { + valid = new List(); + expired = new List(); + } + + public InventoryData(List valid, List expired) + { + this.valid = valid; + this.expired = expired; + } + + public static InventoryData Create(LimitedBalance balance = null) + { + var t = new InventoryData(); + if (balance != null) + { + if (balance.expireAt > InventoryManager.CurrentTimeInMillis) + { + t.valid.Add(balance); + } + } + return t; + } + + + public InventoryData AttachLimitedBalance(LimitedBalance balance) + { + if (balance.expireAt > InventoryManager.CurrentTimeInMillis) + { + valid.Add(balance); + return new InventoryData() + { + valid = this.valid, + expired = this.expired, + }; + } + return this; + } + + /// + /// 回收过期的道具 + /// + /// + public RecycleResult RecycleExpiredBalance() + { + long now = InventoryManager.CurrentTimeInMillis; + List newValid = new List(); + List newExpired = new List(); + int expiredBalance = 0; + foreach(var item in valid) { + if (now >= item.expireAt) { + expiredBalance += item.amount; + newExpired.Add(item); + } else { + newValid.Add(item); + } + } + return new RecycleResult(expiredBalance, new InventoryData(newValid, newExpired)); + } + + + } + + [Serializable] + public class LimitedBalance + { + [JsonProperty("a")] + public int amount = 0; + [JsonProperty("e")] + public long expireAt = -1; + } + + public class RecycleResult { + public int expiredBalance; + public InventoryData InventoryData; + + public RecycleResult(int expiredBalance, InventoryData inventoryData) { + this.expiredBalance = expiredBalance; + this.InventoryData = inventoryData; + } + } + + public class ConsumeResult { + public bool consumed; + public InventoryItem item; + + public ConsumeResult(InventoryItem item, bool consumed) + { + this.item = item; + this.consumed = consumed; + + } + + public static ConsumeResult Success(InventoryItem item) + { + return new ConsumeResult(item, true); + } + + public static ConsumeResult Error(InventoryItem item) + { + return new ConsumeResult(item, false); + } + + } + +} \ No newline at end of file diff --git a/Runtime/GuruInventory/Runtime/InventoryData.cs.meta b/Runtime/GuruInventory/Runtime/InventoryData.cs.meta new file mode 100644 index 0000000..c7d9488 --- /dev/null +++ b/Runtime/GuruInventory/Runtime/InventoryData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 54c27103f7594aa9bc337f5e076fb550 +timeCreated: 1706959298 \ No newline at end of file diff --git a/Runtime/GuruInventory/Runtime/InventoryManager.Const.cs b/Runtime/GuruInventory/Runtime/InventoryManager.Const.cs new file mode 100644 index 0000000..76d98ae --- /dev/null +++ b/Runtime/GuruInventory/Runtime/InventoryManager.Const.cs @@ -0,0 +1,55 @@ +namespace Guru +{ + /// + /// 交易方式 + /// + public enum TransactionMethod + { + unknown = 0, + iap, // IAP购买 + igc, // In-game currency 购买(coin/gems..) + reward, // 奖励获得 + bonus, // 优惠 + prop, // 道具 + free, + } + + /// + /// 道具列别 + /// + public class InventoryCategory + { + public const string Prop = "prop"; + } + + + public partial class InventoryManager + { + + /// + /// 获取交易方式的字段值 + /// + /// + /// + private string ConvertTransactionMethodName(TransactionMethod method) { + switch (method) { + case TransactionMethod.iap: + return "iap_buy"; + case TransactionMethod.igc: + return "igc"; + case TransactionMethod.reward: + return "reward"; + case TransactionMethod.bonus: + return "bonus"; + case TransactionMethod.prop: + return "prop"; + case TransactionMethod.free: + return "prop"; + default: + return "unknown"; + } + } + + + } +} \ No newline at end of file diff --git a/Runtime/GuruInventory/Runtime/InventoryManager.Const.cs.meta b/Runtime/GuruInventory/Runtime/InventoryManager.Const.cs.meta new file mode 100644 index 0000000..6a97470 --- /dev/null +++ b/Runtime/GuruInventory/Runtime/InventoryManager.Const.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0b4e29d06a7f4ddb860e08d3aa81610e +timeCreated: 1706945881 \ No newline at end of file diff --git a/Runtime/GuruInventory/Runtime/InventoryManager.cs b/Runtime/GuruInventory/Runtime/InventoryManager.cs new file mode 100644 index 0000000..dbe1fb0 --- /dev/null +++ b/Runtime/GuruInventory/Runtime/InventoryManager.cs @@ -0,0 +1,578 @@ + +namespace Guru +{ + using SQLite4Unity3d; + using UnityEngine; + using System.Collections.Generic; + using Newtonsoft.Json; + using System; + + public interface IInventoryDelegate + { + string GetInventoryCategory(string id); + } + + /// + /// 道具管理器 + /// + public partial class InventoryManager + { + + private static InventoryManager _instance; + public static InventoryManager Instance + { + get + { + if(_instance == null) _instance = new InventoryManager(); + return _instance; + } + } + private IInventoryDelegate _delegate; + private InventoryTable _table; + + public bool IsReady { get; private set; } = false; + + public static long CurrentTimeInMillis => TimeUtil.GetCurrentTimeStamp(); + + + /// + /// 初始化 + /// + /// + public static void Install(DBService service) + { + if (service != null) + { + Instance._table = InventoryTable.LoadOrCreate(service); + Instance.IsReady = true; + } + } + + + + + /// + /// 获取道具分类 + /// + /// + /// + public string GetInventoryCategory(string id) + { + return _delegate?.GetInventoryCategory(id) ?? InventoryCategory.Prop; + } + + private InventoryItem GetData(string sku) => _table.GetItem(sku); + + + + /// + /// 获取道具(组) + /// 通过[method]中的的特定[specific]方式 获得了指定的 [items] + /// * method: iap -> specific: sku + /// * method: igc -> specific: coin/gems... + /// * method: reward -> specific: ads/lottery/daily/... + /// * method: bonus -> specific: ads/other/... + /// * method: prop -> specific: hint/hammer/swap/magic/.. + /// + /// method 最终会在 earnVirtualCurrency 中成为 item_category + /// specific 最终会在 earnVirtualCurrency 中成为 item_name + /// + /// + /// + /// + public void Acquire(List items, TransactionMethod method, string specific = "") + { + if (!IsReady) return; + + List acquired = new List(items.Count); + string category; + StockItem item; + InventoryItem invItem; + + for (int i = 0; i < items.Count; i++) + { + item = items[i]; + category = GetInventoryCategory(item.sku); + invItem = GetData(item.sku)?.Acquire(method, item.amount) + ?? InventoryItem.Create(item.sku, category, item.attr, balance:item.amount, method: method); + acquired.Add(invItem); + + Analytics.EarnVirtualCurrency( + item.sku, + method:ConvertTransactionMethodName(method), + methodDetails:specific, + balance: invItem.balance, + value: item.amount); + } + + _table.UpdateInventoryItems(acquired); // 更新数据库信息 + } + + + public bool Consume(List items, string contentId, string scene, string category = "") + { + if (!IsReady) return false; + + List consumed = new List(items.Count); + bool isConsumed = true; + + StockItem item; + InventoryItem invItem; + + for (int i = 0; i < items.Count; i++) + { + item = items[i]; + var result = GetData(item.sku)?.Consume(scene, item.amount) ?? null; + if (result == null) + { + Debug.LogError($"consume item not found: {item.sku}"); + return false; + } + + consumed.Add(result.item); + // 这里如果没有消耗掉,将跳出 + if (!result.consumed) + { + Debug.Log($"consume failed: {item.sku}:{item.amount}"); + isConsumed = false; + break; + } + } + + if (consumed.Count == 0) + { + return false; + } + else + { + if (isConsumed && consumed.Count == items.Count) + { + for (int i = 0; i < items.Count; ++i) + { + Analytics.SpendVirtualCurrency( + contentId, category, items[i].amount, + virtualCurrencyName: consumed[i].sku, balance: consumed[i].balance); + } + } + } + + + _table.UpdateInventoryItems(consumed); // 更新数据库信息 + return true; + } + + + public bool Consume(List items, Manifest redeem) + { + return Consume(items, redeem.scene, redeem.contentId, redeem.category); + } + + + + /// + /// 判断是否可以支付 + /// + /// + /// + /// + public bool CanAfford(String id, int amount) { + var item = GetData(id); + return item != null && item.balance > amount; + } + + + } + + + // + /// 库存道具 + /// + public class StockItem + { + [PrimaryKey] + public string sku { get; set; } + public int amount { get; set; } + public int attr { get; set; } + public long expired { get; set; } + + + public static StockItem Consumable(string sku, int amount) + { + return Create(sku, amount, DetailsAttr.Consumable); + } + + public static StockItem Permanent(string sku, int amount) + { + return Create (sku, amount, DetailsAttr.Permanent); + } + + + public static StockItem Create(string sku, int amount, int att, long expiredStamp = 0) + { + return new StockItem + { + sku = sku, + amount = amount, + attr = att, + expired = expiredStamp + }; + } + } + + + [Serializable] + public class InventoryItem: IJsonData + { + [JsonProperty(InventoryTable.dbSku)] + public string sku { get; set; } + [JsonProperty(InventoryTable.dbBalance)] + public int balance { get; set; } + [JsonProperty(InventoryTable.dbCategory)] + public string category { get; set; } + [JsonProperty(InventoryTable.dbAttr)] + public int attr { get; set; } + [JsonProperty(InventoryTable.dbTimeSensitive)] + public InventoryData Inventory { get; set; } + [JsonProperty(InventoryTable.dbUpdateAt)] + public long updateAt{ get; set; } + [JsonProperty(InventoryTable.dbCreateAt)] + public long createAt{ get; set; } + [JsonProperty(InventoryTable.dbDetails)] + public InventoryDetails details { get; set; } + + public static Action ExceptionHandler; + + + + + + public static InventoryItem FromJson(string json) + { + if (JsonDataHelper.Parse(json, out var d)) + { + return d; + } + return null; + } + + public InventoryItem(string sku, int balance, string category, int attr, InventoryDetails details = null, InventoryData inventory = null, long createAt = -1, long updateAt = -1) + { + this.sku = sku; + this.category = category; + this.balance = balance; + this.attr = attr; + this.details = details; + this.Inventory = inventory; + this.createAt = createAt; + this.updateAt = updateAt; + } + + + public static InventoryItem Create(string sku, string category, int attr, + int balance = 0, InventoryDetails details = null, + InventoryData inventory = null, long createAt = -1, long updateAt = -1, + int expireAt = -1, TransactionMethod method = TransactionMethod.unknown) + { + long stamp = InventoryManager.CurrentTimeInMillis; + return new InventoryItem( + sku, + balance, + category, + attr, + details ?? InventoryDetails.Create(balance), + inventory?? InventoryData.Create(new LimitedBalance() { amount = balance}), + stamp, + stamp + ); + } + + /// + /// 获取道具 + /// + /// + /// + /// + public InventoryItem Acquire(TransactionMethod method, int amount) + { + var now = InventoryManager.CurrentTimeInMillis; + var recycled = Inventory.RecycleExpiredBalance(); + var target = balance + amount; + var newBalance = Math.Clamp(target - recycled.expiredBalance, 0, target); + return new InventoryItem(this.sku, newBalance,this.category, this.attr, + details.Acquire(method, amount), + recycled.InventoryData, + createAt, now); + } + + /// + /// 消耗道具 + /// + /// + /// + /// + public ConsumeResult Consume(string scene, int amount) + { + var now = InventoryManager.CurrentTimeInMillis; + var recycled = Inventory.RecycleExpiredBalance(); + var target = Math.Clamp(balance - recycled.expiredBalance, 0, balance); + if (target > amount && attr == DetailsAttr.Consumable) + { + target -= amount; + return ConsumeResult.Success(new InventoryItem(sku, target, category, attr, + details.Consume(scene, amount), recycled.InventoryData, createAt, now)); + } + return ConsumeResult.Error(new InventoryItem( + sku, target, category, attr, details, recycled.InventoryData, createAt, now)); + } + + } + + [Serializable] + public class InventoryDetails: IJsonData + { + [JsonProperty("a")] + public Dictionary acquired { get; set; } + + [JsonProperty("c")] + public Dictionary consumed { get; set; } + + [JsonProperty("d")] + public Dictionary data { get; set; } + + + public InventoryDetails() + { + acquired = new Dictionary(10); + consumed = new Dictionary(10); + data = new Dictionary(10); + } + + public static InventoryDetails Create(int amount) + { + var d = new InventoryDetails(); + if (amount > 0) + { + d.acquired[TransactionMethod.unknown] = amount; + } + return d; + } + + public InventoryDetails Acquire(TransactionMethod method, int amount) + { + if (acquired.TryGetValue(method, out var a)) + { + acquired[method] = a + amount; + } + else + { + acquired[method] = amount; + } + + return this; + } + + public InventoryDetails Consume(string scene, int amount) + { + if (consumed.TryGetValue(scene, out var a)) + { + consumed[scene] = a + amount; + } + else + { + consumed[scene] = amount; + } + return this; + } + + /// + /// 从 JSON 中解析 + /// + /// + /// + public InventoryDetails FromJson(string json) + { + if (JsonDataHelper.Parse(json, out var d)) + { + return d; + } + return new InventoryDetails(); + } + + public void SetValue(String key, object value) { + data[key] = value; + } + + public int GetInt(string key, int defaultValue = 0) + { + if(data.TryGetValue(key, out var value)) + { + return (int)value; + } + return defaultValue; + } + + public double GetDouble(string key, double defaultValue = 0) + { + if(data.TryGetValue(key, out var value)) + { + return (double)value; + } + return defaultValue; + } + + public string GetString(string key, string defaultValue = "") + { + if(data.TryGetValue(key, out var value)) + { + return (string)value; + } + return defaultValue; + } + + public bool GetBool(string key, bool defaultValue = false) + { + if(data.TryGetValue(key, out var value)) + { + return (bool)value; + } + return defaultValue; + } + + } + + /// + /// 内置物品表维护器 + /// + internal class InventoryTable + { + + internal const string tbName = "inventory"; // Product Transaction Table + internal const string dbSku = "sku"; + internal const string dbBalance = "balance"; + internal const string dbCategory = "cat"; + internal const string dbAttr = "attr"; + internal const string dbDetails = "details"; + internal const string dbTimeSensitive = "tsv"; + internal const string dbUpdateAt = "update_at"; + internal const string dbCreateAt = "create_at"; + + + public static InventoryTable LoadOrCreate(DBService db) + { + var table = new InventoryTable(); + table.Setup(db); + return table; + } + + private List _dataList; + + private DBService _db; + private void Setup(DBService db) + { + _db = db; + Refresh(); + + // 执行命令 + // _db.Execute($"CREATE INDEX inventory_item_idx ON {tbName} ({dbSku});", ts => + // { + // _db.Execute($"CREATE INDEX inventory_item_category_idx ON {tbName} ({dbCategory});"); + // }); + + } + + private void Refresh() + { + _dataList = _db.GetTableList(); + } + + private bool HasItem(string id) + { + return _dataList.Exists(c => c.id == id); + } + + /// + /// 更新道具 + /// + /// + public void UpdateInventoryItems(List items) + { + List updates = new List(items.Count); + List insets = new List(items.Count); + InventoryItem item; + inventory inv; + for (int i = 0; i < items.Count; i++) + { + item = items[i]; + inv = inventory.Create(item); + if (!HasItem(item.sku)) + { + insets.Add(inv); + } + else + { + updates.Add(inv); + } + } + _db.InsertAll(insets); + _db.UpdateAll(updates); + + Refresh(); + } + + public InventoryItem GetItem(string sku) + { + if (HasItem(sku)) + { + var inv = _dataList.Find(c => c.sku == sku); + if (inv != null) return inv.ToItem(); + } + return null; + } + + } + + /// + /// 表头及数据结构 + /// + internal class inventory + { + [Indexed(InventoryTable.dbSku, 1)] + public string sku { get; set; } + public int balance { get; set; } + [Indexed(InventoryTable.dbCategory, 2)] + public string cat{ get; set; } + public int attr { get; set; } + public string tsv { get; set; } + public long update_at{ get; set; } + public long create_at{ get; set; } + + [PrimaryKey] + public string id => sku; + + public static inventory Create(InventoryItem item) + { + return new inventory() + { + sku = item.sku, + balance = item.balance, + cat = item.category, + attr = item.attr, + tsv = item.Inventory.ToJson(), + update_at = item.updateAt, + create_at = item.createAt + }; + } + + public InventoryItem ToItem() + { + return InventoryItem.Create( + sku, cat, attr, balance, + null, + InventoryData.FromJson(tsv), + this.update_at, + this.create_at); + } + + } + +} \ No newline at end of file diff --git a/Runtime/GuruInventory/Runtime/InventoryManager.cs.meta b/Runtime/GuruInventory/Runtime/InventoryManager.cs.meta new file mode 100644 index 0000000..f47425d --- /dev/null +++ b/Runtime/GuruInventory/Runtime/InventoryManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 21e2bced768541b5befa3b4397783424 +timeCreated: 1706944831 \ No newline at end of file diff --git a/Runtime/GuruInventory/Runtime/Manifest.cs b/Runtime/GuruInventory/Runtime/Manifest.cs new file mode 100644 index 0000000..cf165b7 --- /dev/null +++ b/Runtime/GuruInventory/Runtime/Manifest.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Unity.Plastic.Newtonsoft.Json; + +namespace Guru +{ + + public class DetailsAttr + { + /// + /// 永久物品 + /// + public const int Permanent = 1; + /// + /// 可消耗 + /// + public const int Consumable = 2; + } + + public class ExtraReservedField { + public const string scene = "__scene"; + public const string offerId = "__offer_id"; + public const string basePlanId = "__base_plan_id"; + public const string sales = "__sales"; + public const string rate = "__rate"; + public const string contentId = "__content_id"; + } + + /// + /// 商品清单 + /// + public class Manifest: IJsonData + { + + [JsonProperty("category")] + public string category; + + [JsonProperty("extra")] + public Dictionary extra; + + public string scene => extra.TryGetValue(ExtraReservedField.scene, out var v)? v : ""; + + public string basePlanId => extra.TryGetValue(ExtraReservedField.basePlanId, out var v)? v : ""; + + public string offerId => extra.TryGetValue(ExtraReservedField.offerId, out var v)? v : ""; + public string contentId => extra.TryGetValue(ExtraReservedField.contentId, out var v)? v : ""; + } +} \ No newline at end of file diff --git a/Runtime/GuruInventory/Runtime/Manifest.cs.meta b/Runtime/GuruInventory/Runtime/Manifest.cs.meta new file mode 100644 index 0000000..e81ad91 --- /dev/null +++ b/Runtime/GuruInventory/Runtime/Manifest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a1e83edde24c4a46bd8ce00aaf09f273 +timeCreated: 1706945145 \ No newline at end of file diff --git a/Runtime/GuruLibs.meta b/Runtime/GuruLibs.meta new file mode 100644 index 0000000..90bbe64 --- /dev/null +++ b/Runtime/GuruLibs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c176d24ea24445f9ab67763aa8ef3f7c +timeCreated: 1706925964 \ No newline at end of file diff --git a/Runtime/GuruLibs/Datebase.meta b/Runtime/GuruLibs/Datebase.meta new file mode 100644 index 0000000..835b6f7 --- /dev/null +++ b/Runtime/GuruLibs/Datebase.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 42b7e7382b1d45d39f1542ed00f4e890 +timeCreated: 1706923167 \ No newline at end of file diff --git a/Runtime/GuruLibs/Datebase/DBService.cs b/Runtime/GuruLibs/Datebase/DBService.cs new file mode 100644 index 0000000..c3d9ace --- /dev/null +++ b/Runtime/GuruLibs/Datebase/DBService.cs @@ -0,0 +1,190 @@ + + +namespace Guru +{ + using SQLite4Unity3d; + using UnityEngine; + using System.Collections; + using System; + using System.IO; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using UnityEngine.Networking; + + public class DBService + { + internal const string DefaultDBName = "data"; + internal const string DefaultDBExtension = ".db"; + internal static string DebugDBPath = "mocks_dir/db"; + private SQLiteConnection _connection; + private string _dbName; + private string _dbPath; + private bool _isDebug = false; + + /// + /// 获取数据库的路径 + /// + public string DatabasePath => _dbPath; + + /// + /// 启动服务 + /// + /// + /// + public DBService(string name, bool isDebug = false) + { + _isDebug = isDebug; + if (string.IsNullOrEmpty(name)) name = DefaultDBName; + _dbName = name; + _dbPath = Path.GetFullPath($"{Application.persistentDataPath}/{_dbName}{DefaultDBExtension}"); + + if (isDebug) + { + _dbPath = Path.GetFullPath($"{Application.persistentDataPath}/{_dbName}_debug{DefaultDBExtension}"); + } + } + + + public static DBService Open(string name, bool isDebug = false) + { + var ds = new DBService(name, isDebug); + ds.Connect(); + return ds; + } + + /// + /// 关闭连接 + /// + public void Close() => _connection?.Close(); + + /// + /// 建立连接, 若不存在则创建数据库 + /// + public void Connect(string dbPath = "") + { + if(!string.IsNullOrEmpty(dbPath)) _dbPath = dbPath; + _connection = new SQLiteConnection(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create); + } + + public bool Exists => File.Exists(_dbPath); + + /// + /// 部署默认的 Database + /// 将内置在 StreamingAssets 中的数据库文件部署到 PersistentDataPath 中 + /// + /// + public void DeployEmbeddedDB(string embeddedPath, Action onComplete) + { + string from = Path.Combine(Application.streamingAssetsPath, embeddedPath); + + var uwr = new UnityWebRequest(from); + uwr.SendWebRequest().completed += ao => + { + var success = false; + try + { + if ( uwr.result == UnityWebRequest.Result.Success) + { + File.WriteAllBytes(_dbPath, uwr.downloadHandler.data); + success = true; + } + } + catch (Exception e) + { + Debug.LogError(e); + } + onComplete?.Invoke(success); + }; + } + + + + + /// + /// 获取表格 + /// + /// + /// + public IEnumerable CreatOrLoadTable() where T: new() + { + _connection.CreateTable(); + return _connection.Table(); + } + + public List GetTableList() where T: new() + { + var tb = CreatOrLoadTable(); + return tb?.ToList() ?? new List(); + } + + + + public int Insert(T value) + { + return _connection.Insert(value); + } + + public int InsertAll(IEnumerable data) + { + return _connection.InsertAll(data); + } + + public int Remove(object primaryKey) + { + return _connection.Delete(primaryKey); + } + + + public int Remove(object primaryKey) + { + return _connection.Delete(primaryKey); + } + + public int UpdateAll(IEnumerable data) + { + return _connection.UpdateAll(data); + } + + public T Get(object primaryKey) where T : new() + { + return _connection.Get(primaryKey); + } + + + public T Find(Expression> predicate) where T : new() + { + return _connection.Find(predicate); + } + + private bool _onCmdExecution = false; + /// + /// 执行 SQL 语句 + /// + /// + /// + /// + /// + public int Execute(string query, Action onExecutionComplete = null, params object[] args) + { + if (_onCmdExecution) return -1; + + SQLiteConnection.TimeExecutionHandler del = null; + del = (t1, t2) => + { + _onCmdExecution = false; + _connection.TimeExecutionEvent -= del; + onExecutionComplete?.Invoke(t2.TotalSeconds); + }; + _connection.TimeExecutionEvent += del; + _onCmdExecution = true; + return _connection.Execute(query, args); + } + + + } + + + + +} \ No newline at end of file diff --git a/Runtime/GuruLibs/Datebase/DBService.cs.meta b/Runtime/GuruLibs/Datebase/DBService.cs.meta new file mode 100644 index 0000000..e39d715 --- /dev/null +++ b/Runtime/GuruLibs/Datebase/DBService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 230a328bcc91476ea24d47a0ecbdf441 +timeCreated: 1706926988 \ No newline at end of file diff --git a/Runtime/GuruLibs/Datebase/GuruDB.cs b/Runtime/GuruLibs/Datebase/GuruDB.cs new file mode 100644 index 0000000..e13ef5f --- /dev/null +++ b/Runtime/GuruLibs/Datebase/GuruDB.cs @@ -0,0 +1,54 @@ + + +using SQLite4Unity3d; + +namespace Guru +{ + using System.Collections.Generic; + + + + public class GuruDB + { + + + private static GuruDB _instance; + public static GuruDB Instance + { + get + { + if(_instance == null) _instance = new GuruDB(); + return _instance; + } + } + + public int dbVersion = 1; + public string dbName = "guru"; + private DBService _dbService; + internal DBService Service => _dbService; + public bool IsDebug { get; private set; } = false; + public bool IsReady { get; private set; } + + public GuruDB() + { +#if UNITY_EDITOR + IsDebug = true; +#endif + _dbService = DBService.Open(dbName, IsDebug); + + IsReady = _dbService != null; + } + } + + + public interface IDBItem + { + [PrimaryKey] + string id { get; set; } + } + + + + + +} \ No newline at end of file diff --git a/Runtime/GuruLibs/Datebase/GuruDB.cs.meta b/Runtime/GuruLibs/Datebase/GuruDB.cs.meta new file mode 100644 index 0000000..f6082ce --- /dev/null +++ b/Runtime/GuruLibs/Datebase/GuruDB.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1c3e8542bfad4731b0c54abf3afe7cf3 +timeCreated: 1706923244 \ No newline at end of file diff --git a/Runtime/GuruLibs/Datebase/UnityTest.meta b/Runtime/GuruLibs/Datebase/UnityTest.meta new file mode 100644 index 0000000..2cf7259 --- /dev/null +++ b/Runtime/GuruLibs/Datebase/UnityTest.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: efa1c022e13449558b8d4d620fae4e69 +timeCreated: 1706927602 \ No newline at end of file diff --git a/Runtime/GuruLibs/Json.meta b/Runtime/GuruLibs/Json.meta new file mode 100644 index 0000000..cb696a3 --- /dev/null +++ b/Runtime/GuruLibs/Json.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 84ef5b243482457ab96d4c376fec23d6 +timeCreated: 1706949578 \ No newline at end of file diff --git a/Runtime/GuruLibs/Json/IJsonData.cs b/Runtime/GuruLibs/Json/IJsonData.cs new file mode 100644 index 0000000..16839a2 --- /dev/null +++ b/Runtime/GuruLibs/Json/IJsonData.cs @@ -0,0 +1,7 @@ +namespace Guru +{ + public interface IJsonData + { + + } +} \ No newline at end of file diff --git a/Runtime/GuruLibs/Json/IJsonData.cs.meta b/Runtime/GuruLibs/Json/IJsonData.cs.meta new file mode 100644 index 0000000..870b1f2 --- /dev/null +++ b/Runtime/GuruLibs/Json/IJsonData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 68451c547db14e748cb50b5e773c6f00 +timeCreated: 1706949587 \ No newline at end of file diff --git a/Runtime/GuruLibs/Json/JsonDataHelper.cs b/Runtime/GuruLibs/Json/JsonDataHelper.cs new file mode 100644 index 0000000..e991ed2 --- /dev/null +++ b/Runtime/GuruLibs/Json/JsonDataHelper.cs @@ -0,0 +1,62 @@ + +using System; +using UnityEngine; + +namespace Guru +{ + using Newtonsoft.Json; + + + + public static class JsonDataHelper + { + public static Action ExceptionHandler; + + + public static string ToJsonString(object obj) + { + try + { + return JsonConvert.SerializeObject(obj); + } + catch (Exception e) + { + ExceptionHandler?.Invoke(e); + Debug.LogError(e); + } + return ""; + } + + + public static bool Parse(string json, out T result) + { + bool success = false; + result = default(T); + + if(string.IsNullOrEmpty(json)) return false; + + try + { + result = JsonConvert.DeserializeObject(json); + success = true; + } + catch (Exception e) + { + ExceptionHandler?.Invoke(e); + Debug.LogError(e); + } + + return success; + } + + + + public static string ToJson(this IJsonData obj) + { + return ToJsonString((object)obj); + } + + + + } +} \ No newline at end of file diff --git a/Runtime/GuruLibs/Json/JsonDataHelper.cs.meta b/Runtime/GuruLibs/Json/JsonDataHelper.cs.meta new file mode 100644 index 0000000..e74c972 --- /dev/null +++ b/Runtime/GuruLibs/Json/JsonDataHelper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ce50b36309b34a41b8bb844efbba1a17 +timeCreated: 1706949612 \ No newline at end of file diff --git a/Runtime/GuruLibs/Property.meta b/Runtime/GuruLibs/Property.meta new file mode 100644 index 0000000..0b45618 --- /dev/null +++ b/Runtime/GuruLibs/Property.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 043ca2bcb28a45fba7edf46e5819a53b +timeCreated: 1706923871 \ No newline at end of file diff --git a/Runtime/GuruLibs/Property/DB.meta b/Runtime/GuruLibs/Property/DB.meta new file mode 100644 index 0000000..2c6b1a5 --- /dev/null +++ b/Runtime/GuruLibs/Property/DB.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e4425fb6790b4f16b10a6ac9545f087c +timeCreated: 1706925944 \ No newline at end of file diff --git a/Runtime/GuruLibs/Property/DB/PropertyDatabase.cs b/Runtime/GuruLibs/Property/DB/PropertyDatabase.cs new file mode 100644 index 0000000..ef8bf91 --- /dev/null +++ b/Runtime/GuruLibs/Property/DB/PropertyDatabase.cs @@ -0,0 +1,48 @@ +using System; + +namespace Guru +{ + + public partial class PropertyDatabase + { + + + + + + + + } + + + + + [Serializable] + public class PropertyEntity + { + const string TableName = "properties"; + // internal const string dbKey = "key"; + // internal const string dbValue = "value"; + // internal const string dbGroup = "gp"; + // internal const string dbUsage = "usage"; + // internal const string dbTag = "tag"; + // internal const string dbUpdateAt = "upt"; + + public string key; + public string value; + public string gp = PropertyKey.DefaultGroup; + public int usage = PropertyKey.UsageGeneral; + public string tag; + public string upt; + public int updateAt = 0; + + + + + + + + } + + +} \ No newline at end of file diff --git a/Runtime/GuruLibs/Property/DB/PropertyDatabase.cs.meta b/Runtime/GuruLibs/Property/DB/PropertyDatabase.cs.meta new file mode 100644 index 0000000..516d3c0 --- /dev/null +++ b/Runtime/GuruLibs/Property/DB/PropertyDatabase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 357ec7a04d0a454086a09bd2f2fcfe79 +timeCreated: 1706926072 \ No newline at end of file diff --git a/Runtime/GuruLibs/Property/PropertyKey.cs b/Runtime/GuruLibs/Property/PropertyKey.cs new file mode 100644 index 0000000..decafb4 --- /dev/null +++ b/Runtime/GuruLibs/Property/PropertyKey.cs @@ -0,0 +1,59 @@ +namespace Guru +{ + public struct PropertyKey + { + public const int UsageGeneral = 0; + public const int UsageSettings = 1; + public const string DefaultGroup = "guru"; + + public string name { get; private set; } + public string key { get; private set; } + public string group { get; private set; } + public string tag { get; private set; } + public int usage { get; private set; } + + public static PropertyKey General(string name, string group = DefaultGroup, string tag = "") + { + return new PropertyKey + { + name = name, + key = $"{group}@{name}", + group = group, + tag = tag, + usage = UsageGeneral + }; + } + + public static PropertyKey Setting(string name, string group = DefaultGroup, string tag = "") + { + return new PropertyKey + { + name = name, + key = $"{group}@{name}", + group = group, + tag = tag, + usage = UsageSettings + }; + } + + /// + /// 判断两个属性 Key 是否相等 + /// + /// + /// + public bool Equals(PropertyKey other) + { + return name == other.name && + key == other.key && + group == other.group && + tag == other.tag && + usage == other.usage; + } + + public override string ToString() + { + return $"#{tag}#[{key}]({usage})"; + } + + } +} \ No newline at end of file diff --git a/Runtime/GuruLibs/Property/PropertyKey.cs.meta b/Runtime/GuruLibs/Property/PropertyKey.cs.meta new file mode 100644 index 0000000..868180e --- /dev/null +++ b/Runtime/GuruLibs/Property/PropertyKey.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5b1517229c974c46ac85c3c8f8b3b330 +timeCreated: 1706924244 \ No newline at end of file diff --git a/Runtime/GuruLibs/Property/Storage.meta b/Runtime/GuruLibs/Property/Storage.meta new file mode 100644 index 0000000..90746da --- /dev/null +++ b/Runtime/GuruLibs/Property/Storage.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d50e6dd9a1324036af9e950f5ec489ed +timeCreated: 1706925986 \ No newline at end of file diff --git a/Runtime/GuruLibs/Property/Storage/IPropertyStorage.cs b/Runtime/GuruLibs/Property/Storage/IPropertyStorage.cs new file mode 100644 index 0000000..c1be5e5 --- /dev/null +++ b/Runtime/GuruLibs/Property/Storage/IPropertyStorage.cs @@ -0,0 +1,19 @@ +namespace Guru +{ + public interface IPropertyStorage + { + void SetDouble(PropertyKey key, double value); + void SetInt(PropertyKey key, int value); + void SetBool(PropertyKey key, bool value); + void SetString(PropertyKey key, string value); + double? GetDouble(PropertyKey key, double? defaultValue); + int? GetInt(PropertyKey key, int? defaultValue); + bool? GetBool(PropertyKey key, bool? defaultValue); + string GetString(PropertyKey key, string defaultValue); + + void remove(PropertyKey key); + void removeAllWithTag(string tag); + + + } +} \ No newline at end of file diff --git a/Runtime/GuruLibs/Property/Storage/IPropertyStorage.cs.meta b/Runtime/GuruLibs/Property/Storage/IPropertyStorage.cs.meta new file mode 100644 index 0000000..5fc3f2f --- /dev/null +++ b/Runtime/GuruLibs/Property/Storage/IPropertyStorage.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 761fa4520c9746b5b14e27078be64003 +timeCreated: 1706923906 \ No newline at end of file diff --git a/Runtime/GuruLibs/SQLite4Unity3d.meta b/Runtime/GuruLibs/SQLite4Unity3d.meta new file mode 100644 index 0000000..906a3bf --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 36d6b6e33eeef40509c8647e2d7c555d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins.meta new file mode 100644 index 0000000..00d3d9f --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4475ec4c51944461ba8479536dfcda61 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android.meta new file mode 100644 index 0000000..2b78603 --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f288c6262551743be968c8c546dd065e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs.meta new file mode 100644 index 0000000..216ab9a --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c1aacb5a55fc94e4da727839f3ea4d24 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/arm64-v8a.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/arm64-v8a.meta new file mode 100644 index 0000000..e6bae48 --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/arm64-v8a.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8a0518887aa4d45798babfb2249cdd6d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/arm64-v8a/libsqlite3.so b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/arm64-v8a/libsqlite3.so new file mode 100644 index 0000000..1e2ce2d Binary files /dev/null and b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/arm64-v8a/libsqlite3.so differ diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/arm64-v8a/libsqlite3.so.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/arm64-v8a/libsqlite3.so.meta new file mode 100644 index 0000000..99ffb87 --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/arm64-v8a/libsqlite3.so.meta @@ -0,0 +1,27 @@ +fileFormatVersion: 2 +guid: 035fff0fb0daa454f882f263a44d8a71 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/armeabi-v7a.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/armeabi-v7a.meta new file mode 100644 index 0000000..2e5a3c1 --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/armeabi-v7a.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a8bf69720c6da40ff9d7ada1a6e52198 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/armeabi-v7a/libsqlite3.so b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/armeabi-v7a/libsqlite3.so new file mode 100644 index 0000000..2c6e2bf Binary files /dev/null and b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/armeabi-v7a/libsqlite3.so differ diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/armeabi-v7a/libsqlite3.so.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/armeabi-v7a/libsqlite3.so.meta new file mode 100644 index 0000000..2a73927 --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/armeabi-v7a/libsqlite3.so.meta @@ -0,0 +1,27 @@ +fileFormatVersion: 2 +guid: fbfa6b2ad32f74cb9ba69b1613f0ac8b +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/x86.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/x86.meta new file mode 100644 index 0000000..b367fb2 --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/x86.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c0bc4319db8fc4e87b92d8849e10effa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/x86/libsqlite3.so b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/x86/libsqlite3.so new file mode 100644 index 0000000..9d08844 Binary files /dev/null and b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/x86/libsqlite3.so differ diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/x86/libsqlite3.so.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/x86/libsqlite3.so.meta new file mode 100644 index 0000000..07b59b5 --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/Android/libs/x86/libsqlite3.so.meta @@ -0,0 +1,27 @@ +fileFormatVersion: 2 +guid: 533c36573354e4e82a0871a3ae353c64 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x64.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x64.meta new file mode 100644 index 0000000..7cc330b --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x64.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 87d29778372204305920a77087f262a4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x64/sqlite3.dll b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x64/sqlite3.dll new file mode 100644 index 0000000..6f07d5e Binary files /dev/null and b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x64/sqlite3.dll differ diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x64/sqlite3.dll.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x64/sqlite3.dll.meta new file mode 100644 index 0000000..ce0b494 --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x64/sqlite3.dll.meta @@ -0,0 +1,52 @@ +fileFormatVersion: 2 +guid: 25884d71c32604d7987793cbcbe801ac +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: x86_64 + DefaultValueInitialized: true + - first: + Standalone: Linux64 + second: + enabled: 1 + settings: + CPU: x86_64 + - first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: + CPU: x86_64 + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 1 + settings: + CPU: x86_64 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x86.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x86.meta new file mode 100644 index 0000000..ceb30a7 --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x86.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f7a5439d4fb9e4828937865037857668 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x86/sqlite3.dll b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x86/sqlite3.dll new file mode 100644 index 0000000..d48a7ef Binary files /dev/null and b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x86/sqlite3.dll differ diff --git a/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x86/sqlite3.dll.meta b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x86/sqlite3.dll.meta new file mode 100644 index 0000000..e7cfc0e --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/Plugins/x86/sqlite3.dll.meta @@ -0,0 +1,52 @@ +fileFormatVersion: 2 +guid: 8f6a8ec41de2b4f569e23b20011eec8c +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: x86 + DefaultValueInitialized: true + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: x86 + - first: + Standalone: Win + second: + enabled: 1 + settings: + CPU: x86 + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruLibs/SQLite4Unity3d/SQLite.cs b/Runtime/GuruLibs/SQLite4Unity3d/SQLite.cs new file mode 100644 index 0000000..18ce6c3 --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/SQLite.cs @@ -0,0 +1,3377 @@ +// +// Copyright (c) 2009-2012 Krueger Systems, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +#if WINDOWS_PHONE && !USE_WP8_NATIVE_SQLITE +#define USE_CSHARP_SQLITE +#endif + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; + +#if USE_CSHARP_SQLITE +using Sqlite3 = Community.CsharpSqlite.Sqlite3; +using Sqlite3DatabaseHandle = Community.CsharpSqlite.Sqlite3.sqlite3; +using Sqlite3Statement = Community.CsharpSqlite.Sqlite3.Vdbe; +#elif USE_WP8_NATIVE_SQLITE +using Sqlite3 = Sqlite.Sqlite3; +using Sqlite3DatabaseHandle = Sqlite.Database; +using Sqlite3Statement = Sqlite.Statement; +#else +using Sqlite3DatabaseHandle = System.IntPtr; +using Sqlite3Statement = System.IntPtr; +#endif + +namespace SQLite4Unity3d +{ + public class SQLiteException : Exception + { + public SQLite3.Result Result { get; private set; } + + protected SQLiteException (SQLite3.Result r,string message) : base(message) + { + Result = r; + } + + public static SQLiteException New (SQLite3.Result r, string message) + { + return new SQLiteException (r, message); + } + } + + public class NotNullConstraintViolationException : SQLiteException + { + public IEnumerable Columns { get; protected set; } + + protected NotNullConstraintViolationException (SQLite3.Result r, string message) + : this (r, message, null, null) + { + + } + + protected NotNullConstraintViolationException (SQLite3.Result r, string message, TableMapping mapping, object obj) + : base (r, message) + { + if (mapping != null && obj != null) { + this.Columns = from c in mapping.Columns + where c.IsNullable == false && c.GetValue(obj) == null + select c; + } + } + + public static new NotNullConstraintViolationException New (SQLite3.Result r, string message) + { + return new NotNullConstraintViolationException (r, message); + } + + public static NotNullConstraintViolationException New (SQLite3.Result r, string message, TableMapping mapping, object obj) + { + return new NotNullConstraintViolationException (r, message, mapping, obj); + } + + public static NotNullConstraintViolationException New (SQLiteException exception, TableMapping mapping, object obj) + { + return new NotNullConstraintViolationException (exception.Result, exception.Message, mapping, obj); + } + } + + [Flags] + public enum SQLiteOpenFlags { + ReadOnly = 1, ReadWrite = 2, Create = 4, + NoMutex = 0x8000, FullMutex = 0x10000, + SharedCache = 0x20000, PrivateCache = 0x40000, + ProtectionComplete = 0x00100000, + ProtectionCompleteUnlessOpen = 0x00200000, + ProtectionCompleteUntilFirstUserAuthentication = 0x00300000, + ProtectionNone = 0x00400000 + } + + [Flags] + public enum CreateFlags + { + None = 0, + ImplicitPK = 1, // create a primary key for field called 'Id' (Orm.ImplicitPkName) + ImplicitIndex = 2, // create an index for fields ending in 'Id' (Orm.ImplicitIndexSuffix) + AllImplicit = 3, // do both above + + AutoIncPK = 4 // force PK field to be auto inc + } + + /// + /// Represents an open connection to a SQLite database. + /// + public partial class SQLiteConnection : IDisposable + { + private bool _open; + private TimeSpan _busyTimeout; + private Dictionary _mappings = null; + private Dictionary _tables = null; + private System.Diagnostics.Stopwatch _sw; + private TimeSpan _elapsed = default(TimeSpan); + + private int _transactionDepth = 0; + private Random _rand = new Random (); + + public Sqlite3DatabaseHandle Handle { get; private set; } + internal static readonly Sqlite3DatabaseHandle NullHandle = default(Sqlite3DatabaseHandle); + + public string DatabasePath { get; private set; } + + // Dictionary of synchronization objects. + // + // To prevent database disruption, a database file must be accessed *synchronously*. + // For the purpose we create synchronous objects for each database file and store in the + // static dictionary to share it among all connections. + // The key of the dictionary is database file path and its value is an object to be used + // by lock() statement. + // + // Use case: + // - database file lock is done implicitly and automatically. + // - To prepend deadlock, application may lock a database file explicity by either way: + // - RunInTransaction(Action) locks the database during the transaction (for insert/update) + // - RunInDatabaseLock(Action) similarly locks the database but no transaction (for query) + private static Dictionary syncObjects = new Dictionary(); + + #region debug tracing + + public bool Trace { get; set; } + public bool TimeExecution { get; set; } + + public delegate void TraceHandler (string message); + public event TraceHandler TraceEvent; + + internal void InvokeTrace (string message) + { + if (TraceEvent != null) { + TraceEvent(message); + } + } + + public delegate void TimeExecutionHandler (TimeSpan executionTime, TimeSpan totalExecutionTime); + public event TimeExecutionHandler TimeExecutionEvent; + + internal void InvokeTimeExecution(TimeSpan executionTime, TimeSpan totalExecutionTime) + { + if (TimeExecutionEvent != null) { + TimeExecutionEvent(executionTime, totalExecutionTime); + } + } + + #endregion + + public bool StoreDateTimeAsTicks { get; private set; } + + /// + /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You + /// absolutely do want to store them as Ticks in all new projects. The default of false is + /// only here for backwards compatibility. There is a *significant* speed advantage, with no + /// down sides, when setting storeDateTimeAsTicks = true. + /// + public SQLiteConnection (string databasePath, bool storeDateTimeAsTicks = false) + : this (databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks) + { + } + + /// + /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You + /// absolutely do want to store them as Ticks in all new projects. The default of false is + /// only here for backwards compatibility. There is a *significant* speed advantage, with no + /// down sides, when setting storeDateTimeAsTicks = true. + /// + public SQLiteConnection (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = false) + { + if (string.IsNullOrEmpty (databasePath)) + throw new ArgumentException ("Must be specified", "databasePath"); + + DatabasePath = databasePath; + mayCreateSyncObject(databasePath); + +#if NETFX_CORE + SQLite3.SetDirectory(/*temp directory type*/2, Windows.Storage.ApplicationData.Current.TemporaryFolder.Path); +#endif + + Sqlite3DatabaseHandle handle; + +#if SILVERLIGHT || USE_CSHARP_SQLITE + var r = SQLite3.Open (databasePath, out handle, (int)openFlags, IntPtr.Zero); +#else + // open using the byte[] + // in the case where the path may include Unicode + // force open to using UTF-8 using sqlite3_open_v2 + var databasePathAsBytes = GetNullTerminatedUtf8 (DatabasePath); + var r = SQLite3.Open (databasePathAsBytes, out handle, (int) openFlags, IntPtr.Zero); +#endif + + Handle = handle; + if (r != SQLite3.Result.OK) { + throw SQLiteException.New (r, String.Format ("Could not open database file: {0} ({1})", DatabasePath, r)); + } + _open = true; + + StoreDateTimeAsTicks = storeDateTimeAsTicks; + + BusyTimeout = TimeSpan.FromSeconds (0.1); + } + + static SQLiteConnection () + { + if (_preserveDuringLinkMagic) { + var ti = new ColumnInfo (); + ti.Name = "magic"; + } + } + + void mayCreateSyncObject(string databasePath) + { + if (!syncObjects.ContainsKey(databasePath)) { + syncObjects[databasePath] = new object(); + } + } + + /// + /// Gets the synchronous object, to be lock the database file for updating. + /// + /// The sync object. + public object SyncObject { get { return syncObjects[DatabasePath];} } + + public void EnableLoadExtension(int onoff) + { + SQLite3.Result r = SQLite3.EnableLoadExtension(Handle, onoff); + if (r != SQLite3.Result.OK) { + string msg = SQLite3.GetErrmsg (Handle); + throw SQLiteException.New (r, msg); + } + } + + static byte[] GetNullTerminatedUtf8 (string s) + { + var utf8Length = System.Text.Encoding.UTF8.GetByteCount (s); + var bytes = new byte [utf8Length + 1]; + utf8Length = System.Text.Encoding.UTF8.GetBytes(s, 0, s.Length, bytes, 0); + return bytes; + } + + /// + /// Used to list some code that we want the MonoTouch linker + /// to see, but that we never want to actually execute. + /// + #pragma warning disable 649 + static bool _preserveDuringLinkMagic; + #pragma warning restore 649 + + /// + /// Sets a busy handler to sleep the specified amount of time when a table is locked. + /// The handler will sleep multiple times until a total time of has accumulated. + /// + public TimeSpan BusyTimeout { + get { return _busyTimeout; } + set { + _busyTimeout = value; + if (Handle != NullHandle) { + SQLite3.BusyTimeout (Handle, (int)_busyTimeout.TotalMilliseconds); + } + } + } + + /// + /// Returns the mappings from types to tables that the connection + /// currently understands. + /// + public IEnumerable TableMappings { + get { + return _tables != null ? _tables.Values : Enumerable.Empty (); + } + } + + /// + /// Retrieves the mapping that is automatically generated for the given type. + /// + /// + /// The type whose mapping to the database is returned. + /// + /// + /// Optional flags allowing implicit PK and indexes based on naming conventions + /// + /// + /// The mapping represents the schema of the columns of the database and contains + /// methods to set and get properties of objects. + /// + public TableMapping GetMapping(Type type, CreateFlags createFlags = CreateFlags.None) + { + if (_mappings == null) { + _mappings = new Dictionary (); + } + TableMapping map; + if (!_mappings.TryGetValue (type.FullName, out map)) { + map = new TableMapping (type, createFlags); + _mappings [type.FullName] = map; + } + return map; + } + + /// + /// Retrieves the mapping that is automatically generated for the given type. + /// + /// + /// The mapping represents the schema of the columns of the database and contains + /// methods to set and get properties of objects. + /// + public TableMapping GetMapping () + { + return GetMapping (typeof (T)); + } + + private struct IndexedColumn + { + public int Order; + public string ColumnName; + } + + private struct IndexInfo + { + public string IndexName; + public string TableName; + public bool Unique; + public List Columns; + } + + /// + /// Executes a "drop table" on the database. This is non-recoverable. + /// + public int DropTable() + { + var map = GetMapping (typeof (T)); + + var query = string.Format("drop table if exists \"{0}\"", map.TableName); + + return Execute (query); + } + + /// + /// Executes a "create table if not exists" on the database. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// The number of entries added to the database schema. + /// + public int CreateTable(CreateFlags createFlags = CreateFlags.None) + { + return CreateTable(typeof (T), createFlags); + } + + /// + /// Executes a "create table if not exists" on the database. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// Type to reflect to a database table. + /// Optional flags allowing implicit PK and indexes based on naming conventions. + /// + /// The number of entries added to the database schema. + /// + public int CreateTable(Type ty, CreateFlags createFlags = CreateFlags.None) + { + if (_tables == null) { + _tables = new Dictionary (); + } + TableMapping map; + if (!_tables.TryGetValue (ty.FullName, out map)) { + map = GetMapping (ty, createFlags); + _tables.Add (ty.FullName, map); + } + var query = "create table if not exists \"" + map.TableName + "\"(\n"; + + var decls = map.Columns.Select (p => Orm.SqlDecl (p, StoreDateTimeAsTicks)); + var decl = string.Join (",\n", decls.ToArray ()); + query += decl; + query += ")"; + + var count = Execute (query); + + if (count == 0) { //Possible bug: This always seems to return 0? + // Table already exists, migrate it + MigrateTable (map); + } + + var indexes = new Dictionary (); + foreach (var c in map.Columns) { + foreach (var i in c.Indices) { + var iname = i.Name ?? map.TableName + "_" + c.Name; + IndexInfo iinfo; + if (!indexes.TryGetValue (iname, out iinfo)) { + iinfo = new IndexInfo { + IndexName = iname, + TableName = map.TableName, + Unique = i.Unique, + Columns = new List () + }; + indexes.Add (iname, iinfo); + } + + if (i.Unique != iinfo.Unique) + throw new Exception ("All the columns in an index must have the same value for their Unique property"); + + iinfo.Columns.Add (new IndexedColumn { + Order = i.Order, + ColumnName = c.Name + }); + } + } + + foreach (var indexName in indexes.Keys) { + var index = indexes[indexName]; + string[] columnNames = new string[index.Columns.Count]; + if (index.Columns.Count == 1) { + columnNames[0] = index.Columns[0].ColumnName; + } else { + index.Columns.Sort((lhs, rhs) => { + return lhs.Order - rhs.Order; + }); + for (int i = 0, end = index.Columns.Count; i < end; ++i) { + columnNames[i] = index.Columns[i].ColumnName; + } + } + count += CreateIndex(indexName, index.TableName, columnNames, index.Unique); + } + + return count; + } + + /// + /// Creates an index for the specified table and columns. + /// + /// Name of the index to create + /// Name of the database table + /// An array of column names to index + /// Whether the index should be unique + public int CreateIndex(string indexName, string tableName, string[] columnNames, bool unique = false) + { + const string sqlFormat = "create {2} index if not exists \"{3}\" on \"{0}\"(\"{1}\")"; + var sql = String.Format(sqlFormat, tableName, string.Join ("\", \"", columnNames), unique ? "unique" : "", indexName); + return Execute(sql); + } + + /// + /// Creates an index for the specified table and column. + /// + /// Name of the index to create + /// Name of the database table + /// Name of the column to index + /// Whether the index should be unique + public int CreateIndex(string indexName, string tableName, string columnName, bool unique = false) + { + return CreateIndex(indexName, tableName, new string[] { columnName }, unique); + } + + /// + /// Creates an index for the specified table and column. + /// + /// Name of the database table + /// Name of the column to index + /// Whether the index should be unique + public int CreateIndex(string tableName, string columnName, bool unique = false) + { + return CreateIndex(tableName + "_" + columnName, tableName, columnName, unique); + } + + /// + /// Creates an index for the specified table and columns. + /// + /// Name of the database table + /// An array of column names to index + /// Whether the index should be unique + public int CreateIndex(string tableName, string[] columnNames, bool unique = false) + { + return CreateIndex(tableName + "_" + string.Join ("_", columnNames), tableName, columnNames, unique); + } + + /// + /// Creates an index for the specified object property. + /// e.g. CreateIndex(c => c.Name); + /// + /// Type to reflect to a database table. + /// Property to index + /// Whether the index should be unique + public void CreateIndex(Expression> property, bool unique = false) + { + MemberExpression mx; + if (property.Body.NodeType == ExpressionType.Convert) + { + mx = ((UnaryExpression)property.Body).Operand as MemberExpression; + } + else + { + mx= (property.Body as MemberExpression); + } + var propertyInfo = mx.Member as PropertyInfo; + if (propertyInfo == null) + { + throw new ArgumentException("The lambda expression 'property' should point to a valid Property"); + } + + var propName = propertyInfo.Name; + + var map = GetMapping(); + var colName = map.FindColumnWithPropertyName(propName).Name; + + CreateIndex(map.TableName, colName, unique); + } + + public class ColumnInfo + { +// public int cid { get; set; } + + [Column ("name")] + public string Name { get; set; } + +// [Column ("type")] +// public string ColumnType { get; set; } + + public int notnull { get; set; } + +// public string dflt_value { get; set; } + +// public int pk { get; set; } + + public override string ToString () + { + return Name; + } + } + + public List GetTableInfo (string tableName) + { + var query = "pragma table_info(\"" + tableName + "\")"; + return Query (query); + } + + void MigrateTable (TableMapping map) + { + var existingCols = GetTableInfo (map.TableName); + + var toBeAdded = new List (); + + foreach (var p in map.Columns) { + var found = false; + foreach (var c in existingCols) { + found = (string.Compare (p.Name, c.Name, StringComparison.OrdinalIgnoreCase) == 0); + if (found) + break; + } + if (!found) { + toBeAdded.Add (p); + } + } + + foreach (var p in toBeAdded) { + var addCol = "alter table \"" + map.TableName + "\" add column " + Orm.SqlDecl (p, StoreDateTimeAsTicks); + Execute (addCol); + } + } + + /// + /// Creates a new SQLiteCommand. Can be overridden to provide a sub-class. + /// + /// + protected virtual SQLiteCommand NewCommand () + { + return new SQLiteCommand (this); + } + + /// + /// Creates a new SQLiteCommand given the command text with arguments. Place a '?' + /// in the command text for each of the arguments. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the command text. + /// + /// + /// A + /// + public SQLiteCommand CreateCommand (string cmdText, params object[] ps) + { + if (!_open) + throw SQLiteException.New (SQLite3.Result.Error, "Cannot create commands from unopened database"); + + var cmd = NewCommand (); + cmd.CommandText = cmdText; + foreach (var o in ps) { + cmd.Bind (o); + } + return cmd; + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// Use this method instead of Query when you don't expect rows back. Such cases include + /// INSERTs, UPDATEs, and DELETEs. + /// You can set the Trace or TimeExecution properties of the connection + /// to profile execution. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The number of rows modified in the database as a result of this execution. + /// + public int Execute (string query, params object[] args) + { + var cmd = CreateCommand (query, args); + + if (TimeExecution) { + if (_sw == null) { + _sw = new Stopwatch (); + } + _sw.Reset (); + _sw.Start (); + } + + var r = cmd.ExecuteNonQuery (); + + if (TimeExecution) { + _sw.Stop (); + _elapsed += _sw.Elapsed; + this.InvokeTimeExecution (_sw.Elapsed, _elapsed); + } + + return r; + } + + public T ExecuteScalar (string query, params object[] args) + { + var cmd = CreateCommand (query, args); + + if (TimeExecution) { + if (_sw == null) { + _sw = new Stopwatch (); + } + _sw.Reset (); + _sw.Start (); + } + + var r = cmd.ExecuteScalar (); + + if (TimeExecution) { + _sw.Stop (); + _elapsed += _sw.Elapsed; + this.InvokeTimeExecution (_sw.Elapsed, _elapsed); + } + + return r; + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the mapping automatically generated for + /// the given type. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// + public List Query (string query, params object[] args) where T : new() + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteQuery (); + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the mapping automatically generated for + /// the given type. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// The enumerator will call sqlite3_step on each call to MoveNext, so the database + /// connection must remain open for the lifetime of the enumerator. + /// + public IEnumerable DeferredQuery(string query, params object[] args) where T : new() + { + var cmd = CreateCommand(query, args); + return cmd.ExecuteDeferredQuery(); + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the specified mapping. This function is + /// only used by libraries in order to query the database via introspection. It is + /// normally not used. + /// + /// + /// A to use to convert the resulting rows + /// into objects. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// + public List Query (TableMapping map, string query, params object[] args) + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteQuery (map); + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the specified mapping. This function is + /// only used by libraries in order to query the database via introspection. It is + /// normally not used. + /// + /// + /// A to use to convert the resulting rows + /// into objects. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// The enumerator will call sqlite3_step on each call to MoveNext, so the database + /// connection must remain open for the lifetime of the enumerator. + /// + public IEnumerable DeferredQuery(TableMapping map, string query, params object[] args) + { + var cmd = CreateCommand(query, args); + return cmd.ExecuteDeferredQuery(map); + } + + /// + /// Returns a queryable interface to the table represented by the given type. + /// + /// + /// A queryable object that is able to translate Where, OrderBy, and Take + /// queries into native SQL. + /// + public TableQuery Table () where T : new() + { + return new TableQuery (this); + } + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The object with the given primary key. Throws a not found exception + /// if the object is not found. + /// + public T Get (object pk) where T : new() + { + var map = GetMapping (typeof(T)); + return Query (map.GetByPrimaryKeySql, pk).First (); + } + + /// + /// Attempts to retrieve the first object that matches the predicate from the table + /// associated with the specified type. + /// + /// + /// A predicate for which object to find. + /// + /// + /// The object that matches the given predicate. Throws a not found exception + /// if the object is not found. + /// + public T Get (Expression> predicate) where T : new() + { + return Table ().Where (predicate).First (); + } + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The object with the given primary key or null + /// if the object is not found. + /// + public T Find (object pk) where T : new () + { + var map = GetMapping (typeof (T)); + return Query (map.GetByPrimaryKeySql, pk).FirstOrDefault (); + } + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The TableMapping used to identify the object type. + /// + /// + /// The object with the given primary key or null + /// if the object is not found. + /// + public object Find (object pk, TableMapping map) + { + return Query (map, map.GetByPrimaryKeySql, pk).FirstOrDefault (); + } + + /// + /// Attempts to retrieve the first object that matches the predicate from the table + /// associated with the specified type. + /// + /// + /// A predicate for which object to find. + /// + /// + /// The object that matches the given predicate or null + /// if the object is not found. + /// + public T Find (Expression> predicate) where T : new() + { + return Table ().Where (predicate).FirstOrDefault (); + } + + /// + /// Whether has been called and the database is waiting for a . + /// + public bool IsInTransaction { + get { return _transactionDepth > 0; } + } + + /// + /// Begins a new transaction. Call to end the transaction. + /// + /// Throws if a transaction has already begun. + public void BeginTransaction () + { + // The BEGIN command only works if the transaction stack is empty, + // or in other words if there are no pending transactions. + // If the transaction stack is not empty when the BEGIN command is invoked, + // then the command fails with an error. + // Rather than crash with an error, we will just ignore calls to BeginTransaction + // that would result in an error. + if (Interlocked.CompareExchange (ref _transactionDepth, 1, 0) == 0) { + try { + Execute ("begin transaction"); + } catch (Exception ex) { + var sqlExp = ex as SQLiteException; + if (sqlExp != null) { + // It is recommended that applications respond to the errors listed below + // by explicitly issuing a ROLLBACK command. + // TODO: This rollback failsafe should be localized to all throw sites. + switch (sqlExp.Result) { + case SQLite3.Result.IOError: + case SQLite3.Result.Full: + case SQLite3.Result.Busy: + case SQLite3.Result.NoMem: + case SQLite3.Result.Interrupt: + RollbackTo (null, true); + break; + } + } else { + // Call decrement and not VolatileWrite in case we've already + // created a transaction point in SaveTransactionPoint since the catch. + Interlocked.Decrement (ref _transactionDepth); + } + + throw; + } + } else { + // Calling BeginTransaction on an already open transaction is invalid + throw new InvalidOperationException ("Cannot begin a transaction while already in a transaction."); + } + } + + /// + /// Creates a savepoint in the database at the current point in the transaction timeline. + /// Begins a new transaction if one is not in progress. + /// + /// Call to undo transactions since the returned savepoint. + /// Call to commit transactions after the savepoint returned here. + /// Call to end the transaction, committing all changes. + /// + /// A string naming the savepoint. + public string SaveTransactionPoint () + { + int depth = Interlocked.Increment (ref _transactionDepth) - 1; + string retVal = "S" + _rand.Next (short.MaxValue) + "D" + depth; + + try { + Execute ("savepoint " + retVal); + } catch (Exception ex) { + var sqlExp = ex as SQLiteException; + if (sqlExp != null) { + // It is recommended that applications respond to the errors listed below + // by explicitly issuing a ROLLBACK command. + // TODO: This rollback failsafe should be localized to all throw sites. + switch (sqlExp.Result) { + case SQLite3.Result.IOError: + case SQLite3.Result.Full: + case SQLite3.Result.Busy: + case SQLite3.Result.NoMem: + case SQLite3.Result.Interrupt: + RollbackTo (null, true); + break; + } + } else { + Interlocked.Decrement (ref _transactionDepth); + } + + throw; + } + + return retVal; + } + + /// + /// Rolls back the transaction that was begun by or . + /// + public void Rollback () + { + RollbackTo (null, false); + } + + /// + /// Rolls back the savepoint created by or SaveTransactionPoint. + /// + /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to + public void RollbackTo (string savepoint) + { + RollbackTo (savepoint, false); + } + + /// + /// Rolls back the transaction that was begun by . + /// + /// true to avoid throwing exceptions, false otherwise + void RollbackTo (string savepoint, bool noThrow) + { + // Rolling back without a TO clause rolls backs all transactions + // and leaves the transaction stack empty. + try { + if (String.IsNullOrEmpty (savepoint)) { + if (Interlocked.Exchange (ref _transactionDepth, 0) > 0) { + Execute ("rollback"); + } + } else { + DoSavePointExecute (savepoint, "rollback to "); + } + } catch (SQLiteException) { + if (!noThrow) + throw; + + } + // No need to rollback if there are no transactions open. + } + + /// + /// Releases a savepoint returned from . Releasing a savepoint + /// makes changes since that savepoint permanent if the savepoint began the transaction, + /// or otherwise the changes are permanent pending a call to . + /// + /// The RELEASE command is like a COMMIT for a SAVEPOINT. + /// + /// The name of the savepoint to release. The string should be the result of a call to + public void Release (string savepoint) + { + DoSavePointExecute (savepoint, "release "); + } + + void DoSavePointExecute (string savepoint, string cmd) + { + // Validate the savepoint + int firstLen = savepoint.IndexOf ('D'); + if (firstLen >= 2 && savepoint.Length > firstLen + 1) { + int depth; + if (Int32.TryParse (savepoint.Substring (firstLen + 1), out depth)) { + // TODO: Mild race here, but inescapable without locking almost everywhere. + if (0 <= depth && depth < _transactionDepth) { +#if NETFX_CORE + Volatile.Write (ref _transactionDepth, depth); +#elif SILVERLIGHT + _transactionDepth = depth; +#else + Thread.VolatileWrite (ref _transactionDepth, depth); +#endif + Execute (cmd + savepoint); + return; + } + } + } + + throw new ArgumentException ("savePoint is not valid, and should be the result of a call to SaveTransactionPoint.", "savePoint"); + } + + /// + /// Commits the transaction that was begun by . + /// + public void Commit () + { + if (Interlocked.Exchange (ref _transactionDepth, 0) != 0) { + Execute ("commit"); + } + // Do nothing on a commit with no open transaction + } + + /// + /// Executes within a (possibly nested) transaction by wrapping it in a SAVEPOINT. If an + /// exception occurs the whole transaction is rolled back, not just the current savepoint. The exception + /// is rethrown. + /// + /// + /// The to perform within a transaction. can contain any number + /// of operations on the connection but should never call or + /// . + /// + public void RunInTransaction (Action action) + { + try { + lock (syncObjects[DatabasePath]) { + var savePoint = SaveTransactionPoint (); + action (); + Release (savePoint); + } + } catch (Exception) { + Rollback (); + throw; + } + } + + /// + /// Executes while blocking other threads to access the same database. + /// + /// + /// The to perform within a lock. + /// + public void RunInDatabaseLock (Action action) + { + lock (syncObjects[DatabasePath]) { + action (); + } + } + + /// + /// Inserts all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// + /// The number of rows added to the table. + /// + public int InsertAll (System.Collections.IEnumerable objects) + { + var c = 0; + RunInTransaction(() => { + foreach (var r in objects) { + c += Insert (r); + } + }); + return c; + } + + /// + /// Inserts all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// + /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// + /// The number of rows added to the table. + /// + public int InsertAll (System.Collections.IEnumerable objects, string extra) + { + var c = 0; + RunInTransaction (() => { + foreach (var r in objects) { + c += Insert (r, extra); + } + }); + return c; + } + + /// + /// Inserts all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows added to the table. + /// + public int InsertAll (System.Collections.IEnumerable objects, Type objType) + { + var c = 0; + RunInTransaction (() => { + foreach (var r in objects) { + c += Insert (r, objType); + } + }); + return c; + } + + /// + /// Inserts the given object and retrieves its + /// auto incremented primary key if it has one. + /// + /// + /// The object to insert. + /// + /// + /// The number of rows added to the table. + /// + public int Insert (object obj) + { + if (obj == null) { + return 0; + } + return Insert (obj, "", obj.GetType ()); + } + + /// + /// Inserts the given object and retrieves its + /// auto incremented primary key if it has one. + /// If a UNIQUE constraint violation occurs with + /// some pre-existing object, this function deletes + /// the old object. + /// + /// + /// The object to insert. + /// + /// + /// The number of rows modified. + /// + public int InsertOrReplace (object obj) + { + if (obj == null) { + return 0; + } + return Insert (obj, "OR REPLACE", obj.GetType ()); + } + + /// + /// Inserts the given object and retrieves its + /// auto incremented primary key if it has one. + /// + /// + /// The object to insert. + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows added to the table. + /// + public int Insert (object obj, Type objType) + { + return Insert (obj, "", objType); + } + + /// + /// Inserts the given object and retrieves its + /// auto incremented primary key if it has one. + /// If a UNIQUE constraint violation occurs with + /// some pre-existing object, this function deletes + /// the old object. + /// + /// + /// The object to insert. + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows modified. + /// + public int InsertOrReplace (object obj, Type objType) + { + return Insert (obj, "OR REPLACE", objType); + } + + /// + /// Inserts the given object and retrieves its + /// auto incremented primary key if it has one. + /// + /// + /// The object to insert. + /// + /// + /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// + /// The number of rows added to the table. + /// + public int Insert (object obj, string extra) + { + if (obj == null) { + return 0; + } + return Insert (obj, extra, obj.GetType ()); + } + + /// + /// Inserts the given object and retrieves its + /// auto incremented primary key if it has one. + /// + /// + /// The object to insert. + /// + /// + /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows added to the table. + /// + public int Insert (object obj, string extra, Type objType) + { + if (obj == null || objType == null) { + return 0; + } + + + var map = GetMapping (objType); + +#if NETFX_CORE + if (map.PK != null && map.PK.IsAutoGuid) + { + // no GetProperty so search our way up the inheritance chain till we find it + PropertyInfo prop; + while (objType != null) + { + var info = objType.GetTypeInfo(); + prop = info.GetDeclaredProperty(map.PK.PropertyName); + if (prop != null) + { + if (prop.GetValue(obj, null).Equals(Guid.Empty)) + { + prop.SetValue(obj, Guid.NewGuid(), null); + } + break; + } + + objType = info.BaseType; + } + } +#else + if (map.PK != null && map.PK.IsAutoGuid) { + var prop = objType.GetProperty(map.PK.PropertyName); + if (prop != null) { + //if (prop.GetValue(obj, null).Equals(Guid.Empty)) { + if (prop.GetGetMethod().Invoke(obj, null).Equals(Guid.Empty)) + { + prop.SetValue(obj, Guid.NewGuid(), null); + } + } + } +#endif + + + var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; + + var cols = replacing ? map.InsertOrReplaceColumns : map.InsertColumns; + var vals = new object[cols.Length]; + for (var i = 0; i < vals.Length; i++) { + vals [i] = cols [i].GetValue(obj); + } + + var insertCmd = map.GetInsertCommand (this, extra); + int count; + + try { + count = insertCmd.ExecuteNonQuery (vals); + } + catch (SQLiteException ex) { + + if (SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + throw NotNullConstraintViolationException.New (ex.Result, ex.Message, map, obj); + } + throw; + } + + if (map.HasAutoIncPK) + { + var id = SQLite3.LastInsertRowid (Handle); + map.SetAutoIncPK (obj, id); + } + + return count; + } + + /// + /// Updates all of the columns of a table using the specified object + /// except for its primary key. + /// The object is required to have a primary key. + /// + /// + /// The object to update. It must have a primary key designated using the PrimaryKeyAttribute. + /// + /// + /// The number of rows updated. + /// + public int Update (object obj) + { + if (obj == null) { + return 0; + } + return Update (obj, obj.GetType ()); + } + + /// + /// Updates all of the columns of a table using the specified object + /// except for its primary key. + /// The object is required to have a primary key. + /// + /// + /// The object to update. It must have a primary key designated using the PrimaryKeyAttribute. + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows updated. + /// + public int Update (object obj, Type objType) + { + int rowsAffected = 0; + if (obj == null || objType == null) { + return 0; + } + + var map = GetMapping (objType); + + var pk = map.PK; + + if (pk == null) { + throw new NotSupportedException ("Cannot update " + map.TableName + ": it has no PK"); + } + + var cols = from p in map.Columns + where p != pk + select p; + var vals = from c in cols + select c.GetValue (obj); + var ps = new List (vals); + ps.Add (pk.GetValue (obj)); + var q = string.Format ("update \"{0}\" set {1} where {2} = ? ", map.TableName, string.Join (",", (from c in cols + select "\"" + c.Name + "\" = ? ").ToArray ()), pk.Name); + + try { + rowsAffected = Execute (q, ps.ToArray ()); + } + catch (SQLiteException ex) { + + if (ex.Result == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + throw NotNullConstraintViolationException.New (ex, map, obj); + } + + throw ex; + } + + return rowsAffected; + } + + /// + /// Updates all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// + /// The number of rows modified. + /// + public int UpdateAll (System.Collections.IEnumerable objects) + { + var c = 0; + RunInTransaction (() => { + foreach (var r in objects) { + c += Update (r); + } + }); + return c; + } + + /// + /// Deletes the given object from the database using its primary key. + /// + /// + /// The object to delete. It must have a primary key designated using the PrimaryKeyAttribute. + /// + /// + /// The number of rows deleted. + /// + public int Delete (object objectToDelete) + { + var map = GetMapping (objectToDelete.GetType ()); + var pk = map.PK; + if (pk == null) { + throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); + } + var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); + return Execute (q, pk.GetValue (objectToDelete)); + } + + /// + /// Deletes the object with the specified primary key. + /// + /// + /// The primary key of the object to delete. + /// + /// + /// The number of objects deleted. + /// + /// + /// The type of object. + /// + public int Delete (object primaryKey) + { + var map = GetMapping (typeof (T)); + var pk = map.PK; + if (pk == null) { + throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); + } + var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); + return Execute (q, primaryKey); + } + + /// + /// Deletes all the objects from the specified table. + /// WARNING WARNING: Let me repeat. It deletes ALL the objects from the + /// specified table. Do you really want to do that? + /// + /// + /// The number of objects deleted. + /// + /// + /// The type of objects to delete. + /// + public int DeleteAll () + { + var map = GetMapping (typeof (T)); + var query = string.Format("delete from \"{0}\"", map.TableName); + return Execute (query); + } + + ~SQLiteConnection () + { + Dispose (false); + } + + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + + protected virtual void Dispose (bool disposing) + { + Close (); + } + + public void Close () + { + if (_open && Handle != NullHandle) { + try { + if (_mappings != null) { + foreach (var sqlInsertCommand in _mappings.Values) { + sqlInsertCommand.Dispose(); + } + } + var r = SQLite3.Close (Handle); + if (r != SQLite3.Result.OK) { + string msg = SQLite3.GetErrmsg (Handle); + throw SQLiteException.New (r, msg); + } + } + finally { + Handle = NullHandle; + _open = false; + } + } + } + } + + /// + /// Represents a parsed connection string. + /// + class SQLiteConnectionString + { + public string ConnectionString { get; private set; } + public string DatabasePath { get; private set; } + public bool StoreDateTimeAsTicks { get; private set; } + +#if NETFX_CORE + static readonly string MetroStyleDataPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path; +#endif + + public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks) + { + ConnectionString = databasePath; + StoreDateTimeAsTicks = storeDateTimeAsTicks; + +#if NETFX_CORE + DatabasePath = System.IO.Path.Combine (MetroStyleDataPath, databasePath); +#else + DatabasePath = databasePath; +#endif + } + } + + [AttributeUsage (AttributeTargets.Class)] + public class TableAttribute : Attribute + { + public string Name { get; set; } + + public TableAttribute (string name) + { + Name = name; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class ColumnAttribute : Attribute + { + public string Name { get; set; } + + public ColumnAttribute (string name) + { + Name = name; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class PrimaryKeyAttribute : Attribute + { + } + + [AttributeUsage (AttributeTargets.Property)] + public class AutoIncrementAttribute : Attribute + { + } + + [AttributeUsage (AttributeTargets.Property)] + public class IndexedAttribute : Attribute + { + public string Name { get; set; } + public int Order { get; set; } + public virtual bool Unique { get; set; } + + public IndexedAttribute() + { + } + + public IndexedAttribute(string name, int order) + { + Name = name; + Order = order; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class IgnoreAttribute : Attribute + { + } + + [AttributeUsage (AttributeTargets.Property)] + public class UniqueAttribute : IndexedAttribute + { + public override bool Unique { + get { return true; } + set { /* throw? */ } + } + + public UniqueAttribute() : base() + { + } + + public UniqueAttribute(string name, int order) : base(name, order) + { + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class MaxLengthAttribute : Attribute + { + public int Value { get; private set; } + + public MaxLengthAttribute (int length) + { + Value = length; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class CollationAttribute: Attribute + { + public string Value { get; private set; } + + public CollationAttribute (string collation) + { + Value = collation; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class NotNullAttribute : Attribute + { + } + + public class TableMapping + { + public Type MappedType { get; private set; } + + public string TableName { get; private set; } + + public Column[] Columns { get; private set; } + + public Column PK { get; private set; } + + public string GetByPrimaryKeySql { get; private set; } + + Column _autoPk; + Column[] _insertColumns; + Column[] _insertOrReplaceColumns; + + public TableMapping(Type type, CreateFlags createFlags = CreateFlags.None) + { + MappedType = type; + +#if NETFX_CORE + var tableAttr = (TableAttribute)System.Reflection.CustomAttributeExtensions + .GetCustomAttribute(type.GetTypeInfo(), typeof(TableAttribute), true); +#else + var tableAttr = (TableAttribute)type.GetCustomAttributes (typeof (TableAttribute), true).FirstOrDefault (); +#endif + + TableName = tableAttr != null ? tableAttr.Name : MappedType.Name; + +#if !NETFX_CORE + var props = MappedType.GetProperties (BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty); +#else + var props = from p in MappedType.GetRuntimeProperties() + where ((p.GetMethod != null && p.GetMethod.IsPublic) || (p.SetMethod != null && p.SetMethod.IsPublic) || (p.GetMethod != null && p.GetMethod.IsStatic) || (p.SetMethod != null && p.SetMethod.IsStatic)) + select p; +#endif + var cols = new List (); + foreach (var p in props) { +#if !NETFX_CORE + var ignore = p.GetCustomAttributes (typeof(IgnoreAttribute), true).Length > 0; +#else + var ignore = p.GetCustomAttributes (typeof(IgnoreAttribute), true).Count() > 0; +#endif + if (p.CanWrite && !ignore) { + cols.Add (new Column (p, createFlags)); + } + } + Columns = cols.ToArray (); + foreach (var c in Columns) { + if (c.IsAutoInc && c.IsPK) { + _autoPk = c; + } + if (c.IsPK) { + PK = c; + } + } + + HasAutoIncPK = _autoPk != null; + + if (PK != null) { + GetByPrimaryKeySql = string.Format ("select * from \"{0}\" where \"{1}\" = ?", TableName, PK.Name); + } + else { + // People should not be calling Get/Find without a PK + GetByPrimaryKeySql = string.Format ("select * from \"{0}\" limit 1", TableName); + } + } + + public bool HasAutoIncPK { get; private set; } + + public void SetAutoIncPK (object obj, long id) + { + if (_autoPk != null) { + _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); + } + } + + public Column[] InsertColumns { + get { + if (_insertColumns == null) { + _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); + } + return _insertColumns; + } + } + + public Column[] InsertOrReplaceColumns { + get { + if (_insertOrReplaceColumns == null) { + _insertOrReplaceColumns = Columns.ToArray (); + } + return _insertOrReplaceColumns; + } + } + + public Column FindColumnWithPropertyName (string propertyName) + { + var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); + return exact; + } + + public Column FindColumn (string columnName) + { + var exact = Columns.FirstOrDefault (c => c.Name == columnName); + return exact; + } + + PreparedSqlLiteInsertCommand _insertCommand; + string _insertCommandExtra; + + public PreparedSqlLiteInsertCommand GetInsertCommand(SQLiteConnection conn, string extra) + { + if (_insertCommand == null) { + _insertCommand = CreateInsertCommand(conn, extra); + _insertCommandExtra = extra; + } + else if (_insertCommandExtra != extra) { + _insertCommand.Dispose(); + _insertCommand = CreateInsertCommand(conn, extra); + _insertCommandExtra = extra; + } + return _insertCommand; + } + + PreparedSqlLiteInsertCommand CreateInsertCommand(SQLiteConnection conn, string extra) + { + var cols = InsertColumns; + string insertSql; + if (!cols.Any() && Columns.Count() == 1 && Columns[0].IsAutoInc) + { + insertSql = string.Format("insert {1} into \"{0}\" default values", TableName, extra); + } + else + { + var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; + + if (replacing) { + cols = InsertOrReplaceColumns; + } + + insertSql = string.Format("insert {3} into \"{0}\"({1}) values ({2})", TableName, + string.Join(",", (from c in cols + select "\"" + c.Name + "\"").ToArray()), + string.Join(",", (from c in cols + select "?").ToArray()), extra); + + } + + var insertCommand = new PreparedSqlLiteInsertCommand(conn); + insertCommand.CommandText = insertSql; + return insertCommand; + } + + protected internal void Dispose() + { + if (_insertCommand != null) { + _insertCommand.Dispose(); + _insertCommand = null; + } + } + + public class Column + { + PropertyInfo _prop; + + public string Name { get; private set; } + + public string PropertyName { get { return _prop.Name; } } + + public Type ColumnType { get; private set; } + + public string Collation { get; private set; } + + public bool IsAutoInc { get; private set; } + public bool IsAutoGuid { get; private set; } + + public bool IsPK { get; private set; } + + public IEnumerable Indices { get; set; } + + public bool IsNullable { get; private set; } + + public int? MaxStringLength { get; private set; } + + public Column(PropertyInfo prop, CreateFlags createFlags = CreateFlags.None) + { + var colAttr = (ColumnAttribute)prop.GetCustomAttributes(typeof(ColumnAttribute), true).FirstOrDefault(); + + _prop = prop; + Name = colAttr == null ? prop.Name : colAttr.Name; + //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead + ColumnType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; + Collation = Orm.Collation(prop); + + IsPK = Orm.IsPK(prop) || + (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && + string.Compare (prop.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); + + var isAuto = Orm.IsAutoInc(prop) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); + IsAutoGuid = isAuto && ColumnType == typeof(Guid); + IsAutoInc = isAuto && !IsAutoGuid; + + Indices = Orm.GetIndices(prop); + if (!Indices.Any() + && !IsPK + && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) + && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) + ) + { + Indices = new IndexedAttribute[] { new IndexedAttribute() }; + } + IsNullable = !(IsPK || Orm.IsMarkedNotNull(prop)); + MaxStringLength = Orm.MaxStringLength(prop); + } + + public void SetValue (object obj, object val) + { + _prop.SetValue (obj, val, null); + } + + public object GetValue (object obj) + { + return _prop.GetGetMethod().Invoke(obj, null); + } + } + } + + public static class Orm + { + public const int DefaultMaxStringLength = 140; + public const string ImplicitPkName = "Id"; + public const string ImplicitIndexSuffix = "Id"; + + public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks) + { + string decl = "\"" + p.Name + "\" " + SqlType (p, storeDateTimeAsTicks) + " "; + + if (p.IsPK) { + decl += "primary key "; + } + if (p.IsAutoInc) { + decl += "autoincrement "; + } + if (!p.IsNullable) { + decl += "not null "; + } + if (!string.IsNullOrEmpty (p.Collation)) { + decl += "collate " + p.Collation + " "; + } + + return decl; + } + + public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks) + { + var clrType = p.ColumnType; + if (clrType == typeof(Boolean) || clrType == typeof(Byte) || clrType == typeof(UInt16) || clrType == typeof(SByte) || clrType == typeof(Int16) || clrType == typeof(Int32)) { + return "integer"; + } else if (clrType == typeof(UInt32) || clrType == typeof(Int64)) { + return "bigint"; + } else if (clrType == typeof(Single) || clrType == typeof(Double) || clrType == typeof(Decimal)) { + return "float"; + } else if (clrType == typeof(String)) { + int? len = p.MaxStringLength; + + if (len.HasValue) + return "varchar(" + len.Value + ")"; + + return "varchar"; + } else if (clrType == typeof(TimeSpan)) { + return "bigint"; + } else if (clrType == typeof(DateTime)) { + return storeDateTimeAsTicks ? "bigint" : "datetime"; + } else if (clrType == typeof(DateTimeOffset)) { + return "bigint"; +#if !NETFX_CORE + } else if (clrType.IsEnum) { +#else + } else if (clrType.GetTypeInfo().IsEnum) { +#endif + return "integer"; + } else if (clrType == typeof(byte[])) { + return "blob"; + } else if (clrType == typeof(Guid)) { + return "varchar(36)"; + } else { + throw new NotSupportedException ("Don't know about " + clrType); + } + } + + public static bool IsPK (MemberInfo p) + { + var attrs = p.GetCustomAttributes (typeof(PrimaryKeyAttribute), true); +#if !NETFX_CORE + return attrs.Length > 0; +#else + return attrs.Count() > 0; +#endif + } + + public static string Collation (MemberInfo p) + { + var attrs = p.GetCustomAttributes (typeof(CollationAttribute), true); +#if !NETFX_CORE + if (attrs.Length > 0) { + return ((CollationAttribute)attrs [0]).Value; +#else + if (attrs.Count() > 0) { + return ((CollationAttribute)attrs.First()).Value; +#endif + } else { + return string.Empty; + } + } + + public static bool IsAutoInc (MemberInfo p) + { + var attrs = p.GetCustomAttributes (typeof(AutoIncrementAttribute), true); +#if !NETFX_CORE + return attrs.Length > 0; +#else + return attrs.Count() > 0; +#endif + } + + public static IEnumerable GetIndices(MemberInfo p) + { + var attrs = p.GetCustomAttributes(typeof(IndexedAttribute), true); + return attrs.Cast(); + } + + public static int? MaxStringLength(PropertyInfo p) + { + var attrs = p.GetCustomAttributes (typeof(MaxLengthAttribute), true); +#if !NETFX_CORE + if (attrs.Length > 0) + return ((MaxLengthAttribute)attrs [0]).Value; +#else + if (attrs.Count() > 0) + return ((MaxLengthAttribute)attrs.First()).Value; +#endif + + return null; + } + + public static bool IsMarkedNotNull(MemberInfo p) + { + var attrs = p.GetCustomAttributes (typeof (NotNullAttribute), true); +#if !NETFX_CORE + return attrs.Length > 0; +#else + return attrs.Count() > 0; +#endif + } + } + + public partial class SQLiteCommand + { + SQLiteConnection _conn; + private List _bindings; + + public string CommandText { get; set; } + + internal SQLiteCommand (SQLiteConnection conn) + { + _conn = conn; + _bindings = new List (); + CommandText = ""; + } + + public int ExecuteNonQuery () + { + if (_conn.Trace) { + _conn.InvokeTrace ("Executing: " + this); + } + + var r = SQLite3.Result.OK; + lock (_conn.SyncObject) { + var stmt = Prepare (); + r = SQLite3.Step (stmt); + Finalize(stmt); + } + if (r == SQLite3.Result.Done) { + int rowsAffected = SQLite3.Changes (_conn.Handle); + return rowsAffected; + } else if (r == SQLite3.Result.Error) { + string msg = SQLite3.GetErrmsg (_conn.Handle); + throw SQLiteException.New (r, msg); + } + else if (r == SQLite3.Result.Constraint) { + if (SQLite3.ExtendedErrCode (_conn.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (_conn.Handle)); + } + } + + throw SQLiteException.New(r, r.ToString()); + } + + public IEnumerable ExecuteDeferredQuery () + { + return ExecuteDeferredQuery(_conn.GetMapping(typeof(T))); + } + + public List ExecuteQuery () + { + return ExecuteDeferredQuery(_conn.GetMapping(typeof(T))).ToList(); + } + + public List ExecuteQuery (TableMapping map) + { + return ExecuteDeferredQuery(map).ToList(); + } + + /// + /// Invoked every time an instance is loaded from the database. + /// + /// + /// The newly created object. + /// + /// + /// This can be overridden in combination with the + /// method to hook into the life-cycle of objects. + /// + /// Type safety is not possible because MonoTouch does not support virtual generic methods. + /// + protected virtual void OnInstanceCreated (object obj) + { + // Can be overridden. + } + + public IEnumerable ExecuteDeferredQuery (TableMapping map) + { + if (_conn.Trace) { + _conn.InvokeTrace ("Executing Query: " + this); + } + + lock (_conn.SyncObject) { + var stmt = Prepare (); + try { + var cols = new TableMapping.Column[SQLite3.ColumnCount (stmt)]; + + for (int i = 0; i < cols.Length; i++) { + var name = SQLite3.ColumnName16 (stmt, i); + cols [i] = map.FindColumn (name); + } + + while (SQLite3.Step (stmt) == SQLite3.Result.Row) { + var obj = Activator.CreateInstance(map.MappedType); + for (int i = 0; i < cols.Length; i++) { + if (cols [i] == null) + continue; + var colType = SQLite3.ColumnType (stmt, i); + var val = ReadCol (stmt, i, colType, cols [i].ColumnType); + cols [i].SetValue (obj, val); + } + OnInstanceCreated (obj); + yield return (T)obj; + } + } finally { + SQLite3.Finalize(stmt); + } + } + } + + public T ExecuteScalar () + { + if (_conn.Trace) { + _conn.InvokeTrace ("Executing Query: " + this); + } + + T val = default(T); + + lock (_conn.SyncObject) { + var stmt = Prepare(); + + try { + var r = SQLite3.Step (stmt); + if (r == SQLite3.Result.Row) { + var colType = SQLite3.ColumnType (stmt, 0); + val = (T)ReadCol (stmt, 0, colType, typeof(T)); + } + else if (r == SQLite3.Result.Done) { + } + else + { + throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); + } + } finally { + Finalize (stmt); + } + } + + return val; + } + + public void Bind (string name, object val) + { + _bindings.Add (new Binding { + Name = name, + Value = val + }); + } + + public void Bind (object val) + { + Bind (null, val); + } + + public override string ToString () + { + var parts = new string[1 + _bindings.Count]; + parts [0] = CommandText; + var i = 1; + foreach (var b in _bindings) { + parts [i] = string.Format (" {0}: {1}", i - 1, b.Value); + i++; + } + return string.Join (Environment.NewLine, parts); + } + + Sqlite3Statement Prepare() + { + var stmt = SQLite3.Prepare2 (_conn.Handle, CommandText); + BindAll (stmt); + return stmt; + } + + void Finalize (Sqlite3Statement stmt) + { + SQLite3.Finalize (stmt); + } + + void BindAll (Sqlite3Statement stmt) + { + int nextIdx = 1; + foreach (var b in _bindings) { + if (b.Name != null) { + b.Index = SQLite3.BindParameterIndex (stmt, b.Name); + } else { + b.Index = nextIdx++; + } + + BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks); + } + } + + internal static IntPtr NegativePointer = new IntPtr (-1); + + internal static void BindParameter (Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks) + { + if (value == null) { + SQLite3.BindNull (stmt, index); + } else { + if (value is Int32) { + SQLite3.BindInt (stmt, index, (int)value); + } else if (value is String) { + SQLite3.BindText (stmt, index, (string)value, -1, NegativePointer); + } else if (value is Byte || value is UInt16 || value is SByte || value is Int16) { + SQLite3.BindInt (stmt, index, Convert.ToInt32 (value)); + } else if (value is Boolean) { + SQLite3.BindInt (stmt, index, (bool)value ? 1 : 0); + } else if (value is UInt32 || value is Int64) { + SQLite3.BindInt64 (stmt, index, Convert.ToInt64 (value)); + } else if (value is Single || value is Double || value is Decimal) { + SQLite3.BindDouble (stmt, index, Convert.ToDouble (value)); + } else if (value is TimeSpan) { + SQLite3.BindInt64(stmt, index, ((TimeSpan)value).Ticks); + } else if (value is DateTime) { + if (storeDateTimeAsTicks) { + SQLite3.BindInt64 (stmt, index, ((DateTime)value).Ticks); + } + else { + SQLite3.BindText (stmt, index, ((DateTime)value).ToString ("yyyy-MM-dd HH:mm:ss"), -1, NegativePointer); + } + } else if (value is DateTimeOffset) { + SQLite3.BindInt64 (stmt, index, ((DateTimeOffset)value).UtcTicks); +#if !NETFX_CORE + } else if (value.GetType().IsEnum) { +#else + } else if (value.GetType().GetTypeInfo().IsEnum) { +#endif + SQLite3.BindInt (stmt, index, Convert.ToInt32 (value)); + } else if (value is byte[]){ + SQLite3.BindBlob(stmt, index, (byte[]) value, ((byte[]) value).Length, NegativePointer); + } else if (value is Guid) { + SQLite3.BindText(stmt, index, ((Guid)value).ToString(), 72, NegativePointer); + } else { + throw new NotSupportedException("Cannot store type: " + value.GetType()); + } + } + } + + class Binding + { + public string Name { get; set; } + + public object Value { get; set; } + + public int Index { get; set; } + } + + object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clrType) + { + if (type == SQLite3.ColType.Null) { + return null; + } else { + if (clrType == typeof(String)) { + return SQLite3.ColumnString (stmt, index); + } else if (clrType == typeof(Int32)) { + return (int)SQLite3.ColumnInt (stmt, index); + } else if (clrType == typeof(Boolean)) { + return SQLite3.ColumnInt (stmt, index) == 1; + } else if (clrType == typeof(double)) { + return SQLite3.ColumnDouble (stmt, index); + } else if (clrType == typeof(float)) { + return (float)SQLite3.ColumnDouble (stmt, index); + } else if (clrType == typeof(TimeSpan)) { + return new TimeSpan(SQLite3.ColumnInt64(stmt, index)); + } else if (clrType == typeof(DateTime)) { + if (_conn.StoreDateTimeAsTicks) { + return new DateTime (SQLite3.ColumnInt64 (stmt, index)); + } + else { + var text = SQLite3.ColumnString (stmt, index); + return DateTime.Parse (text); + } + } else if (clrType == typeof(DateTimeOffset)) { + return new DateTimeOffset(SQLite3.ColumnInt64 (stmt, index),TimeSpan.Zero); +#if !NETFX_CORE + } else if (clrType.IsEnum) { +#else + } else if (clrType.GetTypeInfo().IsEnum) { +#endif + return SQLite3.ColumnInt (stmt, index); + } else if (clrType == typeof(Int64)) { + return SQLite3.ColumnInt64 (stmt, index); + } else if (clrType == typeof(UInt32)) { + return (uint)SQLite3.ColumnInt64 (stmt, index); + } else if (clrType == typeof(decimal)) { + return (decimal)SQLite3.ColumnDouble (stmt, index); + } else if (clrType == typeof(Byte)) { + return (byte)SQLite3.ColumnInt (stmt, index); + } else if (clrType == typeof(UInt16)) { + return (ushort)SQLite3.ColumnInt (stmt, index); + } else if (clrType == typeof(Int16)) { + return (short)SQLite3.ColumnInt (stmt, index); + } else if (clrType == typeof(sbyte)) { + return (sbyte)SQLite3.ColumnInt (stmt, index); + } else if (clrType == typeof(byte[])) { + return SQLite3.ColumnByteArray (stmt, index); + } else if (clrType == typeof(Guid)) { + var text = SQLite3.ColumnString(stmt, index); + return new Guid(text); + } else{ + throw new NotSupportedException ("Don't know how to read " + clrType); + } + } + } + } + + /// + /// Since the insert never changed, we only need to prepare once. + /// + public class PreparedSqlLiteInsertCommand : IDisposable + { + public bool Initialized { get; set; } + + protected SQLiteConnection Connection { get; set; } + + public string CommandText { get; set; } + + protected Sqlite3Statement Statement { get; set; } + internal static readonly Sqlite3Statement NullStatement = default(Sqlite3Statement); + + internal PreparedSqlLiteInsertCommand (SQLiteConnection conn) + { + Connection = conn; + } + + public int ExecuteNonQuery (object[] source) + { + if (Connection.Trace) { + Connection.InvokeTrace ("Executing: " + CommandText); + } + + var r = SQLite3.Result.OK; + + if (!Initialized) { + Statement = Prepare (); + Initialized = true; + } + + //bind the values. + if (source != null) { + for (int i = 0; i < source.Length; i++) { + SQLiteCommand.BindParameter (Statement, i + 1, source [i], Connection.StoreDateTimeAsTicks); + } + } + r = SQLite3.Step (Statement); + + if (r == SQLite3.Result.Done) { + int rowsAffected = SQLite3.Changes (Connection.Handle); + SQLite3.Reset (Statement); + return rowsAffected; + } else if (r == SQLite3.Result.Error) { + string msg = SQLite3.GetErrmsg (Connection.Handle); + SQLite3.Reset (Statement); + throw SQLiteException.New (r, msg); + } else if (r == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (Connection.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + SQLite3.Reset (Statement); + throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (Connection.Handle)); + } else { + SQLite3.Reset (Statement); + throw SQLiteException.New (r, r.ToString ()); + } + } + + protected virtual Sqlite3Statement Prepare () + { + var stmt = SQLite3.Prepare2 (Connection.Handle, CommandText); + return stmt; + } + + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + + private void Dispose (bool disposing) + { + if (Statement != NullStatement) { + try { + SQLite3.Finalize (Statement); + } finally { + Statement = NullStatement; + Connection = null; + } + } + } + + ~PreparedSqlLiteInsertCommand () + { + Dispose (false); + } + } + + public abstract class BaseTableQuery + { + protected class Ordering + { + public string ColumnName { get; set; } + public bool Ascending { get; set; } + } + } + + public class TableQuery : BaseTableQuery, IEnumerable + { + public SQLiteConnection Connection { get; private set; } + + public TableMapping Table { get; private set; } + + Expression _where; + List _orderBys; + int? _limit; + int? _offset; + + BaseTableQuery _joinInner; + Expression _joinInnerKeySelector; + BaseTableQuery _joinOuter; + Expression _joinOuterKeySelector; + Expression _joinSelector; + + Expression _selector; + + TableQuery (SQLiteConnection conn, TableMapping table) + { + Connection = conn; + Table = table; + } + + public TableQuery (SQLiteConnection conn) + { + Connection = conn; + Table = Connection.GetMapping (typeof(T)); + } + + public TableQuery Clone () + { + var q = new TableQuery (Connection, Table); + q._where = _where; + q._deferred = _deferred; + if (_orderBys != null) { + q._orderBys = new List (_orderBys); + } + q._limit = _limit; + q._offset = _offset; + q._joinInner = _joinInner; + q._joinInnerKeySelector = _joinInnerKeySelector; + q._joinOuter = _joinOuter; + q._joinOuterKeySelector = _joinOuterKeySelector; + q._joinSelector = _joinSelector; + q._selector = _selector; + return q; + } + + public TableQuery Where (Expression> predExpr) + { + if (predExpr.NodeType == ExpressionType.Lambda) { + var lambda = (LambdaExpression)predExpr; + var pred = lambda.Body; + var q = Clone (); + q.AddWhere (pred); + return q; + } else { + throw new NotSupportedException ("Must be a predicate"); + } + } + + public TableQuery Take (int n) + { + var q = Clone (); + q._limit = n; + return q; + } + + public TableQuery Skip (int n) + { + var q = Clone (); + q._offset = n; + return q; + } + + public T ElementAt (int index) + { + return Skip (index).Take (1).First (); + } + + bool _deferred; + public TableQuery Deferred () + { + var q = Clone (); + q._deferred = true; + return q; + } + + public TableQuery OrderBy (Expression> orderExpr) + { + return AddOrderBy (orderExpr, true); + } + + public TableQuery OrderByDescending (Expression> orderExpr) + { + return AddOrderBy (orderExpr, false); + } + + public TableQuery ThenBy(Expression> orderExpr) + { + return AddOrderBy(orderExpr, true); + } + + public TableQuery ThenByDescending(Expression> orderExpr) + { + return AddOrderBy(orderExpr, false); + } + + private TableQuery AddOrderBy (Expression> orderExpr, bool asc) + { + if (orderExpr.NodeType == ExpressionType.Lambda) { + var lambda = (LambdaExpression)orderExpr; + + MemberExpression mem = null; + + var unary = lambda.Body as UnaryExpression; + if (unary != null && unary.NodeType == ExpressionType.Convert) { + mem = unary.Operand as MemberExpression; + } + else { + mem = lambda.Body as MemberExpression; + } + + if (mem != null && (mem.Expression.NodeType == ExpressionType.Parameter)) { + var q = Clone (); + if (q._orderBys == null) { + q._orderBys = new List (); + } + q._orderBys.Add (new Ordering { + ColumnName = Table.FindColumnWithPropertyName(mem.Member.Name).Name, + Ascending = asc + }); + return q; + } else { + throw new NotSupportedException ("Order By does not support: " + orderExpr); + } + } else { + throw new NotSupportedException ("Must be a predicate"); + } + } + + private void AddWhere (Expression pred) + { + if (_where == null) { + _where = pred; + } else { + _where = Expression.AndAlso (_where, pred); + } + } + + public TableQuery Join ( + TableQuery inner, + Expression> outerKeySelector, + Expression> innerKeySelector, + Expression> resultSelector) + { + var q = new TableQuery (Connection, Connection.GetMapping (typeof (TResult))) { + _joinOuter = this, + _joinOuterKeySelector = outerKeySelector, + _joinInner = inner, + _joinInnerKeySelector = innerKeySelector, + _joinSelector = resultSelector, + }; + return q; + } + + public TableQuery Select (Expression> selector) + { + var q = Clone (); + q._selector = selector; + return q; + } + + private SQLiteCommand GenerateCommand (string selectionList) + { + if (_joinInner != null && _joinOuter != null) { + throw new NotSupportedException ("Joins are not supported."); + } + else { + var cmdText = "select " + selectionList + " from \"" + Table.TableName + "\""; + var args = new List (); + if (_where != null) { + var w = CompileExpr (_where, args); + cmdText += " where " + w.CommandText; + } + if ((_orderBys != null) && (_orderBys.Count > 0)) { + var t = string.Join (", ", _orderBys.Select (o => "\"" + o.ColumnName + "\"" + (o.Ascending ? "" : " desc")).ToArray ()); + cmdText += " order by " + t; + } + if (_limit.HasValue) { + cmdText += " limit " + _limit.Value; + } + if (_offset.HasValue) { + if (!_limit.HasValue) { + cmdText += " limit -1 "; + } + cmdText += " offset " + _offset.Value; + } + return Connection.CreateCommand (cmdText, args.ToArray ()); + } + } + + class CompileResult + { + public string CommandText { get; set; } + + public object Value { get; set; } + } + + private CompileResult CompileExpr (Expression expr, List queryArgs) + { + if (expr == null) { + throw new NotSupportedException ("Expression is NULL"); + } else if (expr is BinaryExpression) { + var bin = (BinaryExpression)expr; + + var leftr = CompileExpr (bin.Left, queryArgs); + var rightr = CompileExpr (bin.Right, queryArgs); + + //If either side is a parameter and is null, then handle the other side specially (for "is null"/"is not null") + string text; + if (leftr.CommandText == "?" && leftr.Value == null) + text = CompileNullBinaryExpression(bin, rightr); + else if (rightr.CommandText == "?" && rightr.Value == null) + text = CompileNullBinaryExpression(bin, leftr); + else + text = "(" + leftr.CommandText + " " + GetSqlName(bin) + " " + rightr.CommandText + ")"; + return new CompileResult { CommandText = text }; + } else if (expr.NodeType == ExpressionType.Call) { + + var call = (MethodCallExpression)expr; + var args = new CompileResult[call.Arguments.Count]; + var obj = call.Object != null ? CompileExpr (call.Object, queryArgs) : null; + + for (var i = 0; i < args.Length; i++) { + args [i] = CompileExpr (call.Arguments [i], queryArgs); + } + + var sqlCall = ""; + + if (call.Method.Name == "Like" && args.Length == 2) { + sqlCall = "(" + args [0].CommandText + " like " + args [1].CommandText + ")"; + } + else if (call.Method.Name == "Contains" && args.Length == 2) { + sqlCall = "(" + args [1].CommandText + " in " + args [0].CommandText + ")"; + } + else if (call.Method.Name == "Contains" && args.Length == 1) { + if (call.Object != null && call.Object.Type == typeof(string)) { + sqlCall = "(" + obj.CommandText + " like ('%' || " + args [0].CommandText + " || '%'))"; + } + else { + sqlCall = "(" + args [0].CommandText + " in " + obj.CommandText + ")"; + } + } + else if (call.Method.Name == "StartsWith" && args.Length == 1) { + sqlCall = "(" + obj.CommandText + " like (" + args [0].CommandText + " || '%'))"; + } + else if (call.Method.Name == "EndsWith" && args.Length == 1) { + sqlCall = "(" + obj.CommandText + " like ('%' || " + args [0].CommandText + "))"; + } + else if (call.Method.Name == "Equals" && args.Length == 1) { + sqlCall = "(" + obj.CommandText + " = (" + args[0].CommandText + "))"; + } else if (call.Method.Name == "ToLower") { + sqlCall = "(lower(" + obj.CommandText + "))"; + } else if (call.Method.Name == "ToUpper") { + sqlCall = "(upper(" + obj.CommandText + "))"; + } else { + sqlCall = call.Method.Name.ToLower () + "(" + string.Join (",", args.Select (a => a.CommandText).ToArray ()) + ")"; + } + return new CompileResult { CommandText = sqlCall }; + + } else if (expr.NodeType == ExpressionType.Constant) { + var c = (ConstantExpression)expr; + queryArgs.Add (c.Value); + return new CompileResult { + CommandText = "?", + Value = c.Value + }; + } else if (expr.NodeType == ExpressionType.Convert) { + var u = (UnaryExpression)expr; + var ty = u.Type; + var valr = CompileExpr (u.Operand, queryArgs); + return new CompileResult { + CommandText = valr.CommandText, + Value = valr.Value != null ? ConvertTo (valr.Value, ty) : null + }; + } else if (expr.NodeType == ExpressionType.Not) { + var u = (UnaryExpression)expr; + var ty = u.Type; + var valr = CompileExpr (u.Operand, queryArgs); + + return new CompileResult { + CommandText = "NOT " + valr.CommandText, + Value = valr.Value != null ? valr.Value : null + }; + } else if (expr.NodeType == ExpressionType.MemberAccess) { + var mem = (MemberExpression)expr; + + if (mem.Expression!=null && mem.Expression.NodeType == ExpressionType.Parameter) { + // + // This is a column of our table, output just the column name + // Need to translate it if that column name is mapped + // + var columnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name; + return new CompileResult { CommandText = "\"" + columnName + "\"" }; + } else { + object obj = null; + if (mem.Expression != null) { + var r = CompileExpr (mem.Expression, queryArgs); + if (r.Value == null) { + throw new NotSupportedException ("Member access failed to compile expression"); + } + if (r.CommandText == "?") { + queryArgs.RemoveAt (queryArgs.Count - 1); + } + obj = r.Value; + } + + // + // Get the member value + // + object val = null; + +#if !NETFX_CORE + if (mem.Member.MemberType == MemberTypes.Property) { +#else + if (mem.Member is PropertyInfo) { +#endif + var m = (PropertyInfo)mem.Member; + //val = m.GetValue (obj, null); + val = m.GetGetMethod().Invoke(obj, null); +#if !NETFX_CORE + } else if (mem.Member.MemberType == MemberTypes.Field) { +#else + } else if (mem.Member is FieldInfo) { +#endif +#if SILVERLIGHT + val = Expression.Lambda (expr).Compile ().DynamicInvoke (); +#else + var m = (FieldInfo)mem.Member; + val = m.GetValue (obj); +#endif + } else { +#if !NETFX_CORE + throw new NotSupportedException ("MemberExpr: " + mem.Member.MemberType); +#else + throw new NotSupportedException ("MemberExpr: " + mem.Member.DeclaringType); +#endif + } + + // + // Work special magic for enumerables + // + if (val != null && val is System.Collections.IEnumerable && !(val is string) && !(val is System.Collections.Generic.IEnumerable)) { + var sb = new System.Text.StringBuilder(); + sb.Append("("); + var head = ""; + foreach (var a in (System.Collections.IEnumerable)val) { + queryArgs.Add(a); + sb.Append(head); + sb.Append("?"); + head = ","; + } + sb.Append(")"); + return new CompileResult { + CommandText = sb.ToString(), + Value = val + }; + } + else { + queryArgs.Add (val); + return new CompileResult { + CommandText = "?", + Value = val + }; + } + } + } + throw new NotSupportedException ("Cannot compile: " + expr.NodeType.ToString ()); + } + + static object ConvertTo (object obj, Type t) + { + Type nut = Nullable.GetUnderlyingType(t); + + if (nut != null) { + if (obj == null) return null; + return Convert.ChangeType (obj, nut); + } else { + return Convert.ChangeType (obj, t); + } + } + + /// + /// Compiles a BinaryExpression where one of the parameters is null. + /// + /// The non-null parameter + private string CompileNullBinaryExpression(BinaryExpression expression, CompileResult parameter) + { + if (expression.NodeType == ExpressionType.Equal) + return "(" + parameter.CommandText + " is ?)"; + else if (expression.NodeType == ExpressionType.NotEqual) + return "(" + parameter.CommandText + " is not ?)"; + else + throw new NotSupportedException("Cannot compile Null-BinaryExpression with type " + expression.NodeType.ToString()); + } + + string GetSqlName (Expression expr) + { + var n = expr.NodeType; + if (n == ExpressionType.GreaterThan) + return ">"; else if (n == ExpressionType.GreaterThanOrEqual) { + return ">="; + } else if (n == ExpressionType.LessThan) { + return "<"; + } else if (n == ExpressionType.LessThanOrEqual) { + return "<="; + } else if (n == ExpressionType.And) { + return "&"; + } else if (n == ExpressionType.AndAlso) { + return "and"; + } else if (n == ExpressionType.Or) { + return "|"; + } else if (n == ExpressionType.OrElse) { + return "or"; + } else if (n == ExpressionType.Equal) { + return "="; + } else if (n == ExpressionType.NotEqual) { + return "!="; + } else { + throw new NotSupportedException ("Cannot get SQL for: " + n); + } + } + + public int Count () + { + return GenerateCommand("count(*)").ExecuteScalar (); + } + + public int Count (Expression> predExpr) + { + return Where (predExpr).Count (); + } + + public IEnumerator GetEnumerator () + { + if (!_deferred) + return GenerateCommand("*").ExecuteQuery().GetEnumerator(); + + return GenerateCommand("*").ExecuteDeferredQuery().GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + public T First () + { + var query = Take (1); + return query.ToList().First (); + } + + public T FirstOrDefault () + { + var query = Take (1); + return query.ToList().FirstOrDefault (); + } + } + + public static class SQLite3 + { + public enum Result : int + { + OK = 0, + Error = 1, + Internal = 2, + Perm = 3, + Abort = 4, + Busy = 5, + Locked = 6, + NoMem = 7, + ReadOnly = 8, + Interrupt = 9, + IOError = 10, + Corrupt = 11, + NotFound = 12, + Full = 13, + CannotOpen = 14, + LockErr = 15, + Empty = 16, + SchemaChngd = 17, + TooBig = 18, + Constraint = 19, + Mismatch = 20, + Misuse = 21, + NotImplementedLFS = 22, + AccessDenied = 23, + Format = 24, + Range = 25, + NonDBFile = 26, + Notice = 27, + Warning = 28, + Row = 100, + Done = 101 + } + + public enum ExtendedResult : int + { + IOErrorRead = (Result.IOError | (1 << 8)), + IOErrorShortRead = (Result.IOError | (2 << 8)), + IOErrorWrite = (Result.IOError | (3 << 8)), + IOErrorFsync = (Result.IOError | (4 << 8)), + IOErrorDirFSync = (Result.IOError | (5 << 8)), + IOErrorTruncate = (Result.IOError | (6 << 8)), + IOErrorFStat = (Result.IOError | (7 << 8)), + IOErrorUnlock = (Result.IOError | (8 << 8)), + IOErrorRdlock = (Result.IOError | (9 << 8)), + IOErrorDelete = (Result.IOError | (10 << 8)), + IOErrorBlocked = (Result.IOError | (11 << 8)), + IOErrorNoMem = (Result.IOError | (12 << 8)), + IOErrorAccess = (Result.IOError | (13 << 8)), + IOErrorCheckReservedLock = (Result.IOError | (14 << 8)), + IOErrorLock = (Result.IOError | (15 << 8)), + IOErrorClose = (Result.IOError | (16 << 8)), + IOErrorDirClose = (Result.IOError | (17 << 8)), + IOErrorSHMOpen = (Result.IOError | (18 << 8)), + IOErrorSHMSize = (Result.IOError | (19 << 8)), + IOErrorSHMLock = (Result.IOError | (20 << 8)), + IOErrorSHMMap = (Result.IOError | (21 << 8)), + IOErrorSeek = (Result.IOError | (22 << 8)), + IOErrorDeleteNoEnt = (Result.IOError | (23 << 8)), + IOErrorMMap = (Result.IOError | (24 << 8)), + LockedSharedcache = (Result.Locked | (1 << 8)), + BusyRecovery = (Result.Busy | (1 << 8)), + CannottOpenNoTempDir = (Result.CannotOpen | (1 << 8)), + CannotOpenIsDir = (Result.CannotOpen | (2 << 8)), + CannotOpenFullPath = (Result.CannotOpen | (3 << 8)), + CorruptVTab = (Result.Corrupt | (1 << 8)), + ReadonlyRecovery = (Result.ReadOnly | (1 << 8)), + ReadonlyCannotLock = (Result.ReadOnly | (2 << 8)), + ReadonlyRollback = (Result.ReadOnly | (3 << 8)), + AbortRollback = (Result.Abort | (2 << 8)), + ConstraintCheck = (Result.Constraint | (1 << 8)), + ConstraintCommitHook = (Result.Constraint | (2 << 8)), + ConstraintForeignKey = (Result.Constraint | (3 << 8)), + ConstraintFunction = (Result.Constraint | (4 << 8)), + ConstraintNotNull = (Result.Constraint | (5 << 8)), + ConstraintPrimaryKey = (Result.Constraint | (6 << 8)), + ConstraintTrigger = (Result.Constraint | (7 << 8)), + ConstraintUnique = (Result.Constraint | (8 << 8)), + ConstraintVTab = (Result.Constraint | (9 << 8)), + NoticeRecoverWAL = (Result.Notice | (1 << 8)), + NoticeRecoverRollback = (Result.Notice | (2 << 8)) + } + + + public enum ConfigOption : int + { + SingleThread = 1, + MultiThread = 2, + Serialized = 3 + } + +#if !USE_CSHARP_SQLITE && !USE_WP8_NATIVE_SQLITE + [DllImport("sqlite3", EntryPoint = "sqlite3_open", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db); + + [DllImport("sqlite3", EntryPoint = "sqlite3_open_v2", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db, int flags, IntPtr zvfs); + + [DllImport("sqlite3", EntryPoint = "sqlite3_open_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Open(byte[] filename, out IntPtr db, int flags, IntPtr zvfs); + + [DllImport("sqlite3", EntryPoint = "sqlite3_open16", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Open16([MarshalAs(UnmanagedType.LPWStr)] string filename, out IntPtr db); + + [DllImport("sqlite3", EntryPoint = "sqlite3_enable_load_extension", CallingConvention=CallingConvention.Cdecl)] + public static extern Result EnableLoadExtension (IntPtr db, int onoff); + + [DllImport("sqlite3", EntryPoint = "sqlite3_close", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Close (IntPtr db); + + [DllImport("sqlite3", EntryPoint = "sqlite3_initialize", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Initialize(); + + [DllImport("sqlite3", EntryPoint = "sqlite3_shutdown", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Shutdown(); + + [DllImport("sqlite3", EntryPoint = "sqlite3_config", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Config (ConfigOption option); + + [DllImport("sqlite3", EntryPoint = "sqlite3_win32_set_directory", CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Unicode)] + public static extern int SetDirectory (uint directoryType, string directoryPath); + + [DllImport("sqlite3", EntryPoint = "sqlite3_busy_timeout", CallingConvention=CallingConvention.Cdecl)] + public static extern Result BusyTimeout (IntPtr db, int milliseconds); + + [DllImport("sqlite3", EntryPoint = "sqlite3_changes", CallingConvention=CallingConvention.Cdecl)] + public static extern int Changes (IntPtr db); + + [DllImport("sqlite3", EntryPoint = "sqlite3_prepare_v2", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Prepare2 (IntPtr db, [MarshalAs(UnmanagedType.LPStr)] string sql, int numBytes, out IntPtr stmt, IntPtr pzTail); + +#if NETFX_CORE + [DllImport ("sqlite3", EntryPoint = "sqlite3_prepare_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Prepare2 (IntPtr db, byte[] queryBytes, int numBytes, out IntPtr stmt, IntPtr pzTail); +#endif + + public static IntPtr Prepare2 (IntPtr db, string query) + { + IntPtr stmt; +#if NETFX_CORE + byte[] queryBytes = System.Text.UTF8Encoding.UTF8.GetBytes (query); + var r = Prepare2 (db, queryBytes, queryBytes.Length, out stmt, IntPtr.Zero); +#else + var r = Prepare2 (db, query, System.Text.UTF8Encoding.UTF8.GetByteCount (query), out stmt, IntPtr.Zero); +#endif + if (r != Result.OK) { + throw SQLiteException.New (r, GetErrmsg (db)); + } + return stmt; + } + + [DllImport("sqlite3", EntryPoint = "sqlite3_step", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Step (IntPtr stmt); + + [DllImport("sqlite3", EntryPoint = "sqlite3_reset", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Reset (IntPtr stmt); + + [DllImport("sqlite3", EntryPoint = "sqlite3_finalize", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Finalize (IntPtr stmt); + + [DllImport("sqlite3", EntryPoint = "sqlite3_last_insert_rowid", CallingConvention=CallingConvention.Cdecl)] + public static extern long LastInsertRowid (IntPtr db); + + [DllImport("sqlite3", EntryPoint = "sqlite3_errmsg16", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr Errmsg (IntPtr db); + + public static string GetErrmsg (IntPtr db) + { + return Marshal.PtrToStringUni (Errmsg (db)); + } + + [DllImport("sqlite3", EntryPoint = "sqlite3_bind_parameter_index", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindParameterIndex (IntPtr stmt, [MarshalAs(UnmanagedType.LPStr)] string name); + + [DllImport("sqlite3", EntryPoint = "sqlite3_bind_null", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindNull (IntPtr stmt, int index); + + [DllImport("sqlite3", EntryPoint = "sqlite3_bind_int", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindInt (IntPtr stmt, int index, int val); + + [DllImport("sqlite3", EntryPoint = "sqlite3_bind_int64", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindInt64 (IntPtr stmt, int index, long val); + + [DllImport("sqlite3", EntryPoint = "sqlite3_bind_double", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindDouble (IntPtr stmt, int index, double val); + + [DllImport("sqlite3", EntryPoint = "sqlite3_bind_text16", CallingConvention=CallingConvention.Cdecl, CharSet = CharSet.Unicode)] + public static extern int BindText (IntPtr stmt, int index, [MarshalAs(UnmanagedType.LPWStr)] string val, int n, IntPtr free); + + [DllImport("sqlite3", EntryPoint = "sqlite3_bind_blob", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindBlob (IntPtr stmt, int index, byte[] val, int n, IntPtr free); + + [DllImport("sqlite3", EntryPoint = "sqlite3_column_count", CallingConvention=CallingConvention.Cdecl)] + public static extern int ColumnCount (IntPtr stmt); + + [DllImport("sqlite3", EntryPoint = "sqlite3_column_name", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnName (IntPtr stmt, int index); + + [DllImport("sqlite3", EntryPoint = "sqlite3_column_name16", CallingConvention=CallingConvention.Cdecl)] + static extern IntPtr ColumnName16Internal (IntPtr stmt, int index); + public static string ColumnName16(IntPtr stmt, int index) + { + return Marshal.PtrToStringUni(ColumnName16Internal(stmt, index)); + } + + [DllImport("sqlite3", EntryPoint = "sqlite3_column_type", CallingConvention=CallingConvention.Cdecl)] + public static extern ColType ColumnType (IntPtr stmt, int index); + + [DllImport("sqlite3", EntryPoint = "sqlite3_column_int", CallingConvention=CallingConvention.Cdecl)] + public static extern int ColumnInt (IntPtr stmt, int index); + + [DllImport("sqlite3", EntryPoint = "sqlite3_column_int64", CallingConvention=CallingConvention.Cdecl)] + public static extern long ColumnInt64 (IntPtr stmt, int index); + + [DllImport("sqlite3", EntryPoint = "sqlite3_column_double", CallingConvention=CallingConvention.Cdecl)] + public static extern double ColumnDouble (IntPtr stmt, int index); + + [DllImport("sqlite3", EntryPoint = "sqlite3_column_text", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnText (IntPtr stmt, int index); + + [DllImport("sqlite3", EntryPoint = "sqlite3_column_text16", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnText16 (IntPtr stmt, int index); + + [DllImport("sqlite3", EntryPoint = "sqlite3_column_blob", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnBlob (IntPtr stmt, int index); + + [DllImport("sqlite3", EntryPoint = "sqlite3_column_bytes", CallingConvention=CallingConvention.Cdecl)] + public static extern int ColumnBytes (IntPtr stmt, int index); + + public static string ColumnString (IntPtr stmt, int index) + { + return Marshal.PtrToStringUni (SQLite3.ColumnText16 (stmt, index)); + } + + public static byte[] ColumnByteArray (IntPtr stmt, int index) + { + int length = ColumnBytes (stmt, index); + var result = new byte[length]; + if (length > 0) + Marshal.Copy (ColumnBlob (stmt, index), result, 0, length); + return result; + } + + [DllImport ("sqlite3", EntryPoint = "sqlite3_extended_errcode", CallingConvention = CallingConvention.Cdecl)] + public static extern ExtendedResult ExtendedErrCode (IntPtr db); + + [DllImport ("sqlite3", EntryPoint = "sqlite3_libversion_number", CallingConvention = CallingConvention.Cdecl)] + public static extern int LibVersionNumber (); +#else + public static Result Open(string filename, out Sqlite3DatabaseHandle db) + { + return (Result) Sqlite3.sqlite3_open(filename, out db); + } + + public static Result Open(string filename, out Sqlite3DatabaseHandle db, int flags, IntPtr zVfs) + { +#if USE_WP8_NATIVE_SQLITE + return (Result)Sqlite3.sqlite3_open_v2(filename, out db, flags, ""); +#else + return (Result)Sqlite3.sqlite3_open_v2(filename, out db, flags, null); +#endif + } + + public static Result Close(Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_close(db); + } + + public static Result BusyTimeout(Sqlite3DatabaseHandle db, int milliseconds) + { + return (Result)Sqlite3.sqlite3_busy_timeout(db, milliseconds); + } + + public static int Changes(Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_changes(db); + } + + public static Sqlite3Statement Prepare2(Sqlite3DatabaseHandle db, string query) + { + Sqlite3Statement stmt = default(Sqlite3Statement); +#if USE_WP8_NATIVE_SQLITE + var r = Sqlite3.sqlite3_prepare_v2(db, query, out stmt); +#else + stmt = new Sqlite3Statement(); + var r = Sqlite3.sqlite3_prepare_v2(db, query, -1, ref stmt, 0); +#endif + if (r != 0) + { + throw SQLiteException.New((Result)r, GetErrmsg(db)); + } + return stmt; + } + + public static Result Step(Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_step(stmt); + } + + public static Result Reset(Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_reset(stmt); + } + + public static Result Finalize(Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_finalize(stmt); + } + + public static long LastInsertRowid(Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_last_insert_rowid(db); + } + + public static string GetErrmsg(Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_errmsg(db); + } + + public static int BindParameterIndex(Sqlite3Statement stmt, string name) + { + return Sqlite3.sqlite3_bind_parameter_index(stmt, name); + } + + public static int BindNull(Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_bind_null(stmt, index); + } + + public static int BindInt(Sqlite3Statement stmt, int index, int val) + { + return Sqlite3.sqlite3_bind_int(stmt, index, val); + } + + public static int BindInt64(Sqlite3Statement stmt, int index, long val) + { + return Sqlite3.sqlite3_bind_int64(stmt, index, val); + } + + public static int BindDouble(Sqlite3Statement stmt, int index, double val) + { + return Sqlite3.sqlite3_bind_double(stmt, index, val); + } + + public static int BindText(Sqlite3Statement stmt, int index, string val, int n, IntPtr free) + { +#if USE_WP8_NATIVE_SQLITE + return Sqlite3.sqlite3_bind_text(stmt, index, val, n); +#else + return Sqlite3.sqlite3_bind_text(stmt, index, val, n, null); +#endif + } + + public static int BindBlob(Sqlite3Statement stmt, int index, byte[] val, int n, IntPtr free) + { +#if USE_WP8_NATIVE_SQLITE + return Sqlite3.sqlite3_bind_blob(stmt, index, val, n); +#else + return Sqlite3.sqlite3_bind_blob(stmt, index, val, n, null); +#endif + } + + public static int ColumnCount(Sqlite3Statement stmt) + { + return Sqlite3.sqlite3_column_count(stmt); + } + + public static string ColumnName(Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_name(stmt, index); + } + + public static string ColumnName16(Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_name(stmt, index); + } + + public static ColType ColumnType(Sqlite3Statement stmt, int index) + { + return (ColType)Sqlite3.sqlite3_column_type(stmt, index); + } + + public static int ColumnInt(Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_int(stmt, index); + } + + public static long ColumnInt64(Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_int64(stmt, index); + } + + public static double ColumnDouble(Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_double(stmt, index); + } + + public static string ColumnText(Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text(stmt, index); + } + + public static string ColumnText16(Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text(stmt, index); + } + + public static byte[] ColumnBlob(Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_blob(stmt, index); + } + + public static int ColumnBytes(Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_bytes(stmt, index); + } + + public static string ColumnString(Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text(stmt, index); + } + + public static byte[] ColumnByteArray(Sqlite3Statement stmt, int index) + { + return ColumnBlob(stmt, index); + } + + public static Result EnableLoadExtension(Sqlite3DatabaseHandle db, int onoff) + { + return (Result)Sqlite3.sqlite3_enable_load_extension(db, onoff); + } + + public static ExtendedResult ExtendedErrCode(Sqlite3DatabaseHandle db) + { + return (ExtendedResult)Sqlite3.sqlite3_extended_errcode(db); + } +#endif + + public enum ColType : int + { + Integer = 1, + Float = 2, + Text = 3, + Blob = 4, + Null = 5 + } + } +} diff --git a/Runtime/GuruLibs/SQLite4Unity3d/SQLite.cs.meta b/Runtime/GuruLibs/SQLite4Unity3d/SQLite.cs.meta new file mode 100644 index 0000000..81a00dd --- /dev/null +++ b/Runtime/GuruLibs/SQLite4Unity3d/SQLite.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 26aa670e0ee4a47a8bb1d3dfcbec8533 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: